Using Terraform & Ansible Together
In the worlds of infrastructure as code (IaC) and configuration management, Terraform and Ansible have emerged as two key players. Both of these tools are powerful on their own, but you can use them together to achieve end-to-end elevated workflows. Terraform in a nutshell Terraform is an IaC tool developed by HashiCorp that focuses on managing and provisioning infrastructure across a wide array of cloud platforms and other platforms such as Kubernetes, and RabbitMQ. It uses a declarative language called HashiCorp Configuration Language (HCL) that describes what the desired state should be. This contrasts with imperative approaches, where commands are given to reach the desired state. The language is developed for reusability, enabling engineers to implement modules (reusable IaC components), use loops and conditionals, and define different input variables for the configurations. Terraform maintains a state file to keep track of the resources it manages. This enables it to identify the difference between the current state of the infrastructure and the desired state defined in the configuration files. Ansible in a nutshell Ansible is an agentless configuration management tool that relies on SSH for Linux hosts or WinRM for Windows hosts to communicate and execute commands directly, eliminating the need for a separate agent to be installed. Sometimes you will see it called ssh on steroids, which is really accurate. Ansible configurations are written in playbooks using YAML, and these playbooks describe automation jobs. A single playbook can consist of one or multiple plays, each targeting a set of hosts. One of Ansible's core features is idempotence, which means that you can run the same command any number of times and the result will always be the same. This is critical for configuration management and automation, ensuring consistent results. Ansible can also be used for infrastructure provisioning, but it is not on par with Terraform's capabilities. Terraform and Ansible - Why do you need both? Terraform is used to manage your infrastructure, but Ansible is a better option if you want to install and configure software on your compute instances. You could use Terraform's provisioners to achieve Ansible's configuration management capabilities, but they are unreliable, and even HashiCorp recommends using them as a last resort. That's why using Terraform and Ansible together can improve your workflows. Alternatively, you could use Ansible to provision your infrastructure via collections implemented for cloud providers, but it is much harder to have a highly customized infrastructure without writing a ton of code. Although they can sometimes overlap in functionality, they excel in different areas. See also - Terraform vs. Ansible: Key Differences and Comparison Example setup - How to use Ansible with Terraform A common pattern is to use Terraform to set up base infrastructure, including networking, VM instances, and other foundational resources. Once that's done, Ansible can be invoked (either manually or via Terraform) to configure those instances, set up necessary software, and deploy applications. Let's look at an example that will provision a couple of EC2 instances in AWS and after that will configure them with Ansible: provider "aws" { region = "eu-west-1" } data "aws_ami" "ubuntu" { most_recent = true filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] } filter { name = "virtualization-type" values = ["hvm"] } filter { name = "architecture" values = ["x86_64"] } owners = ["099720109477"] #canonical } locals { instances = { instance1 = { ami = data.aws_ami.ubuntu.id instance_type = "t2.micro" } instance2 = { ami = data.aws_ami.ubuntu.id instance_type = "t2.micro" } instance3 = { ami = data.aws_ami.ubuntu.id instance_type = "t2.micro" } } } resource "aws_key_pair" "ssh_key" { key_name = "ec2" public_key = file(var.public_key) } resource "aws_instance" "this" { for_each = local.instances ami = each.value.ami instance_type = each.value.instance_type key_name = aws_key_pair.ssh_key.key_name associate_public_ip_address = true tags = { Name = each.key } } In the above example, we are creating three EC2 instances, and adding an ssh key to all of them. We are assigning public ips to these instances, to speed up the process of configuring them via Ansible. In the real world, usually, all these instances will have only private ips, and they will be configured through a bastion host. As the code was written with a for_each loop, we can easily extend the number of instances, just by modifying the local Terraform variable. We've also created an output containing

In the worlds of infrastructure as code (IaC) and configuration management, Terraform and Ansible have emerged as two key players. Both of these tools are powerful on their own, but you can use them together to achieve end-to-end elevated workflows.
Terraform in a nutshell
Terraform is an IaC tool developed by HashiCorp that focuses on managing and provisioning infrastructure across a wide array of cloud platforms and other platforms such as Kubernetes, and RabbitMQ.
It uses a declarative language called HashiCorp Configuration Language (HCL) that describes what the desired state should be. This contrasts with imperative approaches, where commands are given to reach the desired state. The language is developed for reusability, enabling engineers to implement modules (reusable IaC components), use loops and conditionals, and define different input variables for the configurations.
Terraform maintains a state file to keep track of the resources it manages. This enables it to identify the difference between the current state of the infrastructure and the desired state defined in the configuration files.
Ansible in a nutshell
Ansible is an agentless configuration management tool that relies on SSH for Linux hosts or WinRM for Windows hosts to communicate and execute commands directly, eliminating the need for a separate agent to be installed. Sometimes you will see it called ssh on steroids, which is really accurate.
Ansible configurations are written in playbooks using YAML, and these playbooks describe automation jobs. A single playbook can consist of one or multiple plays, each targeting a set of hosts.
One of Ansible's core features is idempotence, which means that you can run the same command any number of times and the result will always be the same. This is critical for configuration management and automation, ensuring consistent results.
Ansible can also be used for infrastructure provisioning, but it is not on par with Terraform's capabilities.
Terraform and Ansible - Why do you need both?
Terraform is used to manage your infrastructure, but Ansible is a better option if you want to install and configure software on your compute instances. You could use Terraform's provisioners to achieve Ansible's configuration management capabilities, but they are unreliable, and even HashiCorp recommends using them as a last resort. That's why using Terraform and Ansible together can improve your workflows.
Alternatively, you could use Ansible to provision your infrastructure via collections implemented for cloud providers, but it is much harder to have a highly customized infrastructure without writing a ton of code.
Although they can sometimes overlap in functionality, they excel in different areas.
See also - Terraform vs. Ansible: Key Differences and Comparison
Example setup - How to use Ansible with Terraform
A common pattern is to use Terraform to set up base infrastructure, including networking, VM instances, and other foundational resources. Once that's done, Ansible can be invoked (either manually or via Terraform) to configure those instances, set up necessary software, and deploy applications.
Let's look at an example that will provision a couple of EC2 instances in AWS and after that will configure them with Ansible:
provider "aws" {
region = "eu-west-1"
}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
owners = ["099720109477"] #canonical
}
locals {
instances = {
instance1 = {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
instance2 = {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
instance3 = {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
}
}
resource "aws_key_pair" "ssh_key" {
key_name = "ec2"
public_key = file(var.public_key)
}
resource "aws_instance" "this" {
for_each = local.instances
ami = each.value.ami
instance_type = each.value.instance_type
key_name = aws_key_pair.ssh_key.key_name
associate_public_ip_address = true
tags = {
Name = each.key
}
}
In the above example, we are creating three EC2 instances, and adding an ssh key to all of them.
We are assigning public ips to these instances, to speed up the process of configuring them via Ansible. In the real world, usually, all these instances will have only private ips, and they will be configured through a bastion host.
As the code was written with a for_each loop, we can easily extend the number of instances, just by modifying the local Terraform variable.
We've also created an output containing all the public ips of the instances:
output "aws_instances" {
value = [for instance in aws_instance.this : instance.public_ip]
}
After running an apply, this is what we observe:
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
aws_instances = [
"54.74.244.166",
"34.254.227.95",
"63.33.71.25",
]
We can now prepare an inventory file based on the above servers for Ansible. The inventory file in Ansible is a configuration file for your hosts and groups and hosts that tells Ansible where to execute tasks, allowing users to define specific variables and configurations for different hosts.
This is how I created my inventory file:
inventory.ini
[all]
54.74.244.166
34.254.227.95
63.33.71.25
We will create a simple ansible playbook that installs htop.
install_htop.yaml
---
- name: Install htop
hosts: all
become: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
- name: Install htop
apt:
name: htop
state: present
Now we can run Ansible and install htop on all of the servers:
ansible-playbook -i inventory -u ubuntu install_htop.yaml
PLAY [Install htop] ************************************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************************
ok: [34.254.227.95]
ok: [54.74.244.166]
ok: [63.33.71.25]
TASK [Update apt cache] ********************************************************************************************************************
changed: [63.33.71.25]
changed: [34.254.227.95]
changed: [54.74.244.166]
TASK [Install htop] ************************************************************************************************************************
ok: [34.254.227.95]
ok: [54.74.244.166]
ok: [63.33.71.25]
PLAY RECAP *********************************************************************************************************************************
34.254.227.95 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
54.74.244.166 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
63.33.71.25 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now, if we add another EC2 instance in the Terraform configuration, we will have to manually re-run both Terraform and Ansible, which can sometimes be problematic. Whenever you have multiple things to do after you commit your code, the chances for errors increase.
This is where Spacelift comes to the rescue.