Ansible Terraform Integration: Orchestrate Infrastructure and Configuration Together
By Luca Berton · Published 2024-01-01 · Category: installation
Integrate Ansible with Terraform for end-to-end infrastructure automation. Provision with Terraform, configure with Ansible, using dynamic inventories.
Introduction
Terraform provisions infrastructure. Ansible configures it. Together they cover the full automation lifecycle — from creating cloud resources to installing software and managing configurations. This guide shows how to integrate them effectively for enterprise automation.
See also: AI DevOps Ansible Community on Skool
When to Use Each
| Task | Terraform | Ansible | |------|-----------|---------| | Create VMs, networks, LBs | ✅ Primary | Possible but not ideal | | Install packages, configure OS | Possible but limited | ✅ Primary | | Manage Kubernetes clusters | ✅ Cluster creation | ✅ App deployment | | Database provisioning | ✅ RDS/Cloud SQL | ✅ Self-managed DB config | | State tracking | ✅ Built-in state file | ❌ Stateless | | Configuration drift | ❌ Detects, doesn't fix | ✅ Enforces desired state |
Integration Patterns
Pattern 1: Terraform Provisions → Ansible Configures
Terraform Ansible
┌──────────┐ ┌──────────────┐
│ Create │ Output IPs │ Configure │
│ EC2, VPC,│ ──────────→ │ packages, │
│ SG, ELB │ │ services, │
│ │ │ deploy apps │
└──────────┘ └──────────────┘
Pattern 2: Ansible Orchestrates Terraform
- name: Full stack deployment
hosts: localhost
tasks:
- name: Provision infrastructure
cloud.terraform.terraform:
project_path: ./terraform/
state: present
register: tf_output
- name: Add hosts to inventory
ansible.builtin.add_host:
name: "{{ item }}"
groups: webservers
loop: "{{ tf_output.outputs.instance_ips.value }}"
- name: Wait for SSH
ansible.builtin.wait_for:
host: "{{ item }}"
port: 22
timeout: 300
loop: "{{ tf_output.outputs.instance_ips.value }}"
- name: Configure provisioned servers
hosts: webservers
become: true
roles:
- common
- webserver
- monitoring
Pattern 3: Terraform Dynamic Inventory
#!/usr/bin/env python3
# terraform_inventory.py — dynamic inventory from Terraform state
import json
import subprocess
def get_terraform_state():
result = subprocess.run(
['terraform', 'output', '-json'],
capture_output=True, text=True,
cwd='./terraform/'
)
return json.loads(result.stdout)
outputs = get_terraform_state()
inventory = {
'webservers': {
'hosts': outputs['web_ips']['value'],
'vars': {
'ansible_user': 'ubuntu',
'ansible_ssh_private_key_file': '~/.ssh/deploy.pem'
}
},
'databases': {
'hosts': outputs['db_ips']['value'],
'vars': {
'ansible_user': 'ubuntu',
'db_endpoint': outputs['rds_endpoint']['value']
}
}
}
print(json.dumps(inventory))
See also: Ansible London Meetup 2024: Automation & DevOps Insights
Terraform Ansible Module
Install the cloud.terraform collection:
ansible-galaxy collection install cloud.terraform
pip install python-hcl2
Run Terraform from Ansible
- name: Manage Terraform infrastructure
cloud.terraform.terraform:
project_path: ./terraform/aws-vpc/
state: present
force_init: true
variables:
environment: production
instance_count: 3
instance_type: t3.medium
register: tf
- name: Show Terraform outputs
ansible.builtin.debug:
msg: "VPC ID: {{ tf.outputs.vpc_id.value }}"
Destroy Infrastructure
- name: Tear down environment
cloud.terraform.terraform:
project_path: ./terraform/aws-vpc/
state: absent
Terraform with Ansible Provisioner
In main.tf, trigger Ansible after resource creation:
resource "aws_instance" "web" {
count = 3
ami = "ami-0abcdef1234567890"
instance_type = "t3.medium"
key_name = "deploy-key"
tags = {
Name = "web-${count.index + 1}"
Environment = "production"
Role = "webserver"
}
provisioner "local-exec" {
command = <<-EOT
ANSIBLE_HOST_KEY_CHECKING=False \
ansible-playbook -i '${self.public_ip},' \
--private-key ~/.ssh/deploy.pem \
-u ubuntu \
playbooks/configure-web.yml
EOT
}
}
output "instance_ips" {
value = aws_instance.web[*].public_ip
}
See also: Learn Ansible: Complete Beginner's Guide & Learning Path (2026)
CI/CD Pipeline
# .github/workflows/deploy.yml
name: Infrastructure Deployment
on:
push:
branches: [main]
jobs:
provision:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.9"
- name: Terraform Init & Apply
working-directory: ./terraform
run: |
terraform init
terraform apply -auto-approve
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Get outputs
id: tf
working-directory: ./terraform
run: echo "ips=$(terraform output -json instance_ips)" >> $GITHUB_OUTPUT
- name: Install Ansible
run: pip install ansible-core boto3
- name: Configure servers
run: |
ansible-playbook playbooks/site.yml \
-i "${{ steps.tf.outputs.ips }}" \
--private-key <(echo "${{ secrets.SSH_PRIVATE_KEY }}")
AWS Example: Full Stack
Terraform (terraform/main.tf)
# VPC, Subnets, Security Groups
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0"
name = "app-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
}
# EC2 instances
resource "aws_instance" "app" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = module.vpc.private_subnets[count.index % 2]
key_name = var.key_name
tags = {
Name = "app-${count.index + 1}"
Role = "application"
}
}
# RDS Database
resource "aws_db_instance" "main" {
identifier = "app-db"
engine = "postgresql"
engine_version = "16"
instance_class = "db.t3.medium"
# ... additional config
}
output "app_private_ips" {
value = aws_instance.app[*].private_ip
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
}
Ansible Playbook (playbooks/configure-app.yml)
---
- name: Configure application servers
hosts: app_servers
become: true
vars:
db_endpoint: "{{ hostvars['localhost']['tf_outputs']['db_endpoint']['value'] }}"
roles:
- common
- role: app
vars:
database_url: "postgresql://{{ db_user }}:{{ db_pass }}@{{ db_endpoint }}/myapp"
- monitoring
- log_shipping
State Management
# Use remote state for team collaboration
# terraform/backend.tf
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Best Practices
Terraform for provisioning, Ansible for configuration — Don't fight each tool's strengths Terraform outputs → Ansible inventory — Use outputs to dynamically generate host lists Separate state per environment — Different Terraform workspaces or state files for dev/staging/prod Avoid Terraform provisioners — Prefer running Ansible separately afterterraform apply for better error handling
Pin provider versions — Both Terraform providers and Ansible collections
Use the cloud.terraform collection — Native Ansible-Terraform integration
CI/CD orchestration — Pipeline runs Terraform first, then Ansible, with proper error handling
Tag resources consistently — Same tagging schema in Terraform and Ansible for cross-referencing
FAQ
Why not just use Terraform for everything?
Terraform excels at cloud resource lifecycle (create/update/destroy with state tracking). But OS-level configuration — installing packages, managing services, deploying code — is Ansible's strength. Using both gives you the best of each.
Can AAP run Terraform?
Yes — use the cloud.terraform collection in AAP playbooks. Store Terraform code in the same Git project, and AAP handles both provisioning and configuration in a single workflow.
How to handle Terraform state in team environments?
Use remote backends (S3, Azure Blob, GCS) with state locking (DynamoDB for AWS). Never commit terraform.tfstate to Git.
Conclusion
Terraform and Ansible are complementary — not competing — tools. Terraform manages the infrastructure lifecycle with state tracking while Ansible handles configuration management with agentless simplicity. Together they provide complete infrastructure automation from cloud resources to application deployment.
Related Articles
• Ansible vs Terraform Comparison • Ansible AWS Complete Guide • Ansible GitOps with AAPCategory: installation