
September 10, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
Remote state is the first production decision in any Terraform rollout. Terraform azurerm and gcs state backends store your infrastructure snapshot in Azure Storage and Google Cloud Storage instead of a local terraform.tfstate file on a laptop. That matters the moment two engineers run terraform apply on the same stack, or when CI needs a shared source of truth. This guide walks through backend blocks, locking, authentication, and the trade-offs I weigh when a client spans Azure and GCP. For broader context, see our write-up on Terraform state management and remote backends.
backend block, authenticate with RBAC or service accounts, and never commit state files to Git.What are Terraform azurerm and gcs state backends?
A Terraform backend decides where state is read and written. The default local backend keeps a JSON file on disk. That works for solo experiments. It fails in teams.
The azurerm backend writes state to an Azure Storage container. The gcs backend writes state to a Google Cloud Storage bucket. Both are object-store backends. They behave like the popular S3 backend but use each cloud's native APIs, IAM, and locking primitives.
State holds resource IDs, dependency graphs, and sometimes sensitive values. Treat the backend bucket as a production asset. Back it up. Restrict access. Enable versioning. These rules apply whether you run Laravel on a VPS or a full enterprise application on multi-cloud infrastructure.
Each backend block is configured once per root module. You cannot mix backends inside one configuration. A common multi-cloud pattern uses separate root modules: one for Azure resources with an azurerm backend, one for GCP resources with a gcs backend. Tools like Terragrunt can DRY the backend config across stacks. Our Terragrunt guide covers that pattern in detail.
Why object storage beats local state
- Team members share one authoritative state file.
- CI runners do not need a copied state artifact.
- Versioning lets you roll back after a bad apply.
- Encryption and IAM keep secrets out of Git repos.
- Locking stops two applies from corrupting state.
On production deployments I maintain, losing state is worse than losing code. Code is in Git. State is not. The backend is your recovery anchor.
How do you configure the azurerm backend for remote state?
Start by creating an Azure Storage account and a private container. Terraform needs Blob Storage, not Azure Files. Enable blob versioning on the storage account. It costs little and saves you after a mistaken overwrite.
Create a resource group, storage account, and container with the Azure CLI:
az group create --name rg-tfstate-prod --location eastus
az storage account create \
--name sttfstateprod001 \
--resource-group rg-tfstate-prod \
--sku Standard_GRS \
--encryption-services blob \
--min-tls-version TLS1_2
az storage container create \
--name tfstate \
--account-name sttfstateprod001 \
--auth-mode login Assign the operator or pipeline identity the Storage Blob Data Contributor role on the container scope. Account keys work but RBAC is cleaner for CI. Keys rotate; managed identities do not leak into logs as easily.
Backend block for azurerm
Add this to your root module. Only the backend "azurerm" block belongs here. Resource definitions go elsewhere.
terraform {
required_version = ">= 1.5.0"
backend "azurerm" {
resource_group_name = "rg-tfstate-prod"
storage_account_name = "sttfstateprod001"
container_name = "tfstate"
key = "prod/network/terraform.tfstate"
}
} The key field is the blob path. Use a folder structure that mirrors environments and stacks. Example: dev/app/terraform.tfstate, prod/app/terraform.tfstate. One blob per root module keeps blast radius small.
Run terraform init after adding the backend. Terraform uploads local state on first init if a terraform.tfstate file already exists. It prompts you to migrate. Say yes. After migration, delete the local file.
Authentication options
The azurerm backend supports several auth methods. Pick one and stick with it per environment.
- Azure CLI login — fine for local dev:
az login, thenuse_azuread_auth = truein the backend block. - Service principal — set
ARM_CLIENT_ID,ARM_CLIENT_SECRET,ARM_TENANT_ID,ARM_SUBSCRIPTION_IDin CI. - OIDC federation — preferred for GitHub Actions and GitLab CI; no long-lived secrets.
- Managed identity — use when Terraform runs on an Azure VM or Container App.
Example backend block with Azure AD auth:
backend "azurerm" {
resource_group_name = "rg-tfstate-prod"
storage_account_name = "sttfstateprod001"
container_name = "tfstate"
key = "prod/app/terraform.tfstate"
use_azuread_auth = true
} State locking uses Azure Blob leases. Terraform acquires a lease during plan and apply. If another process holds the lock, you get a clear error instead of corrupted JSON. This mirrors what we document for Terraform remote state on S3 with locking, but uses native Azure APIs.
Official reference: HashiCorp documents every azurerm backend argument at developer.hashicorp.com/terraform/language/settings/backends/azurerm. Cross-check argument names before upgrading Terraform major versions.
How do you set up a gcs backend with state locking?
The gcs backend stores state in a Google Cloud Storage bucket. Create the bucket in a central project. Keep it separate from workload projects. A dedicated tf-state project with tight IAM is a pattern I recommend for any team running more than three stacks.
gcloud projects create tf-state-prod-001 --name="Terraform State"
gcloud storage buckets create gs://tf-state-prod-001 \
--project=tf-state-prod-001 \
--location=asia-south1 \
--uniform-bucket-level-access \
--public-access-prevention
gcloud storage buckets update gs://tf-state-prod-001 --versioning Enable uniform bucket-level access. Disable public access. Turn on versioning. For Nepal-based workloads, asia-south1 (Mumbai) keeps latency reasonable without leaving the region group many South Asian teams already use.
Backend block for gcs
terraform {
required_version = ">= 1.5.0"
backend "gcs" {
bucket = "tf-state-prod-001"
prefix = "prod/network"
}
} Terraform writes state to gs://tf-state-prod-001/prod/network/default.tfstate. The prefix acts like a folder. Use distinct prefixes per stack, same as the Azure key path.
GCS native locking landed in Terraform 1.5+. You no longer need a separate DynamoDB-style table. Locking is built into the backend. Upgrade if you still run an older release pinned in CI.
Service account and CI auth
Create a service account with Storage Object Admin on the state bucket only. Scope permissions to the bucket, not the whole project.
gcloud iam service-accounts create tf-state-admin \
--project=tf-state-prod-001
gcloud storage buckets add-iam-policy-binding gs://tf-state-prod-001 \
--member="serviceAccount:tf-state-admin@tf-state-prod-001.iam.gserviceaccount.com" \
--role="roles/storage.objectAdmin" In CI, use Workload Identity Federation instead of downloading JSON keys. Keys rot in ticket queues and leak in build logs. Google documents WIF setup at cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines.
For local development, gcloud auth application-default login sets Application Default Credentials. Terraform picks them up automatically.
Validate JSON state files in CI with a linter before apply. Our JSON formatter and validator helps inspect exported state snippets during incident review. Never paste full production state into public tools.
How do azurerm and gcs state backends compare for multi-cloud teams?
Both backends solve the same problem on different clouds. Your choice usually follows where the infrastructure lives, not abstract preference. State should sit in the same cloud as the resources it tracks when possible. Cross-cloud state adds latency, auth complexity, and a single point that spans two billing accounts.
| Criteria | azurerm backend | gcs backend |
|---|---|---|
| Storage service | Azure Blob Storage | Google Cloud Storage |
| Locking mechanism | Blob lease | Native GCS lock (Terraform 1.5+) |
| Path config key | key (blob name) | prefix (folder path) |
| Recommended auth | Azure AD / OIDC / managed identity | Workload Identity Federation |
| Encryption | Microsoft-managed or CMK | Google-managed or CMEK |
| Versioning | Blob versioning on storage account | Object versioning on bucket |
| Multi-region | GRS / GZRS replication | Dual-region or multi-region buckets |
| Typical cost | Low — pennies per GB/month | Low — similar object-store pricing |
For a team provisioning Azure VMs and GCP Kubernetes clusters, run two root modules. Each gets its own backend in its own cloud. Use terraform_remote_state data sources to read outputs across stacks. Avoid one mega-module spanning both providers with a single backend. That coupling hurts.
Our multi-cloud state management guide expands on directory layout and remote state data sources. The same principles apply whether you deploy to AWS, Azure, or GCP.
On a booking platform I helped architect, Azure hosted the public site while analytics sat in GCP BigQuery. Two backends, two state files, one pipeline matrix. Clean boundaries kept permissions simple. Each pipeline job only needed credentials for one cloud.
How do you secure and troubleshoot remote state in production?
State files often contain database connection strings, private IPs, and resource IDs attackers can probe. Treat read access like production database access.
Security checklist
- Enable versioning on every state bucket or storage account.
- Block public access at the account and bucket level.
- Use RBAC scoped to the container or bucket, not subscription-wide admin.
- Enable encryption with customer-managed keys for regulated workloads.
- Turn on access logging and send logs to a SIEM or storage audit bucket.
- Scan IaC for misconfigurations before merge — see our Checkov scanning guide.
- Never commit
*.tfstateor*.tfstate.backupto Git. Add them to.gitignore.
Run terraform state pull | jq . during incidents to inspect live state. Pipe output through a local formatter. Redact before sharing in tickets.
Common errors and fixes
Error acquiring the state lock. A crashed CI job left a stale lease. For azurerm, break the lease in the Azure Portal under the blob's lease menu. For gcs, use terraform force-unlock LOCK_ID only after confirming no active apply runs.
403 Forbidden on init. The identity lacks write permission. Verify role assignment scope matches the exact container or bucket. Azure AD propagation can take a few minutes after role assignment.
Backend configuration changed. Terraform refuses silent backend moves. Run terraform init -migrate-state when renaming keys or prefixes. Test in a non-production stack first.
State drift from manual console edits. Run terraform plan on a schedule. A nightly plan in CI catches drift before it compounds. Wire this into your Terraform CI/CD pipeline.
In my experience working on production systems, most state disasters come from permissions that are too broad, not too narrow. A developer who can overwrite prod state can also destroy prod infrastructure. Separate dev and prod state containers. Use Terraform workspaces or directory-based environments consistently.
For teams that also run Linux VPS workloads alongside cloud resources, backend setup sits next to server hardening and backup policy. Our Linux system administration service covers the ops layer that Terraform does not manage.
Projects like Adventure Third Pole Trek run on Laravel with managed infrastructure underneath. Even application-heavy shops benefit from remote state when staging and production environments multiply.
Partial backend configuration for CI
Do not hardcode secrets in backend blocks. Use partial configuration and pass values at init time:
terraform {
backend "azurerm" {}
} Then in CI:
terraform init \
-backend-config="resource_group_name=rg-tfstate-prod" \
-backend-config="storage_account_name=sttfstateprod001" \
-backend-config="container_name=tfstate" \
-backend-config="key=prod/app/terraform.tfstate" \
-backend-config="use_azuread_auth=true" Store backend config in encrypted CI variables. The same pattern works for gcs with -backend-config="bucket=..." and -backend-config="prefix=...".
If you evaluate open-source forks, OpenTofu supports the same azurerm and gcs backends with compatible syntax. Read our OpenTofu comparison before switching runtimes in production.
HashiCorp's full backend catalog lives at developer.hashicorp.com/terraform/language/settings/backends. Bookmark it when onboarding new team members.
Key Takeaways
- Store state in Azure Blob (azurerm) or GCS (gcs) — never commit state files to Git.
- Enable versioning and locking on every remote backend before the first production apply.
- Co-locate state with the cloud where resources run; split backends for multi-cloud setups.
- Prefer OIDC and Workload Identity over long-lived keys for CI authentication.
- Use partial backend config in pipelines so storage account names stay out of source code.
- Schedule nightly
terraform planjobs to catch drift and stale locks early.
People Also Ask
Can Terraform use both azurerm and gcs backends in one project?
No. Each root module supports exactly one backend block. Multi-cloud teams run separate root modules — one per cloud — each with its own azurerm or gcs backend. Cross-stack dependencies use terraform_remote_state data sources to read outputs.
Does the gcs backend require a separate database for locking?
Not on Terraform 1.5 and later. GCS native locking is built in. Older setups sometimes used alternative patterns. Upgrade Terraform and enable versioning on the bucket instead of maintaining extra lock infrastructure.
What happens to local state after migrating to a remote backend?
terraform init -migrate-state copies local state to the remote bucket, then uses the remote copy on every subsequent run. Delete the local terraform.tfstate file after confirming the remote object exists. Keep versioning enabled so you can restore if migration goes wrong.
Is remote state encrypted in transit and at rest?
Yes. Both Azure Blob Storage and Google Cloud Storage encrypt data at rest by default. Traffic uses HTTPS. For compliance requirements, configure customer-managed encryption keys on the storage account or bucket and restrict key access through each cloud's KMS service.
Build multi-cloud infrastructure with confidence
Terraform azurerm and gcs state backends are not optional extras for team-based IaC. They are the foundation that makes shared, locked, recoverable state possible across Azure and Google Cloud. Start with versioning and RBAC on day one. Split backends by cloud. Wire CI auth through federation, not static keys.
If you are standing up Terraform pipelines alongside application delivery — Laravel, APIs, or eCommerce — the backend choice affects every deploy that follows. Read our practical Terraform IaC guide and state safety checklist for the full workflow.
Need help designing remote state layout, CI pipelines, or multi-cloud infrastructure for a production app? Contact us to discuss architecture, or explore ongoing support and maintenance for systems already in production.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

