Infrastructure as Code (IaC) is the practice of defining and managing infrastructure —servers, networks, databases, load balancers, security rules— through versioned code files, instead of configuring it by hand through web consoles or one-off commands. Just as your application's code lives in a repository, so does the description of your infrastructure, with everything that brings: version control, peer review, exact reproducibility, and automation in pipelines. It matters because it eliminates manual configuration (slow, error-prone, and undocumented), avoids "configuration drift" (the actual environment deviating from what is expected), and lets you create, replicate, or destroy complete environments in minutes reliably. This lesson closes the module by teaching you the key concepts and tools, with Terraform as the main example.

Contents

  1. The problem IaC solves.
  2. Declarative versus imperative.
  3. Tool comparison: Terraform, CloudFormation, and Ansible.
  4. Idempotency: the central concept.
  5. Practical example with Terraform.
  6. IaC in CI/CD pipelines.
  7. Common mistakes and tips.
  8. Exercises and solutions.

  1. The problem IaC solves

Manual infrastructure management (clicking around in the provider's console) has serious problems: it is not reproducible (setting up the same environment twice gives different results), it is not documented (no one knows why that firewall rule exists), it is not reviewable (there is no way to review a change before applying it), and it produces configuration drift: the actual environment gradually diverges from what was intended. IaC turns infrastructure into code: a single source of truth, versioned in Git, reviewable via pull requests, and applicable automatically and repeatably.

  1. Declarative versus imperative

There are two approaches to describing infrastructure:

Aspect Imperative Declarative
What you describe How to reach the state (step by step) What state you want (the result)
Analogy A cooking recipe A photo of the finished dish
The tool computes Nothing, you run commands The difference between current and desired state
Example tool Scripts, Ansible (in part) Terraform, CloudFormation
Re-execution May duplicate actions Only applies what is missing
  • Imperative: you give sequential instructions ("create a server, then install this, then open this port"). You control the how.
  • Declarative: you describe the desired final state ("I want a server with these characteristics and this port open") and the tool figures out what actions are needed to reach it from the current state. It is the dominant approach in modern IaC because it is more robust and easier to reason about.

  1. Tool comparison

Tool Approach Scope State Ideal for
Terraform Declarative Multi-cloud (agnostic) Own state file Provisioning infrastructure on any provider
CloudFormation Declarative AWS only Managed by AWS Those who live 100% in AWS
Ansible Imperative/declarative Configuration and provisioning No persistent state Configuring OS and software on servers
Pulumi Declarative (with general-purpose languages) Multi-cloud Similar to Terraform Teams that prefer TypeScript/Python

Key distinction that is often confused: provisioning (creating the infrastructure: VMs, networks) versus configuration management (installing and configuring software inside the machines). Terraform and CloudFormation excel at provisioning; Ansible excels at configuring the interior of servers. It is very common to combine them: Terraform creates the machines and Ansible configures them. Terraform uses its own language, HCL (HashiCorp Configuration Language); CloudFormation uses YAML/JSON and is tied to AWS.

  1. Idempotency: the central concept

Idempotency means that applying the same operation once or many times produces the same final result, without cumulative side effects. In IaC it is fundamental: running terraform apply ten times against a configuration that has not changed must not create ten servers, but leave the state as it is. The tool compares the desired state (your code), the actual state (what exists in the provider), and its state file (what it believes it manages), and only applies the difference (the plan). Thanks to idempotency, IaC is safe to re-run, which allows automating it in pipelines without fear of duplicating resources.

  1. Practical example with Terraform

Let's define a server instance and its security group on AWS with HCL. Terraform works in a cycle: write the code, view the plan (what is going to change), and apply.

# main.tf

# 1. Provider: tells Terraform we are going to work with AWS
provider "aws" {
  region = "eu-west-1"   # Ireland region
}

# 2. Parameterizable variable (reusable across environments)
variable "instance_type" {
  description = "Instance size"
  type        = string
  default     = "t3.micro"
}

# 3. Resource: a security group that allows inbound HTTP
resource "aws_security_group" "web_sg" {
  name        = "web-sg"
  description = "Allows HTTP traffic"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # from any origin
  }
}

# 4. Resource: the instance, which references the previous security group
resource "aws_instance" "web_server" {
  ami                    = "ami-0abcdef1234567890"
  instance_type          = var.instance_type            # uses the variable
  vpc_security_group_ids = [aws_security_group.web_sg.id] # implicit dependency

  tags = {
    Name        = "web-server"
    Environment = "production"
  }
}

# 5. Output: shows the public IP after applying
output "public_ip" {
  value = aws_instance.web_server.public_ip
}

Explanation block by block:

  • provider "aws": configures the provider and the region. Terraform will download the appropriate plugin.
  • variable "instance_type": declares a parameter with a default value of t3.micro, which can be overridden per environment (this way you reuse the same code in testing and production).
  • resource "aws_security_group" "web_sg": creates a security group (a firewall) that allows inbound traffic (ingress) to port 80 from any IP (0.0.0.0/0).
  • resource "aws_instance" "web_server": creates the machine. instance_type = var.instance_type uses the variable. vpc_security_group_ids = [aws_security_group.web_sg.id] references the previous group: Terraform detects this dependency and creates the security group first and then the instance, in the correct order. The tags label the resource.
  • output "public_ip": after applying, Terraform will show the instance's public IP.

The workflow in the terminal:

# Initializes the directory and downloads the provider plugins
terraform init

# Shows the PLAN: which resources it will create, modify, or destroy (changes nothing)
terraform plan

# Applies the changes to reach the desired state (asks for confirmation)
terraform apply

# Destroys all the managed infrastructure (useful in ephemeral environments)
terraform destroy

Explanation: terraform init prepares the directory. terraform plan is the key safety step: it shows exactly what is going to change before touching anything, which allows reviewing it. terraform apply executes the plan and converges to the desired state (it is idempotent: if nothing changed, it does nothing). terraform destroy removes what was created, very useful for test environments that are spun up and torn down.

  1. IaC in CI/CD pipelines

The true power of IaC appears when integrating it into an automated pipeline: every change to the infrastructure goes through review and is applied on its own. A typical flow with GitHub Actions:

name: Terraform
on:
  pull_request:        # on each PR, only show the plan
  push:
    branches: [main]   # on merge to main, apply
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan          # plan on each PR for review
      - if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve   # apply only on main

Explanation: the pipeline is triggered on pull requests and on push to main. On a PR it runs terraform plan, so reviewers see the proposed infrastructure change before approving it (just as code is reviewed). Only when the change is merged to main does terraform apply -auto-approve run, applying the infrastructure automatically. This way, infrastructure is governed with the same rigor as software: review, traceability, and automated deployment. This practice is known as GitOps when the Git repository is the source of truth for the desired state.

Common Mistakes and Tips

  • Not versioning the state file correctly. Terraform state is sensitive and critical: store it in a shared remote backend (with locking) and never push it in plain text to Git, as it may contain secrets.
  • Editing by hand what IaC manages. Changing resources through the console causes drift: the code stops reflecting reality. Always change through code.
  • Not reviewing the plan before applying. terraform plan exists to avoid surprises (like accidentally destroying a database). Always read it.
  • Hardcoded secrets in the IaC code. Use variables, secrets managers, or environment variables; never embed credentials in the .tf files.
  • Giant IaC monoliths. A single state for the entire company is fragile and slow. Split by environments and domains (modules, workspaces).
  • Tip: treat infrastructure as disposable and reproducible software. If you cannot recreate an environment from scratch with a single command, you do not yet have true IaC. Remember that in regulated sectors infrastructure changes may require additional controls and approvals; validate the flow with your governance and compliance team.

Exercises

  1. Explain in your own words what it means for terraform apply to be idempotent and why that is important for running it inside a pipeline.
  2. Your colleague has manually changed the size of an instance from the provider's console, but the IaC code still has the old value. What problem arises and what will Terraform do on the next apply?
  3. You want to create machines in the cloud and, in addition, install and configure software inside them. Which tools would you combine and what role would each one play?

Solutions

  1. That terraform apply is idempotent means that applying the same configuration once or several times leaves the system in the same final state: if nothing has changed in the code, it creates or modifies no resources. This is important in a pipeline because it allows running it automatically and repeatedly without fear of duplicating infrastructure or causing cumulative effects; it always converges to the desired state.
  2. Configuration drift occurs: the actual state diverges from what is declared in the code. On the next terraform plan/apply, Terraform will detect that the actual resource does not match the desired one and will propose reverting the manual change to return to the code's value (or will show the difference so you can decide). This is why you should not edit by hand what IaC manages.
  3. I would combine Terraform (or CloudFormation) to provision the infrastructure: create the machines, networks, and security groups; and Ansible for configuration management: install packages, deploy the application, and adjust the operating system inside those machines. Terraform creates the physical/virtual "container" and Ansible gets it ready to work.

Conclusion

You have learned what Infrastructure as Code is and why it replaces manual configuration: reproducibility, versioning, and automation. We distinguished the declarative approach from the imperative, compared Terraform, CloudFormation, and Ansible, understood idempotency as a central concept, wrote a complete example in Terraform, and integrated it into a CI/CD pipeline with plan review. With this you close Module 8 on Cloud Architecture and Deployment: you now have the pieces to design, containerize, run (with or without a server), and provision cloud-native applications from start to finish, ready to scale and operate reliably in production.

Application Architecture Course

Module 1: Fundamentals of Application Architecture

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved