Sailing with AWS Lightsail
Welcome
I have a daughter! What a piece of news, isn’t it? I can also add again the same line:
Newsletter is very time-consuming, and being a consultant is time-consuming. Everything is time-consuming.
However, I had some spare time to play a bit with a small client’s project. For example, not everyone needs cutting-edge technology, Kubernetes, and functional programming.
Sometimes we just need a small VM with IPv4, for PoC, or small old-style application, or even workshops about Linux administration. Here I want to introduce a bit unpopular AWS solution - Lightsail. It’s a neat service. Fast, very easy to use, and cheap. The smallest bundle is 3.5$/mo. However, it has some limitations. For example, it is unpopular - there is no ansible dynamic inventory plugin for it - what a shame. Let’s fix it then!
Tools used in this episode
- python3
- ansible
- CloudFormation
Spin the infrastructure - intro
I decided to use CloudFormation this time. That leads us to some strange issues. There is no good scripted way for attaching key pairs. Lightsail keys are a different object type, then EC2’s, and adding it via console is the easiest way. How to do it? It’s tricky.
Click Create Instance , yes there is no dedicated panel for it |
Change SSH key pair , and again there is no direct Add |
Now, we can use Upload New button, and finally uplaod our key pair! |
Formation of the cloud
Ufff, that was a painful experience. Now, we can just run our fantastic CF code.
It’s very simple. We will create 3 similar machines, the only difference will be tagging. The last machine will be tagged as dev
, rest of them become prod
.
# lightsail.yaml
Description: Create some low-cost fleet
Parameters:
BundleTypeParameter:
Type: String
Default: nano_2_0
AllowedValues:
- nano_2_0
- micro_2_0
- small_2_0
Description: Allow user to choose the size
BlueprintParameter:
Type: String
Default: ubuntu_20_04
AZParameter:
Type: String
Default: 'eu-central-1a'
KeyPairNameParameter:
Type: String
Default: 'MyKeyPair'
Description: Name of a keypair
Resources:
SmallVM1:
Type: 'AWS::Lightsail::Instance'
Properties:
AvailabilityZone: !Ref AZParameter
BlueprintId: !Ref BlueprintParameter
BundleId: !Ref BundleTypeParameter
InstanceName: 'SmallVM1'
KeyPairName: !Ref KeyPairNameParameter
Tags:
- Key: env
Value: prod
- Key: owner
Value: 3sky.dev
- Key: project
Value: awesome-deployment
UserData: 'sudo apt-get update -y && sudo apt-get install nginx -y'
SmallVM2:
Type: 'AWS::Lightsail::Instance'
Properties:
AvailabilityZone: !Ref AZParameter
BlueprintId: !Ref BlueprintParameter
BundleId: !Ref BundleTypeParameter
InstanceName: 'SmallVM2'
KeyPairName: !Ref KeyPairNameParameter
Tags:
- Key: env
Value: prod
- Key: owner
Value: 3sky.dev
- Key: project
Value: awesome-deployment
UserData: 'sudo apt-get update -y && sudo apt-get install nginx -y'
SmallVM3:
Type: 'AWS::Lightsail::Instance'
Properties:
AvailabilityZone: !Ref AZParameter
BlueprintId: !Ref BlueprintParameter
BundleId: !Ref BundleTypeParameter
InstanceName: 'SmallVM3'
KeyPairName: !Ref KeyPairNameParameter
Tags:
- Key: env
Value: dev
- Key: owner
Value: 3sky.dev
- Key: project
Value: awesome-deployment
UserData: 'sudo apt-get update -y && sudo apt-get install nginx -y'
Great, now we can create our stack.
aws cloudformation create-stack \
--stack-name lightsail \
--template-body file://lightsail.yaml \
--no-cli-pager
There is no console-based preview of progress like we have in Terraform, however, we can use describe-stack-events
, which is much easier in case of parsing outputs and building some automation.
aws cloudformation describe-stack-events \
--stack-name lightsail \
--output json \
--max-items 2 \
--no-cli-pager
That was fast and simple. The whole stack is stored in the AWS console, so even inexperienced users can make changes, or watch every resource. Sometimes simplicity could be the biggest benefit.
Release the python
Yes, programming is my passion. You can probably see it during the code review. Fortunately, Ansible Plugins are quite easy to write, even for people like me. So here is the code:
#!/usr/bin/env python3
'''
Custom dynamic inventory script for AWS Lightsail.
@3sky
User need to provide:
- AWS_KEY_ID
- AWS_ACCESS_KEY
- ENV_TAG
as environment variables
export AWS_KEY_ID=xxxx
export AWS_ACCESS_KEY=xxx
export ENV_TAG=xxx
'''
import os
import sys
import argparse
try:
import json
except ImportError:
import simplejson as json
try:
import boto3
import botocore
except ImportError:
print("Install boto3 first - pip3 install boto3 botocore")
os.exit()
class LightsailInventory(object):
def __init__(self):
self.inventory = {}
self.read_cli_args()
if self.args.list:
## set tags for checking machines
self.inventory = self.lightsail_inventory(os.environ['ENV_TAG'])
elif self.args.host:
# Not implemented, since we return _meta info `--list`.
self.inventory = self.empty_inventory()
else:
self.inventory = self.empty_inventory()
print(json.dumps(self.inventory));
def lightsail_inventory(self, input_tag):
try:
# that is important, boto3 call
client = boto3.client(
'lightsail',
aws_access_key_id=os.environ['AWS_KEY_ID'],
aws_secret_access_key=os.environ['AWS_ACCESS_KEY'],
region_name='eu-central-1'
)
except botocore.exceptions.ClientError as error:
print("AWS auth problem")
os.exit()
raise error
# whole logic goes here
response = client.get_instances()
# build a inventory template
# tip: ubuntu used as default user
machine_list = {'lightsail_group': {'hosts': [], 'vars': {'ansible_ssh_user': 'ubuntu'}}}
for instance in response['instances']:
for tag in instance['tags']:
# my k;v pair is based on key=env
if tag['key'] == 'env' and tag['value'] == input_tag:
machine_list['lightsail_group']['hosts'].append(instance['publicIpAddress'])
return machine_list
# Empty inventory for testing.
def empty_inventory(self):
return {'_meta': {'hostvars': {}}}
# Read the command line args passed to the script.
def read_cli_args(self):
parser = argparse.ArgumentParser()
parser.add_argument('--list', action = 'store_true')
parser.add_argument('--host', action = 'store')
self.args = parser.parse_args()
# Get the inventory.
LightsailInventory()
Yes, that’s all. Nothing else. With assumptions that we already have AWS_KEY_ID
and AWS_ACCESS_KEY
. Let’s list our inventory.
$ ENV_TAG="prod" ansible-inventory -i lightsail_inventory.py --list
{
"_meta": {
"hostvars": {
"18.195.131.192": {
"_meta": {
"hostvars": {}
},
"ansible_ssh_user": "ubuntu"
}
}
},
"all": {
"children": [
"lightsail_group",
"ungrouped"
]
},
"lightsail_group": {
"hosts": [
"18.195.131.192"
]
}
}
Looks good. Let’s test it against some basic playbook.
# playbook.yml
---
- name: Deploy on AWS
become: true
hosts: lightsail_group
tasks:
- name: Debug
ansible.builtin.debug:
var: ansible_default_ipv4.address
$ ANSIBLE_HOST_KEY_CHECKING=False ENV_TAG="dev" \
ansible-playbook -i lightsail_inventory.py playbook.yml \
--private-key=~/.ssh/id_ed25519
PLAY [Deploy on AWS] ****************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************
ok: [18.195.131.192]
TASK [Debug] ************************************************************************************************************************************************
ok: [18.195.131.192] => {
"ansible_default_ipv4.address": "172.26.16.78"
}
PLAY RECAP **************************************************************************************************************************************************
18.195.131.192 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
And again successfully tested!
Summary
CloudFormation is fun because is straightforward. No fancy markup or syntax, just YAML. Documentation is good, stacks are stored in the cloud, and it’s fast. Ah, and it’s a part of AWS CLI = no external tooling! For small, or basic infrastructure it’s IMO the best solution.
Next, we have Lightsail. Service similar to DigitalOcean Droplets. Cheap, and easy to use, however, it’s still a member of the AWS portfolio which means access to many tools and awesome reliability.
Then Ansible. Yes, it’s boring, and yes, it’s slow, however, it makes the job done. Plugin extensions are great. Easy to use, even if you can’t program well. I strongly recommend everyone to play a bit with their plugins, this or your own. Feel the joy o creating, new things on stable fundamentals. It’s a relaxing expiration.
Yes, there is some text, however, most of the article is code or commands. Why? Because it’s fun. For me technical work, it’s why I’m here, why I’m writing this blog and own consulting company. Take care, and see you next time.