Platforms & Data
Infrastructure automation with Terraform, Ansible and Jenkins
Terraform provisions the AWS network and compute, Ansible configures the hosts it just created, and Jenkins ties the two into a repeatable pipeline.
Problem
Standing up a cloud environment by hand, clicking through the console to create a network, launch instances, and then SSH in to configure each one, is slow and drifts out of sync the moment anyone makes a change nobody recorded. The next person cannot tell what the environment is supposed to look like, and rebuilding it is guesswork.
This project turns the environment into a definition that can be applied repeatedly. Terraform declares the AWS network and compute, Ansible declares how each host is configured once it exists, and Jenkins runs the two together so the whole stack rebuilds the same way every time.
How it works
Terraform provisions the AWS layer: a VPC with DNS enabled, a public and a private subnet in every availability zone the region offers (sized with cidrsubnet off a single VPC CIDR variable), an internet gateway and routing, security groups, and EC2 instances launched from the latest Ubuntu 22.04 AMI resolved at plan time. Instance count, type, and root volume size are all variables, and a random_id suffix keeps resource names unique across rebuilds. State lives in Terraform Cloud, so the environment has one authoritative record.
Each instance runs a userdata bootstrap on first boot (set its hostname, install and enable Grafana), and a Terraform local-exec provisioner appends the new public IP to an Ansible inventory file as the instance comes up, with a matching destroy-time provisioner that strips the IP back out. That inventory is what Ansible then targets to configure the hosts, so the machines Terraform created are handed straight to Ansible without a manual copy step. A resize helper script grows the EBS root volume from instance metadata when a node needs more disk.
Jenkins pipeline
-> terraform apply (Terraform Cloud state)
VPC + public/private subnets per AZ + IGW + routing + SGs
EC2 (Ubuntu 22.04, count/type/size as vars) + userdata (hostname, Grafana)
local-exec: append public IP -> aws_hosts (Ansible inventory)
-> ansible-playbook -i aws_hosts (configure the hosts Terraform created)
-> terraform destroy: local-exec strips IPs back out of aws_hostsHard parts
- Bridging provisioning and configuration: a local-exec provisioner writes each new instance IP into the Ansible inventory as Terraform creates it, and a destroy-time provisioner removes it, so the inventory Ansible consumes always matches the running fleet without a manual edit.
- AZ-agnostic networking: subnets are generated with a count over the region availability zones and carved from one VPC CIDR with cidrsubnet, so the same code lays out public and private subnets correctly in any region.
- Rebuild safety: a random_id suffix on resource names and create_before_destroy on the VPC let the stack rebuild without name collisions or a destroy-then-recreate gap.
- One source of truth for state: Terraform Cloud holds remote state so the environment definition is shared and can be locked during changes.
- First-boot configuration and disk growth: a userdata template sets the hostname and installs Grafana on launch, and a metadata-driven resize script grows the EBS root volume on demand.
Results
The repository provisions a complete, repeatable AWS environment from code: a multi-AZ VPC with public and private subnets, count-driven EC2 nodes on a resolved-latest Ubuntu AMI, remote state in Terraform Cloud, and an Ansible inventory populated automatically as instances come up. The full environment can be created and torn down with a single Terraform run, and the same definition rebuilds it identically.
Artifacts
- Public GitHub repository with the Terraform configuration (providers, networking, compute, backend, variables), the userdata bootstrap template, the auto-populated Ansible inventory, and the EBS resize helper.
- Terraform Cloud workspace as the remote state backend.
- Parameterized instance count, type, and volume size so the same code runs whether you launch one node or many.