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:
- Terraform -> Ansible: Use Terraform to provision infrastructure, then invoke Ansible for configuration
- Ansible -> Terraform: Use Ansible to invoke Terraform for infrastructure provisioning
- 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:
# 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:
# 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:
#!/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:
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:
# 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:
#!/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:
chmod +x deploy.sh
Step 5: Run the Deployment
Now you can run the entire deployment with a single command:
./deploy.sh
This will:
- Initialize and apply the Terraform configuration
- Generate an Ansible inventory from Terraform output
- 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:
# 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
# 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
#!/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:
# 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:
# 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
# site.yml
---
- import_playbook: db_server.yml
- import_playbook: web_server.yml
Deployment Script
#!/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:
-
Maintain clear separation of concerns:
- Use Terraform for infrastructure provisioning
- Use Ansible for configuration management
-
Use dynamic inventories:
- Generate Ansible inventories from Terraform output
- Consider using Ansible's AWS dynamic inventory plugin for more complex setups
-
Handle SSH connection issues:
- Add delays or retries for SSH connections
- Consider using Ansible's wait_for module to wait for SSH to be available
-
Secure sensitive information:
- Use Terraform's sensitive variables
- Use Ansible Vault for secrets in playbooks
-
Use version control:
- Store both Terraform and Ansible code in the same repository
- Use gitignore for sensitive files and Terraform state
-
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:
- Check if your security groups allow SSH access
- Verify the SSH key is correct and has proper permissions
- Make sure the user has sudo privileges
- Add a delay after Terraform provisioning to allow instances to fully boot
State Synchronization
If Terraform and Ansible get out of sync:
- Run
terraform refresh
to update Terraform's state - Regenerate your Ansible inventory
- 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
-
Basic Integration: Modify the basic example to deploy a different type of server (e.g., a Redis cache).
-
Dynamic Configuration: Update the multi-tier example to pass more variables from Terraform to Ansible (e.g., instance types, AMI IDs).
-
Advanced Networking: Extend the examples to use VPCs, subnets, and load balancers.
-
Multiple Environments: Create configurations for development, staging, and production environments using Terraform workspaces and Ansible inventory groups.
-
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! :)