🌙
Managing Infrastructure as Code (IaC): A Practical Guide with Terraform and Ansible
How to define, maintain, and integrate infrastructure with system configuration in CI/CD pipelines.
Table of Contents
1. Introduction to IaC
In today’s dynamic IT world, where infrastructure is increasingly complex and distributed, managing it manually becomes inefficient, error-prone, and hard to scale. This is where the concept of Infrastructure as Code (IaC) comes into play. IaC is an approach that involves managing and provisioning infrastructure (networks, virtual machines, databases, load balancers, etc.) using code rather than manual processes or graphical interfaces.
Why implement IaC?
- Consistency and repeatability: Infrastructure is always created in the same way, eliminating human errors and “configuration drift.”
- Automation: The processes of creating, updating, and destroying infrastructure are automated, accelerating deployment and management.
- Versioning: Infrastructure code can be stored in version control systems (e.g., Git), allowing you to track changes, roll back to previous versions, and collaborate as a team.
- Faster deployments: The ability to quickly provision entire environments (dev, test, prod) shortens the time needed to roll out new applications.
- Security and compliance: Easier to audit and enforce security policies through code inspection.
i IaC is the foundation of DevOps practices, enabling fast and secure software delivery.
2. Terraform Basics
Terraform is one of the most popular IaC tools, created by HashiCorp. It is a declarative tool, meaning you describe the desired state of your infrastructure, and Terraform ensures that state is achieved.
2.1. Installation and Project Initialization
Installing Terraform is straightforward: download the binary from HashiCorp’s website and place it in your system PATH. After installation, run in your project directory:
terraform init
This command initializes the working directory, downloads the necessary plugins (called providers) for the declared cloud (or local) services, and prepares Terraform to work.
2.2. Providers
Providers are plugins that allow Terraform to communicate with different platforms and services. Examples of popular providers include:
- AWS: Managing resources in Amazon Web Services.
- Azure: Managing resources in Microsoft Azure.
- Google Cloud Platform (GCP): Managing resources in Google Cloud.
- DigitalOcean: Managing resources in DigitalOcean.
- VMware vSphere: Managing VMware virtual infrastructure.
- local/remote-exec: Running scripts locally or remotely on machines.
Provider definition in main.tf:
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "eu-central-1" # Example AWS region }
2.3. Your First main.tf – VM, Network, Storage Resources
In main.tf, you define your infrastructure resources. Below is an example of creating a simple virtual machine (EC2) in AWS, a VPC network, and an S3 bucket:
# Create VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" tags = { Name = "my-iac-vpc" } } # Create Subnet resource "aws_subnet" "main" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "eu-central-1a" tags = { Name = "my-iac-subnet" } } # Create EC2 Instance (Virtual Machine) resource "aws_instance" "web_server" { ami = "ami-0abcdef1234567890" # Replace with a current AMI for your region and OS instance_type = "t2.micro" subnet_id = aws_subnet.main.id tags = { Name = "web-server-iac" } } # Create S3 Bucket resource "aws_s3_bucket" "my_bucket" { bucket = "my-unique-iac-bucket-2025" # Bucket name must be globally unique acl = "private" tags = { Environment = "Dev" Project = "IaC-Guide" } }
After defining your resources, run:
terraform plan
terraform plan shows what changes will be made to your infrastructure before they are applied. This is a key step for verification. Then, to apply the changes:
terraform apply
Terraform will ask for confirmation and then proceed to create the resources.
3. Modules and State Management
3.1. Organizing Code into Modules
As your infrastructure grows in complexity, keeping all resources in a single main.tf file becomes impractical. Terraform offers modules, which allow you to group related resources into logical, reusable blocks.
Directory structure with modules:
. ├── main.tf ├── variables.tf ├── outputs.tf └── modules/ ├── vpc/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── ec2-instance/ ├── main.tf ├── variables.tf └── outputs.tf
Using a module in main.tf:
module "my_vpc" { source = "./modules/vpc" vpc_cidr_block = "10.0.0.0/16" } module "my_web_server" { source = "./modules/ec2-instance" ami_id = "ami-0abcdef1234567890" instance_type = "t2.micro" subnet_id = module.my_vpc.subnet_id }
3.2. Remote Backend (S3, Consul)
Terraform stores its state in the terraform.tfstate file. This file maps your Terraform configuration to the real-world resources. In team environments, local state management is risky because it can lead to overwritten changes and errors. Therefore, remote backends are used:
- Amazon S3: The most common choice, often combined with DynamoDB for state locking.
- HashiCorp Consul: A service discovery and configuration store solution.
- Terraform Cloud/Enterprise: A managed service by HashiCorp with built-in state management and workflows.
S3 backend configuration in main.tf:
terraform { backend "s3" { bucket = "my-iac-tfstate-bucket" key = "dev/network/terraform.tfstate" region = "eu-central-1" encrypt = true dynamodb_table = "terraform-lock-table" # For state locking } }
3.3. Practical Tips on State Locking
State locking prevents simultaneous operations on the same Terraform state file, which is crucial for team collaboration. With S3, you use a DynamoDB table for locking. Always ensure your backend supports state locking and that it is enabled.
Tip Always use a remote backend for Terraform state in production environments.
4. Ansible as a Configuration Layer
While Terraform focuses on provisioning infrastructure, Ansible is a powerful tool for configuration management—installing software, configuring services, managing users, etc. Ansible is agentless (no agent required on managed machines), communicating over SSH.
4.1. Playbooks, Roles, Inventory
- Inventory: A file (usually YAML or INI) listing the hosts Ansible manages, along with groups and variables.
- Playbooks: YAML files defining sets of tasks to run on specified hosts.
- Roles: A structured way to organize playbooks, variables, templates, and files, promoting code reuse.
Example inventory file (inventory.ini):
[webservers] web_server_1 ansible_host=10.0.1.10 web_server_2 ansible_host=10.0.1.11 [databases] db_server_1 ansible_host=10.0.1.20
Example simple playbook (install_nginx.yml):
---
- name: Install Nginx web server
- name: Update apt cache
- name: Install Nginx
- name: Start Nginx service
hosts: webservers become: true # Run tasks with root privileges tasks:
apt: update_cache: yes
apt: name: nginx state: present
service: name: nginx state: started enabled: yes
4.2. Example Web/DB Server Configuration
Running the playbook:
ansible-playbook -i inventory.ini install_nginx.yml
5. Terraform ↔ Ansible Integration
After provisioning infrastructure with Terraform, you need Ansible to configure the newly created machines. The key is dynamically passing IP addresses or hostnames from Terraform into Ansible’s inventory.
5.1. Generating a Dynamic Inventory
You can use Terraform outputs to generate an Ansible inventory file. Below is an example of producing a JSON (or INI) dynamic inventory:
# output.tf in the Terraform root directory output "ansible_inventory" { value = { _meta = { hostvars = { } } webservers = { hosts = [for instance in aws_instance.web_server : instance.public_ip] # Assuming public IPs } } sensitive = true # May contain sensitive data }
Then, after terraform apply, run:
terraform output -json ansible_inventory > inventory.json
and use this file with Ansible.
5.2. Calling Playbooks with local-exec/remote-exec
Terraform lets you run local (local-exec) or remote (remote-exec) commands as part of a resource’s lifecycle. This is often used to trigger Ansible playbooks.
resource "aws_instance" "web_server" { # ... instance configuration ... provisioner "remote-exec" { inline = [ "sudo apt update", "sudo apt install -y python3", # Ansible requires Python ] connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip } } provisioner "local-exec" { command =