Linking Terraform and Ansible

This Ansible AWS tutorial shows how to use Ansible and its dynamic inventory to provision the software and configuration of infrastructure that has been created with Terraform.

See Using Terraform to create an AWS VPC with an EC2 Instance and a MariaDB RDS Database for a tutorial to create Cloud infrastructure in AWS using Terraform.

The tutorial assumes that you have already completed the deployment of the infrastructure in AWS from the previous article.

Download de the code from IT Wonder Lab public GitHub repository.

Using AWS Tags with Ansible

In order to link the Terraform infrastructure with Ansible, we will use the AWS tags created with Terraform to identify the elements and apply Ansible playbooks.

The following screenshot from AWS EC2 Console shows the tags applied to an EC2 instance.

ansible-aws-ec2-terraform-tags - ansible-aws-ec2-terraform-tags-ec2.png

Following IT Wonder Lab best practices all the AWS resources created with Terraform had tags added.

IT Wonder Lab recommends adding at least these tags to all resources:

  • Name [name]: the name of the instance or resource. It should be unique and follows the Cloud-Resource-Environment-Visibility-Name/ID format (see EC2 Instances and Resource Security for details)
  • Private Name [private_name] The private name for this element, it is used for DNS registration in private zone and should follow a standard and be unique. It can be used for monitoring and server naming.
  • Public Name [public_name]: The public name for the element, it can be used in DNS registration in public zones and can be the same for many instances, as instances can be behind load balancers. In RDS it is the same as the private name.
  • App [app]: The name of the main application that will be used in the resource.
  • App ID [app_id]: A unique characteristic of the application or a number that can be used to differentiate multiple different instances of the same application, for example imagine you have to releases of the same application in the same environment, App ID could be the release number.
  • OS [os]: The operating system of the instance, useful for applying basic configuration.
  • Environment [environment]: Used for environment identification, it is a 3 letter acronym for the environment:
  • Cost Center [cost_center]:  one or many cost centers that this resource should be assigned to. The cost center is used in billing to classify resources, for example if you provide resources for different customers, some resources are shared and other are costs associated to a specific customer.

Values should all be in lowercase without spaces.


The tutorial assumes that you have already completed the deployment of the AWS infrastructure using Terraform  following the tutorial Using Terraform to create an AWS VPC with an EC2 Instance and a MariaDB RDS Database.

Download de the code from IT Wonder Lab public GitHub repository.

Copy the private key file created when deploying the infrastructure to ${HOME}/keys/ditwl_kp_infradmin.pem

For simplicity we are using a the same private key for everything, but in a real scenario, I recommend having a different key for each combination of:

  • cloud
  • environment
  • user

See Multiple Environments below for an explanation.

Set the correct access permissions for the private key file:

Set the correct access permissions for the inventory file that is part of the downloaded source code:

Review the configuration from the file

The value of AWS_PROFILE is the name used for the profile that has the AWS credentials. It will be stored outside Ansible ~/.aws/credentials and its is the same used in Terraform.

The EC2_REGION is the AWS region where the Cloud services are located. Setting the region speeds up Ansible AWS inventory creation.

The ANSIBLE_INVENTORY is a path to a file containing an Inventory of Hosts or a script. We will be using a script to generate the inventory dynamically by querying the AWS API.

The ANSIBLE_PRIVATE_KEY points to a file that will be used for SSH authentication when connecting to the AWS EC2 Linux hosts.

Before running any ansible commands, source the configuration file to set the appropiate environment variables.

Manually create a MariaDB schema for wordpress and delegate the DNS zone to the Terraform created DNS servers, this two activities have not been automated in the example.

Ansible Dynamic Inventory

Ansible will run and cache the results of the dynamic inventory when a playbook is applied.

We can also run the dynamic inventory script to obtain a JSON representation of all the groups and properties of our AWS Infrastructure.


For each EC2 instance, a node is added to hostvars, the node contains properties that will be available when running a playbook.


In Ansible Dynamic Inventory the EC2 instance “ditwl_ec2_pro_pub_wp01_1” has the following properties:

Those properties are used inside Ansible Playbooks, in the following example the ec2_tag_Name is used to set the instance host name:

The value of ec2_tag_Name will be extracted from the corresponding Inventory host entry.


Instance Tags are added to the hostvars properties as shown in the previous entry and also a group is created.

The Ansible Dynamic Inventory creates groups for each tag that is present in the AWS VPC and adds the name of the hosts inside the group.

This grouping is used to target our ansible playbooks to selected operating systems, applications and releases.

The pattern starts with the name “tag” and adds the name of the tag after that.


The group tag_environment_pro corresponds to all EC2 instances that have a tag with name environment and value pro.

The group tag_os_ubuntu corresponds to all EC2 instances that have a tag with name os and value ubuntu.

The group tag_os_id_ubuntu_16 corresponds to all EC2 instances that have a tag with name os_id and value ubuntu_16.

From our Terraform configuration file, we see that the EC2 instance aws_ec2_pro_pub_wp_01 has those tags.

Ansible converts the underscore “_” in values to “-” in Inventory as can be seen in the OS_ID tag.

If we had EC2 instances in a pre environment, the inventory will show another group called tag_environment_pre with the instances.


Using the AWS Tags as Ansible Targets

Ansible playbooks use the hosts filter to select the target hosts, those are the hosts that will have each rol applied.

Following IT Wonder Lab best practices all the AWS resources created with Terraform include tags that Ansible will use as follows.

For configuration:

  • Name [name]: Will only be used for naming of instances but will never be used for Ansible group selection.
  • Private Name [private_name]:  Will never be used for Ansible group selection but can be used for configuration of a package that needs the private DNS name of the instance.
  • Public Name [public_name]: Will never be used for Ansible group selection but can be used for configuration of a package that needs the public DNS name of the instance.
  • Cost Center [cost_center]:  Not currently used in Ansible.

For Group Selection:

  • App [app]:  To select all hosts that run the same application, for example to select all hosts that will run a WordPress instance.
  • App ID [app_id]: App ID is an specialization of the App selector, it is used to select all hosts that run the same application when there are many “flavours” or “releases” of the application, for example to select all hosts that will run WordPress 4.9.4.
  • OS [os]:  To select all hosts that share the same Operating System.
  • OS ID [os_id]:  OS ID is an specialization of the OS. It is used to select all hosts that share the same Operating System release.
  • Environment [environment]: Used to select all hosts in the same environment.

I like to have a single playbook file for environment so that the infrastructure can be configured with a single command.

The file ditwl_pro.yml defines the hosts selectors and the roles that should be applied to each one. We use :& to AND groups for selectors.

Line 1 starts with a hosts selector that will select hosts that are members of the dynamic inventory group tag_os_ubuntu AND also from the group tag_environment_pro.

Resulting hosts are the ones created by Terraform with the tags:

  • environment=pro
  • os=ubuntu

Ansible will apply the roles:

  • linux/pam_limits
  • linux/hosts_file
  • linux/host_name

Line 10 selects the hosts that have a specific release (or ID) of Ubuntu that are in environment production, in our case the ones tagged by Terraform with:

  • os_id = ubuntu-16
  • environment=pro

Those hosts will have the rol linux/add_packages applied.

This example is used to show how easily is to have a specific set of roles that are only applied to a specific Operating System release or ID.

Line 17 selects the hosts that are tagged as application wp and are in production environment, it apply the role linux/wordpress.

If we have a more complex example where we need to differentiate from multiple hosts that have the same application tag but have some kind of difference we could use the app_id tag to further select.

Multiple Environments

In case we had a pre environment we could create a new playbook file ditwl_pre.yml and use the tag_environment_pre instead of the tag_environment_pro for hosts selectors.

Each environment should have:

A playbook file that uses tag_environment_ENV as selector, example:

Environment variables for sourcing with the correct SSH key file.

Following IT Wonder Lab best practices I like to put it difficult to make errors.

Imagine applying the PRE environment configuration to the PRO environment. It will be a horrible error.

To prevent that from happening I like to have different SSH Key files for each environment and for each user that has to have SSH access either by console or using Ansible.

The file that has the private key for SSH is configured as and environment variable ANSIBLE_PRIVATE_KEY_FILE in the file

Since the is stored in the shared source code repository it should not have specific user information, as it points to the private key file, we should instead agree on the location (i.e. ${HOME}/keys/ditwl_kp_ENV_infradmin.pem) but have our own private key as its content.

Each user therefore needs to create a pem file with the agreed name and place its private key inside.

Ansible Playbook Structure

Ansible Playbook structure is defined in official documentation but the recommended way to group hosts and apply roles is something that each user would have to decide for itself.

IT Wonder Lab best practices are:

  • Avoid using individual host names to select hosts. In cloud all hosts should be treated as cattle, not as pets.
  • Create groups of hosts by:
    • Operating System and mayor release
    • Application and application release
    • Environment
  • Give safe defaults to all roles and use the group_vars to redefine values
  • Use a single playbook definition file for each environment
Folder Description
group_vars\all Contains default values for variables that will be applied to all hosts, independently from its membership to other groups.
group_vars\tag_app_wp Contains values to variables from hosts in group tag_app_wp (AWS tag app=wp).
group_vars\tag_environment_pre Contains values to variables from hosts in group tag_environment_pre (AWS tag environment=pre).
group_vars\tag_environment_pro Contains values to variables from hosts in group tag_environment_pre (AWS tag environment=pro).
inventory Since we are using Ansible Dynamic Inventory, it has the ec2.ini and files.
roles\ Root roles directory
roles\linux\add_packages Role to install packages and its repositories
roles\linux\host_name Role to set the host name of the instance
roles\linux\hosts_file Role to modify local hosts file for resolver
roles\linux\pam_limits Role to set various pam limits for kernel configuration
roles\linux\wordpress Role to install WordPress
ansible.cfg Local ansible configuration Sets environment variables for PRO environment
ditwl_pro.yml Playbook for PRO environment

Ansible Roles Granularity

I recommend building Ansible roles that are highly reusable by configuration, but I also recommend pragmatism as my highest priority.

Roles for systems and applications designed to be standalone, or before the Cloud was around, are called “non native cloud applications”, often those are stateful applications that can not be clustered without sharing the underlying storage and in general are not designed to be recreated or distribute load between servers.

For the “non native cloud applications”  I apply pragmatism and don’t make a big effort in creating reusable roles, instead I don’t mind creating a single role that makes all the necessary changes and configurations for the application even if it has actions available in other roles. I prefer to have a working role than spending a huge amount of time fixing complicated dependencies.

You can find an example of such a role in the installation of WordPress. That role configures nginx, php, WordPress and adds Let’s Encrypt – Free SSL/TLS Certificates by requesting a certificate and automating the necessary renew tasks.

Using a single role instead of three or four roles avoids the complexity that a single nginx role will have, needing to support too many configurations to be reusable for WordPress and also allowing for a high degree of specialization.

An example of a role defined for reusability is the add_package role, it was designed to install default packages defined in the all group_vars and it is also used as a dependency for the Wordpress role.

By defining a dependency with parameters inside the meta folder, we add other roles that should be executed before.

The add_packages role is used at the begging of the playbook installing common software in all machines, and later on by using dependencies to add the specific packages needed for role.

Run the playbook

Click on the play button to see the execution of Ansible playbook.

Configure WordPress

Open the URL and follow the wizard to configure the new WordPress site.

ansible-aws-ec2-terraform-tags - Ansible-wordPress-site.png

Leave a Reply


This site uses Akismet to reduce spam. Learn how your comment data is processed.

Notify of