Various Ways To Launch Amazon EC2 Instance Using Ansible
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:
- Directly using all parameters needed
- Create Launch Template
- 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:
- AWS CLI and setup at least one credential;
- Ansible;
- Ansible collection for AWS by running
ansible-galaxy collection install community.aws
.
For the inventory, we'll have two versions or groups.
- Localhost Used as target host to create EC2 instances.
- 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:
Playbook: ec2.yml
- name: ec2
hosts: localhost
connection: local
gather_facts: no
tasks:
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
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
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
(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
$ 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"
]
}
]
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
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
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
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
$ 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"
]
}
]
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
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
$ aws ec2 describe-images --filters "Name=name,Values=amazonlinux2_httpd_ami" --query 'Images[].{Name:Name, ID:ImageId}'
[
{
"Name": "amazonlinux2_httpd_ami",
"ID": "ami-0c1cfb0a18f5e4451"
}
]
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
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
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
$ 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"
]
}
]
$ curl http://108.136.150.180
Hello World!
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:
$ 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 >>
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
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