End To End Deployment with Terraform and Ansible

Tzvika Tubis
HiredScore Engineering
5 min readAug 7, 2022

A very common DevOps task is deploying and configuring an EC2 instance. This might be a frustrating serial process, involving at least two different tools one after another.

To address this challenge, in our organization, we deploy the instances using Terraform and configure them using Ansible. The Ansible is triggered manually each time we deploy an instance.

One day as I applied this process, the penny dropped that there must be a solution to unify these two allegedly separate tasks.

Motivation

As HiredScore’s R&D grew, we faced scaling challenges so we decided to shift left our infrastructure approach towards the developers, thus enhancing the speed of the development cycles.

The first step was to split our Terraform state into separate states for each service in order to make it easier for the teams to manage their services infrastructure (you can read more about it here).

Later on, we faced other challenges deploying an application on EC2 instances without DevOps intervention. We created the solution below to harness the superpowers of both Terraform and Ansible to deploy instances in one simple click.

Overview

Terraform and Ansible are two widely adopted tools used to manage and maintain your infrastructure. Terraform is an infrastructure as a code tool that is used to manage cloud resources and Ansible is a tool for configuring the operating system, both tools are state-aware.

Changes and configurations on an EC2 instance might be applied from different platforms such as your cloud provider’s console. To validate the infrastructure components we use Terraform. It gives you the ability to make changes to all the resources in a documented and audited single source of truth.

In addition, security patches, mount points, and packages need to be managed too. Ansible gives you the ability to maintain a consistent and standardized configuration across all of your instances.

By combining these tools you can assure that all the instances are configured well by your organization's best practices but at the end of the day these are two separate tools and we would like to bind these two abilities in one place.

Assumptions & Considerations

  • All instances are deployed via Terraform
  • The organization uses Ansible as the main configuration management tool
  • Basic knowledge using Ansible and Terraform
  • Using AWS as a cloud provider

Architecture

  1. Terraform creates EC2 instances according to the TF file and the given SSH key.
  2. Terraform triggers Ansible with the given SSH key and runs the configuration playbook.

How does it actually work?

Basic configuration via Ansible Playbook

First, write a simple Ansible playbook to apply a basic configuration to your instances. In the playbook, we will write a simple task that configures the hostname and installs some default packages.

my-playbook.yml:

- hosts: '{{ target }}'
become: yes
tasks:
- name: hostname configuration
hostname:
name: "{{ hostname }}"
- name: install packages
yum:
update_cache: yes
name:
- git
- jq

Run it with the following command:
ansible-playbook my-playbook.yml --extra-vars "target=INVENTORY hostname=HOSTNAME"

Terraform EC2 module

Build a module that deploys the instances using Ansible. In the following example the module will do the following:

  1. Create an Ansible user with an ssh key. To do so we will create a Bash script on the instance. With this method, it will be easier to run a bunch of commands to set all the ansible requirements.
  2. Run the Ansible playbook with the instance as a target.

The Ansible playbook will be triggered by Terraform provisioners. Make sure to use the provisioner after the instance is accessible.

variables.tf:

variable "instance_type" {
type = string
}
variable "ami_id" {
type = string
}
variable "hostname" {
type = string
}
variable "default_user" {
type = string
}
variable "ssh_key_name" {
type = string
}
variable "ssh_public_key" {
type = string
}
variable "ssh_private_key" {
type = string
}
variable "subnet_id" {
type = string
}
variable "security_groups_id" {
type = list(string)
}

main.tf:

resource "aws_instance" "instance" {
instance_type = var.instance_type
ami = var.ami_id
key_name = var.ssh_key_name
subnet_id = var.subnet_id
vpc_security_group_ids = var.security_groups_id
connection {
type = "ssh"
user = var.default_user
private_key = var.ssh_private_key
host = aws_instance.instance.private_ip
}
provisioner "file" {
content = <<EOT
#!/bin/bash
PUBLIC_KEY=$1
adduser --disabled-password --gecos "" ansible
echo "ansible ALL=(ALL) NOPASSWD:ALL" > \\
/etc/sudoers.d/ansible
mkdir /home/ansible/.ssh
chmod 400 /home/ansible/.ssh
echo $PUBLIC_KEY > /home/ansible/.ssh/authorized_keys
chown -R ansible:ansible /home/ansible/.ssh
EOT
destination = "/tmp/init.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/init.sh",
"/tmp/init.sh ${var.ssh_public_key}",
]
}
provisioner "local-exec" {
command = <<EOT
ansible-playbook my-playbook.yml --extra-vars \\
"target=${aws_instance.instance.private_dns} \\
hostname=${var.hostname}"
EOT
}
}

Note: The Ansible playbook runs with a local-exec provisioner. That means when someone runs terraform apply the provisioner executes the Ansible command locally, on your machine.

Congrats! You created your first EC2 deployment using Terraform and Ansible. Now you can polish your deployment and make it much faster and more efficient. Go grab your cup of coffee and watch the instances deployed without your intervention 🥳☕️

Final thoughts

As DevOps engineers at HiredScore, we always aim to nurture our culture throughout the R&D to apply E2E solutions in every pipeline, whether it is technical or operational. It allows for faster and more efficient development cycles and enables the other teams to maintain and be responsible for their solution.

I hope you enjoyed this post, and that it inspires you to rethink and improve your EC2 deployment process! 🤞

Interested in this type of work? We’re always looking for talented people to join our team!

Thanks to Avner Cohen, Ilan Spiegel, Litan Shamir and Ezra Wanetik.

--

--