AWS/Ansible – Provision a Lightsail instance using Infrastructure as Code

Note: This article has been duplicated from the previous article which uses Terraform and has been modified for Ansible.

AWS has introduced Lightsail to compete with Digital Ocean, Linode, etc. for an inexpensive VPS (Virtual Private Server) offering.

In this article we will use Ansible (Infrastructure as Code) to swiftly bring up an AWS Lightsail instance in us-east-1 on a dynamic IP and install docker/docker-compose on it.

We will use ‘myweb’ as an example in this article, using the same base path of ‘dev’ that was previously created, the container-admin group and using ~/.local/bin|lib for the binaries/libraries.

Please use ‘Get started with Lightsail for free’ prior to commencing with this article.

Go in to the dev directory/link located within your home directory:

$ cd ~/dev

Upgrade the AWS CLI on your host:

$ pip3 install awscli --upgrade --user && chmod 754 ~/.local/bin/aws

Install/Upgrade Ansible:

$ pip3 install ansible --upgrade --user && chmod 754 ~/.local/bin/ansible ~/.local/bin/ansible-playbook

Install/Upgrade Boto3:

$ pip3 install boto3 --upgrade --user

Create a work folder and change in to it:

$ mkdir -p ansible/myweb/scripts && cd ansible/myweb

Add an IAM Policy to the container-admin group so it will have access to the Lightsail API:
AWS UI Console -> Services -> Security, Identity, & Compliance -> IAM -> Policies -> Create Policy -> JSON (replace <AWS ACCOUNT ID> in the Resource arn with your Account’s ID (shown under the top right drop-down (of your name) within the My Account page next to the Account Id: under Account Settings)):

     "Version": "2012-10-17",
     "Statement": [
             "Effect": "Allow",
             "Action": [
             "Resource": "*"         
             "Effect": "Allow",
             "Action": "lightsail:",
             "Resource": [
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:StaticIp/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:ExportSnapshotRecord/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:Instance/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:CloudFormationStackRecord/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:RelationalDatabaseSnapshot/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:RelationalDatabase/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:InstanceSnapshot/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:Domain/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:LoadBalancer/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:KeyPair/*",
                 "arn:aws:lightsail::<AWS ACCOUNT ID>:Disk/*"

Review Policy ->

Name: AllowLightsail
Description: Allow access to Lightsail.

Create Policy.

Groups -> container-admin -> Attach Policy -> Search for AllowLightsail -> Attach Policy.

Generate an SSH Key Pair (no password) and restrict permissions on it:

$ ssh-keygen -q -t rsa -b 2048 -N '' -f ~/.ssh/myweb && chmod 400 ~/.ssh/myweb

Import the public key to Lightsail:

$ aws lightsail import-key-pair --key-pair-name myweb --public-key-base64 file://~/.ssh/

Create a hosts file and specify localhost:

$ cat << 'EOF' > hosts
> [local]
> localhost

Create a micro instance based off of Ubuntu 18_04 and reference an extraneous file for user_data (run once script on Virtual Machine boot):

$ cat << 'EOF' > lightsail.yml
> # Create a new AWS Lightsail instance, register the instance details and add a way to destroy it
> ---
> - hosts: local
>   connection: local
>   tasks:
>     - lightsail:
>         state: present
>         name:
>         region: us-east-1
>         zone: us-east-1a
>         blueprint_id: ubuntu_18_04
>         bundle_id: micro_2_0
>         key_pair_name: myweb
>         user_data: "{{ lookup('file', './scripts/') }}"
>         wait_timeout: 500
>       register: myweb
>     - debug: msg="Public IP is {{ myweb.instance.public_ip_address }} for {{ }}"
>       when: myweb.instance.public_ip_address is defined
>     - debug: msg="Run this playbook for {{ }} shortly to list the Public IP."
>       when: myweb.instance.public_ip_address is not defined
>     - lightsail:
>         state: absent
>         name:
>         region: us-east-1
>       tags: [ 'never', 'destroy' ]

Create the shell script for user_data:

$ cat << 'EOF' > scripts/
> #!/bin/bash
> MY_HOME="/home/ubuntu"
> export DEBIAN_FRONTEND=noninteractive
> # Install prereqs
> apt update
> apt install -y python3-pip apt-transport-https ca-certificates curl software-properties-common
> # Install docker
> curl -fsSL | apt-key add -
> add-apt-repository "deb [arch=amd64] $(lsb_release -cs) stable"
> apt update
> apt install -y docker-ce
> # Install docker-compose
> su ubuntu -c "mkdir -p $MY_HOME/.local/bin"
> su ubuntu -c "pip3 install docker-compose --upgrade --user && chmod 754 $MY_HOME/.local/bin/docker-compose"
> usermod -aG docker ubuntu
> # Add PATH
> printf "\nexport PATH=\$PATH:$MY_HOME/.local/bin\n" >> $MY_HOME/.bashrc
> exit 0

Run the playbook:

$ ansible-playbook -i hosts lightsail.yml

Log on to the instance (up to ~30 seconds may be needed for the attachment of the dynamic IP to the instance):

$ ssh -i ~/.ssh/myweb ubuntu@<The value of public_ip_address that was reported.  One can also re-run the playbook to print it again.>

Type yes and hit enter to accept.

On the host (a short while is needed for the run-once script to complete):

$ docker --version
$ docker-compose --version
$ logout

Tear down the instance:

$ ansible-playbook -i hosts lightsail.yml --tags "destroy"