← Back to Blog
Terraform10 min read

Terraform Best Practices I Wish I Knew When I Started

TerraformInfrastructure as CodeDevOpsAWS

I've been working with Terraform for years now. Hundreds of projects. Thousands of hours. And I've made pretty much every mistake you can imagine.

Here are the practices that actually matter - the ones that separate infrastructure code that just works from infrastructure code that saves your sanity at 2 AM when production is down.

1. Use State Locking (This One is Non-Negotiable)

You know that moment when two team members run terraform apply at the same time and everything breaks? Yeah, I've been there. That's why state locking exists.

If you're using S3 for state storage (which you should be), always enable DynamoDB state locking:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "project/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}

This simple addition prevents 90% of the "state corruption" issues I used to see. It's cheap (DynamoDB charges pennies), and it's worth every penny.

2. Separate Environments, Separate State

One mistake I see a lot: teams using the same Terraform state for dev, staging, and production. This is a disaster waiting to happen.

Each environment should have its own state file. Period. This gives you:

  • Isolation - changes in dev don't affect production
  • Safety - you can experiment without fear
  • Flexibility - different environments can use different versions

3. Use Workspaces or Separate Directories

I prefer separate directories over workspaces. Here's why:

project/
  environments/
    dev/
      main.tf
      terraform.tfvars
    staging/
      main.tf
      terraform.tfvars
    prod/
      main.tf
      terraform.tfvars
  modules/
    ec2/
      main.tf
      variables.tf
      outputs.tf

This structure makes it crystal clear which environment you're working with. No "wait, am I in the right workspace?" moments.

4. Modules Are Your Friends

But here's the thing about modules: they need to be reusable, not just reusable in theory.

A good module:

  • Has clear inputs (variables)
  • Has clear outputs
  • Does one thing well
  • Has documentation

Don't over-modularize. If you're only using something once, a module might be overkill. But if you're repeating yourself, extract it.

5. Use Version Constraints

Always pin your provider versions. Always. Unpinned versions are like Russian roulette - it might work fine today, but tomorrow AWS might release a breaking change.

terraform {
  required_version = ">= 1.5.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

The ~> allows patch versions, which is usually what you want. It gives you bug fixes without breaking changes.

6. Don't Put Secrets in Terraform

I've seen teams put passwords, API keys, and other secrets directly in Terraform files. Please don't do this. Please.

Use:

  • Environment variables
  • AWS Secrets Manager
  • HashiCorp Vault
  • Variables passed via CI/CD

Anything but hardcoding secrets. Your future self will thank you.

7. Use Data Sources Instead of Hardcoding

Hardcoding VPC IDs, subnet IDs, and other resources? Don't. Use data sources:

data "aws_vpc" "main" {
  filter {
    name   = "tag:Name"
    values = ["production"]
  }
}

data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.main.id]
  }
  
  tags = {
    Type = "private"
  }
}

This makes your Terraform more portable and less brittle. If infrastructure changes, your code adapts.

8. Tag Everything

Tags matter. They help with cost allocation, resource organization, and compliance. Set up a default_tags block:

provider "aws" {
  default_tags {
    tags = {
      Environment   = var.environment
      Project       = var.project_name
      ManagedBy     = "Terraform"
      CreatedDate   = timestamp()
    }
  }
}

Now every resource gets these tags automatically. Life-changing.

9. Plan Before You Apply

I know this seems obvious, but I've seen teams skip terraform plan and go straight to apply. Don't do this.

Always run terraform plan first. Review the plan. Understand what's going to change. Then apply.

In CI/CD, fail the build if terraform plan shows unexpected changes. This catches issues before they hit production.

10. Use Remote State

Local state files are fine for learning, but for any real project, use remote state (S3, Terraform Cloud, etc.).

Remote state:

  • Enables collaboration
  • Prevents state loss
  • Allows state locking
  • Makes state backups automatic

What I Wish I Knew Earlier

When I started with Terraform, I thought the hardest part would be learning the syntax. Turns out, the syntax is easy. The hard part is building infrastructure code that other people can understand, modify, and maintain.

These practices aren't just about writing "correct" Terraform. They're about writing Terraform that stands the test of time - code that your future self (or teammates) won't curse at.

Start with the basics: remote state, state locking, and version constraints. Add the rest as you go. Infrastructure as Code is a journey, not a destination.

What Terraform practices have saved you the most headaches? I'd love to hear what's worked (or hasn't worked) for your team.

We are a company for impatient brands.

Let's Start a Journey Together

Get in Touch

Have a project in mind? Want to discuss your DevOps needs? Fill out the form and we'll get back to you as soon as possible.

Copyright © 2026 All rights reserved.