Infrastructure as Code
Stage 3 · DevOps, Deployment & Operations · B.U.I.L.D. letter: D
You already know how to ship a feature with a prompt. Now let's make the server that runs it just as easy to reproduce, review, and version-control as the code itself.
⚠️ The vibe trap
You spun up a database in the cloud console, clicked "add environment variable," manually enabled a CDN toggle, and pushed your app to prod. Everything works — today. But when a teammate tries to reproduce your setup, or you need a staging environment, or something blows up and you need to rebuild from scratch at 2 AM, you're staring at 47 settings screens you only half-remember. Nobody reviewed those clicks. Nothing is in git. The infra exists only inside a cloud dashboard and inside your head, and neither one is reliable.
🗺️ What Infrastructure as Code Actually Is
Infrastructure as Code (IaC) means you write your infra — servers, databases, DNS records, storage buckets, environment configs — in plain text files, check those files into git, and use a tool to apply them to reality. When reality drifts from the file, you change the file and re-apply. The cloud dashboard becomes read-only.
Mental model: think of it as a package.json for your servers. You declare what you want; the tool figures out how to get there.
Why it matters:
- Reproducible — spin up an identical staging environment in minutes
- Reviewable — every infra change goes through a pull request, not a 3-click Monday-morning accident
- Auditable — git history tells you who changed the database tier and when
- Recoverable — rebuild from scratch with one command instead of one nightmare
Common mistake: treating IaC as "just documentation." The file is the source of truth. If you tweak something in the console and don't update the file, you've created drift — and the next apply will revert your manual change.
📋 Declarative vs. Imperative
Most modern IaC tools are declarative: you describe the desired end state, not a sequence of steps.
Imperative (scripting):
Step 1: Create a bucket named "my-uploads"
Step 2: If bucket exists, skip
Step 3: Set bucket region to us-east-1
Step 4: Enable versioning on bucket
Step 5: Attach IAM policy...
Declarative (IaC):
resource "aws_s3_bucket" "uploads" {
bucket = "my-uploads"
region = "us-east-1"
}
# Tool figures out all the steps automatically.
Mental model: declarative IaC is like ordering at a restaurant ("I want the pasta, medium spice") rather than walking into the kitchen and narrating every step.
Why it matters: the tool handles idempotency for you. Run the same config twice — or a hundred times — and the result is identical. Nothing gets duplicated, nothing blows up on re-run.
Common mistake: writing a bash script full of aws cli calls and calling it IaC. That is imperative automation — fine for one-offs, but it breaks on re-runs and is hard to diff.
🏗️ Start Where You Are: Platform Config Files
You do not need Terraform on day one. Every major hosting platform has its own IaC-lite format. This is the right place to start.
Vercel
{
"version": 2,
"name": "my-app",
"regions": ["iad1"],
"env": {
"NODE_ENV": "production"
},
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
],
"redirects": [
{
"source": "/old-path",
"destination": "/new-path",
"permanent": true
}
]
}
Render
services:
- type: web
name: my-app
env: node
region: oregon
plan: starter
buildCommand: npm install && npm run build
startCommand: npm start
envVars:
- key: NODE_ENV
value: production
- key: DATABASE_URL
fromDatabase:
name: my-db
property: connectionString
databases:
- name: my-db
plan: starter
region: oregon
Mental model: these files are your deployment config. Check them in. When you change the region or add a header, open a PR, get a review, merge, and the platform picks it up automatically.
Why it matters: a vercel.json or render.yaml in your repo means any collaborator — or any future-you — can look at one file and understand the full deployment shape.
Common mistake: keeping these files out of git because they "feel like config." They are code. They belong in version control.
🌍 Terraform: The Full Picture
When you outgrow platform files, Terraform (and OpenTofu, its open-source fork) lets you describe any cloud resource in a provider-agnostic language called HCL.
terraform {
required_providers {
vercel = {
source = "vercel/vercel"
version = "~> 1.0"
}
}
}
provider "vercel" {
# reads VERCEL_API_TOKEN from env — never hardcode secrets
}
resource "vercel_project" "my_app" {
name = "my-app"
framework = "nextjs"
git_repository = {
type = "github"
repo = "acme/my-app"
}
}
resource "vercel_project_environment_variable" "node_env" {
project_id = vercel_project.my_app.id
key = "NODE_ENV"
value = "production"
target = ["production"]
}
Mental model: each resource block is a "thing that should exist." Terraform reads the current state of the world, compares it to your files, and produces a diff — called a plan — before touching anything.
Why it matters: Terraform manages state — a record of what it created and how those resources map to your config. This is what makes idempotency reliable at scale.
Common mistake: editing resources directly in the cloud console after Terraform created them. Terraform's state no longer matches reality. The next plan will show a confusing diff and may revert your change.
🔄 The Plan → Apply Loop
This is the core IaC workflow. Never apply blind.
# 1. Initialize (download providers, set up state backend — first time only)
terraform init
# 2. Format your files (keeps diffs clean)
terraform fmt
# 3. Validate syntax before touching infra
terraform validate
# 4. Preview what will change — read this carefully before continuing
terraform plan
# 5. Apply the changes (prompts for confirmation unless you add -auto-approve)
terraform apply
A real plan output looks like this:
Terraform will perform the following actions:
# vercel_project.my_app will be created
+ resource "vercel_project" "my_app" {
+ id = (known after apply)
+ name = "my-app"
+ framework = "nextjs"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions? (yes/no)
Mental model: terraform plan is a dry run that shows you exactly what will change in production before you commit. Green + means create, yellow ~ means modify, red - means destroy. Read the red lines very carefully.
Why it matters: IaC without plan review is just as dangerous as clicking around in the console — it just fails reproducibly. The plan step is what turns IaC from "powerful" into "safe and powerful."
Common mistake: running terraform apply -auto-approve in a script without first reviewing the plan. Always pipe plan output to a review step in CI. Never destroy resources you did not intend to destroy.
🗂️ State, Drift, and Keeping Things Clean
Terraform stores a state file (terraform.tfstate) that maps your HCL resources to real cloud objects. Treat this file with respect:
- Never commit raw state to git — it may contain secrets and it changes on every apply
- Use a remote backend (Terraform Cloud, S3 + DynamoDB, etc.) for team projects so everyone shares the same state
- Never edit state by hand — use
terraform statecommands if you need to manipulate it
# List all resources Terraform currently knows about
terraform state list
# Remove a resource from state without destroying it in the cloud
# (useful when importing manually-created resources)
terraform state rm vercel_project.old_app
# Import an existing cloud resource into Terraform management
terraform import vercel_project.my_app prj_abc123
Mental model: state is Terraform's memory of what it built. If state and reality diverge, you have drift — and Terraform will try to fix it on the next apply, which may surprise you.
Common mistake: deleting the state file to "start fresh." This makes Terraform think nothing exists, so it tries to create everything again — often resulting in duplicate resources or conflicts.
🛠️ Your Mission
Pick one piece of your app's infrastructure that currently only exists in a cloud dashboard or in your head. Capture it as a version-controlled file.
Good starting points:
- A
vercel.jsonwith your headers, redirects, and region - A
render.yamldescribing your web service and database - A
fly.tomlfor a Fly.io app - A
terraform/main.tffor a single Vercel project resource
The goal is not to capture everything. The goal is to capture something, put it in git, and prove to yourself that a teammate could read that file and understand your deployment without asking you.
✅ You're done when…
- Your infra file passes the Production-Readiness Checklist: no hardcoded secrets (only env var references), committed to git, and readable by a teammate cold
- You can delete the file, restore it from git, and re-apply it to get an identical result — idempotency confirmed
- A
terraform plan(or equivalent preview) shows zero unexpected changes after a clean apply - Every resource in the file has a comment explaining why it exists, not just what it is
➡️ Next: Observability: Logging. Build It Right, Or Don't Build It At All. 🏛️