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
- The problem IaC solves.
- Declarative versus imperative.
- Tool comparison: Terraform, CloudFormation, and Ansible.
- Idempotency: the central concept.
- Practical example with Terraform.
- IaC in CI/CD pipelines.
- Common mistakes and tips.
- Exercises and solutions.
- 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.
- 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.
- 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.
- 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.
- 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 oft3.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_typeuses 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. Thetagslabel 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.
- 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 mainExplanation: 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 planexists 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
.tffiles. - 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
- Explain in your own words what it means for
terraform applyto be idempotent and why that is important for running it inside a pipeline. - 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? - 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
- That
terraform applyis 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. - 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. - 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
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
