Sailing with AWS Lightsail
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
.
1# lightsail.yaml
2Description: Create some low-cost fleet
3Parameters:
4 BundleTypeParameter:
5 Type: String
6 Default: nano_2_0
7 AllowedValues:
8 - nano_2_0
9 - micro_2_0
10 - small_2_0
11 Description: Allow user to choose the size
12 BlueprintParameter:
13 Type: String
14 Default: ubuntu_20_04
15 AZParameter:
16 Type: String
17 Default: 'eu-central-1a'
18 KeyPairNameParameter:
19 Type: String
20 Default: 'MyKeyPair'
21 Description: Name of a keypair
22Resources:
23 SmallVM1:
24 Type: 'AWS::Lightsail::Instance'
25 Properties:
26 AvailabilityZone: !Ref AZParameter
27 BlueprintId: !Ref BlueprintParameter
28 BundleId: !Ref BundleTypeParameter
29 InstanceName: 'SmallVM1'
30 KeyPairName: !Ref KeyPairNameParameter
31 Tags:
32 - Key: env
33 Value: prod
34 - Key: owner
35 Value: 3sky.dev
36 - Key: project
37 Value: awesome-deployment
38 UserData: 'sudo apt-get update -y && sudo apt-get install nginx -y'
39 SmallVM2:
40 Type: 'AWS::Lightsail::Instance'
41 Properties:
42 AvailabilityZone: !Ref AZParameter
43 BlueprintId: !Ref BlueprintParameter
44 BundleId: !Ref BundleTypeParameter
45 InstanceName: 'SmallVM2'
46 KeyPairName: !Ref KeyPairNameParameter
47 Tags:
48 - Key: env
49 Value: prod
50 - Key: owner
51 Value: 3sky.dev
52 - Key: project
53 Value: awesome-deployment
54 UserData: 'sudo apt-get update -y && sudo apt-get install nginx -y'
55 SmallVM3:
56 Type: 'AWS::Lightsail::Instance'
57 Properties:
58 AvailabilityZone: !Ref AZParameter
59 BlueprintId: !Ref BlueprintParameter
60 BundleId: !Ref BundleTypeParameter
61 InstanceName: 'SmallVM3'
62 KeyPairName: !Ref KeyPairNameParameter
63 Tags:
64 - Key: env
65 Value: dev
66 - Key: owner
67 Value: 3sky.dev
68 - Key: project
69 Value: awesome-deployment
70 UserData: 'sudo apt-get update -y && sudo apt-get install nginx -y'
Great, now we can create our stack.
1aws cloudformation create-stack \
2 --stack-name lightsail \
3 --template-body file://lightsail.yaml \
4 --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.
1aws cloudformation describe-stack-events \
2 --stack-name lightsail \
3 --output json \
4 --max-items 2 \
5 --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:
1#!/usr/bin/env python3
2
3'''
4Custom dynamic inventory script for AWS Lightsail.
5@3sky
6User need to provide:
7- AWS_KEY_ID
8- AWS_ACCESS_KEY
9- ENV_TAG
10as environment variables
11export AWS_KEY_ID=xxxx
12export AWS_ACCESS_KEY=xxx
13export ENV_TAG=xxx
14'''
15
16import os
17import sys
18import argparse
19
20try:
21 import json
22except ImportError:
23 import simplejson as json
24
25try:
26 import boto3
27 import botocore
28except ImportError:
29 print("Install boto3 first - pip3 install boto3 botocore")
30 os.exit()
31
32class LightsailInventory(object):
33
34 def __init__(self):
35 self.inventory = {}
36 self.read_cli_args()
37
38 if self.args.list:
39 ## set tags for checking machines
40 self.inventory = self.lightsail_inventory(os.environ['ENV_TAG'])
41 elif self.args.host:
42 # Not implemented, since we return _meta info `--list`.
43 self.inventory = self.empty_inventory()
44 else:
45 self.inventory = self.empty_inventory()
46
47 print(json.dumps(self.inventory));
48
49
50 def lightsail_inventory(self, input_tag):
51
52 try:
53 # that is important, boto3 call
54 client = boto3.client(
55 'lightsail',
56 aws_access_key_id=os.environ['AWS_KEY_ID'],
57 aws_secret_access_key=os.environ['AWS_ACCESS_KEY'],
58 region_name='eu-central-1'
59 )
60
61 except botocore.exceptions.ClientError as error:
62 print("AWS auth problem")
63 os.exit()
64 raise error
65
66 # whole logic goes here
67 response = client.get_instances()
68 # build a inventory template
69 # tip: ubuntu used as default user
70 machine_list = {'lightsail_group': {'hosts': [], 'vars': {'ansible_ssh_user': 'ubuntu'}}}
71 for instance in response['instances']:
72 for tag in instance['tags']:
73 # my k;v pair is based on key=env
74 if tag['key'] == 'env' and tag['value'] == input_tag:
75 machine_list['lightsail_group']['hosts'].append(instance['publicIpAddress'])
76 return machine_list
77
78 # Empty inventory for testing.
79 def empty_inventory(self):
80 return {'_meta': {'hostvars': {}}}
81
82 # Read the command line args passed to the script.
83 def read_cli_args(self):
84 parser = argparse.ArgumentParser()
85 parser.add_argument('--list', action = 'store_true')
86 parser.add_argument('--host', action = 'store')
87 self.args = parser.parse_args()
88
89# Get the inventory.
90LightsailInventory()
Yes, that's all. Nothing else. With assumptions that we already have AWS_KEY_ID
and AWS_ACCESS_KEY
. Let's list our inventory.
1$ ENV_TAG="prod" ansible-inventory -i lightsail_inventory.py --list
2
3{
4 "_meta": {
5 "hostvars": {
6 "18.195.131.192": {
7 "_meta": {
8 "hostvars": {}
9 },
10 "ansible_ssh_user": "ubuntu"
11 }
12 }
13 },
14 "all": {
15 "children": [
16 "lightsail_group",
17 "ungrouped"
18 ]
19 },
20 "lightsail_group": {
21 "hosts": [
22 "18.195.131.192"
23 ]
24 }
25}
Looks good. Let's test it against some basic playbook.
1# playbook.yml
2---
3- name: Deploy on AWS
4 become: true
5 hosts: lightsail_group
6
7 tasks:
8 - name: Debug
9 ansible.builtin.debug:
10 var: ansible_default_ipv4.address
1$ ANSIBLE_HOST_KEY_CHECKING=False ENV_TAG="dev" \
2ansible-playbook -i lightsail_inventory.py playbook.yml \
3 --private-key=~/.ssh/id_ed25519
4
5
6PLAY [Deploy on AWS] ****************************************************************************************************************************************
7
8TASK [Gathering Facts] **************************************************************************************************************************************
9ok: [18.195.131.192]
10
11TASK [Debug] ************************************************************************************************************************************************
12ok: [18.195.131.192] => {
13 "ansible_default_ipv4.address": "172.26.16.78"
14}
15
16PLAY RECAP **************************************************************************************************************************************************
1718.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.