Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Terraform azurerm and gcs State Backends

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.

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.

Remote State ArchitectureEngineersterraform CLICI PipelineGitHub Actionsazurerm BackendAzure Blob + lease lockgcs BackendGCS bucket + native lockOne state file per stack — never in GitLocking prevents concurrent apply corruption
How Terraform azurerm and gcs state backends centralise state for engineers and CI pipelines across Azure and Google Cloud.

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.

  1. Azure CLI login — fine for local dev: az login, then use_azuread_auth = true in the backend block.
  2. Service principal — set ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID in CI.
  3. OIDC federation — preferred for GitHub Actions and GitLab CI; no long-lived secrets.
  4. 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.

azurerm Backend Flowterraform initterraform planAcquire leaseterraform applyAzure Blob Storage Containerprod/app/terraform.tfstateRelease lease — unlock for next run
azurerm backend lifecycle: init connects to Azure Storage, plan/apply acquire a blob lease, then release it after the run completes.

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.

gcs Backend with LockingTerraform CLIplan / applyCI RunnerWIF authGCS Bucket: tf-state-prod-001Versioning on · Uniform access · Native lockprefix/prod/network/default.tfstateLock held during apply — second run waits or fails
GCS backend stores versioned state objects and uses native locking so concurrent Terraform runs cannot overwrite each other.

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.

Criteriaazurerm backendgcs backend
Storage serviceAzure Blob StorageGoogle Cloud Storage
Locking mechanismBlob leaseNative GCS lock (Terraform 1.5+)
Path config keykey (blob name)prefix (folder path)
Recommended authAzure AD / OIDC / managed identityWorkload Identity Federation
EncryptionMicrosoft-managed or CMKGoogle-managed or CMEK
VersioningBlob versioning on storage accountObject versioning on bucket
Multi-regionGRS / GZRS replicationDual-region or multi-region buckets
Typical costLow — pennies per GB/monthLow — 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.

Backend Choice DecisionWhere do resources live?Azure onlyUse azurermGCP onlyUse gcsBoth cloudsSplit backendsNever store Azure state in GCS unlesscompliance forces a central non-cloud vault
Choose Terraform azurerm and gcs state backends based on where managed resources run — co-locate state with infrastructure for simpler IAM.

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

  1. Enable versioning on every state bucket or storage account.
  2. Block public access at the account and bucket level.
  3. Use RBAC scoped to the container or bucket, not subscription-wide admin.
  4. Enable encryption with customer-managed keys for regulated workloads.
  5. Turn on access logging and send logs to a SIEM or storage audit bucket.
  6. Scan IaC for misconfigurations before merge — see our Checkov scanning guide.
  7. Never commit *.tfstate or *.tfstate.backup to 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 plan jobs 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

They are remote backends that store Terraform state in Azure Blob Storage (azurerm) or Google Cloud Storage (gcs) instead of a local terraform.tfstate file on disk.

No. Each root module supports exactly one backend block. Multi-cloud teams run separate root modules, each with its own backend in the matching cloud.

Both are low-cost object storage — typically pennies per GB per month for state files, which are usually small JSON blobs.

Create an Azure Storage account with a private container, enable blob versioning, and assign Storage Blob Data Contributor at container scope. Add a backend azurerm block with resource_group_name, storage_account_name, container_name, and key — the key is the blob path, such as prod/app/terraform.tfstate. Run terraform init to connect; Terraform prompts to migrate existing local state on first run. Set required_version to at least 1.5.0. Prefer RBAC or Azure AD auth over storage account keys, especially in CI pipelines where keys rotate and can leak into logs.

Create a dedicated GCS bucket in a central tf-state project with uniform bucket-level access, public access prevention, and object versioning enabled. Add a backend gcs block with bucket and prefix fields; Terraform writes to gs://bucket/prefix/default.tfstate. Native GCS locking works in Terraform 1.5 and later — upgrade CI if you still pin an older release. Grant a service account Storage Object Admin scoped to the bucket only. For local development, run gcloud auth application-default login so Terraform picks up Application Default Credentials automatically.

Both organise state paths inside object storage, but they map to different backend APIs. The azurerm backend uses key as the full blob name, for example prod/network/terraform.tfstate. The gcs backend uses prefix as a folder path; Terraform appends default.tfstate automatically, so prefix prod/network becomes gs://bucket/prod/network/default.tfstate. Use distinct keys or prefixes per root module and environment so each stack has its own state file and a smaller blast radius if something goes wrong during apply or migration.

Both prevent concurrent terraform apply runs from corrupting shared state, but the mechanisms differ. The azurerm backend acquires an Azure Blob lease during plan and apply, then releases it when the run finishes. If another process holds the lease, Terraform returns a clear lock error instead of overwriting JSON mid-write. The gcs backend uses native GCS locking built into Terraform 1.5+, so you no longer need a separate DynamoDB-style lock table. In practice, always confirm no active apply is running before breaking a stale Azure lease or running terraform force-unlock on GCS.

Avoid long-lived storage account keys in CI variables. The article recommends OIDC federation for GitHub Actions and GitLab CI because it eliminates secrets that rot in ticket queues. Service principals work by setting ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, and ARM_SUBSCRIPTION_ID, but someone must rotate those credentials. Managed identity fits when Terraform runs on an Azure VM or Container App. For local development, az login plus use_azuread_auth = true in the backend block is fine. Assign Storage Blob Data Contributor on the container scope, not subscription-wide admin.

Use Workload Identity Federation instead of downloading service account JSON keys. Keys frequently end up in build logs and shared ticket queues, and they are painful to rotate under pressure. Create a service account with Storage Object Admin on the state bucket only — not the entire project — then configure WIF per Google’s deployment-pipeline documentation. For local runs, gcloud auth application-default login sets Application Default Credentials that Terraform reads automatically. Keep the state bucket in a dedicated tf-state project with tight IAM so pipeline jobs never need broad project-level permissions just to read and write state.

Both solve the same remote-state problem on different clouds using object storage, encryption at rest, versioning, and locking. Choose based on where managed resources live, not personal preference. Co-locate state with infrastructure: Azure stacks use azurerm on Blob Storage with blob-lease locking and a key path; GCP stacks use gcs with native locking and a prefix path. Cross-cloud state adds latency, dual billing, and auth complexity. Run separate root modules per cloud and read outputs across stacks with terraform_remote_state. Terragrunt can DRY backend configuration across many stacks without coupling Azure and GCP into one mega-module.

Yes, when possible. The article recommends keeping state in the same cloud as the resources it tracks because cross-cloud state introduces extra latency, authentication complexity, and a dependency spanning two billing accounts. For a team provisioning Azure VMs and GCP Kubernetes clusters, run two root modules — each with its own backend in its own cloud — and wire them together with terraform_remote_state data sources. Each pipeline job then needs credentials for only one cloud, which keeps permissions simpler and reduces the blast radius if a CI secret leaks or a role assignment is misconfigured.

Terraform 1.5.0 or higher. GCS native locking landed in Terraform 1.5+, replacing the older pattern that required a separate lock mechanism similar to DynamoDB on AWS. Both backend examples in the article set required_version to at least 1.5.0. If your CI pipeline still pins an older Terraform release, upgrade before relying on built-in GCS locking. The same version floor applies whether you use HashiCorp Terraform or OpenTofu, which supports compatible azurerm and gcs backend syntax — but verify argument names against the official backend catalog before major version upgrades.

Add the backend block to your root module, then run terraform init. If a local terraform.tfstate file already exists, Terraform detects it and prompts you to migrate state to the remote backend — accept the migration. After a successful init, delete the local state file and confirm it is listed in .gitignore alongside any backup files. Never commit state to Git; remote backends exist precisely so teams and CI share one authoritative copy. If you later rename the key or prefix, run terraform init -migrate-state and test the move in a non-production stack first to avoid surprises.

A crashed CI job often leaves a stale lock behind. First confirm no active terraform apply or plan is still running anywhere in your pipeline or on a developer laptop. For azurerm, break the blob lease in the Azure Portal under the affected blob’s lease menu. For gcs, use terraform force-unlock with the lock ID shown in the error — only after you are certain nothing is applying. Schedule nightly terraform plan jobs in CI to catch drift and stale locks early. Separating dev and prod state containers or prefixes also limits how much damage one stuck lock can cause across environments.

State files often hold database connection strings, private IPs, and resource IDs attackers can probe, so treat read access like production database access. Enable versioning on every storage account or bucket, block public access at account and bucket level, and scope RBAC to the container or bucket — not subscription-wide admin. Use customer-managed encryption keys for regulated workloads and send access logs to a SIEM or audit bucket. Never commit terraform.tfstate files to Git. In CI, use partial backend configuration passed at init time so storage account names stay out of source code. Separate dev and prod state paths consistently.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: