Table of Contents
In this section we will use ansible to create new EC2 instances on AWS and further work on these instances. These operations can be completely automated using ansible.
Setup Environment for AWS provisioning
We have already setup our Ansible environment using AWS EC2 instance with one controller and two managed nodes. But now to provision new AWS instance automatically using ansible we must have fulfil few pre-requisites.
Choose the ec2 module to provision AWS EC2 Instances
We know that Ansible works with modules so to work with AWS EC2 Instances we need a separate module. You can get the complete list of cloud modules from Ansible. Here you can see a long list of modules used for different cloud environment. We will use "ec2 module" for this tutorial.
Create Access Key
We must create an access key for our ansible on the controller node to be able to access the AWS account. Click on the username on your AWS Portal and from the drop down menu select "My Security Credentials"
Next click on "Access keys (access key ID and secret access key)" to expand the drop down menu and click on "Create New Access Key"
This should create a new access key. It is IMPORTANT that you either download the key file or save both Access Key ID and Secret Access Key as you will not be able to retrieve this later as the message says on the console.
Install boto3 module
Next we would need boto3
module on the controller node as required by the ec2 module. We had already installed boto3
when working with Ansible Facts, but you can install it using pip3
. If you want to install these only for the current user then append --user
to this command or use sudo as this would require root level privilege.
[ansible@controller ~]$ pip3 install boto boto3 --user
Get an AWS Amazon Machine Images (AMI) ID
We would need an AMI ID which will be used to launch an instance. You can get the AMI ID in the AWS marketplace or you can click on "Launch Instance" which will show you a list of available Images
This will bring you a list of AMI. I intend to use CentOS so I am using Community AMIs to get the AMI ID of CentOS 8.2
We will use this AMI ID in our playbook later. If you created any custom AMI then you can use the AMI ID of the respective Image available under Images → AMI.Â
Install awscli
We will use awscli
to store our login credentials instead of the playbook for better security which requires awscli
tool. We can install awscli
using pip3
again:
[ansible@controller ~]$ pip3 install awscli --user
...
Installing collected packages: colorama, botocore, docutils, rsa, awscli
Successfully installed awscli-1.18.147 botocore-1.18.6 colorama-0.4.3 docutils-0.15.2 rsa-4.5
Now we can store our access key using awscli
. Execute aws configure
from the console as ansible user:
[ansible@controller ~]$ aws configure AWS Access Key ID [None]: AKIAIKIJ6FV4C3QGWMGQ AWS Secret Access Key [None]: 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 Default region name [None]: us-east-2 Default output format [None]:
I am provided region name as us-east-2a
where my other instances are launched. You can collect the zone information from the dashboard of EC2 → Instances and mapping region information from Regions, Availability Zones, and Local Zones
Now you can access your configuration which will be stored in the home folder of ansible user under ~/.aws
[ansible@controller ~]$ cat .aws/config [default] region = us-east-2 [ansible@controller ~]$ cat .aws/credentials [default] aws_access_key_id = AKIAIKIJ6FV4C3QGWNGQ aws_secret_access_key = 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
Create ansible playbook
We are all done with the pre-requisites. Now let us create our ansible playbook to launch AWS EC2 instance using Ansible. This is our sample playbook launch_ec2.yml
which contains multiple information about the instance. We will learn more about these in the next chapter, you can exclude those and leave everything to default if you have your custom AMI ID.
--- - name: Working with AWS EC2 Instance hosts: localhost connection: local gather_facts: false tasks: - name: Create ec2 instance ec2: instance_type: t2.micro image: ami-000e7ce4dd68e7a11 count: 1 key_name: ssh-1 group: allow-all region: us-east-2
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook launch_ec2.yml PLAY [Working with AWS EC2 Instamce] ********************************************************************************* TASK [Create ec2 instance] ******************************************************************************************* changed: [localhost] PLAY RECAP *********************************************************************************************************** localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can connect to your AWS EC2 Dashboard console and verify if a new instance has been launched:
Once the machine is in running state we can try to connect to the server using our existing key pair which we have specified in our playbook. I have a Linux client using which I will connect to my new instance along with the private key:
[root@client ~]# ssh -i /tmp/ssh-1.pem centos@ec2-3-131-169-183.us-east-2.compute.amazonaws.com
The authenticity of host 'ec2-3-131-169-183.us-east-2.compute.amazonaws.com (3.131.169.183)' can't be established.
ECDSA key fingerprint is SHA256:30lgYagKT3VxHTNrm4N8N3jb8pux+A2MOE9FPFDz5TU.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'ec2-3-131-169-183.us-east-2.compute.amazonaws.com,3.131.169.183' (ECDSA) to the list of known hosts.
Activate the web console with: systemctl enable --now cockpit.socket
[centos@ip-172-31-40-15 ~]$ sudo su -
[root@ip-172-31-40-15 ~]#
Define tags and security groups when launching an EC2 instance
In the previous example I had defined groups which is used to assign security group to the instance. We also have a bunch of other options which we can choose to assign when creating an instance. You can get the complete list from the ec2 module page from docs.ansible.com
In this sample playbook we will define some custom tags and security group to the instance. To get the security group information you can connect to your AWS EC2 Dashboard and click on Security Groups under Network & Security from the LEFT TAB
You can get the security group name here and place it in the playbook.
To assign a tag you must provide the Key and Value of the tag which doesn't require any changes on the AWS Console so let's start creating our playbook launch_ec2_2.yml
:
--- - name: Working with AWS EC2 Instance hosts: localhost connection: local gather_facts: false tasks: - name: Create ec2 instance ec2: instance_type: t2.micro image: ami-000e7ce4dd68e7a11 count: 1 key_name: ssh-1 group: allow-all wait: yes region: us-east-2 instance_tags: Name: server4 Env: db
We have used some new definitions in our playbook. The tag can be assigned using instance_tags
where Name
and Env
are our Key while server4
and db
are the mapped value respectively. We have also added wait: yes
which means that the playbook will wait for the instance to be created before exiting unlike our first example.
Let us execute our playbook now:
[ansible@controller ~]$ ansible-playbook launch_ec2_2.yml PLAY [Working with AWS EC2 Instance] ********************************************************************************* TASK [Create ec2 instance] ******************************************************************************************* changed: [localhost] PLAY RECAP *********************************************************************************************************** localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now you can verify on your EC2 dashboard, a new instance should be getting initialized.
Once the instance is in running state you can verify the security group and tags assigned to the instance
Start, Stop and Terminate your EC2 Instance with Ansible
In this section we will use one of the instance we created in the previous example to perform start, stop and terminate operation. Similar to handling of services where we change the state of the service to started, restarted, stopped, we also can perform similar operation to the EC2 instances:
For example to start the instance we can use state: running
. Following is my playbook start_instance.yml
to start the instance:
--- - name: Working with AWS EC2 Instance hosts: localhost connection: local gather_facts: false tasks: - name: Start ec2 instance ec2: instance_ids: i-09f7e5ca89f23cd03 region: us-east-2 state: running
You can get the instance id from your AWS EC2 Dashboard. Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook start_instance.yml PLAY [Working with AWS EC2 Instance] ********************************************************************************* TASK [Start ec2 instance] ******************************************************************************************** ok: [localhost] PLAY RECAP *********************************************************************************************************** localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The playbook was successfully executed but the changed=0 which means no action was performed. This is because the instance is already in running state so the playbook didn't make any change.
Let us create another playbook stop_instance.yml
to stop the instance with state: stopped
:
--- - name: Working with AWS EC2 Instance hosts: localhost connection: local gather_facts: false tasks: - name: Stop ec2 instance ec2: instance_ids: i-09f7e5ca89f23cd03 region: us-east-2 state: stopped
We have just changed the name and state of the play to perform stop operation: Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook stop_instance.yml PLAY [Working with AWS EC2 Instance] ********************************************************************************* TASK [Stop ec2 instance] ******************************************************************************************** changed: [localhost] PLAY RECAP *********************************************************************************************************** localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now we see the status contains changed=1
which means the play has successfully stopped the instance, you can manually verify this by checking on the AWS EC2 Console.
Similarly to terminate an instance we will use state:absent
, following is the playbook terminate_instance.yml
to terminate our instance:
--- - name: Working with AWS EC2 Instance hosts: localhost connection: local gather_facts: false tasks: - name: Terminate ec2 instance ec2: instance_ids: i-09f7e5ca89f23cd03 region: us-east-2 state: absent
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook terminate_instance.yml PLAY [Working with AWS EC2 Instance] ********************************************************************************* TASK [Terminate ec2 instance] **************************************************************************************** changed: [localhost] PLAY RECAP *********************************************************************************************************** localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We have changed=1
which means the action was successfully executed. Let us verify the AWS EC2 dashboard:
Access EC2 instances using tags
In the previous example we performed start, stop and terminate operation on EC2 instances using instance-id
. Now the problem here is we have to always check and store the instance id for any new instance which is created. So to automate this process we can use tags. We will assign common tags to all our instance and then use the tag's Key:value
pair to get the instance_id
of all the instances with mapping tag.
I have assigned tag Name as Env
and value as db
to all my instances. If you re-call we used gather_facts
to collect system information when working with managed nodes, similarly we have ec_instance_info
(in earlier ansible version this is referred as ec2_instance_facts
) which will give us similar information about all the instances part of your ID.
In this sample playbook ec2_instance_2.yml
I am using loop module to loop through all the available instances with the matching tag and value and then store the instance_id
in a variable which we will use later to start, stop and terminate the instance.
--- - name: Access instance with TAGS hosts: localhost connection: local gather_facts: false tasks: - name: Locate instance id with tags ec2_instance_info: region: us-east-2 filters: "tag:Env": db register: ec2_info - name: Displaying output debug: msg: "{{ item.instance_id }}" loop: "{{ ec2_info.instances }}"
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook ec2_instance_2.yml PLAY [Access instance with TAGS] ************************************************************************************* TASK [Locate instance id with tags] ********************************************************************************** ok: [localhost] TASK [Displaying output] ********************************************************************************************* ok: [localhost] => (item={'ami_launch_index': 0, 'image_id': 'ami-000e7ce4dd68e7a11', 'instance_id': 'i-0c87855a86a2d96cf', 'instance_type': 't2.micro', 'key_name': 'ssh-1', 'launch_time': '2020-09-26T14:08:22+00:00', 'monitoring': {'state': 'disabled'}, 'placement': {'availability_zone': 'us-east-2c', 'group_name': '', 'tenancy': 'default'}, 'private_dns_name': 'ip-172-31-40-15.us-east-2.compute.internal', 'private_ip_address': '172.31.40.15', 'product_codes': [], 'public_dns_name': 'ec2-18-218-228-48.us-east-2.compute.amazonaws.com', 'public_ip_address': '18.218.228.48', 'state': {'code': 16, 'name': 'running'}, 'state_transition_reason': '', 'subnet_id': 'subnet-3b9bec77', 'vpc_id': 'vpc-232f8148', 'architecture': 'x86_64', 'block_device_mappings': [{'device_name': '/dev/sda1', 'ebs': {'attach_time': '2020-09-26T09:54:19+00:00', 'delete_on_termination': True, 'status': 'attached', 'volume_id': 'vol-045e152e0b325e061'}}, {'device_name': '/dev/sdb', 'ebs': {'attach_time': '2020-09-26T09:54:19+00:00', 'delete_on_termination': False, 'status': 'attached', 'volume_id': 'vol-0cc64005e32c93cd6'}}], 'client_token': '', 'ebs_optimized': False, 'ena_support': True, 'hypervisor': 'xen', 'network_interfaces': [{'association': {'ip_owner_id': 'amazon', 'public_dns_name': 'ec2-18-218-228-48.us-east-2.compute.amazonaws.com', 'public_ip': '18.218.228.48'}, 'attachment': {'attach_time': '2020-09-26T09:54:19+00:00', 'attachment_id': 'eni-attach-020b2922a37d25990', 'delete_on_termination': True, 'device_index': 0, 'status': 'attached'}, 'description': '', 'groups': [{'group_name': 'allow-all', 'group_id': 'sg-0cd0de2fe8ed6dd63'}], 'ipv6_addresses': [], 'mac_address': '0a:e4:b1:27:96:ea', 'network_interface_id': 'eni-0edd456abff86eb54', 'owner_id': '311590943723', 'private_dns_name': 'ip-172-31-40-15.us-east-2.compute.internal', 'private_ip_address': '172.31.40.15', 'private_ip_addresses': [{'association': {'ip_owner_id': 'amazon', 'public_dns_name': 'ec2-18-218-228-48.us-east-2.compute.amazonaws.com', 'public_ip': '18.218.228.48'}, 'primary': True, 'private_dns_name': 'ip-172-31-40-15.us-east-2.compute.internal', 'private_ip_address': '172.31.40.15'}], 'source_dest_check': True, 'status': 'in-use', 'subnet_id': 'subnet-3b9bec77', 'vpc_id': 'vpc-232f8148', 'interface_type': 'interface'}], 'root_device_name': '/dev/sda1', 'root_device_type': 'ebs', 'security_groups': [{'group_name': 'allow-all', 'group_id': 'sg-0cd0de2fe8ed6dd63'}], 'source_dest_check': True, 'tags': {'Env': 'db'}, 'virtualization_type': 'hvm', 'cpu_options': {'core_count': 1, 'threads_per_core': 1}, 'capacity_reservation_specification': {'capacity_reservation_preference': 'open'}, 'hibernation_options': {'configured': False}, 'metadata_options': {'state': 'applied', 'http_tokens': 'optional', 'http_put_response_hop_limit': 1, 'http_endpoint': 'enabled'}}) => { "msg": "i-0c87855a86a2d96cf" } ok: [localhost] => (item={'ami_launch_index': 1, 'image_id': 'ami-000e7ce4dd68e7a11', 'instance_id': 'i-02352fdfd57a3372e', 'instance_type': 't2.micro', 'key_name': 'ssh-1', 'launch_time': '2020-09-26T14:08:38+00:00', 'monitoring': {'state': 'disabled'}, 'placement': {'availability_zone': 'us-east-2a', 'group_name': '', 'tenancy': 'default'}, 'private_dns_name': 'ip-172-31-4-189.us-east-2.compute.internal', 'private_ip_address': '172.31.4.189', 'product_codes': [], 'public_dns_name': 'ec2-3-22-250-122.us-east-2.compute.amazonaws.com', 'public_ip_address': '3.22.250.122', 'state': {'code': 16, 'name': 'running'}, 'state_transition_reason': '', 'subnet_id': 'subnet-4690412d', 'vpc_id': 'vpc-232f8148', 'architecture': 'x86_64', 'block_device_mappings': [{'device_name': '/dev/sda1', 'ebs': {'attach_time': '2020-09-20T14:42:31+00:00', 'delete_on_termination': True, 'status': 'attached', 'volume_id': 'vol-0d9fb968fa03e3183'}}], 'client_token': '', 'ebs_optimized': False, 'ena_support': True, 'hypervisor': 'xen', 'network_interfaces': [{'association': {'ip_owner_id': 'amazon', 'public_dns_name': 'ec2-3-22-250-122.us-east-2.compute.amazonaws.com', 'public_ip': '3.22.250.122'}, 'attachment': {'attach_time': '2020-09-20T14:42:30+00:00', 'attachment_id': 'eni-attach-02d217c9750d194ca', 'delete_on_termination': True, 'device_index': 0, 'status': 'attached'}, 'description': '', 'groups': [{'group_name': 'allow-all', 'group_id': 'sg-0cd0de2fe8ed6dd63'}], 'ipv6_addresses': [], 'mac_address': '02:87:5e:1d:6a:00', 'network_interface_id': 'eni-090e668edc1f12699', 'owner_id': '311590943723', 'private_dns_name': 'ip-172-31-4-189.us-east-2.compute.internal', 'private_ip_address': '172.31.4.189', 'private_ip_addresses': [{'association': {'ip_owner_id': 'amazon', 'public_dns_name': 'ec2-3-22-250-122.us-east-2.compute.amazonaws.com', 'public_ip': '3.22.250.122'}, 'primary': True, 'private_dns_name': 'ip-172-31-4-189.us-east-2.compute.internal', 'private_ip_address': '172.31.4.189'}], 'source_dest_check': True, 'status': 'in-use', 'subnet_id': 'subnet-4690412d', 'vpc_id': 'vpc-232f8148', 'interface_type': 'interface'}], 'root_device_name': '/dev/sda1', 'root_device_type': 'ebs', 'security_groups': [{'group_name': 'allow-all', 'group_id': 'sg-0cd0de2fe8ed6dd63'}], 'source_dest_check': True, 'tags': {'Name': 'server1', 'Env': 'db'}, 'virtualization_type': 'hvm', 'cpu_options': {'core_count': 1, 'threads_per_core': 1}, 'capacity_reservation_specification': {'capacity_reservation_preference': 'open'}, 'hibernation_options': {'configured': False}, 'metadata_options': {'state': 'applied', 'http_tokens': 'optional', 'http_put_response_hop_limit': 1, 'http_endpoint': 'enabled'}}) => { "msg": "i-02352fdfd57a3372e" } PLAY RECAP *********************************************************************************************************** localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The output contains a bunch of information related to each instance but our variable item.instance_id
is able to get the instance_id
of individual instance with mapping TAG value.
Now we can use this variable to perform different operations on the instance but the output has too much information which is not required. So we will add a loop_conrol
to limit the information which is only required i.e. instance_id
Let me update my playbook with loop_control:
--- - name: Access instance with TAGS hosts: localhost connection: local gather_facts: false tasks: - name: Locate instance id with tags ec2_instance_info: region: us-east-2 filters: "tag:Env": db register: ec2_info - name: Displaying output debug: msg: "{{ item.instance_id }}" loop: "{{ ec2_info.instances }}" loop_control: label: "{{ item.instance_id }}"
Now if I execute the playbook I get only the instance_id
:
[ansible@controller ~]$ ansible-playbook ec2_instance_2.yml PLAY [Access instance with TAGS] ************************************************************************************* TASK [Locate instance id with tags] ********************************************************************************** ok: [localhost] TASK [Displaying output] ********************************************************************************************* ok: [localhost] => (item=i-0c87855a86a2d96cf) => { "msg": "i-0c87855a86a2d96cf" } ok: [localhost] => (item=i-02352fdfd57a3372e) => { "msg": "i-02352fdfd57a3372e" } PLAY RECAP *********************************************************************************************************** localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Let us further modify our playbook to start, stop and terminate instance in a single playbook. It is IMPORTANT that in such case we use wait: yes
so that the playbook will wait for individual operation to finish or else the play will fail.
Now I will further update my playbook to start, stop and terminate instance using the instance_id
variable. Additionally I have added tags to each task section so we can use the tags to control the individual task operation. If you are not familiar with the tags concept then I would suggest you should go through my tutorial on "Ansible Tags" to understand the difference between different tag name.
HINT:
Following is my updated playbook ec2_instance_2.yml
:
--- - name: Access instance with TAGS hosts: localhost connection: local gather_facts: false tasks: - name: Locate instance id with tags ec2_instance_info: region: us-east-2 filters: "tag:Env": db register: ec2_info tags: - always - name: Start ec2 instance ec2: instance_ids: "{{ item.instance_id }}" region: us-east-2 state: running wait: yes loop: "{{ ec2_info.instances }}" loop_control: label: "{{ item.instance_id }}" tags: - start - never - name: Stop ec2 instance ec2: instance_ids: "{{ item.instance_id }}" region: us-east-2 state: stopped wait: yes loop: "{{ ec2_info.instances }}" loop_control: label: "{{ item.instance_id }}" tags: - stop - never - name: Terminate ec2 instance ec2: instance_ids: "{{ item.instance_id }}" region: us-east-2 state: absent wait: yes loop: "{{ ec2_info.instances }}" loop_control: label: "{{ item.instance_id }}" tags: - terminate - never
Now let us try to perform stop operation using --tags stop
as my instances are already in running state:
[ansible@controller ~]$ ansible-playbook ec2_instance_2.yml --tags stop PLAY [Access instance with TAGS] ************************************************************************************* TASK [Locate instance id with tags] ********************************************************************************** ok: [localhost] TASK [Stop ec2 instance] ********************************************************************************************* changed: [localhost] => (item=i-0c87855a86a2d96cf) changed: [localhost] => (item=i-02352fdfd57a3372e) PLAY RECAP *********************************************************************************************************** localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We have changed=1
so this means that the playbook has successfully executed the play. Let us verify the instance status which shows as stopped:
Next let us try to start the instance using --tags start
:
[ansible@controller ~]$ ansible-playbook ec2_instance_2.yml --tags start PLAY [Access instance with TAGS] ************************************************************************************* TASK [Locate instance id with tags] ********************************************************************************** ok: [localhost] TASK [Start ec2 instance] ******************************************************************************************** changed: [localhost] => (item=i-0c87855a86a2d96cf) changed: [localhost] => (item=i-02352fdfd57a3372e) PLAY RECAP *********************************************************************************************************** localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The playbook has successfully executed, let us verify the status of the instances on the AWS EC2 Dashboard:
Now similarly we can perform terminate operation but I would like to avoid that as again I have to create another instance so let me save some work for myself :). But you can try the playbook and let me know if you face any issues.
What’s Next
This is the last chapter of our Ansible Tutorial. Now you can start practicing on your own by writing playbooks to automate tasks in your test environment.
I think your howto has gotten out of date.
Have you tried some troubleshooting? I don’t have the environment anymore so can’t check this.