05 · Remote State & Backends
Local state works for solo experimentation. The moment two people run terraform apply, or you need consistent state between CI/CD and local machines, you need a remote backend. Remote backends are also the mechanism by which independent Terraform configurations can share data.
Why Remote State
Three problems with local state that remote backends solve:
Concurrent modification: two applies on the same local state file will corrupt it. Remote backends implement distributed locking.
Accessibility: local state lives on your machine. Your CI/CD pipeline, your teammates, and your disaster recovery process can't reach it. Remote backends store state in a shared, accessible location.
Security: a local state file containing database passwords is an unencrypted file sitting on someone's laptop. Remote backends support encryption at rest and access controls.
Terraform Cloud as a Backend
Terraform Cloud is the simplest remote backend for most teams — it handles storage, locking, and encryption without managing separate infrastructure.
Beyond state storage, Terraform Cloud adds: - VCS-driven runs: connects to a git repo and plans/applies automatically on push - Policy as code: enforce rules before apply (e.g., "no instances larger than t3.medium in dev") - Audit logging: who ran what, when, and what changed - Team access controls: who can plan vs. apply vs. manage variables
The free tier supports unlimited state storage and single-user workspaces. For teams, the paid tier adds RBAC and policy enforcement.
S3 + DynamoDB (AWS)
The standard self-managed backend for AWS users:
terraform {
backend "s3" {
bucket = "my-tfstate-bucket"
key = "services/api/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
kms_key_id = "arn:aws:kms:..." # optional CMK for encryption
}
}
The S3 bucket stores the state file; the DynamoDB table implements locking. Best practice for the bucket: - Versioning enabled (natural state history and rollback) - Public access blocked - Server-side encryption with KMS - Bucket policy limiting access to specific IAM roles
Sharing State Between Configurations
Large infrastructures are split into multiple Terraform configurations (stacks): one for networking, one for databases, one for applications. These configurations need to reference each other's outputs.
The terraform_remote_state data source reads outputs from another configuration's state:
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-tfstate-bucket"
key = "core/network/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
...
}
The app configuration doesn't manage the VPC or subnets — it just reads the output from the configuration that does. This is the standard pattern for sharing values across Terraform boundaries.
Tight coupling via remote state
terraform_remote_state creates a read dependency between configurations. If the network configuration changes its output names or structure, the app configuration breaks. Design outputs as a stable interface — version them if needed, and avoid leaking internal implementation details.
Workspaces and State Isolation
Each Terraform workspace has its own state file. Workspaces let you manage multiple environments (dev, staging, production) from the same configuration directory:
When using workspaces, the terraform.workspace value can be used to vary configuration:
However, workspaces have limitations — they share the same provider configuration and variable definitions, just with different state. For environments with significantly different configurations, separate directories are often cleaner (covered in the next chapter).
Backend Migration
Moving state from one backend to another is safe:
Terraform prompts you to confirm, copies the state to the new backend, and removes it from the old location. The migration is atomic — if it fails, the original state is preserved.
This is how you graduate from local state to a remote backend: change the backend block, run init -migrate-state, done.