Most startups start infrastructure the same way: someone clicks through the AWS or GCP console, spins up a database, an S3 bucket, a load balancer, and ships. It works, right up until a second engineer joins and asks “how do I get a copy of this environment?” — and the honest answer is “ask the person who clicked the buttons.”
That’s the moment infrastructure-as-code stops being a nice-to-have. Terraform doesn’t need to be adopted all at once, and it doesn’t need a rewrite of everything you’ve already built. This guide covers the five patterns that matter before your infrastructure outgrows tribal knowledge, plus the one trick that lets you adopt Terraform without tearing down what’s already running.
Why manual infrastructure breaks down
Console-clicked infrastructure has no history, no review process, and no reliable way to reproduce it. The failure modes are predictable:
- No audit trail. A security group rule changes and nobody knows who changed it or why.
- No reproducibility. Staging and production drift apart because someone patched staging by hand.
- Bus factor of one. The one person who set everything up leaves, and the runbook lives in their head.
- No review. A change that would get caught in a pull request instead goes straight to production.
Terraform fixes all four: every change is a diffable plan, reviewed like code, applied consistently, and versioned in git.
The 5 Terraform patterns every startup needs
1. Remote state in S3 or GCS — never local state
Terraform tracks the real-world resources it manages in a state file. If that file lives on your laptop, you have a single point of failure and no way for a teammate to safely run terraform apply.
terraform {
backend "s3" {
bucket = "yourco-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
The dynamodb_table line enables state locking — it prevents two people (or a person and a CI job) from running apply at the same time and corrupting state. On GCP, use a GCS backend instead; it locks natively without a separate table.
Set this up before you write a single resource block. Migrating state later is possible but adds risk you don’t need to take on.
2. Module structure — don’t repeat yourself across environments
A common early mistake is one giant main.tf with everything in it, copy-pasted between dev and prod with small edits. That copy-paste is where drift creeps in.
Instead, structure reusable pieces as modules and call them per environment:
modules/
vpc/
database/
service/
environments/
dev/
main.tf # calls modules/vpc, modules/database, modules/service
staging/
main.tf
prod/
main.tf
Each environment’s main.tf is a short file that wires modules together with different inputs — instance size, replica count, domain name. The module code itself is the same everywhere, which means a fix or improvement in modules/database benefits every environment the moment it’s applied.
3. Workspaces (or directories) per environment
Terraform gives you two ways to separate dev, staging, and prod: workspaces (one state file, multiple named instances) or separate directories (fully independent state per environment, as shown above). For startups, separate directories per environment are usually the safer default — a mistake in dev can’t accidentally touch prod state, because they’re not in the same state file at all.
Reserve terraform workspace for cases where environments are truly identical and short-lived, like ephemeral PR preview environments.
4. Variable files, not hardcoded values
Every environment-specific value — instance type, domain, replica count — belongs in a .tfvars file, not hardcoded in the module call:
# environments/prod/prod.tfvars
instance_type = "db.r6g.large"
replica_count = 3
domain_name = "app.yourco.com"
terraform plan -var-file="prod.tfvars"
This makes the diff between environments explicit and reviewable in a single file, instead of buried across multiple .tf files. It also means promoting a config change from staging to prod is a one-line diff in the .tfvars file — easy to review, easy to audit.
5. terraform import — onboard legacy resources without a rewrite
This is the pattern that actually unblocks adoption. You don’t need to destroy your console-created database and recreate it in Terraform — you can bring it under management as-is:
# Write the resource block first, matching the real resource's config
resource "aws_db_instance" "main" {
# ... configuration matching the existing instance
}
# Then import the existing resource into state
terraform import aws_db_instance.main your-db-instance-id
# Run a plan — it should show "no changes" if your config matches reality
terraform plan
The key discipline: after importing, run terraform plan and make sure it shows zero changes before you touch anything else. If it shows a diff, your .tf config doesn’t match the real resource yet — fix the config, not the infrastructure, until the plan is clean. Only then start making changes through Terraform going forward.
Import one resource type at a time — start with something low-risk like an S3 bucket or a security group, not your production database, so the team builds confidence in the workflow before touching anything critical.
Cost of chaos: manual vs. IaC over 12 months
| Manual (console) | Terraform (IaC) | |
|---|---|---|
| New environment setup | 1–2 days, error-prone | Minutes, terraform apply |
| Onboarding a new engineer | Days of tribal knowledge transfer | Read the module code |
| Prod incident: “what changed?” | Grep through CloudTrail, ask around | git log on the infra repo |
| Disaster recovery | Rebuild from memory | terraform apply in a new region |
| Estimated ops time/month at 10 engineers | 15–25 hours firefighting drift | 3–5 hours, mostly planned changes |
The gap isn’t dramatic in month one. It compounds every time someone touches infrastructure by hand instead of through a reviewed plan — and by month six, teams that skipped IaC are spending real engineering time just keeping environments in sync.
Where to start this week
You don’t need to Terraform everything on day one. The realistic 30-day path:
- Week 1 — set up remote state (S3/GCS + locking), pick your module structure, import your most-touched resource (usually the VPC or a database).
- Week 2 — bring the rest of networking and compute under management via
terraform import. - Week 3 — split environments into separate directories with
.tfvars, verify staging and prod both plan clean. - Week 4 — wire
terraform planinto CI on every pull request, so every infra change gets reviewed beforeapply.
What’s next
Terraform pairs naturally with the CI/CD pipeline you’re already running — the same PR review that gates your application code should gate infrastructure changes too. If you haven’t set that up yet, read Your First CI/CD Pipeline Checklist for the exact sequence, and the DevOps for Startups guide for the full platform picture.
Sitting on a console-built environment and want a second opinion before you start importing it into Terraform? Book a free intro call — we’ll map your current infrastructure and hand you a prioritized IaC adoption plan.
