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
| Type | Tools | Approach |
|---|---|---|
| Declarative (config) | Terraform, CloudFormation, ARM | Define desired state |
| Declarative (K8s) | Crossplane, Pulumi K8s | Kubernetes-style resources |
| General-purpose | Pulumi, CDK | Real programming languages |
| Configuration Mgmt | Ansible, Chef, Puppet | Procedural/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,
.tofufile 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.