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.
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.
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.
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
hosts: webservers
become: true # Run tasks with root privileges
tasks:
- name: Update apt cache
apt:
update_cache: yes
- name: Install Nginx
apt:
name: nginx
state: present
- name: Start Nginx service
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 = <
local-exec
/remote-exec
to invoke Ansible is simple, but remember the ideal approach is dedicated CI/CD integrations where Terraform finishes provisioning and Ansible runs in the next pipeline step.
6. CI/CD Pipeline
Automating IaC processes within Continuous Integration/Continuous Delivery (CI/CD) pipelines is crucial for efficiency and reliability.
6.1. Example with GitLab CI (or GitHub Actions)
Sample stage in a .gitlab-ci.yml
file for a Terraform project:
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}
TF_STATE_NAME: ${CI_COMMIT_REF_SLUG} # Unique state name per branch
default:
image:
name: hashicorp/terraform:latest
entrypoint:
- '/usr/bin/env'
- 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
.terraform_template: &terraform_template
before_script:
- apk add --no-cache curl git openssh-client # Install SSH/Git tools
- terraform --version
- terraform init -backend-config="bucket=my-iac-tfstate-bucket" -backend-config="key=${TF_STATE_NAME}/terraform.tfstate" -backend-config="region=eu-central-1"
validate:
<<: *terraform_template
stage: validate
script:
- terraform validate
- terraform fmt -check=true # Code formatting
- terraform-docs . # Generate module docs
allow_failure: true
plan:
<<: *terraform_template
stage: plan
script:
- terraform plan -out=plan.tfplan
artifacts:
paths:
- plan.tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
apply:
<<: *terraform_template
stage: apply
script:
- terraform apply -auto-approve plan.tfplan
- # You can invoke Ansible here using the dynamic inventory or further steps
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # Manual approval for main/prod
- if: $CI_COMMIT_BRANCH != "main"
The GitHub Actions workflow is analogous, using actions like hashicorp/setup-terraform
and similar commands.
7. Practical Best Practices
- Version control: Treat all infrastructure as application code and store it in a VCS (e.g., Git).
- Secrets Management: Never hard-code sensitive data (API keys, passwords) in Terraform or Ansible code. Use dedicated solutions like HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, or encrypted files (Ansible Vault).
- Infrastructure Testing:
- Terratest: A Go library for writing end-to-end automated tests for Terraform-managed infrastructure. It lets you verify that deployed resources behave as expected.
- InSpec: A framework for compliance and security testing of systems, often used with Ansible to verify configurations.
- Idempotence: Ensure your Terraform and Ansible configurations are idempotent, meaning multiple runs yield the same result.
- Modularity: Create small, reusable modules and roles that are easy to test and maintain.
- Documentation: Automatically generate module documentation (e.g., via
terraform-docs
) and maintain clear READMEs.
8. Conclusion and Next Steps
Managing infrastructure as code with Terraform for provisioning and Ansible for configuration is a powerful combination that allows you to build, deploy, and manage complex environments automatically, consistently, and repeatably. Integrating these tools into a CI/CD pipeline further accelerates processes and minimizes the risk of errors.
Next steps:
- Review the official documentation: Terraform Docs, Ansible Docs.
- Experiment with different cloud providers (AWS, Azure, GCP) to learn their specifics.
- Create your first CI/CD pipeline for simple infrastructure, then expand it.
- Explore secrets management and infrastructure testing tools.