Infrastructure as Code
Terraform, Pulumi, and CloudFormation for declarative infrastructure management, state management best practices, module reuse, drift detection, and testing with Terratest and Checkov.
What it is
Infrastructure as Code (IaC) is the practice of managing and provisioning infrastructure (compute, networking, storage, managed services) through machine-readable configuration files rather than through manual processes or interactive UIs. The configuration is version-controlled alongside application code, enabling infrastructure changes to follow the same review, testing, and deployment workflows as software changes.
The dominant approaches: Terraform (HashiCorp, now BUSL licensed / OpenTofu for open source fork) uses HCL (HashiCorp Configuration Language) and a declarative model with a provider ecosystem covering essentially every cloud and service. It maintains a state file that records the mapping between declared resources and actual cloud resources, enabling it to compute a diff (plan) and make only the necessary changes. Pulumi uses general-purpose programming languages (TypeScript, Python, Go, C#) to define infrastructure — familiar tooling, real loops and functions, better testability. AWS CloudFormation and Azure Bicep / ARM templates are cloud-native IaC tools deeply integrated with their respective cloud control planes. Ansible is imperative (playbooks describe steps to execute) rather than declarative and is better suited to configuration management than infrastructure provisioning.
The declarative vs imperative distinction is significant. Declarative IaC (Terraform, CloudFormation, Pulumi) describes the desired end state; the tool determines what changes are needed to reach it. Imperative IaC (Ansible, shell scripts) describes the steps to execute; the tool follows the steps regardless of current state. Declarative is generally preferred for infrastructure because it naturally supports idempotency (running twice produces the same result) and drift correction.
Why it exists
Manual cloud console operations create undocumented infrastructure that cannot be reliably reproduced. A production environment provisioned manually over 18 months has hundreds of resources whose existence, configuration, and interdependencies are known only to the people who created them. IaC transforms this into a versioned, auditable, reproducible specification: the entire infrastructure is documented by definition (it is the code), every change has a commit message and author, environments can be recreated from scratch in minutes, and developers can provision identical environments for testing without operations team involvement.
When to use
- All cloud infrastructure — there is no scenario where managing cloud resources manually at scale is preferable to IaC.
- Multi-environment setups (dev/staging/production) where environment parity is achieved by applying the same modules with different variable inputs.
- Teams wanting infrastructure changes reviewed like code — plan outputs can be reviewed in PRs before apply.
When not to use
- Quick one-off resources for personal experiments — the setup cost of IaC is not worth it for truly ephemeral personal work. However, any shared infrastructure should be in IaC from day one.
Typical architecture
TERRAFORM PROJECT STRUCTURE:
infra/
modules/
vpc/
main.tf # VPC, subnets, route tables
variables.tf
outputs.tf
eks-cluster/
main.tf # EKS control plane, node groups
variables.tf
outputs.tf
rds/
main.tf # RDS instance, subnet group, security group
variables.tf
outputs.tf
environments/
staging/
main.tf # Composes modules for staging
terraform.tfvars # staging-specific values
backend.tf # S3 state backend, DynamoDB lock
production/
main.tf
terraform.tfvars
backend.tf
TERRAFORM MODULE USAGE:
# environments/production/main.tf
module "vpc" {
source = "../../modules/vpc"
name = "prod-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}
module "eks" {
source = "../../modules/eks-cluster"
cluster_name = "prod-cluster"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
node_min_size = 3
node_max_size = 20
node_instance = "m6i.xlarge"
}
STATE BACKEND (remote, with locking):
terraform {
backend "s3" {
bucket = "myorg-terraform-state"
key = "production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-state-lock" # Prevents concurrent applies
}
}
CI/CD PIPELINE FOR IaC:
Pull Request:
- terraform fmt --check (formatting)
- terraform validate (syntax)
- checkov -d . (security policy checks)
- terraform plan (post plan as PR comment)
Merge to main:
- terraform apply -auto-approve (requires plan approval)
Post-apply:
- terraform plan (check for drift)
- Alert if plan shows unexpected changes
DRIFT DETECTION (scheduled):
# Run plan daily, alert if non-empty
terraform plan -detailed-exitcode
# Exit code 2 = changes detected (drift)
# Alert on-call if production has drift
Pros and cons
Pros
- Infrastructure is auditable and reproducible: the entire environment is documented in version-controlled code; disaster recovery means
terraform applynot archaeology through cloud console click history. - Environment parity: staging and production use the same modules with different variable values, eliminating "works in staging but not production" due to infrastructure differences.
- Change review: terraform plan outputs show exactly what will change before it changes; teams can review infrastructure modifications with the same PR process as application code.
Cons
- State management complexity: Terraform state files contain the mapping between configuration and cloud resources; corrupted, lost, or out-of-sync state causes plan failures and potentially dangerous applies; remote state backends and locking are essential but add setup overhead.
- Import burden for existing infrastructure: teams adopting IaC for pre-existing cloud infrastructure must import every resource into state — a time-consuming and error-prone process for large environments.
- Terraform provider ecosystem quality varies: popular providers (AWS, GCP, Azure) are excellent; smaller cloud services or newer features may have incomplete providers requiring workarounds or null_resource hacks.
Implementation notes
Never store Terraform state locally — use remote backends (S3 + DynamoDB for AWS, GCS for GCP, Terraform Cloud/HCP Terraform for cloud-agnostic). State files contain sensitive data (resource IDs, outputs that may include credentials) and must be encrypted at rest. DynamoDB-based state locking prevents concurrent applies that can corrupt state. For large organisations, consider using Terragrunt to reduce module boilerplate and enforce DRY state configuration across many environments and regions.
Security-scan IaC before apply. Checkov is a static analysis tool that checks Terraform configurations against hundreds of security best practices: S3 buckets should have encryption and block public access, security groups should not allow 0.0.0.0/0 inbound, RDS should have deletion protection, etc. Running Checkov in CI on PRs catches misconfigurations before they reach production. tfsec and Trivy (which includes IaC scanning) are alternatives. For policy-as-code enforcement, HashiCorp Sentinel or OPA Conftest can enforce custom policies (e.g., "all production resources must have team tags").
Common failure modes
- State file corruption from concurrent applies: Two engineers run
terraform applysimultaneously without state locking; state becomes inconsistent; subsequent plans show phantom changes; recovery requires manual state surgery. - Manual changes cause state drift: Engineer manually modifies a security group in the console to fix an urgent issue; next terraform apply reverts the change; team is confused why the "fix" disappeared.
- Monolithic state files: Entire infrastructure in one Terraform workspace; a plan takes 10 minutes and touches hundreds of resources; a mistake in one resource's configuration blocks all other infrastructure changes; partition state by environment and domain.
Decision checklist
- Is Terraform state stored in a remote backend with encryption and locking?
- Is state partitioned appropriately (by environment, by domain) to limit blast radius?
- Are IaC changes reviewed via PR with terraform plan output posted as a comment?
- Is Checkov or equivalent security scanning running in CI?
- Is drift detection scheduled (regular plan runs alerting on unexpected changes)?
- Are sensitive outputs (passwords, keys) stored in secrets management, not in state?
Example use cases
- Environment-on-demand: Product team requests a staging environment for a new feature; platform team has parameterised Terraform modules; running
terraform apply -var="env=feature-payments"creates a complete environment (VPC, EKS cluster, RDS, Redis) in 15 minutes; environment torn down withterraform destroyafter feature merges. - Compliance audit: Security audit requires evidence that all S3 buckets have server-side encryption and versioning; team runs Checkov against IaC repository to generate report; 100% of buckets pass because IaC modules enforce encryption by default; audit evidence is the IaC policy + Checkov report.
- Cross-region disaster recovery: Primary region goes down; IaC for secondary region exists but was last applied 3 months ago; team runs
terraform applyin secondary region; infrastructure is current within 20 minutes; manual configuration steps that would otherwise be required are zero.
Related patterns
- GitOps — GitOps extends IaC principles to Kubernetes workload management.
- Container Orchestration — IaC provisions the cluster; Kubernetes manages what runs on it.
- Secret Management — Secrets generated by IaC must be stored securely, not in state files.