Terraform State on GCS: Treat the Bucket as Foundation Infrastructure

This post is for cloud engineers building a GCP foundation with Terraform. You will get a short, opinionated setup for keeping state in Cloud Storage. My argument is that the state bucket is foundation infrastructure in its own right. Create it first, guard it like production, and choose its encryption knowing which choices are hard to undo. The backend "gcs" block itself is the easy part.

Why state leaves your laptop

By default, Terraform keeps state locally in a file named terraform.tfstate. Google’s tutorial on storing Terraform state in a Cloud Storage bucket points out that this makes team use difficult when several people run Terraform at the same time, because each machine has its own picture of the infrastructure. A foundation is shared by definition, with an org, folders, networking and IAM that many people depend on. For a foundation, remote state is required from the first apply.

The gcs backend documentation says it stores state as an object under a configurable prefix and supports state locking. It also says the bucket must exist before you configure the backend. That last requirement shapes everything else.

Bootstrap the bucket in its own step

Because the bucket has to exist first, you have a chicken-and-egg problem. In Google’s tutorial, you run terraform init while the bucket doesn’t exist, so state starts in a local backend. Then terraform apply creates the bucket and writes a backend.tf file pointing at it. Then terraform init -migrate-state moves the local state into the bucket.

I’d keep that bootstrap as its own small configuration, separate from the rest of the foundation. The bucket that holds everyone’s state should not be destroyable by a routine change to some unrelated module.

Copy the bucket settings from Google’s sample backend template:

On versioning, HashiCorp’s docs say it is “highly recommended” for recovering from accidental deletion and human error. Google notes that versioning increases storage costs and suggests Object Lifecycle Management to delete old versions. I’d turn versioning on without debate. Set a lifecycle rule too, but keep enough history to get you back from a bad apply that nobody notices for a while.

Keep admin narrow

Google’s guidance is specific here. Only a small set of users, such as the main cloud administrator and a backup, should have admin permissions on the bucket. Other developers should only read and write objects. HashiCorp says the credentials Terraform uses need the Storage Object Admin role on the bucket.

For identity, I’d use service account impersonation over a downloaded key file wherever you can. The backend supports impersonate_service_account. The caller needs roles/iam.serviceAccountTokenCreator on the target account. Then there’s no key file to look after. On a workstation, HashiCorp notes that user Application Default Credentials expire. You refresh them with gcloud auth application-default login.

Two operational details will save you an afternoon. First, HashiCorp warns that IAM changes on buckets are eventually consistent and can take up to a few minutes to take effect, and Terraform will return 403 errors until they settle. So propagation can explain a temporary 403 right after a grant. Allow a few minutes, then check credentials and permissions if the error persists. Second, credentials passed through -backend-config or hardcoded in the block end up in the .terraform directory and in plan files. Supply sensitive values through environment variables instead.

Choose encryption for the long run

This is the decision I’d slow down on, because the backend treats the two customer-key options very differently.

HashiCorp is blunt about the stakes. State encrypted with a lost or deleted key is not recoverable. If your compliance requirements call for customer-controlled keys, I’d pick Cloud KMS over customer-supplied keys every time. Rotating keys or moving off them later is a supported path with KMS and a manual one with customer-supplied keys. If nothing requires customer keys, I wouldn’t add them to the bootstrap just to feel safer.

Prefixes and layout

The prefix setting places state inside the bucket. Workspace states are stored at <prefix>/<name>.tfstate. I’d give each foundation layer its own prefix in a single bucket rather than one bucket per configuration. That keeps one place to secure and audit, with the separation expressed in paths.

When this doesn’t apply

If you are one person in a throwaway sandbox project, local state is fine. The advice above applies once more than one person, or a pipeline, will touch the same infrastructure. For a GCP foundation, that is from the start.

So before you write the first module, build the state bucket in its own bootstrap configuration. Use versioning, public access prevention, uniform access and force_destroy = false. Give Object Admin to the identities that run Terraform and keep bucket admin to a small set of administrators. Decide KMS or no customer keys now, not after the first audit asks.