Jérôme Decoster

Jérôme Decoster

3x AWS Certified - Architect, Developer, Cloud Practionner

26 Oct 2021

EC2 + Ansible + AWS CLI

The Goal
  • Create a Load balancer and EC2 instances via Autoscaling
  • EC2 machines host an Apache site installed via a user-data script
  • Do everything using AWS CLI
  • Ping the machines with Ansible
  • Update the Apache site with Ansible via a playbook
  • Machine inventory will be dynamic : a bash script using AWS CLI

    architecture.svg

    Install and setup the project

    Get the code from this github repository :

    # download the code
    $ git clone \
        --depth 1 \
        https://github.com/jeromedecoster/ec2-ansible.git \
        /tmp/aws
    
    # cd
    $ cd /tmp/aws
    

    To setup the project, run the following command :

    # create load balancer + instances using autoscaling
    $ make ec2-create
    

    This command will :

    Now we have 2 instances :

    ec2-instances.png

    A Load balancer :

    load-balancer.png

    A Launch template :

    launch-template.png

    A very simple website :

    user-data-install.png

    You can get the Load Balancer URL with :

    # get load balancer dns
    $ make load-balancer-dns
    

    Ping the EC2 machines using Ansible

    To ping the machin we use :

    # ping ec2 instances with ansible
    $ make ansible-ping
    

    This command execute a simple command :

    $ ansible aws -m ping
    

    But this command is executed in a specific playbook context

    The ansible.cfg file execute a bash inventory file

    [defaults]
    inventory = ./inventory.sh
    remote_user = ec2-user
    private_key_file = ../ec2-ansible.pem
    

    The inventory.sh file is an executable file :

    • Some aws cli commands collect the instances ids and public address IP
    • A bash script build and output some specific JSON data
    #!/bin/bash
    ID=$(aws autoscaling describe-auto-scaling-instances \
        --query "AutoScalingInstances[?AutoScalingGroupName=='ec2-ansible'].InstanceId" \
        --output text)
    
    IP=$(aws ec2 describe-instances \
        --instance-ids $ID \
        --query "Reservations[].Instances[?State.Name=='running'].[InstanceId, PublicIpAddress]" \
        --output text)
    
    # ...
    
    while read h; do
        id=$(echo "$h" | cut -f 1)
        ip=$(echo "$h" | cut -f 2)
        TMP=$(echo '"'$id'": { "ansible_host": "'$ip'" },')
        HOSTSVARS=${HOSTSVARS}${TMP}
        TMP=$(echo '"'$id'",')
        HOSTS=${HOSTS}${TMP}
    done < <(echo "$IP")
    
    cat << EOF
    {
        "_meta": {
            "hostvars": {
                ${HOSTSVARS}
            }
        },
        "aws": {
            "hosts": [
                ${HOSTS}
            ]
        }
    }
    EOF
    

    If we run this script directly in the terminal we get a JSON :

    $ ./playbooks/inventory.sh
    {
        "_meta": {
            "hostvars": {
                "i-084569174edd3c352": { "ansible_host": "15.237.149.180" },"i-013b7597d9f698b39": { "ansible_host": "15.236.95.164" },
            }
        },
        "aws": {
            "hosts": [
                "i-084569174edd3c352","i-013b7597d9f698b39",
            ]
        }
    }
    

    By executing our command we must answer yes to the SSH question several times :

    $ make ansible-ping 
     EXECUTE  ansible-ping
    The authenticity of host '15.237.149.180 (15.237.149.180)' can't be established.
    ECDSA key fingerprint is SHA256:3Xni...
    The authenticity of host '15.236.95.164 (15.236.95.164)' can't be established.
    ECDSA key fingerprint is SHA256:Yxvv...
    Are you sure you want to continue connecting (yes/no/[fingerprint])? yes # <-- answer yes
    
    i-084569174edd3c352 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": false,
        "ping": "pong"
    }
    
    yes # <-- answer yes
    i-013b7597d9f698b39 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": false,
        "ping": "pong"
    }
    

    It’s rather unpleasant because it interrupts the flow of the script.

    Update our site with Ansible

    We will update all our EC2 machines with Ansible

    Our playbook is very simple :

    • We are replacing the home page with a new template
    • We restart the Apache service
    ---
    - name: Configure webservers
      hosts: aws
      become: True
      tasks:
        - name: copy index.html
          template: src=templates/index.html.j2 dest=/var/www/html/index.html mode=0644
        - name: restart httpd
          service: name=httpd state=restarted
    

    The template is very simple :

    <h3>Installed with Ansible</h3>
    <p>Running on {{ hostvars[inventory_hostname].ansible_facts.default_ipv4.address }}</p>
    

    Ansible facts are a lot of data related to your remote systems, including operating systems, IP addresses, attached filesystems, and more.

    We update our ec2 instances with this simple command :

    # update website on ec2 instances with ansible
    $ make ansible-update
    
    PLAY [Configure webservers] ********************************************************************************************************************************************
    
    TASK [Gathering Facts] *************************************************************************************************************************************************
    ok: [i-084569174edd3c352]
    ok: [i-013b7597d9f698b39]
    
    TASK [copy index.html] *************************************************************************************************************************************************
    changed: [i-084569174edd3c352]
    changed: [i-013b7597d9f698b39]
    
    TASK [restart httpd] ***************************************************************************************************************************************************
    changed: [i-084569174edd3c352]
    changed: [i-013b7597d9f698b39]
    
    PLAY RECAP *************************************************************************************************************************************************************
    i-013b7597d9f698b39        : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    i-084569174edd3c352        : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    

    By updating our browser we see that the update has been made :

    ansible-update.png

    The demonstration is over. We can delete our resources with this command :

    # destroy all resources
    $ make destroy
    

    Warning : deleting resources can take a long time and sometimes fail along the way.

    It is important to verify via the AWS website that the resources have indeed disappeared.

    In this case, you have to delete them manually, which is not necessarily very easy.