Skip to main content

Ansible Terraform Integration

Introduction

When working with modern infrastructure, having powerful automation tools is essential. Terraform and Ansible are two of the most popular infrastructure automation tools, but they serve different purposes. Terraform excels at provisioning infrastructure resources, while Ansible specializes in configuration management. By integrating these tools, you can leverage the strengths of both to create a comprehensive infrastructure automation solution.

In this tutorial, we'll explore how to integrate Ansible with Terraform to build and configure cloud infrastructure in a seamless workflow. This integration allows you to:

  • Use Terraform to provision your infrastructure resources (VMs, networks, storage)
  • Use Ansible to configure those resources (installing software, setting up services)
  • Maintain a consistent, repeatable, and version-controlled infrastructure

Understanding Terraform and Ansible

Before we dive into the integration, let's clarify what each tool does:

Terraform

Terraform is an Infrastructure as Code (IaC) tool that allows you to define and provision infrastructure resources across multiple cloud providers using declarative configuration files. Terraform's key strengths include:

  • Declarative syntax: You define the desired end state, not the steps to get there
  • Provider ecosystem: Support for major cloud providers (AWS, Azure, GCP) and hundreds of services
  • State management: Tracks the current state of your infrastructure
  • Plan and apply workflow: Preview changes before applying them

Ansible

Ansible is a configuration management and automation tool that uses a procedural approach to configure systems. Ansible's key strengths include:

  • Agentless architecture: No need to install software on managed nodes
  • YAML-based playbooks: Easy-to-read syntax for defining automation tasks
  • Rich module library: Pre-built modules for common configuration tasks
  • Idempotent operations: Tasks can be run multiple times without causing problems

Integration Approaches

There are several ways to integrate Terraform and Ansible:

  1. Terraform -> Ansible: Use Terraform to provision infrastructure, then invoke Ansible for configuration
  2. Ansible -> Terraform: Use Ansible to invoke Terraform for infrastructure provisioning
  3. Hybrid approach: Use both tools in a CI/CD pipeline

We'll focus on the first approach, which is the most common and straightforward.

Setting Up the Integration

Let's walk through the steps to set up a Terraform-Ansible integration:

Prerequisites

  • Terraform installed (version 0.14+)
  • Ansible installed (version 2.9+)
  • Access to a cloud provider (AWS in our examples)
  • Basic knowledge of both tools

Step 1: Create the Terraform Configuration

First, let's create a basic Terraform configuration that provisions an EC2 instance in AWS:

hcl
# main.tf

provider "aws" {
region = "us-west-2"
}

resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2 AMI
instance_type = "t2.micro"
key_name = "my-key-pair"

vpc_security_group_ids = [aws_security_group.web.id]

tags = {
Name = "web-server"
Role = "web"
}
}

resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Allow SSH and HTTP traffic"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

Step 2: Create an Ansible Dynamic Inventory

To connect Terraform with Ansible, we'll generate a dynamic inventory from Terraform's state. Let's create a Terraform output that provides the information Ansible needs:

hcl
# outputs.tf

output "web_server_ip" {
value = aws_instance.web_server.public_ip
}

Now, let's create a script that generates an Ansible inventory from Terraform output:

bash
#!/bin/bash
# terraform-inventory.sh

# Get the IP address from Terraform output
WEB_IP=$(terraform output -raw web_server_ip)

# Create the Ansible inventory file
cat > inventory.ini <<EOF
[web_servers]
web ansible_host=${WEB_IP} ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/my-key-pair.pem

[all:vars]
ansible_python_interpreter=/usr/bin/python3
EOF

echo "Ansible inventory generated at inventory.ini"

Make the script executable:

bash
chmod +x terraform-inventory.sh

Step 3: Create an Ansible Playbook

Now, let's create an Ansible playbook to configure our EC2 instance as a web server:

yaml
# web_server.yml

---
- name: Configure Web Server
hosts: web_servers
become: yes

tasks:
- name: Update all packages
yum:
name: "*"
state: latest
update_only: yes

- name: Install Apache
yum:
name: httpd
state: present

- name: Start and enable Apache
service:
name: httpd
state: started
enabled: yes

- name: Create a test page
copy:
content: |
<!DOCTYPE html>
<html>
<head>
<title>Welcome to my web server</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This server was provisioned with Terraform and configured with Ansible.</p>
</body>
</html>
dest: /var/www/html/index.html

Step 4: Create a Wrapper Script

To tie everything together, let's create a wrapper script that runs Terraform and then Ansible:

bash
#!/bin/bash
# deploy.sh

set -e

echo "Applying Terraform configuration..."
terraform init
terraform apply -auto-approve

echo "Generating Ansible inventory..."
./terraform-inventory.sh

echo "Waiting for SSH to become available..."
sleep 30

echo "Running Ansible playbook..."
ansible-playbook -i inventory.ini web_server.yml

echo "Deployment complete!"
echo "Web server available at: http://$(terraform output -raw web_server_ip)"

Make the script executable:

bash
chmod +x deploy.sh

Step 5: Run the Deployment

Now you can run the entire deployment with a single command:

bash
./deploy.sh

This will:

  1. Initialize and apply the Terraform configuration
  2. Generate an Ansible inventory from Terraform output
  3. Run the Ansible playbook to configure the web server

Advanced Integration: Using Terraform's local-exec Provisioner

Another approach to integrate Terraform with Ansible is to use Terraform's local-exec provisioner to run Ansible directly from Terraform.

Let's update our Terraform configuration:

hcl
# main.tf with local-exec provisioner

resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
key_name = "my-key-pair"

vpc_security_group_ids = [aws_security_group.web.id]

tags = {
Name = "web-server"
Role = "web"
}

# Wait for SSH to become available
provisioner "local-exec" {
command = "sleep 30"
}

# Run Ansible
provisioner "local-exec" {
command = <<-EOT
cat > inventory.ini <<EOF
[web_servers]
web ansible_host=${self.public_ip} ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/my-key-pair.pem

[all:vars]
ansible_python_interpreter=/usr/bin/python3
EOF

ansible-playbook -i inventory.ini web_server.yml
EOT
}
}

With this approach, Terraform will automatically run Ansible after the EC2 instance is created.

Real-World Example: Multi-Tier Application

Let's look at a more complex example: deploying a multi-tier application with a web server and a database server.

Terraform Configuration

hcl
# multi-tier.tf

provider "aws" {
region = "us-west-2"
}

# Web Server
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
key_name = "my-key-pair"

vpc_security_group_ids = [aws_security_group.web.id]

tags = {
Name = "web-server"
Role = "web"
}
}

# Database Server
resource "aws_instance" "db" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.small"
key_name = "my-key-pair"

vpc_security_group_ids = [aws_security_group.db.id]

tags = {
Name = "db-server"
Role = "database"
}
}

# Security Group for Web Server
resource "aws_security_group" "web" {
name = "web-server-sg"
description = "Allow SSH and HTTP traffic"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

# Security Group for Database Server
resource "aws_security_group" "db" {
name = "db-server-sg"
description = "Allow SSH and MySQL traffic"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

# Outputs
output "web_server_ip" {
value = aws_instance.web.public_ip
}

output "db_server_ip" {
value = aws_instance.db.public_ip
}

Ansible Inventory Script

bash
#!/bin/bash
# multi-tier-inventory.sh

# Get IPs from Terraform output
WEB_IP=$(terraform output -raw web_server_ip)
DB_IP=$(terraform output -raw db_server_ip)

# Create the Ansible inventory file
cat > inventory.ini <<EOF
[web_servers]
web ansible_host=${WEB_IP} ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/my-key-pair.pem

[db_servers]
db ansible_host=${DB_IP} ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/my-key-pair.pem

[all:vars]
ansible_python_interpreter=/usr/bin/python3
EOF

echo "Ansible inventory generated at inventory.ini"

Ansible Playbooks

For the web server:

yaml
# web_server.yml

---
- name: Configure Web Server
hosts: web_servers
become: yes
vars:
db_server: "{{ hostvars[groups['db_servers'][0]]['ansible_host'] }}"

tasks:
- name: Update all packages
yum:
name: "*"
state: latest
update_only: yes

- name: Install Apache and PHP
yum:
name:
- httpd
- php
- php-mysqlnd
state: present

- name: Start and enable Apache
service:
name: httpd
state: started
enabled: yes

- name: Create a test PHP page
copy:
content: |
<?php
$dbhost = '{{ db_server }}';
$dbuser = 'webapp';
$dbpass = 'secret';
$dbname = 'appdb';

$conn = new mysqli($dbhost, $dbuser, $dbpass, $dbname);

if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}

echo "<h1>Hello, World!</h1>";
echo "<p>Successfully connected to the database.</p>";

$sql = "SELECT * FROM messages";
$result = $conn->query($sql);

if ($result->num_rows > 0) {
echo "<h2>Messages:</h2>";
echo "<ul>";
while($row = $result->fetch_assoc()) {
echo "<li>" . $row["message"] . "</li>";
}
echo "</ul>";
} else {
echo "<p>No messages found.</p>";
}

$conn->close();
?>
dest: /var/www/html/index.php

For the database server:

yaml
# db_server.yml

---
- name: Configure Database Server
hosts: db_servers
become: yes

tasks:
- name: Update all packages
yum:
name: "*"
state: latest
update_only: yes

- name: Install MariaDB
yum:
name:
- mariadb-server
- mariadb
state: present

- name: Start and enable MariaDB
service:
name: mariadb
state: started
enabled: yes

- name: Create application database
mysql_db:
name: appdb
state: present

- name: Create database user
mysql_user:
name: webapp
password: secret
priv: 'appdb.*:ALL'
host: '%'
state: present

- name: Create sample table
mysql_query:
login_db: appdb
query: |
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
message TEXT NOT NULL
);
INSERT INTO messages (message) VALUES ('Hello from Terraform and Ansible!');
INSERT INTO messages (message) VALUES ('Infrastructure as Code is awesome!');

Main Playbook

yaml
# site.yml

---
- import_playbook: db_server.yml
- import_playbook: web_server.yml

Deployment Script

bash
#!/bin/bash
# deploy-multi-tier.sh

set -e

echo "Applying Terraform configuration..."
terraform init
terraform apply -auto-approve

echo "Generating Ansible inventory..."
./multi-tier-inventory.sh

echo "Waiting for SSH to become available..."
sleep 30

echo "Running Ansible playbooks..."
ansible-playbook -i inventory.ini site.yml

echo "Deployment complete!"
echo "Web application available at: http://$(terraform output -raw web_server_ip)"

Best Practices for Terraform-Ansible Integration

When integrating Terraform and Ansible, follow these best practices:

  1. Maintain clear separation of concerns:

    • Use Terraform for infrastructure provisioning
    • Use Ansible for configuration management
  2. Use dynamic inventories:

  3. Handle SSH connection issues:

  4. Secure sensitive information:

  5. Use version control:

    • Store both Terraform and Ansible code in the same repository
    • Use gitignore for sensitive files and Terraform state
  6. Consider state management:

    • Use remote state for Terraform
    • Share state between team members securely

Troubleshooting Common Issues

SSH Connection Issues

If Ansible can't connect to your instances:

  1. Check if your security groups allow SSH access
  2. Verify the SSH key is correct and has proper permissions
  3. Make sure the user has sudo privileges
  4. Add a delay after Terraform provisioning to allow instances to fully boot

State Synchronization

If Terraform and Ansible get out of sync:

  1. Run terraform refresh to update Terraform's state
  2. Regenerate your Ansible inventory
  3. Consider using tags to selectively apply configurations

Summary

Integrating Terraform and Ansible provides a powerful automation solution for your infrastructure:

  • Terraform handles the provisioning of cloud resources
  • Ansible manages the configuration of those resources
  • Together, they provide a complete infrastructure automation solution

This integration allows you to maintain a clear separation of concerns while leveraging the strengths of each tool. By following the patterns and examples in this tutorial, you can create robust, repeatable, and scalable infrastructure deployments.

Additional Resources

Exercises

  1. Basic Integration: Modify the basic example to deploy a different type of server (e.g., a Redis cache).

  2. Dynamic Configuration: Update the multi-tier example to pass more variables from Terraform to Ansible (e.g., instance types, AMI IDs).

  3. Advanced Networking: Extend the examples to use VPCs, subnets, and load balancers.

  4. Multiple Environments: Create configurations for development, staging, and production environments using Terraform workspaces and Ansible inventory groups.

  5. CI/CD Pipeline: Integrate the Terraform-Ansible workflow into a CI/CD pipeline using GitHub Actions or Jenkins.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)