Infrastructure as Code — Terraform across all three providers
Clicking through a cloud console to create infrastructure is fine for learning. For anything that matters, your infrastructure should be defined in code, stored in version control, and deployed through automation. Infrastructure as Code (IaC) gives you reproducibility, peer review, audit trails, and the ability to rebuild everything from scratch when disaster strikes.
Infrastructure as Code -- Terraform Drift + State Lock
Plan + Apply Pipeline
Config (.tf)
Desired state
terraform plan
Three-way diff
EC2 + Security Group
Pending
State: S3 + DynamoDB
Drift Detection
Console Edit
Security Group changed manually
Real Infra
Differs from state
Plan: Revert
Drift detected + fixed
Resource Deleted
RDS gone from console
State: Still Listed
Plan: Recreate
Destructive: new RDS
State Lock: Concurrent Apply
Without Lock
Engineer A
apply
Engineer B
apply (same time)
State Corrupted
Resources orphaned
With DynamoDB Lock
Engineer A
apply (has lock)
Engineer B
waiting...
State Intact
Sequential, safe
Plan: Security Group
Ingress 0.0.0.0/0
Sentinel / OPA
Policy check
REJECTED
Before apply
Config + state + real infra: three-way diff catches drift. Locks prevent corruption. Policy stops bad plans before apply.
Why IaC Matters
Without IaC, your infrastructure is a snowflake — the product of hundreds of manual decisions that nobody documented. When it breaks at 3am, you're reverse-engineering what someone clicked six months ago. When you need a second environment, you're spending days clicking through the same screens and inevitably missing something.
IaC gives you four things. Reproducibility: run the same code, get the same infrastructure, every time. Code review: infrastructure changes go through pull requests, just like application code. Audit trail: git history shows who changed what, when, and why. Disaster recovery: your entire infrastructure can be rebuilt from a repo — the ultimate backup.
Declarative vs Imperative
Declarative tools (Terraform, CloudFormation, Bicep) describe the desired end state: "I want a VPC with these subnets." The tool figures out what to create, modify, or delete to reach that state. Imperative tools (scripts, Pulumi in imperative mode, CDK) describe the steps: "Create this VPC, then create this subnet, then attach this route table." Declarative is the dominant paradigm because it handles idempotency — you can run it multiple times safely.
Terraform: The Core Model
Terraform is the de facto standard for multi-cloud IaC. It works with all three major providers (and hundreds of others) through a plugin architecture.
Providers are plugins that know how to talk to a specific API — aws, google, azurerm. You configure them with credentials and region, and they expose resources you can manage.
resource "aws_s3_bucket" "data" {
bucket = "my-app-data-prod"
}
resource "google_storage_bucket" "data" {
name = "my-app-data-prod"
location = "US"
}
Data sources let you read existing infrastructure that Terraform doesn't manage. Variables parameterize your configuration. Outputs expose values for other configurations to consume. Modules package reusable infrastructure — your VPC module, your EKS cluster module, your standard database module.
State: The Hard Part
Terraform maintains a state file that maps your configuration to real resources. When you declare aws_s3_bucket.data, state records the actual bucket ID, ARN, and all its attributes. Without state, Terraform has no idea what already exists.
State must be remote and locked. If two engineers run terraform apply simultaneously with local state files, they'll create duplicate resources or corrupt the state. Use S3 + DynamoDB (AWS), GCS (GCP), or Azure Blob Storage with state locking.
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Secrets in state are a real problem. When you create an RDS instance with a master password, that password is stored in plaintext in the state file. Encrypt your state backend. Restrict access to the state bucket. Treat state like a credential.
Plan and Apply
The terraform plan command shows you exactly what will change — resources to create, modify, or destroy — without doing anything. terraform apply executes those changes. This plan/apply cycle is your review gate: run plan in CI, post the output to the pull request, and require approval before apply runs.
Never run apply without reading the plan. A misconfigured change can delete your production database. Terraform will tell you 1 to destroy — read it.
Drift
Drift happens when someone changes infrastructure outside Terraform — clicking in the console, running a CLI command, or another tool modifying the same resource. Now state disagrees with reality. terraform plan will show changes to bring reality back to what your code says, which might undo the manual change. This is why you need organizational discipline: once you adopt IaC, the console is read-only.
AWS Implementation
The aws provider is the most mature Terraform provider. It covers virtually every AWS service. State lives in S3 with DynamoDB locking. AWS also offers CloudFormation (AWS-native, YAML/JSON, tightly integrated with AWS services) and CDK (write CloudFormation in TypeScript, Python, etc.). CloudFormation handles state automatically but is AWS-only and verbose.
GCP Implementation
The google provider covers GCP services well. State lives in GCS with built-in locking. GCP also offers Deployment Manager (GCP-native, YAML/Jinja2), though it's less popular than Terraform. GCP's IAM model with service accounts for Terraform requires careful scoping — a Terraform service account with project-owner permissions is a significant security risk.
Azure Implementation
The azurerm provider covers Azure services. State lives in Azure Blob Storage with lease-based locking. Azure offers Bicep (a clean DSL that compiles to ARM templates) and ARM templates (verbose JSON). Bicep is genuinely good — if you're Azure-only, it's a strong alternative to Terraform with better type checking and IntelliSense.
Side-by-Side Comparison
| Aspect | AWS | GCP | Azure |
|---|---|---|---|
| Terraform provider | aws (most mature) | google | azurerm |
| Native IaC | CloudFormation / CDK | Deployment Manager | Bicep / ARM Templates |
| State backend | S3 + DynamoDB | GCS (built-in locking) | Blob Storage (lease locking) |
| Provider-managed state | CloudFormation | Deployment Manager | ARM/Bicep |
| Terraform coverage | Excellent | Very good | Very good |
| Native IaC quality | CloudFormation is verbose, CDK is good | Deployment Manager is limited | Bicep is excellent |
Module Design and Alternatives
Good modules are opinionated. A VPC module shouldn't expose every possible parameter — it should encode your organization's network standards (CIDR scheme, subnet layout, flow logs enabled) and expose only what varies between environments. Version your modules with git tags and reference specific versions so a module update doesn't break every consumer.
Alternatives to Terraform: Pulumi lets you write IaC in real programming languages (TypeScript, Python, Go) with loops, conditionals, and testing — genuinely powerful but smaller ecosystem. Crossplane brings IaC into Kubernetes, managing cloud resources as custom resources. OpenTofu is the open-source fork of Terraform after HashiCorp's license change to BSL — API-compatible, community-governed.
Policy as Code and Testing
Policy as code enforces rules on your infrastructure before it deploys. Sentinel (HashiCorp, commercial) and OPA/Rego (open source) evaluate policies against Terraform plans. Checkov and tfsec scan your HCL files for security misconfigurations — public S3 buckets, unencrypted databases, overly permissive security groups. Run these in CI on every pull request.
Testing verifies your modules work. Terratest (Go library) provisions real infrastructure, validates it, and tears it down. Plan review in CI catches breaking changes. Ephemeral environments — spinning up a full environment per pull request — give you confidence that changes work before merging.
Key Takeaways
- Infrastructure as Code gives you reproducibility, peer review, audit trails, and disaster recovery — the console becomes read-only once you adopt it
- Terraform state maps config to real resources and must be remote, locked, and encrypted — concurrent applies without locking corrupt state
- The plan/apply cycle is your review gate; never apply without reading the plan, especially lines that say "destroy"
- Drift happens when someone changes infrastructure outside Terraform; organizational discipline is as important as tooling
- Secrets end up in state in plaintext — encrypt your state backend and restrict access to it
- Scan IaC with Checkov/tfsec in CI, test modules with Terratest, and version modules with git tags
Next, we'll look at how code gets from a repository to running infrastructure — CI/CD pipelines.
Enjoyed this breakdown?
Get new lessons in your inbox.