Remote state & locking
What we’re building
Section titled “What we’re building”The remote_state block in each cloud’s root.hcl — the piece that moves state off your laptop and into S3, GCS, and Azure Blob Storage, each with locking so two applies can’t collide. We’ll use path_relative_to_include() so every unit’s state lands at a path that mirrors the filesystem, and generate the right provider per cloud.
This closes the Terragrunt foundation. After this, every module in the course inherits remote, locked, encrypted state for free — the network, cluster, data, and iam layers just work.
Local state failed the two tests from Module 2: it can’t be shared and it isn’t encrypted. Remote state fixes both. State lives in a cloud bucket, encrypted at rest; locking ensures that while one apply holds the lock, a second one waits instead of corrupting the file. That’s what lets a teammate — or a CI job — apply safely without clobbering you.
Each cloud keeps its own backend: AWS state in S3, GCP state in GCS, Azure state in Azure Storage. This isn’t just tidiness — it’s blast-radius isolation and data residency. A problem with one cloud’s backend can’t take down the other two, and each cloud’s state stays in that cloud.
path_relative_to_include() is the small function that makes this scale. It returns a unit’s path relative to the root.hcl it includes, so live/aws/network writes its state to network/terraform.tfstate and live/aws/cluster to cluster/terraform.tfstate — automatically, with no per-unit backend config.
Pros & cons
Section titled “Pros & cons”Remote state with locking vs. local state:
- Pros: Shared, encrypted, and safe for concurrent applies. State survives a lost laptop, and CI can apply against the same source of truth you do.
- Cons: You must bootstrap the backend first (the chicken-and-egg below), and a stuck lock occasionally needs
terragrunt force-unlock. A small operational cost for a large safety gain.
Each cloud keeps its own backend vs. one central backend for all three:
- Pros: Isolation — one backend’s outage or misconfiguration can’t affect the others — plus data residency and IAM that stay within each cloud.
- Cons: Three backends to create and secure instead of one. We accept it: centralizing three clouds’ state in a single provider would hand that provider the keys to the whole platform, which defeats the point of multi-cloud.
Set it up
Section titled “Set it up”1. Bootstrap the backends
Section titled “1. Bootstrap the backends”The backend has to exist before Terraform can store state in it — you can’t manage the state bucket with the state that lives in it. Create each once, by hand:
# AWS — S3 bucket + DynamoDB lock tableaws s3api create-bucket --bucket clouddeploy-tfstate-aws --region us-east-1aws s3api put-bucket-versioning --bucket clouddeploy-tfstate-aws \ --versioning-configuration Status=Enabledaws dynamodb create-table --table-name clouddeploy-locks \ --attribute-definitions AttributeName=LockID,AttributeType=S \ --key-schema AttributeName=LockID,KeyType=HASH \ --billing-mode PAY_PER_REQUEST --region us-east-1
# GCP — GCS bucket (locking is built in, no lock table needed)gcloud storage buckets create gs://clouddeploy-tfstate-gcp \ --location=us-central1 --uniform-bucket-level-access
# Azure — storage account + container (blob leasing provides locking)az group create --name clouddeploy-tfstate --location eastusaz storage account create --name clouddeploytfstate \ --resource-group clouddeploy-tfstate --location eastus --sku Standard_LRSaz storage container create --name tfstate --account-name clouddeploytfstate2. live/aws/root.hcl — S3 backend + DynamoDB lock
Section titled “2. live/aws/root.hcl — S3 backend + DynamoDB lock”remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { bucket = "clouddeploy-tfstate-aws" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "clouddeploy-locks" }}
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "aws" { region = "us-east-1"}EOF}Recent Terraform can lock S3 state natively with
use_lockfile = true, retiring the separate DynamoDB table. This course keepsdynamodb_tablebecause it’s still the most widely deployed setup you’ll meet — but on a greenfield backend, the native lockfile is the simpler choice.
3. live/gcp/root.hcl — GCS backend + google provider
Section titled “3. live/gcp/root.hcl — GCS backend + google provider”GCS locks state natively, so there’s no lock table to manage:
remote_state { backend = "gcs" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { bucket = "clouddeploy-tfstate-gcp" prefix = "${path_relative_to_include()}" }}
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "google" { project = "YOUR_GCP_PROJECT_ID" region = "us-central1"}EOF}4. live/azure/root.hcl — azurerm backend + azurerm provider
Section titled “4. live/azure/root.hcl — azurerm backend + azurerm provider”Azure locks state via blob leasing, also built in:
remote_state { backend = "azurerm" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { resource_group_name = "clouddeploy-tfstate" storage_account_name = "clouddeploytfstate" container_name = "tfstate" key = "${path_relative_to_include()}/terraform.tfstate" }}
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "azurerm" { features {}}EOF}Same shape on every cloud: a remote_state block whose key comes from path_relative_to_include(), and a generate "provider" block. Only the backend type and provider differ.
Verify
Section titled “Verify”Re-initialize the AWS network unit; Terragrunt now configures the S3 backend and, if you had local state, offers to migrate it:
cd live/aws/networkterragrunt initInitializing the backend...Successfully configured the backend "s3"! Terraform will automaticallyuse this backend unless the backend configuration changes.Apply, then confirm the state object actually landed in the bucket — mirroring the unit’s path:
terragrunt applyaws s3 ls s3://clouddeploy-tfstate-aws/network/# → terraform.tfstateProve locking works: start an apply and, while it’s mid-run, launch a second one in another terminal. The second refuses to proceed:
Error acquiring the state lockLock Info: ID: 3f2a... Operation: OperationTypeApply Who: you@laptop ...Finally, plan the whole cloud now that state is remote and locked:
cd live/awsterragrunt run --all plan # No changes across the tree — state matches reality.You’re done when init reports the backend configured, the state object appears in the bucket at the unit’s path, and a second concurrent apply is blocked by the lock.
Check your understanding:
- Why must you create the state bucket and lock table before Terraform can use them — and how would you manage them without a chicken-and-egg problem?
- What does state locking prevent, and how do the S3, GCS, and azurerm backends each provide it?
- What does
path_relative_to_include()compute, and why does using it in the state key scale to a whole tree of units? - Give two reasons each cloud keeps its own backend instead of pooling all three clouds’ state in one.
State now lives in each cloud’s backend, encrypted and locked, with keys laid out to mirror the live/ tree — written once in root.hcl and inherited by every unit. That completes the IaC foundation: reusable modules, DRY Terragrunt units, and remote locked state on all three clouds. Every module from here builds on it. Next we write the first real infrastructure module — the network — and stand it up three ways behind one interface.
Next: Networking →