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.
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.

Diagram of how Terraform works

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
  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 = <
! Using 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.
Tip Start with small projects and gradually increase complexity to build strong IaC foundations.

Partnerzy strategiczni

wpisz help
Terminal
$
Switch language