Skip to main content

Modules and State

As your infrastructure grows, you need two things: a way to reuse patterns (modules) and a safe way to track what exists (state).

Terraform Modules

A module is a container for multiple resources that are used together. Every Terraform configuration is technically a module — the root module.

Why Modules?

Without modules, your code becomes a massive flat file. Modules let you:

  • Reuse the same pattern across environments (dev, staging, prod)
  • Encapsulate complexity behind a simple interface
  • Standardize infrastructure across teams
  • Test components independently

Module Structure

modules/
└── vpc/
    ├── main.tf        # Resources
    ├── variables.tf   # Inputs
    ├── outputs.tf     # Outputs
    └── README.md      # Usage docs

Writing a Module

modules/vpc/variables.tf:

variable "name" {
  type        = string
  description = "Name prefix for all resources"
}

variable "cidr_block" {
  type        = string
  default     = "10.0.0.0/16"
  description = "VPC CIDR block"
}

variable "public_subnets" {
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
  description = "Public subnet CIDR blocks"
}

modules/vpc/main.tf:

resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  tags = { Name = "${var.name}-vpc" }
}

resource "aws_subnet" "public" {
  count                   = length(var.public_subnets)
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnets[count.index]
  map_public_ip_on_launch = true
  tags = { Name = "${var.name}-public-${count.index}" }
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  tags   = { Name = "${var.name}-igw" }
}

modules/vpc/outputs.tf:

output "vpc_id" {
  value = aws_vpc.this.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

Using a Module

module "production_vpc" {
  source         = "./modules/vpc"
  name           = "production"
  cidr_block     = "10.0.0.0/16"
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
}

module "staging_vpc" {
  source         = "./modules/vpc"
  name           = "staging"
  cidr_block     = "10.1.0.0/16"
  public_subnets = ["10.1.1.0/24"]
}

# Reference module outputs
resource "aws_instance" "web" {
  subnet_id = module.production_vpc.public_subnet_ids[0]
  # ...
}

Registry Modules

The Terraform Registry has thousands of pre-built modules:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.16.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]

  enable_nat_gateway = true
}

Terraform State

State is how Terraform knows what infrastructure it manages. It's stored in a file called terraform.tfstate.

What State Tracks

{
  "resources": [
    {
      "type": "aws_instance",
      "name": "web",
      "instances": [
        {
          "attributes": {
            "id": "i-0abc123def456",
            "ami": "ami-0c55b159cbfafe1f0",
            "instance_type": "t3.micro",
            "public_ip": "54.210.100.50"
          }
        }
      ]
    }
  ]
}

State maps your configuration to real-world resources. Without it, Terraform wouldn't know that aws_instance.web corresponds to i-0abc123def456.

The Problem with Local State

By default, state is stored locally in terraform.tfstate. This breaks down with teams:

ProblemWhat Happens
No sharingOnly you can run Terraform
No lockingTwo people apply at once, infrastructure corrupts
No backupLaptop dies, state is gone, orphaned resources everywhere
Secrets in statePasswords and keys stored in a local file

Remote Backends

Store state remotely so your team can share it safely.

S3 + DynamoDB (AWS):

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

The DynamoDB table provides state locking — only one person can modify state at a time.

Terraform Cloud:

terraform {
  cloud {
    organization = "my-org"
    workspaces {
      name = "production"
    }
  }
}

Terraform Cloud handles state storage, locking, encryption, and access control.

State Locking

When someone runs terraform apply, the state is locked:

User A: terraform apply
   Acquires lock 
   Makes changes
   Releases lock 

User B: terraform apply (during User A's apply)
  → Lock already held ✗
"Error: Error acquiring the state lock"

If a lock gets stuck (crashed process), force-unlock it:

terraform force-unlock LOCK_ID

State Commands

# List all resources in state
terraform state list

# Show details of a specific resource
terraform state show aws_instance.web

# Move a resource (rename without destroying)
terraform state mv aws_instance.web aws_instance.app

# Remove a resource from state (keeps the real resource)
terraform state rm aws_instance.web

# Import existing infrastructure into state
terraform import aws_instance.web i-0abc123def456

State Best Practices

  1. Always use remote state for team projects
  2. Enable encryption — state contains secrets
  3. Enable locking — prevent concurrent modifications
  4. Never edit state manually — use terraform state commands
  5. Separate state per environment — don't put dev and prod in one state file
  6. Back up state — enable versioning on your S3 bucket

Environment Separation

Use separate state files for each environment:

environments/
├── dev/
   ├── main.tf       # source = "../../modules/..."
   └── terraform.tfvars
├── staging/
   ├── main.tf
   └── terraform.tfvars
└── prod/
    ├── main.tf
    └── terraform.tfvars

Each environment has its own backend configuration and state file. A bug in dev can never corrupt prod state.

Summary

  • Modules let you reuse infrastructure patterns with different inputs
  • Write modules with variables.tf, main.tf, and outputs.tf
  • Use registry modules for common patterns (VPCs, EKS clusters, etc.)
  • State tracks the mapping between your code and real infrastructure
  • Always use remote state with locking for team projects
  • Separate state per environment to isolate blast radius