Skip to content

Remote state & locking

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.

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.

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:

Terminal window
# AWS — S3 bucket + DynamoDB lock table
aws s3api create-bucket --bucket clouddeploy-tfstate-aws --region us-east-1
aws s3api put-bucket-versioning --bucket clouddeploy-tfstate-aws \
--versioning-configuration Status=Enabled
aws 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 eastus
az storage account create --name clouddeploytfstate \
--resource-group clouddeploy-tfstate --location eastus --sku Standard_LRS
az storage container create --name tfstate --account-name clouddeploytfstate

2. live/aws/root.hcl — S3 backend + DynamoDB lock

Section titled “2. live/aws/root.hcl — S3 backend + DynamoDB lock”
live/aws/root.hcl
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 = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}

Recent Terraform can lock S3 state natively with use_lockfile = true, retiring the separate DynamoDB table. This course keeps dynamodb_table because 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:

live/gcp/root.hcl
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 = <<EOF
provider "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:

live/azure/root.hcl
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 = <<EOF
provider "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.

Re-initialize the AWS network unit; Terragrunt now configures the S3 backend and, if you had local state, offers to migrate it:

Terminal window
cd live/aws/network
terragrunt init
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Apply, then confirm the state object actually landed in the bucket — mirroring the unit’s path:

Terminal window
terragrunt apply
aws s3 ls s3://clouddeploy-tfstate-aws/network/
# → terraform.tfstate

Prove 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 lock
Lock Info:
ID: 3f2a... Operation: OperationTypeApply
Who: you@laptop ...

Finally, plan the whole cloud now that state is remote and locked:

Terminal window
cd live/aws
terragrunt 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:

  1. 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?
  2. What does state locking prevent, and how do the S3, GCS, and azurerm backends each provide it?
  3. What does path_relative_to_include() compute, and why does using it in the state key scale to a whole tree of units?
  4. 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 →