
Last Reviewed for Accuracy by Ryan Fee on May 1, 2026.
In the contemporary IT landscape, the ability to rapidly provision, configure, and manage infrastructure is paramount. The dual pressures of accelerating development cycles and maintaining stable, secure environments have driven widespread adoption of Infrastructure as Code (IaC) and Configuration as Code (CaC) principles.
HashiCorp Terraform and Ansible (now part of Red Hat) have emerged as leading tools enabling these practices. While each possesses distinct strengths, their combined use offers a powerful paradigm for comprehensive, end-to-end automation of the entire infrastructure lifecycle.
Terraform excels at infrastructure provisioning and orchestration, allowing organizations to define and manage infrastructure resources across cloud providers and on-premises environments through declarative configuration files. Ansible, by contrast, is a robust automation engine focused on configuration management, application deployment, and task automation within provisioned environments.
This guide explores how to leverage these two powerful tools together, examining integration patterns, practical use cases, best practices, and how to incorporate them into CI/CD pipelines for maximum effectiveness in 2026 and beyond.
The most fundamental distinction between Terraform and Ansible lies in their primary focus:
Terraform is predominantly an orchestration tool. Its core strength is provisioning and managing the lifecycle of infrastructure resources—creating, updating, and destroying virtual machines, networks, storage, and DNS entries across various cloud providers and on-premises systems. Terraform excels at defining the "what"—the desired state of infrastructure components and their interdependencies.
Ansible is primarily a configuration management tool. Its forte is automating the setup and maintenance of software and systems within provisioned infrastructure. This includes installing packages, configuring services, deploying applications, and ensuring systems adhere to specific configurations. Ansible excels at defining the "how"—the steps to bring a system to its desired configured state.
While some overlap exists (Terraform can perform basic configuration via provisioners, and Ansible can provision infrastructure via cloud modules), their architectures and design principles are optimized for these distinct roles.
Terraform employs a declarative approach. Users define the desired end state of infrastructure in HCL (HashiCorp Configuration Language). Terraform then analyzes this desired state against the current actual state (tracked in its state file) and determines the necessary actions (create, update, delete) to achieve it. The order of resource definitions is generally not significant, as Terraform builds a dependency graph to determine execution sequence.
Ansible utilizes a procedural (imperative) approach. Ansible Playbooks, written in YAML, consist of tasks executed in the order they are defined. Users specify explicit steps to reach desired configuration, providing direct control over execution flow.
Terraform is stateful. It maintains a state file (terraform.tfstate) that stores a representation of managed infrastructure, mapping resources defined in configuration to real-world objects. This state file is crucial for planning changes, tracking dependencies, and managing resource lifecycles.
Ansible is largely stateless. It does not maintain persistent records of configuration state between runs. Each playbook execution assesses current node state and performs actions to achieve the desired configuration. While Ansible modules aim for idempotency, the tool doesn't rely on stored state like Terraform does.
| Feature | Terraform | Ansible |
|---|---|---|
| Primary Use Case | Infrastructure Provisioning & Orchestration | Configuration Management & Deployment |
| Approach | Declarative | Procedural/Imperative |
| Language | HCL | YAML |
| State Management | Stateful (maintains tfstate file) | Largely Stateless |
| Infrastructure Type | Favors Immutable | Often used with Mutable |
| Resource Lifecycle | Strong (create, update, delete) | Limited (configuration focus) |
| Agent Requirement | Agentless (APIs) | Agentless (SSH/WinRM) |
| Dependency Handling | Builds resource graph | Executes tasks in order |
The core idea behind using Terraform and Ansible together is that they address different, sequential layers of the automation stack:
Terraform handles "Day 0" activities—initial provisioning and lifecycle management of infrastructure components like virtual machines, networks, and load balancers. It answers: "What infrastructure do I need, and where?"
Ansible handles "Day 1 and beyond" tasks—configuration of provisioned resources. This includes installing software, applying security policies, deploying application code, and managing ongoing system states. It answers: "Now that I have this infrastructure, how do I make it do what I need?"
This division of labor leverages the best of both worlds:
By integrating Terraform and Ansible, organizations can automate the entire service lifecycle:
Successfully combining Terraform and Ansible requires a well-defined integration strategy. Several patterns have emerged, each with advantages and typical use cases.
How it works: The local-exec provisioner runs a command locally on the machine executing Terraform. It can invoke an Ansible playbook targeting newly created resources, passing IP addresses or other identifiers from Terraform to Ansible.
Example:
resource "aws_instance" "web" {
ami = "ami-0c55b31ad2c455b55"
instance_type = "t2.micro"
key_name = "your-ssh-key-pair"
tags = {
Name = "WebServer"
}
}
resource "null_resource" "wait_for_ssh" {
depends_on = [aws_instance.web]
provisioner "remote-exec" {
inline = ["echo 'SSH is up'"]
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/your-ssh-key.pem")
host = aws_instance.web.public_ip
}
}
}
resource "null_resource" "ansible_provision" {
depends_on = [null_resource.wait_for_ssh]
provisioner "local-exec" {
command = <<EOT
ansible-playbook \
-i "${aws_instance.web.public_ip}," \
--private-key ~/.ssh/your-ssh-key.pem \
-u ec2-user \
playbooks/configure-nginx.yml
EOT
}
}Pros:
Cons:
How it works: Terraform provisions infrastructure and generates outputs (IP addresses, instance IDs, DNS names). Ansible reads these outputs to build its inventory, decoupling the two tools.
Terraform outputs:
output "web_server_ips" {
value = aws_instance.web[*].public_ip
}
output "web_server_ids" {
value = aws_instance.web[*].id
}Ansible inventory file (terraform_inventory.ini):
[web_servers]
web_server_1 ansible_host=<IP_from_terraform>
web_server_2 ansible_host=<IP_from_terraform>
[web_servers:vars]
ansible_user=ec2-user
ansible_ssh_private_key_file=~/.ssh/your-key.pemPros:
Cons:
How it works: Ansible uses built-in inventory plugins (aws_ec2, azure_rm, gcp_compute) that query cloud provider APIs to discover resources. Terraform applies specific tags (environment:prod, role:webserver) that the Ansible plugin uses to filter and group hosts.
Ansible inventory plugin example (aws_inventory.yml):
plugin: aws_ec2
regions:
- us-east-1
keyed_groups:
- key: 'tags.Environment'
prefix: env
- key: 'tags.Role'
prefix: role
filters:
tag:Provisioner: terraform
hostnames:
- ip-addressTerraform (applying tags):
resource "aws_instance" "web" {
ami = "ami-0c55b31ad2c455b55"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
Environment = "production"
Role = "webserver"
Provisioner = "terraform"
}
}Pros:
Cons:
The Terraform Ansible provisioner provides direct integration, allowing Ansible to run immediately after resource creation.
resource "aws_instance" "example" {
ami = "ami-0c55b31ad2c455b55"
instance_type = "t2.micro"
provisioner "ansible" {
plays {
playbook {
file_path = "${path.module}/playbook.yml"
}
}
on_failure = continue # or fail
}
depends_on = [aws_instance.example]
}resource "null_resource" "configure_web_servers" {
provisioner "local-exec" {
command = "ansible-playbook -i ansible/inventory.ini playbooks/web_setup.yml"
}
depends_on = [aws_instance.web]
}resource "null_resource" "run_playbook" {
provisioner "local-exec" {
command = "ansible-playbook -i inventory.ini playbooks/app_deploy.yml -e 'app_version=${var.app_version} environment=${var.environment}'"
}
}Ansible can read Terraform state files to build inventory, though this requires careful access management.
Using terraform_state plugin:
---
plugin: community.general.terraform_state
hostnames:
- private_ip
groups:
tag_Name: tags.NameAccessing remote state:
For remote backends (Terraform Cloud, S3), ensure Ansible has appropriate credentials and access controls.
A more secure approach uses Terraform to generate inventory files:
locals {
ansible_inventory = {
all = {
children = {
web_servers = {
hosts = {
for instance in aws_instance.web :
instance.tags["Name"] => {
ansible_host = instance.public_ip
ansible_user = "ec2-user"
}
}
}
}
}
}
}
resource "local_file" "inventory" {
content = yamlencode(local.ansible_inventory)
filename = "${path.module}/inventory.yml"
}Terraform outputs for Ansible:
output "database_endpoint" {
value = aws_db_instance.main.endpoint
description = "Database endpoint for Ansible configuration"
}
output "app_servers" {
value = {
for instance in aws_instance.app :
instance.tags["Name"] => instance.private_ip
}
}Ansible reading outputs (via local script):
#!/bin/bash
TERRAFORM_OUTPUTS=$(terraform output -json)
DB_ENDPOINT=$(echo $TERRAFORM_OUTPUTS | jq -r '.database_endpoint.value')Terraform reading Ansible facts:
resource "null_resource" "gather_facts" {
provisioner "local-exec" {
command = "ansible-playbook playbooks/gather_facts.yml --extra-vars 'output_file=/tmp/facts.json'"
}
}
locals {
ansible_facts = jsondecode(file("/tmp/facts.json"))
}GitLab CI/CD example:
stages:
- provision
- configure
- test
provision_infrastructure:
stage: provision
script:
- terraform init
- terraform plan -out=tfplan
- terraform apply tfplan
- terraform output -json > terraform_outputs.json
artifacts:
paths:
- terraform_outputs.json
- .terraform
configure_with_ansible:
stage: configure
dependencies:
- provision_infrastructure
script:
- ansible-playbook -i inventory.ini playbooks/app_setup.yml
only:
- main
smoke_tests:
stage: test
script:
- ansible-playbook playbooks/smoke_tests.ymlGitHub Actions example:
name: Infrastructure and Configuration
on: [push]
jobs:
provision:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: hashicorp/setup-terraform@v1
- run: terraform init
- run: terraform apply -auto-approve
- run: terraform output -json > outputs.json
- uses: actions/upload-artifact@v2
with:
name: terraform-outputs
path: outputs.json
configure:
needs: provision
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: terraform-outputs
- run: pip install ansible
- run: ansible-playbook -i inventory.ini playbooks/setup.ymlinfrastructure/
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── environments/
│ ├── dev/
│ ├── staging/
│ └── production/
└── ansible/
├── inventory/
│ ├── dev.ini
│ ├── staging.ini
│ └── production.ini
├── playbooks/
│ ├── common.yml
│ ├── app_deploy.yml
│ └── monitoring_setup.yml
└── roles/
├── web_server/
├── database/
└── monitoring/
For a deeper look at the broader tooling landscape, see our guide to the top GitOps tools for 2025.
# Terraform manages infrastructure
resource "aws_autoscaling_group" "app" {
min_size = 2
max_size = 10
desired_capacity = 4
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
}
# User data triggers configuration
resource "aws_launch_template" "app" {
user_data = base64encode(<<EOF
#!/bin/bash
# Wait for instance to be ready
sleep 30
# Configure with Ansible (golden image approach preferred)
# or trigger dynamic inventory update
EOF
)
}Preferred approach:
Avoid:
# Store Terraform state securely
terraform {
backend "s3" {
bucket = "terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}locals {
common_tags = {
Environment = var.environment
ManagedBy = "terraform"
Provisioner = "scalr" # or your management platform
Team = var.team
CostCenter = var.cost_center
}
}# Ansible playbooks should be idempotent
---
- name: Configure web servers
hosts: web_servers
gather_facts: yes
roles:
- common
- web_server
- monitoring
handlers:
- name: restart nginx
service:
name: nginx
state: restarted# Test playbook syntax
ansible-playbook playbooks/site.yml --syntax-check
# Dry run before applying
ansible-playbook playbooks/site.yml --check
# Validate with ansible-lint
ansible-lint playbooks/
# Test with Terraform
terraform validate
terraform plan -detailed-exitcode# Use Terraform Cloud/Enterprise for secrets
variable "database_password" {
sensitive = true
type = string
}# Use Ansible Vault for sensitive playbook data
ansible-vault create group_vars/databases/vault.yml
ansible-playbook playbooks/site.yml --ask-vault-pass# Keep Terraform state and Ansible playbooks in sync
# Document recovery procedures
# Test recovery regularly
# Maintain backups of state filesChallenge: Securely storing and managing Terraform state files across teams
Solution:
Challenge: Keeping Ansible inventory synchronized with Terraform-provisioned resources
Solution:
Challenge: Handling sensitive data (API keys, passwords, credentials) securely
Solution:
Challenge: Enabling multiple teams to work with Terraform and Ansible safely
Solution:
The combination of Terraform and Ansible provides a powerful approach to infrastructure and configuration automation. Terraform handles the "what" of infrastructure, while Ansible handles the "how" of configuration management. Used together with proper integration patterns, they enable organizations to achieve:
The key to success is choosing the right integration pattern for your specific needs—favoring loose coupling through dynamic inventory over tight coupling through provisioners, maintaining clear separation of concerns, implementing proper state and secrets management, and using higher-level orchestration platforms when operational complexity demands it.
As infrastructure automation continues to evolve, the Terraform-Ansible combination remains a foundational approach for organizations of all sizes seeking to automate their infrastructure lifecycle effectively.
For more information on orchestration alternatives and management platforms, see our guide on Ansible Tower and Automation Controller Alternatives.
