Post

Managing Terraform State for Small Teams: A Practical Guide

In this article, let us look at how to manage Terraform state when you are working in a small team. If you have used Terraform alone on your local machine, you might have just ignored the terraform.tfstate file. But once more than one person starts running Terraform, state management becomes something you cannot ignore.

When I first started using Terraform, I would run everything from my laptop. The state file sat in my project directory, and I added it to .gitignore without thinking much about it. Things worked fine until a colleague needed to make a change to the same infrastructure. That is when I realised that local state does not work for teams.

Why Terraform State Matters

Terraform uses a state file to map the resources in your configuration to the real resources in your cloud provider. Without it, Terraform has no idea what it already created.

The state file holds more than just resource mappings. It also keeps track of resource dependencies, caches attribute values so Terraform does not have to fetch them on every plan, and stores metadata Terraform needs for operations like terraform destroy.

For a single developer, a local state file works. For a team, you need something shared. If two people run terraform apply from their own machines with their own state files, Terraform will try to create duplicate resources, and things will break in unpredictable ways.

Options for Managing Shared State

There are a few ways to handle this. Let us go through the common approaches and where they make sense.

1. Remote Backend with Object Storage

The most common approach is to store the state file in a cloud storage bucket and configure Terraform to use it as a remote backend. Every team member reads and writes to the same state file.

Here is how you set it up for GCP:

First, create a GCS bucket manually (this is the one resource you create by hand since Terraform needs the bucket to exist before it can store any state):

1
gsutil mb gs://my-team-terraform-state

Enable object versioning on the bucket so you can recover an older state if something goes wrong:

1
gsutil versioning set on gs://my-team-terraform-state

In your Terraform configuration, add the backend block:

1
2
3
4
5
6
terraform {
  backend "gcs" {
    bucket = "my-team-terraform-state"
    prefix = "terraform/state/production"
  }
}

The prefix lets you store state for multiple environments or modules in the same bucket. Each one gets its own path.

Now when someone runs terraform init, Terraform configures the backend. The first person to run terraform apply writes the initial state. Everyone after that runs terraform plan or terraform apply will lock and use that same state.

2. Terraform Cloud / HCP Terraform

Terraform Cloud (formerly Terraform Enterprise, now called HCP Terraform) is HashiCorp’s managed solution. It handles state storage, provides a UI for viewing state, manages runs, and takes care of locking.

For small teams, the free tier covers up to 500 resources per month. It is enough for a modest setup.

To use it, your backend block looks like:

1
2
3
4
5
6
7
8
terraform {
  cloud {
    organization = "my-org"
    workspaces {
      name = "my-app-production"
    }
  }
}

The main benefit over a raw GCS bucket is that you get state locking, a web UI, run history, and policy checks without setting anything up yourself. The downside is you are locked into HashiCorp’s platform and pricing.

Some teams check their state file into Git. I have seen this suggested in old tutorials, and it almost always ends badly.

ApproachLockingEase of SetupCostBest For
Local stateNoneTrivialFreeSolo dev, learning
GCS backendBuilt-inManual bucket setupBucket storage costSmall teams on GCP
S3 + DynamoDBDynamoDB tableTwo AWS resourcesS3 + DynamoDB costSmall teams on AWS
Terraform CloudAutomaticMinimal setupFree tier availableTeams wanting less ops
Git-based stateNoneNoneFreeNever recommended

State Locking and Why You Need It

State locking prevents two people from running terraform apply at the same time. Without it, one person’s apply might overwrite the state written by another, leaving your infrastructure in a mess.

GCS supports locking out of the box. When someone runs terraform apply, the backend acquires a lock on the state file. If someone else tries to run apply at the same time, they get an error:

1
Error: Error acquiring the state lock

You can also force-unlock if a lock gets stuck:

1
terraform force-unlock <lock-id>

Be careful with force-unlock. Only use it if you are sure no one else is actually running an apply. If you force-unlock while an apply is in progress, you can corrupt your state.

Organising State for Multiple Environments

When you have dev, staging, and production, you need separate state files for each. Otherwise, a change you make to test in dev might accidentally affect production.

You have two main options: workspaces or separate backends.

Terraform Workspaces

Workspaces let you have multiple state files within the same backend configuration:

1
2
3
terraform workspace new dev
terraform workspace new staging
terraform workspace new production

Switch between them:

1
terraform workspace select production

Workspaces are simple and built in, but they have one big limitation: all workspaces use the same backend configuration and credentials. If you want your production resources to live in a separate GCP project with distinct credentials, workspaces do not help you there.

Separate Backend Configurations

A better approach for small teams is to use separate backend configurations per environment. You can do this with a backend config file or partial configuration.

Create a file backend-dev.hcl:

1
2
bucket = "my-team-terraform-state"
prefix = "terraform/state/dev"

And pass it during init:

1
terraform init -backend-config=backend-dev.hcl

This keeps your production and development state completely separated, and you can use different credentials for each.

Practical Tips for Small Teams

Here are a few things I have learned the hard way:

Enable versioning on your state bucket. If you ever need to roll back state, you will thank yourself. GCS versioning keeps every version of the state file, so you can restore a previous one if needed.

Use a dedicated service account for Terraform. Do not use your personal account credentials. Create a service account with exactly the permissions Terraform needs, and store its key as a GitHub secret (or better, use workload identity federation to avoid long-lived keys entirely).

Run Terraform from CI/CD, not local machines. In a team, it is tempting to let everyone run terraform apply from their laptops. But CI/CD gives you a consistent environment, and you can review changes through pull requests before they are applied. A basic GitHub Actions workflow using hashicorp/setup-terraform is all you need to start.

Keep state files small. If your state file grows to hundreds of thousands of lines, terraform plan and apply will slow down. Split your infrastructure into smaller modules, each with its own state. A module for networking, one for compute, one for databases, and so on. You can use terraform_remote_state or data sources to pass information between them.

Do not edit state manually unless you know exactly what you are doing. The terraform state mv and terraform state rm commands are powerful but dangerous. I have seen people remove resources from state and then have Terraform try to recreate them on the next apply, which can cause downtime.

What Changes in a Production Setup

The GCS backend setup I described above works fine for a small team starting out. In a larger or more sensitive production environment, you would add a few things:

  • Use workload identity federation instead of service account keys, so there are no long-lived credentials to leak
  • Add a CI/CD pipeline that runs terraform plan on pull requests and terraform apply on merge to main, with manual approval for production changes
  • Set up state file encryption with customer-managed keys (CMEK) if your compliance requirements call for it
  • Monitor your state bucket access logs for unexpected reads or writes
  • Consider using OPA or Sentinel policies to enforce rules before apply

These are not things you need on day one, but they are worth keeping in mind as your setup matures.

Managing Terraform state properly is one of those things that feels like overhead when you are a solo developer but becomes essential the moment you add a second person. Start with a remote backend in a versioned GCS bucket, enforce state locking, separate your environments, and you will avoid most of the headaches that come with shared state.

This post is licensed under CC BY 4.0 by the author.