MFormations
Modern DevOps Engineering

Chapitre 5

05 — Infrastructure as Code (IaC) & Terraform

05 — Infrastructure as Code (IaC) & Terraform

Course: Infrastructure as Code & Terraform

1. Infrastructure as Code Principles

1.1 What is IaC?

Infrastructure as Code is the practice of managing infrastructure (servers, networks, databases, etc.) through machine-readable definition files, rather than manual processes.

Benefits:

  • Repeatability: Identical environments every time
  • Version control: Full audit trail via Git
  • Automation: No manual clicking in UIs
  • Self-service: Developers provision their own infrastructure
  • Drift detection: State file vs real world

1.2 Categories

TypeToolsApproach
Declarative (config)Terraform, CloudFormation, ARMDefine desired state
Declarative (K8s)Crossplane, Pulumi K8sKubernetes-style resources
General-purposePulumi, CDKReal programming languages
Configuration MgmtAnsible, Chef, PuppetProcedural/stateful

2. Terraform Core Concepts

2.1 HCL Syntax

# main.tf
terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
    }
  }
}

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.environment}-vpc"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "${var.environment}-private-${count.index + 1}"
    Type = "private"
  }
}

2.2 Variables and Outputs

# variables.tf
variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Must be dev, staging, or prod."
  }
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "private_subnet_cidrs" {
  type = list(string)
}

# outputs.tf
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

2.3 State Management

Why remote state matters:

  • Shared state for team collaboration
  • State locking prevents corruption
  • Sensitive data encryption at rest
  • Version history and rollback

Backend patterns:

# AWS S3
backend "s3" {
  bucket         = "tf-state-${var.aws_account_id}"
  key            = "${var.environment}/terraform.tfstate"
  region         = "us-east-1"
  dynamodb_table = "tf-state-lock"
  encrypt        = true
}

# GCP GCS
backend "gcs" {
  bucket = "tf-state-${var.project_id}"
  prefix = "${var.environment}"
}

# Azure RM
backend "azurerm" {
  storage_account_name = "tfstate${var.environment}"
  container_name       = "tfstate"
  key                  = "terraform.tfstate"
}

2.4 Workspaces

# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# Select workspace
terraform workspace select staging

# Conditional logic
resource "aws_instance" "web" {
  instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
}

3. Modules

3.1 Module Structure

modules/
├── networking/
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   └── README.md
├── compute/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
└── database/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

3.2 Using Modules

module "vpc" {
  source = "./modules/networking"

  environment = var.environment
  vpc_cidr    = var.vpc_cidr
}

module "ec2" {
  source = "terraform-aws-modules/ec2-instance/aws"
  version = "~> 5.0"

  name  = "${var.environment}-web"
  subnet_id = module.vpc.public_subnet_ids[0]
}

3.3 Module Registry

  • Public Registry: registry.terraform.io (official + community modules)
  • Private Registry: Terraform Cloud / Enterprise
  • Git source: source = "git::https://github.com/org/repo.git?ref=v1.0.0"

4. OpenTofu

OpenTofu is the open-source fork of Terraform (created after HashiCorp's BSL license change).

# Same HCL syntax
tofu init
tofu plan
tofu apply
tofu destroy

Key differences:

  • 100% open-source (MPL 2.0)
  • Terraform-compatible state files
  • Additional features: client-side provider encryption, .tofu file extension support
  • Backward compatible with most Terraform configurations

5. Pulumi

Pulumi allows IaC with real programming languages:

import * as aws from "@pulumi/aws";

const vpc = new aws.ec2.Vpc("main", {
  cidrBlock: "10.0.0.0/16",
  tags: { Name: "main-vpc" },
});

const subnet = new aws.ec2.Subnet("private", {
  vpcId: vpc.id,
  cidrBlock: "10.0.1.0/24",
});

export const vpcId = vpc.id;

Supported languages: TypeScript, Python, Go, C#, Java, YAML

6. Crossplane

Crossplane extends Kubernetes to provision and manage infrastructure:

apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: default
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds
---
apiVersion: ec2.aws.upbound.io/v1beta1
kind: VPC
metadata:
  name: crossplane-vpc
spec:
  forProvider:
    cidrBlock: "10.0.0.0/16"
    enableDnsHostnames: true
    region: eu-west-1
  providerConfigRef:
    name: default

7. Terragrunt

Terragrunt keeps Terraform configurations DRY:

# terragrunt.hcl
remote_state {
  backend = "s3"
  config = {
    bucket         = "my-tf-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "eu-west-1"
}
EOF
}

8. Testing with Terratest

package test

import (
  "testing"
  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/stretchr/testify/assert"
)

func TestVPCDeployment(t *testing.T) {
  terraformOptions := &terraform.Options{
    TerraformDir: "../examples/vpc",
    Vars: map[string]interface{}{
      "environment": "test",
    },
  }

  defer terraform.Destroy(t, terraformOptions)
  terraform.InitAndApply(t, terraformOptions)

  vpcID := terraform.Output(t, terraformOptions, "vpc_id")
  assert.NotEmpty(t, vpcID)
}

9. Policy as Code

9.1 OPA / Rego

package terraform.analysis

# Deny EC2 instances without tags
deny[msg] {
  resource := input.resource.aws_instance[_]
  not resource.tags
  msg := sprintf("EC2 instance %v must have tags", [resource.name])
}

# Deny S3 buckets without encryption
deny[msg] {
  bucket := input.resource.aws_s3_bucket[_]
  bucket.server_side_encryption_configuration == []
  msg := sprintf("S3 bucket %v must have encryption enabled", [bucket.name])
}

9.2 Sentinel (Terraform Cloud)

# require-mandatory-tags.sentinel
import "tfplan"

mandatory_tags = ["Environment", "Owner", "CostCenter"]

main = rule {
  all tfplan.resource_changes as _, rc {
    rc.mode is "destroyed" or
    all mandatory_tags as t {
      rc.change.after.tags contains t
    }
  }
}

10. Secrets Management

# From AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_creds" {
  secret_id = "prod/db/credentials"
}

locals {
  db_creds = jsondecode(data.aws_secretsmanager_secret_version.db_creds.secret_string)
}

# From Vault
data "vault_kv_secret_v2" "api_key" {
  mount = "secret"
  name  = "api/production"
}

# From environment variable
variable "github_token" {
  type      = string
  sensitive = true
}

Summary

Terraform is the dominant IaC tool, but the ecosystem now includes OpenTofu (OSS fork), Pulumi (programming languages), Crossplane (Kubernetes-native), and Terragrunt (DRY patterns). Remote state, modules, workspaces, policy as code (OPA/Sentinel), and testing (Terratest) are essential production practices.