Bridging Terraform & Ansible on AWS EC2
Stop clicking through the AWS Console and manually installing software. In this lab, we use Terraform to provision a blank Ubuntu server (Infrastructure as Code), and then pass the baton to Ansible to automatically install Nginx and deploy a dynamic web page using Jinja2 templates (Configuration Management).
Directory Structure
devops-demo/
├── devops-class-key.pem
├── main.tf
├── inventory.ini
├── playbook.yml
└── roles/
└── webserver/
├── tasks/
│ └── main.yml
└── templates/
└── index.html.j2
0 Required Tools & Environment
Terraform
Provisioning Engine
Ansible
Config Management
AWS Account
Cloud Provider
WSL / Ubuntu
For Windows Users
1 Project & Security Key Setup
First, download your `.pem` SSH key from the AWS Console and place it in your project folder. Ensure you lock down its permissions, otherwise Ansible will refuse to connect.
mkdir devops-demo && cd devops-demo
# Secure the SSH key (Crucial for Ansible)
chmod 400 devops-class-key.pem
# Create base files
touch main.tf inventory.ini playbook.yml
# Create the Ansible role structure
mkdir -p roles/webserver/tasks
mkdir -p roles/webserver/templates
touch roles/webserver/tasks/main.yml
touch roles/webserver/templates/index.html.j2
2 Provisioning the Server (Terraform)
Open main.tf. This script configures a Security Group
allowing HTTP/SSH and spins up the latest Ubuntu 22.04 instance. Notice the output block that
will hand us the IP address for Ansible.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-south-1"
}
resource "aws_security_group" "web_sg" {
name = "ansible_web_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"]
}
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
key_name = "devops-class-key"
vpc_security_group_ids = [aws_security_group.web_sg.id]
tags = {
Name = "Ansible-Target-Node"
}
}
output "instance_public_ip" {
value = aws_instance.web_server.public_ip
}
3 Configuration Management (Ansible)
We will use an Ansible Playbook and Role to install Nginx and push a dynamic HTML file containing live server metrics using Jinja2 facts.
A. The Master Playbook
(playbook.yml)
- name: Configure Web Server from Terraform Target
hosts: webservers
gather_facts: true
vars:
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
roles:
- webserver
B. The Role Tasks
(roles/webserver/tasks/main.yml)
---
- name: Ensure Nginx is installed
apt:
name: nginx
state: present
update_cache: yes
become: true
- name: Ensure Nginx service is running and enabled on boot
service:
name: nginx
state: started
enabled: yes
become: true
- name: Deploy the dynamic HTML app using Jinja2 templates
template:
src: index.html.j2
dest: /var/www/html/index.html
owner: www-data
group: www-data
mode: '0644'
become: true
C. The Jinja2 Template
(roles/webserver/templates/index.html.j2)
<!DOCTYPE html>
<html lang="en">
<head>
<title>DevOps Web Server | Ash7 Technologies Hub</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-900 flex items-center justify-center min-h-screen text-slate-200">
<div class="bg-slate-800 p-10 rounded-2xl border border-slate-700 text-center">
<h1 class="text-4xl font-extrabold text-emerald-400 mb-4">Hello World!</h1>
<p class="text-lg text-slate-400 mb-6">Provisioned by Terraform & Configured by Ansible.</p>
<div class="mt-6 bg-slate-900 rounded-xl p-5 border border-slate-700 text-left">
<h2 class="text-emerald-400 font-semibold mb-3 border-b border-slate-700 pb-2">Live Instance Details</h2>
<ul class="text-sm space-y-2 font-mono">
<li><strong>Hostname:</strong> {{ ansible_hostname }}</li>
<li><strong>Internal IP:</strong> {{ ansible_default_ipv4.address | default('N/A') }}</li>
<li><strong>OS Release:</strong> {{ ansible_distribution }} {{ ansible_distribution_version }}</li>
</ul>
</div>
</div>
</body>
</html>
Execution & Verification
1. Spin up the EC2 Instance
terraform init
terraform apply -auto-approve
Copy the `instance_public_ip` that outputs at the end.
2. Update the Inventory
Paste the IP into your inventory.ini
file:
[webservers]
PASTE_IP_HERE ansible_user=ubuntu ansible_ssh_private_key_file=./devops-class-key.pem
3. Run the Configuration Playbook
ansible-playbook -i inventory.ini playbook.yml
Once finished, paste the EC2 IP into your web browser to see your dynamically generated site!
Critical Gotchas & Cleanup
The Windows/WSL Path Issue
If you are running WSL on Windows, do not leave your .pem key on the
Windows C:\ drive mount. Windows handles file permissions differently, and Ansible will
reject the key as "too open." Always move the key to a Linux directory (like your
project folder) and run chmod 400.
Teardown to Avoid Billing
When you finish the lab, ensure you destroy the infrastructure so AWS doesn't bill you for a running EC2 instance.
terraform destroy -auto-approve