Skip to main content
Back to module
DevOps & Cloud Delivery · Infrastructure as Code

State and modules

Reading24mLesson 2 of 3 in this module

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 apply runs 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:

CommandUse
terraform state listList resource addresses in state
terraform state show <addr>Show the recorded attributes of one resource
moved { from = ... to = ... } blockRename or move a resource in code without destroying it
import { to = ... id = ... } block or terraform importBring an existing, hand-made resource under Terraform
terraform plan -refresh-onlySee 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 apply at once.
  • One state file for every environment.
  • Renaming resources without moved blocks 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/import blocks, 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.

Also in this lesson: