Infrastructure as Code with Terraform
Infrastructure as Code with Terraform
Why Infrastructure as Code?
Infrastructure as Code (IaC) is the fundamental pillar of modern DevOps. Instead of manually configuring servers through graphical interfaces, we describe infrastructure in versioned configuration files.
Key advantages
- Reproducibility: a single file always produces the same infrastructure
- Version control: every change is tracked in Git
- Code review: infrastructure changes go through pull requests
- Automation: deployment with a single
terraform apply - Living documentation: the code is the documentation
Terraform: advanced fundamentals
Providers and resources
Terraform uses providers to interact with cloud APIs. Each provider exposes resources and data sources.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "mon-terraform-state"
key = "prod/infrastructure.tfstate"
region = "eu-west-3"
}
}
provider "aws" {
region = var.aws_region
}
Reusable modules
A Terraform module encapsulates a set of resources into a reusable component:
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
availability_zones = ["eu-west-3a", "eu-west-3b", "eu-west-3c"]
environment = var.environment
tags = local.common_tags
}
module "eks_cluster" {
source = "./modules/eks"
cluster_name = "prod-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
node_count = 3
instance_type = "t3.xlarge"
depends_on = [module.vpc]
}
State management and workspaces
The state is the file that maintains the mapping between your code and real resources. In a team setting, it must be stored in a remote backend.
# Using workspaces to manage multiple environments
# terraform workspace new staging
# terraform workspace new production
resource "aws_instance" "app" {
count = terraform.workspace == "production" ? 3 : 1
ami = data.aws_ami.ubuntu.id
instance_type = terraform.workspace == "production" ? "t3.xlarge" : "t3.medium"
tags = {
Environment = terraform.workspace
}
}
Advanced patterns
Loops and dynamic expressions
variable "services" {
type = map(object({
port = number
replicas = number
cpu = string
memory = string
}))
default = {
api = { port = 3000, replicas = 3, cpu = "500m", memory = "512Mi" }
web = { port = 4200, replicas = 2, cpu = "250m", memory = "256Mi" }
worker = { port = 0, replicas = 1, cpu = "1000m", memory = "1Gi" }
}
}
resource "kubernetes_deployment" "service" {
for_each = var.services
metadata {
name = each.key
labels = {
app = each.key
}
}
spec {
replicas = each.value.replicas
selector {
match_labels = {
app = each.key
}
}
template {
spec {
container {
name = each.key
image = "${var.registry}/${each.key}:${var.image_tag}"
resources {
requests = {
cpu = each.value.cpu
memory = each.value.memory
}
}
}
}
}
}
}
Secret management with Vault
data "vault_generic_secret" "db_credentials" {
path = "secret/data/production/database"
}
resource "aws_db_instance" "main" {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.r6g.xlarge"
username = data.vault_generic_secret.db_credentials.data["username"]
password = data.vault_generic_secret.db_credentials.data["password"]
storage_encrypted = true
multi_az = true
backup_retention_period = 30
deletion_protection = true
}
Best practices
- Always use a remote backend for state (S3, GCS, Azure Blob)
- Lock provider and module versions
- Separate environments with workspaces or distinct directories
- Always run
terraform planbeforeapply - Never store secrets in plain text in
.tffiles - Structure as modules to reuse and test infrastructure
- Integrate Terraform into CI/CD with automatic plans on PRs