Chapter 14 — Infrastructure as Code

Infrastructure as Code (IaC) means defining your servers, networks, databases, and all cloud resources in version-controlled configuration files — not clicking through web consoles.

Why IaC

Terraform — The Industry Standard

# main.tf — Deploy a web app on AWS

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  # Store state remotely (never in git!)
  backend "s3" {
    bucket = "myapp-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "eu-west-1"
  }
}

provider "aws" {
  region = var.region
}

# ─── VPC ──────────────────────────────────────────────────
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0"

  name = "${var.app_name}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["${var.region}a", "${var.region}b"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.11.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true    # Cost saving for non-prod
}

# ─── Database ─────────────────────────────────────────────
resource "aws_db_instance" "main" {
  identifier     = "${var.app_name}-db"
  engine         = "postgres"
  engine_version = "16"
  instance_class = "db.t3.micro"

  allocated_storage = 20
  storage_encrypted = true

  db_name  = var.app_name
  username = "admin"
  password = var.db_password    # From secrets, never hardcoded

  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  skip_final_snapshot = false
  final_snapshot_identifier = "${var.app_name}-final-snapshot"

  backup_retention_period = 7
  multi_az               = var.environment == "prod" ? true : false
}

# ─── EC2 Instance ─────────────────────────────────────────
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  subnet_id              = module.vpc.public_subnets[0]
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name               = aws_key_pair.deploy.key_name

  user_data = templatefile("${path.module}/userdata.sh", {
    app_name = var.app_name
    db_host  = aws_db_instance.main.endpoint
  })

  tags = {
    Name        = "${var.app_name}-web"
    Environment = var.environment
  }
}

# ─── Variables ────────────────────────────────────────────
# variables.tf
variable "app_name" { default = "myapp" }
variable "region" { default = "eu-west-1" }
variable "environment" { default = "prod" }
variable "instance_type" { default = "t3.small" }
variable "db_password" { sensitive = true }
# Terraform workflow:
terraform init          # Download providers, initialize backend
terraform plan          # Preview changes (ALWAYS review this)
terraform apply         # Apply changes (creates/modifies resources)
terraform destroy       # Tear down everything (careful!)

Ansible — Configuration Management

Terraform creates infrastructure. Ansible configures it (installs software, deploys apps, manages configs).

# playbook.yml — Configure a web server
---
- name: Configure web server
  hosts: webservers
  become: yes
  vars:
    app_name: myapp
    app_port: 3000
    node_version: "20"

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install required packages
      apt:
        name: [nginx, certbot, python3-certbot-nginx, ufw]
        state: present

    - name: Configure UFW firewall
      ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop: ['22', '80', '443']

    - name: Enable UFW
      ufw:
        state: enabled
        policy: deny

    - name: Install Node.js
      shell: |
        curl -fsSL https://deb.nodesource.com/setup_{{ node_version }}.x | bash -
        apt-get install -y nodejs
      args:
        creates: /usr/bin/node

    - name: Deploy application
      git:
        repo: "https://github.com/you/{{ app_name }}.git"
        dest: "/opt/{{ app_name }}"
        version: main
      notify: restart app

    - name: Install dependencies
      npm:
        path: "/opt/{{ app_name }}"
        production: yes

    - name: Create systemd service
      template:
        src: templates/app.service.j2
        dest: "/etc/systemd/system/{{ app_name }}.service"
      notify: restart app

    - name: Configure Nginx
      template:
        src: templates/nginx.conf.j2
        dest: "/etc/nginx/sites-available/{{ app_name }}"
      notify: reload nginx

  handlers:
    - name: restart app
      systemd:
        name: "{{ app_name }}"
        state: restarted
        daemon_reload: yes

    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded
# Run Ansible:
ansible-playbook -i inventory.yml playbook.yml

# inventory.yml
webservers:
  hosts:
    web1:
      ansible_host: 93.184.216.34
      ansible_user: deploy

GitOps Principles

Terraform vs Ansible vs CloudFormation:
Terraform: Multi-cloud, creates infrastructure (VMs, networks, DBs). Industry standard.
Ansible: Configures existing servers (install software, deploy apps). Agentless, SSH-based.
CloudFormation: AWS-only IaC. Use if you're 100% AWS and want native integration.
Pulumi: IaC in real programming languages (TypeScript, Python, Go). Good for developers who dislike HCL.

Common combo: Terraform to create infrastructure + Ansible to configure it. Or Terraform + Docker (no config management needed — the container IS the config).