Nuxtstop

For all things nuxt.js

Various Ways To Launch Amazon EC2 Instance Using Ansible

Various Ways To Launch Amazon EC2 Instance Using Ansible
7 0

Amazon EC2 is one of the most famous services of AWS. It's like a first step to move or start using cloud infrastructure. From installing server directly on physical server, using virtualization hosted on data center, and then hosted on cloud. As I said before, this is first step not final step. Since technology always grows, we still have containers, serverless, and more. In this section, we won't discuss about them now but let's focus on EC2 or Elastic Compute Cloud.

More about Amazon EC2, click here!

Note: If you heed to my previous post about VPC, then this is the next part to host any web server using EC2 instances. So, I'm going to launch three EC2 instances and each instance will be placed in different AZ. Here I'm gonna show you of various ways we can do to launch EC2 instances, 3 in total. Those are:

  1. Directly using all parameters needed
  2. Create Launch Template
  3. Build custom AMI from existing EC2 instance

Then, again! I won't go through the console but I'll use ansible instead. Why? If you've ever seen my previous posts, you should know why :)

Prerequisites:

  1. AWS CLI and setup at least one credential;
  2. Ansible;
  3. Ansible collection for AWS by running ansible-galaxy collection install community.aws.

For the inventory, we'll have two versions or groups.

  1. Localhost Used as target host to create EC2 instances.
  2. EC2 Instances Used as target hosts if they already created (running) to do some configuration on the servers.

Inventory: hosts.yml (first version)

---

localhost:
  hosts:
    127.0.0.1:
Enter fullscreen mode Exit fullscreen mode

Playbook: ec2.yml

- name: ec2
  hosts: localhost
  connection: local
  gather_facts: no
  tasks:
Enter fullscreen mode Exit fullscreen mode

Optional: Import Key Pair
Before we launch any EC2 instances, we can create a keypair by creating a new one generated by AWS or by importing your SSH public key. Here I'll choose to import key pair with the default name of each OS. In this case, I'll use ec2-user since I'm going to use Amazon Linux 2. Then, when the instances are running. I can directly connect using it without enclose the key file as usual.

    - name: import keypair
      amazon.aws.ec2_key:
        name: ec2-user
        key_material: "{{ lookup('file', '/home/nurulramadhona/.ssh/id_rsa.pub') }}"
      tags:
        - ec2_create
        - ec2_keypair
Enter fullscreen mode Exit fullscreen mode

1. Launch EC2 Instance + User Data

    - name: launch new instance + user data
      amazon.aws.ec2_instance:
        name: amazonlinux2a
        region: ap-southeast-3
        key_name: ec2-user
        instance_type: t3.micro
        security_group: ssh-web
        vpc_subnet_id: subnet-0276d466994fa3087
        network:
          assign_public_ip: true
          delete_on_termination: true
        image_id: ami-0de34ee5744189c60 
        user_data: "{{ lookup('file', 'user_data.sh') }}"
        volumes: 
          - device_name: /dev/xvda
            ebs: 
              volume_size: 8
              volume_type: gp2
              delete_on_termination: true
      tags:
        - ec2_create
Enter fullscreen mode Exit fullscreen mode

I guess you already familiar with any parameter above but one thing I want to tell you is vpc_subnet_id. This parameter has any implicit informations. By define subnet id, we already choose which VPC we'll use and which AZ we placed the EC2 instance so we don't need define those two things anymore.

User data: user_data.sh

#!/bin/bash
yum update -y
yum install -y httpd
systemctl enable httpd
systemctl start httpd
Enter fullscreen mode Exit fullscreen mode

(Just a simple bash script to install web server)

Let's run our first playbook since I've added ec2_create on key pair and launch tasks!

$ ansible-playbook -i host.yml ec2.yml -t ec2_create

PLAY [ec2] **************************************************************************************************************************************************************

TASK [import keypair] ***************************************************************************************************************************************************
changed: [127.0.0.1]

TASK [launch new instance + user data] **********************************************************************************************************************************
changed: [127.0.0.1]

PLAY RECAP **************************************************************************************************************************************************************
127.0.0.1                  : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
Enter fullscreen mode Exit fullscreen mode
$ aws ec2 describe-instances --query 'Reservations[].Instances[].{ID:InstanceId, PrivateIP:PrivateIpAddress, PublicIP:PublicIpAddress, Name:Tags[?Key==`Name`].Value}'
[
    {
        "ID": "i-0187e4bb5d2f2007c",
        "PrivateIP": "10.0.1.7",
        "PublicIP": "108.136.226.235",
        "Name": [
            "amazonlinux2a"
        ]
    }
]
Enter fullscreen mode Exit fullscreen mode

2. Launch EC2 Instance From Template (Include User Data)
Create template:

    - name: create launch template
      community.aws.ec2_launch_template:
        name: amazonlinux2_httpd_template
        image_id: ami-0de34ee5744189c60
        key_name: ec2-user
        instance_type: t3.micro
        region: ap-southeast-3
        network_interfaces:
          - associate_public_ip_address: true
            delete_on_termination: true
            device_index: 0
        block_device_mappings:
          - device_name: /dev/xvda
            ebs:
              delete_on_termination: true
              volume_size: 8
              volume_type: gp2
        user_data: "{{ lookup('file', 'user_data.txt') }}"
      tags:
        - ec2_template
Enter fullscreen mode Exit fullscreen mode

There are two things we can't do when using template. Those are security group can't be defined together with network_interfaces (so I'll keep network_interfaces and not define security_groups) and device_index should be defined along with network_interfaces.

The important thing when you use user_data, the file should be base64 encoded. So, I encode the user_data.sh (file I use when launch first instance above).

$ base64 user_data.sh > user_data.txt
Enter fullscreen mode Exit fullscreen mode

Then, since we already defined user_data on the template. We shouldn't repeat to use user_data when launch the instances using this template or it'll be replaced.

To launch instance using template, use launch_template parameter.

    - name: launch new instance from template
      amazon.aws.ec2_instance:
        name: amazonlinux2b
        launch_template: 
          name: amazonlinux2_httpd_template
        security_group: ssh-web
        vpc_subnet_id: subnet-07bb6501337e4f87b
      tags:
        - ec2_template
Enter fullscreen mode Exit fullscreen mode

As we can see, we remove some parameters that already defined on the template.

Let's run our second playbook to launch instance using template!

$ ansible-playbook -i host.yml ec2.yml -t ec2_template

PLAY [ec2] **************************************************************************************************************************************************************

TASK [create launch template] *******************************************************************************************************************************************
changed: [127.0.0.1]

TASK [launch new instance from template] ********************************************************************************************************************************
changed: [127.0.0.1]

PLAY RECAP **************************************************************************************************************************************************************
127.0.0.1                  : ok=2    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
Enter fullscreen mode Exit fullscreen mode
$ aws ec2 describe-instances --query 'Reservations[].Instances[].{ID:InstanceId, PrivateIP:PrivateIpAddress, PublicIP:PublicIpAddress, Name:Tags[?Key==`Name`].Value}'
[
    {
        "ID": "i-0187e4bb5d2f2007c",
        "PrivateIP": "10.0.1.7",
        "PublicIP": "108.136.226.235",
        "Name": [
            "amazonlinux2a"
        ]
    },
    {
        "ID": "i-09c46dba004ed7bd8",
        "PrivateIP": "10.0.2.8",
        "PublicIP": "108.136.235.232",
        "Name": [
            "amazonlinux2b"
        ]
    }
]
Enter fullscreen mode Exit fullscreen mode

3. Launch EC2 Instance Using Custom AMI
To launch instance using custom AMI, I separated into two tasks and won't run them together. Why? Because we need image-id of the custom AMI to launch instance. To get it, the AMI should be created first and here we'll create it from an instance (first one).

First task:

    - name: create custom ami from an instance
      amazon.aws.ec2_ami:
        instance_id: i-0187e4bb5d2f2007c
        wait: no
        name: amazonlinux2_httpd_ami
      tags: 
        - ec2_ami1
Enter fullscreen mode Exit fullscreen mode

Let's run the third playbook to create AMI!

$ ansible-playbook -i host.yml ec2.yml -t ec2_ami1

PLAY [ec2] **************************************************************************************************************************************************************

TASK [create custom ami from an instance] *******************************************************************************************************************************
changed: [127.0.0.1]

PLAY RECAP **************************************************************************************************************************************************************
127.0.0.1                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
Enter fullscreen mode Exit fullscreen mode
$ aws ec2 describe-images --filters "Name=name,Values=amazonlinux2_httpd_ami" --query 'Images[].{Name:Name, ID:ImageId}'
[
    {
        "Name": "amazonlinux2_httpd_ami",
        "ID": "ami-0c1cfb0a18f5e4451"
    }
]
Enter fullscreen mode Exit fullscreen mode

Second task:

    - name: launch new instance using custom ami
      amazon.aws.ec2_instance:
        name: amazonlinux2c
        region: ap-southeast-3
        key_name: ec2-user
        instance_type: t3.micro
        security_group: ssh-web
        vpc_subnet_id: subnet-00b4e72d63a2125de
        network:
          assign_public_ip: true
          delete_on_termination: true
        image_id: ami-0c1cfb0a18f5e4451 
        volumes: 
          - device_name: /dev/xvda
            ebs: 
              volume_size: 8
              volume_type: gp2
              delete_on_termination: true
        user_data: "{{ lookup('file', 'user_data2.sh') }}"
      tags:
        - ec2_ami2
Enter fullscreen mode Exit fullscreen mode

As we can see, the parameter seems like when we create first EC2 instance. The different is only on the image we use. We can't treat it same as when we launch it from template. It's totally different and will give you higher speed. Here we also can define user_data without replace anything.

User data: user_data2.sh

#!/bin/bash
echo 'Hello World!' >> /var/www/html/index.html
Enter fullscreen mode Exit fullscreen mode

We created an AMI from (first) instance which already have httpd installed. So, I'll create new user data only to modify the homepage.

Let's run the third playbook to launch instance using the created AMI!

$ ansible-playbook -i host.yml ec2.yml -t ec2_ami2

PLAY [ec2] **************************************************************************************************************************************************************

TASK [launch new instance using custom ami] *****************************************************************************************************************************
changed: [127.0.0.1]

PLAY RECAP **************************************************************************************************************************************************************
127.0.0.1                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
Enter fullscreen mode Exit fullscreen mode
$ aws ec2 describe-instances --query 'Reservations[].Instances[].{ID:InstanceId, PrivateIP:PrivateIpAddress, PublicIP:PublicIpAddress, Name:Tags[?Key==`Name`].Value}'
[
    {
        "ID": "i-0187e4bb5d2f2007c",
        "PrivateIP": "10.0.1.7",
        "PublicIP": "108.136.226.235",
        "Name": [
            "amazonlinux2a"
        ]
    },
    {
        "ID": "i-09c46dba004ed7bd8",
        "PrivateIP": "10.0.2.8",
        "PublicIP": "108.136.235.232",
        "Name": [
            "amazonlinux2b"
        ]
    },
    {
        "ID": "i-02c7573fff1215e65",
        "PrivateIP": "10.0.3.11",
        "PublicIP": "108.136.150.180",
        "Name": [
            "amazonlinux2c"
        ]
    }
]
Enter fullscreen mode Exit fullscreen mode
$ curl http://108.136.150.180
Hello World!
Enter fullscreen mode Exit fullscreen mode

Alright, now we have 3 EC2 instances in total but we only modified the homepage of the last instance. Then, I want to modify the homepage of the other two instances. I'll use ansible ad-hoc in this case to straightly run the command on them. Before that, let's add the IP of all EC2 instances as the target hosts on inventory!

Inventory: hosts.yml (second version)

---

localhost:
  hosts:
    127.0.0.1:

ec2:
  hosts:
    108.136.226.235:
    108.136.235.232:
    108.136.150.180:
Enter fullscreen mode Exit fullscreen mode
$ ansible -i host.yml ec2 --become -u ec2-user -m shell -a 'echo "Hello World!" >> /var/www/html/index.html' -l "108.136.226.235, 108.136.235.232"
The authenticity of host '108.136.226.235 (108.136.226.235)' can't be established.
ECDSA key fingerprint is SHA256:EdObxEIn7UGhb8AmZOI1c0OEU9KUa9mNd4G2siLPKaA.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
108.136.235.232 | CHANGED | rc=0 >>

108.136.226.235 | CHANGED | rc=0 >>

Enter fullscreen mode Exit fullscreen mode

Now, we have all web servers with homepage modified.

$ ansible -i host.yml ec2 --become -u ec2-user -m shell -a 'curl http://localhost'
108.136.235.232 | CHANGED | rc=0 >>
Hello World!  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13  100    13    0     0  18361      0 --:--:-- --:--:-- --:--:-- 13000
108.136.226.235 | CHANGED | rc=0 >>
Hello World!  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13  100    13    0     0  17473      0 --:--:-- --:--:-- --:--:-- 13000
108.136.150.180 | CHANGED | rc=0 >>
Hello World!  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    13  100    13    0     0  18465      0 --:--:-- --:--:-- --:--:-- 13000
Enter fullscreen mode Exit fullscreen mode

That's it for the EC2! On the next part, I'll discuss about any "essentials" configuration you need to do before you use server (Amazon Linux 2) for any purposes. Let's move to the next post!

References:
https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_instance_module.html
https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_key_module.html
https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_ami_module.html
https://docs.ansible.com/ansible/latest/collections/community/aws/ec2_launch_template_module.html