In team-based Terraform workflows, the state file must be stored remotely. On GCP, using a GCS bucket as the backend is the simplest, easiest-to-operate option.
This article explains the GCS backend from a practical perspective: the basics, security design, initialization and migration, authentication and CI/CD, operations and troubleshooting, and an exam-prep checklist.
Terraform's GCS backend stores the state file in Google Cloud Storage and uses locks to prevent concurrent edits by your team. GCS object versioning and strong consistency enable safe reads and writes.
The backend itself is simple: all you need is a bucket name, an optional prefix, and authentication. To improve recoverability, always enable object versioning on the bucket.
| Backend | Locking mechanism | Versioning / recovery | Encryption |
|---|---|---|---|
| gcs | Lock via object generation preconditions | Recovery via GCS object versioning | Bucket default encryption (CMEK supported) |
| s3 | Lock via DynamoDB table | S3 Versioning | SSE-S3 / SSE-KMS |
| azurerm | Lock via Blob lease | Snapshot / version | Storage encryption + CMK |
| local | Process-local only (no shared lock) | None | OS dependent |
GCS backend data flow (including locking)
Minimal backend configuration example (static definition)
terraform {
backend "gcs" {
bucket = "my-tfstate-bucket"
prefix = "envs/prod"
# credentials can be omitted (ADC is used). To use a key file:
# credentials = "/secure/path/sa-key.json"
}
}
State is one of your most sensitive assets. Align bucket design with environment and project boundaries and clearly separate access boundaries. A common pattern is to split buckets per environment (prod/stg/dev) and organize the contents with prefix and workspace like directories.
Security is enforced at the bucket level. Enable Uniform bucket-level access and Public Access Prevention. Configure object versioning and a default CMEK to combine recoverability with encryption. Apply least privilege via IAM, granting the Terraform service account roles that allow reading and writing objects plus the create/delete operations required for locks.
Baseline bucket configuration (gsutil / gcloud examples)
# Create the bucket (pick a region close to where the team runs it)
gsutil mb -p ${PROJECT_ID} -l ${REGION} gs://${BUCKET}
# Enable uniform bucket-level access
gsutil uniformbucketlevelaccess set on gs://${BUCKET}
# Enable public access prevention
gcloud storage buckets update gs://${BUCKET} --public-access-prevention
# Enable object versioning
gsutil versioning set on gs://${BUCKET}
# Set a default CMEK (create the KMS key first)
# Give projects/PRJ/locations/LOC/keyRings/RING/cryptoKeys/KEY
gcloud storage buckets update gs://${BUCKET} \
--default-encryption-key=${KMS_KEY_RESOURCE}
# Grant IAM (for example, object admin on the runner service account)
gsutil iam ch serviceAccount:${TF_SA}:roles/storage.objectAdmin gs://${BUCKET}
By Terraform syntax, the backend block cannot use variables or references. Substitute values via terraform init's -backend-config. The pragmatic pattern is to hardcode most of it and inject only secrets or per-environment differences via -backend-config.
Migration from existing local state can be performed safely with terraform init -migrate-state. On the first init, if existing state is present, you will be prompted to confirm the copy.
Partial backend definition and init command examples
# main.tf (partially pinned)
terraform {
backend "gcs" {
bucket = "my-tfstate-bucket"
prefix = "team1/appA"
# credentials are passed on the CLI side (not needed when using ADC)
}
}
# Inject per-environment differences at init time (when not using ADC)
terraform init \
-backend-config="credentials=/secure/path/sa-key.json"
# Migrating from an existing local state
terraform init -migrate-state
# Using backend.hcl (for CI)
# backend.hcl
# bucket = "my-tfstate-bucket"
# prefix = "team1/appA"
# credentials = "/secure/path/sa-key.json"
terraform init -backend-config=backend.hcl
ADC (Application Default Credentials) is recommended. Locally, use gcloud auth application-default login. On GCE/Cloud Run/Cloud Build, grant least-privilege roles to the runtime service account, and you can omit credentials from the backend block.
Avoid embedding key files in CI/CD; use Workload Identity Federation or run directly as a service account. If you absolutely must use a key file, store it in Secret Manager or similar and have a rotation plan in place.
Representative authentication examples for local and CI
# Local (ADC)
# Authenticate in the browser and set up ADC
gcloud auth application-default login
# init without credentials
terraform init
# Using a service account key file (discouraged, shown for completeness)
export GOOGLE_APPLICATION_CREDENTIALS=/secure/path/sa-key.json
terraform init -backend-config="credentials=${GOOGLE_APPLICATION_CREDENTIALS}"
# Cloud Build (the service account already has permissions / ADC is used)
# Can be run as-is inside a cloudbuild.yaml step
- name: hashicorp/terraform:light
entrypoint: bash
args:
- -c
- |
terraform init -backend-config=backend.hcl
terraform plan -input=false
Locks are normally released automatically, but they can linger after abnormal process termination. The error message includes the lock ID; once you have verified it is safe to clear, run force-unlock.
If you accidentally update state, you can recover via GCS object versioning by fetching a previous version. Rather than overwriting directly, copy to a different name first, then consider terraform state push/pull (push is usually disabled, so handle recovery carefully).
Unlocking and version recovery examples
# Release the lock (use the ID from the error message)
terraform force-unlock 12345678-90ab-cdef-1234-567890abcdef
# Fetch the current state
terraform state pull > current.tfstate
# List every generation of the object (-a)
gsutil ls -a gs://${BUCKET}/${PREFIX}/default.tfstate
# Copy a specific generation (#NUM) under another name to compare
gsutil cp gs://${BUCKET}/${PREFIX}/default.tfstate#NUM ./recovered.tfstate
# Review the diff by eye, then choose the recovery path carefully (porting entries by hand, for example)
Questions concentrate on the basics: backend immutability, locking, recovery, and authentication. For design-choice questions, the key is whether you pick versioning, least privilege, and keyless operation (ADC / Workload Identity).
Common topics include the correct use of migration and reinitialization flags (-migrate-state vs -reconfigure), the fact that variables cannot be used in the backend block, and the relationship between workspaces and prefix.
Reinitialization and backend config swap (frequently tested)
# Re-initialize explicitly after changing the backend config (a prefix change, for example)
terraform init -reconfigure -backend-config=backend.hcl
# Migrate an existing local state to GCS
terraform init -migrate-state
Associate / Pro
Question 1
Which of the following is a correct statement about Terraform's GCS backend?
Correct answer: A
A is correct. The GCS backend supports locking, and GCS object versioning is effective for recovery; the backend block is static and cannot use variables. B is wrong (KMS is set via bucket default encryption). C is wrong (ADC / Workload Identity is recommended; a key file is not required). D is wrong (terraform init -migrate-state migrates safely).
Should I split buckets per environment, or separate them by prefix/workspace?
From a confidentiality and blast-radius standpoint, it is safer to split prod from non-prod into separate buckets. Within a bucket, organize further with prefix and workspace. A single bucket is fine if you can cleanly enforce access boundaries via IAM, but per-environment buckets are easier to manage given the risk of misconfigured permissions.
Can I run from CI without a service account key file?
Yes. On Cloud Build/Run/GCE and similar runtimes, grant the runtime service account least-privilege roles and authenticate via ADC (Application Default Credentials); no key file is needed. For stricter setups, keyless operation via Workload Identity Federation is recommended.
Is it OK to use a multi-region bucket?
Yes, it works. Choose based on where your team runs Terraform, latency requirements, and data residency requirements. GCS is strongly consistent so there are basically no feature-level limitations, but cost and latency characteristics change with region selection.
Practice with certification-focused question sets
Try free questionsNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...