Configuration and Deployment of Application on AWS using Ansible and Terraform

Filipe Pacheco
5 min readJan 23, 2024

--

Hello Medium readers, after a brief hiatus from the blog, taking advantage of the holiday season to unwind, I’m thrilled to be back, sharing insights about a new application I’ve developed in the DevOps realm. This time, I’ve harnessed the power of Infrastructure as Code, combining the strengths of Terraform and Ansible to seamlessly work in tandem.

Task of the day

In this project, rooted in a real-world scenario, I employed Ansible configuration management automation to set up and deploy the innovative HumanGov SaaS application on AWS EC2 instances designed to serve the entire United States.

This task serves as a continuation of the previous post, where I delved deeper into the concept behind this project. In the earlier article, I utilized Terraform Modules to configure the EC2 instances, installing packages and modifying configurations to prepare for deploying the application in a production environment.

While Terraform Modules are adept at handling minor configurations or modifications in EC2 instances, they are not specifically tailored for this type of activity. Ansible, on the other hand, is a tool better suited for such tasks. If you’re interested in understanding the foundation of Ansible, I discussed it for the first time in this blog post — feel free to check it out for more insights.

Solution Architecture proposed.

The concept of the deployment remains unchanged. It is essential to deploy a segregated application for each state in the US. Leveraging Terraform and Ansible provides a marginal time gain, as the entire process can be automated, eliminating the need to repeat it 50 times. To put it in perspective, each deployment, for each state, can easily take anywhere from 10 to 20 minutes.

The deployment process had to adhere to a chronological order. Initially, I utilized Terraform to deploy the AWS infrastructure. Once that was set up and ready, I could then execute the Ansible Playbook to deploy all the configurations on the EC2 instances.

Services used

The services utilized in this task are the same ones I discussed in a previous publication. In this article, I’ll delve deeper into the utilization of Ansible. As mentioned earlier, Terraform plays a significant role in automating various configurations that, under different circumstances, would require manual intervention. These configurations include:

  • Launching EC2 instances
  • Creating DynamoDB tables
  • Creating S3 Buckets
  • Creating, configuring, and attaching Security Groups to the EC2 instances, allowing connections to the public internet.

It’s worth noting that I’m incorporating the AWS Cloud9 service, an AWS proprietary IDE, which greatly streamlines the developer’s workflow.

Services used in this implementation.

Ansible

In total, I employed six different types of “services” available in Ansible to automate the deployment of this application. Now, let’s delve deeper into each of them.

Configuration

Ansible offers a multitude of configurations that you can adjust to streamline your tasks. I’ve created an ansible.cfg file specifically to address and eliminate some warnings, as outlined below.

[defaults]
deprecation_warnings = False

Role

Roles in Ansible are akin to Modules in Terraform, serving as an organized approach to constructing and managing the deployment of applications. In the code snippet below, you’ll find an example of the organization created by the role, as documented in Ansible. The creation of folders is a manual process, typically done on the computer you are using — in my case, on Cloud9.

roles/
common/ # this hierarchy represents a "role"
tasks/ #
main.yml # <-- tasks file can include smaller files if warranted
handlers/ #
main.yml # <-- handlers file
templates/ # <-- files for use with the template resource
ntp.conf.j2 # <------- templates end in .j2
files/ #
bar.txt # <-- files for use with the copy resource
foo.sh # <-- script files for use with the script resource
vars/ #
main.yml # <-- variables associated with this role
defaults/ #
main.yml # <-- default lower priority variables for this role
meta/ #
main.yml # <-- role dependencies
library/ # roles can also include custom modules
module_utils/ # roles can also include custom module_utils
lookup_plugins/ # or other types of plugins, like lookup in this case

webtier/ # same kind of structure as "common" was above, done for the webtier role
monitoring/ # ""
fooapp/ # ""

Loops

I utilized loops to automate the creation of folders and files to accommodate the packages of the application. Below is an example of how a loop functions in Ansible.

- name: Add several users
ansible.builtin.user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
loop:
- { name: 'testuser1', groups: 'wheel' }
- { name: 'testuser2', groups: 'root' }

Conditionals

Conditionals are frequently employed in the Ansible world. I used conditionals to automate two choices. Firstly, to alter the username for accessing the EC2 instance — if a Debian OS is employed in EC2, the user ID becomes “admin,” whereas with RedHat, it changes to “ec2-user.” Additionally, for installing packages, in Debian OS, the command to install Apache is “apache2,” while in RedHat, it’s “httpd.” Conditionals prove handy in managing such choices. Refer to the example below.

tasks:
- name: Shut down CentOS 6 and Debian 7 systems
ansible.builtin.command: /sbin/shutdown -t now
when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") or
(ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "7")
tasks:
- name: Get the CPU temperature
set_fact:
temperature: "{{ ansible_facts['cpu_temperature'] }}"

- name: Restart the system if the temperature is too high
when: temperature | float > 90
shell: "reboot"

Variables

Well, this is self-explanatory for programmers, right? I used to receive several pieces of information, including:

  • S3 Bucket name
  • DynamoDB table name
  • EC2 instance Public IP DNS
  • State name

Below is an example of the syntax of variables. Of course, variables can be associated with loops to elevate the automation level even further:

- hosts: app_servers
vars:
app_path: "{{ base_path }}/22"

Playbook

Once I’ve employed a Role, the playbook becomes the starting point for every configuration and action required when using Ansible. Below is a syntax example of a playbook.

- hosts: all
roles:
- humangov_webapp

After this step, it’s just a matter of observing and waiting for the magic to unfold. Setting up everything may take some time initially, but once completed, it becomes entirely repeatable and automated. In the image below, you can observe the kind of output generated by Terraform and Ansible once the process is complete.

Terraform output example.
Ansible output example.

Conclusion

In summary, my AWS task involved efficiently deploying the HumanGov SaaS application across multiple U.S. states using Terraform and Ansible. From organizing with roles to automating with loops and conditionals, these tools showcased versatility.

Beyond the automation perspective and focused on long-term deployment maintenance, Terraform and Ansible provided a standardized, repeatable approach for a seamless application deployment experience.

--

--

Filipe Pacheco

Senior Data Scientist | AI, ML & LLM Developer | MLOps | Databricks & AWS Practitioner