Explain what Terraform state is, why teams keep it in a locked remote backend, and how modules package reusable infrastructure.
Why it matters
Terraform felt simple in the previous lesson because one person ran it on one laptop. On a team, two questions appear quickly. Where does Terraform's record of what it manages live, so that everyone sees the same thing? And how do you avoid copying the same 200 lines of network code into every environment? The answers are remote state and modules. Both come up in nearly every cloud engineering interview and in the Terraform Associate exam.
What state is
Terraform keeps a state file that maps each resource address in your code (docker_container.web) to a real object (a container ID, an AWS instance ID) together with its last known attributes. Terraform uses state to:
- know which real objects it owns, so it updates them instead of creating duplicates;
- work out the plan quickly without asking the API about every attribute;
- track dependencies, so it destroys things in the right order.
By default state is a local file, terraform.tfstate. That is fine for practice and wrong for a team:
- If two people have different copies, they will make conflicting changes.
- If the laptop dies, Terraform forgets what it created, and the resources keep running (and billing) with nothing tracking them.
- State often contains secrets in plain text: database passwords, private keys, connection strings. It must not live in Git or on shared drives.
Remote backends and locking
A backend tells Terraform where to store state. Teams use a remote backend:
terraform {
backend "s3" {
bucket = "acme-terraform-state"
key = "networking/prod.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
Common choices are an S3 bucket (AWS), an Azure Storage container (azurerm), a Google Cloud Storage bucket (gcs), or HCP Terraform. A good backend gives you:
- One shared copy that everyone and every CI job reads.
- Locking, so two
applyruns cannot write at the same time and corrupt state. (The S3 backend supports a lock file in the bucket in recent Terraform versions; older setups used a DynamoDB table.) - Encryption and access control, because of the secrets inside.
- Versioning, so a damaged state can be restored.
Split state by environment and by area (for example networking/prod, app/staging). One giant state file makes every plan slow and every mistake large; this split is often called limiting the blast radius.
Working with state safely
Never edit the state file by hand. Use commands:
| Command | Use |
|---|---|
terraform state list | List resource addresses in state |
terraform state show <addr> | Show the recorded attributes of one resource |
moved { from = ... to = ... } block | Rename or move a resource in code without destroying it |
import { to = ... id = ... } block or terraform import | Bring an existing, hand-made resource under Terraform |
terraform plan -refresh-only | See drift: differences between state and reality |
The moved block is worth remembering. If you rename aws_instance.web to aws_instance.app without it, Terraform plans to destroy the old one and create a new one.
Modules
A module is a folder of .tf files. The folder you run Terraform in is the root module; folders it calls are child modules. A module is used like a function: it takes variables as inputs and returns outputs.
# modules/web/variables.tf
variable "name" { type = string }
variable "external_port" { type = number }
# modules/web/main.tf
resource "docker_container" "this" {
name = var.name
image = "nginx:1.27-alpine"
ports {
internal = 80
external = var.external_port
}
}
# modules/web/outputs.tf
output "url" { value = "http://localhost:${var.external_port}" }
# main.tf (root module)
module "web_staging" {
source = "./modules/web"
name = "web-staging"
external_port = 8081
}
module "web_prod" {
source = "./modules/web"
name = "web-prod"
external_port = 8082
}
output "staging_url" { value = module.web_staging.url }
The two environments now share one tested definition and differ only in their inputs. Resource addresses inside a module are prefixed, for example module.web_prod.docker_container.this.
Modules can come from a local path, a Git URL with a tag, or a registry such as the public Terraform Registry (terraform-aws-modules/vpc/aws). Pin a version (or a Git tag) for anything not local, and run terraform init again after adding or changing a module source.
Good modules are small and focused (a network, a service, a database), expose only the inputs callers need, and document their outputs. A module that wraps a single resource with no added value is usually not worth it.
Common mistakes
- Keeping team state on a laptop or in Git.
- No locking, so two pipelines run
applyat once. - One state file for every environment.
- Renaming resources without
movedblocks and accidentally destroying them. - Pulling registry modules without pinning a version.
Quick recap
- State maps code to real objects; it can hold secrets.
- Teams use an encrypted, versioned, locked remote backend, split by environment.
- Change state with commands and
moved/importblocks, never by hand. - Modules take variables, return outputs, and let environments share one definition.
Try it
This lesson has no hands-on activity. Take the quiz in the Do tab.