3sky's notes

Minimal blog about IT

Sailing with AWS Lightsail

2022-12-28 6 min read 3sky

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. 

lightsail-1
Click Create Instance, yes there is no dedicated panel for it
lightsail-1
Change SSH key pair, and again there is no direct Add
lightsail-1
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.