All writing
Platform EngineeringMarch 18, 202610 min read

5 Terraform Lessons From Real Infrastructure Projects

Terraform looks simple when you start.

You write some HCL, run terraform apply, and infrastructure appears. It feels clean. Then you try to manage it across multiple environments, with real client constraints, and you discover that Terraform rewards discipline and punishes shortcuts in equal measure.

These five lessons came from real IaC work across real projects.

1. State is everything — treat it that way

Terraform state is the record of what Terraform believes exists in your infrastructure. If state is wrong, Terraform makes wrong decisions. If state is lost, Terraform no longer knows what it manages.

Never store state locally for anything beyond a personal experiment. Use remote state. On AWS, that means S3 for storage and a DynamoDB table for locking. The locking prevents two concurrent terraform apply runs from corrupting the state file simultaneously.

Never edit the state file manually unless you fully understand what you are changing and have a verified backup.

2. Workspaces handle environments — but only if you design for them

Terraform workspaces let you use the same configuration against different environments — dev, staging, production — each with its own isolated state. The workspace name becomes a variable you can use inside your configuration to differentiate resource names, sizing, or behaviour per environment.

The mistake is assuming workspaces work without designing the configuration around them. If your resource names are hard-coded, workspaces give you separate state but identical resource names in every environment, which causes conflicts.

Design your configurations with terraform.workspace in mind from the start.

3. Modules should have a single responsibility

A module that creates a VPC, subnets, security groups, an EC2 instance, a database, and an S3 bucket is not a module — it is a script. It is untestable, unreusable, and unreadable six months later.

A VPC module creates a VPC and its associated networking resources. A database module creates a database. An application module creates the compute layer. Each has a clear boundary, a defined set of inputs, and a defined set of outputs.

Outputs are what make modules composable. The VPC module outputs its VPC ID. The application module accepts that VPC ID as an input variable. The modules stay separate and can be updated independently.

4. Read the plan. Every time.

terraform plan shows you exactly what Terraform intends to do before it does it. Reading it is not optional.

Look for resources being destroyed that you did not intend to destroy, resources being replaced rather than updated, and changes to resources outside the scope of what you changed.

A plan showing 2 to destroy when you expected 1 to add is a plan worth stopping on. Most infrastructure mistakes happen when someone applies without reading the plan.

5. Variables and outputs are how configurations stay useful long-term

A Terraform configuration with hard-coded values is a configuration you will rewrite for every new client or environment. Region, instance type, bucket name, CIDR block — all of these should be variables.

When a client needs a staging environment that mirrors production but with smaller instance sizes, a configuration built with variables lets you create it by changing a variable file. Hard-coded values require you to duplicate and edit the entire configuration.

Outputs serve the same purpose at the module level. They make the results of one module available as inputs to another, so configurations can be composed without duplicating resource lookups.

Terraform done well is infrastructure that a team can work on, that can be reviewed in a pull request, that can be promoted from dev to staging to production with confidence. That requires structural work upfront. There is no shortcut that gets you there without it.

Back to all posts
Share on LinkedInBuild With JAA