State & Configuration
State management is one of those topics that seems boring until it becomes critical. Most people start with Terraform storing state locally and everything works fine—until they need to collaborate with teammates, or their laptop crashes, or they accidentally run terraform destroy
in the wrong directory. Suddenly, that innocent-looking terraform.tfstate
file becomes the most important file in your project.
Understanding state isn’t just about avoiding disasters (though it definitely helps with that). It’s about building infrastructure systems that can evolve, scale, and be maintained by teams over time. The patterns in this part separate hobby projects from production-ready infrastructure.
Understanding Terraform State
Terraform state is a JSON file that maps your configuration to real-world resources. When you run terraform apply
, Terraform doesn’t just create resources—it records what it created, with all the details the cloud provider returned.
Here’s why this matters: cloud APIs are eventually consistent and don’t always return complete information. The state file gives Terraform a reliable source of truth about what exists and what properties those resources have.
# Look at your state file (but never edit it directly)
terraform show
# See the raw state data
cat terraform.tfstate
The state file contains sensitive information—resource IDs, IP addresses, and sometimes even passwords. Treat it like you would treat database credentials.
Remote State Backends
Storing state locally works for learning, but it’s a disaster waiting to happen in real projects. What happens when your laptop crashes? What happens when your teammate needs to make changes? Remote backends solve these problems by storing state in a shared, durable location.
The most common backend is S3 with DynamoDB for locking:
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
This configuration stores your state in S3 and uses DynamoDB to prevent multiple people from running Terraform at the same time (which would corrupt the state).
Setting up the backend requires creating the S3 bucket and DynamoDB table first. It’s a chicken-and-egg problem that most teams solve by creating these resources manually or with a separate “bootstrap” Terraform configuration.
Variables and Input Flexibility
Hard-coding values in your Terraform configuration makes it brittle. Variables let you create flexible configurations that work across different environments and use cases.
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
default = "dev"
}
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
variable "allowed_cidr_blocks" {
description = "CIDR blocks allowed to access the application"
type = list(string)
default = ["10.0.0.0/8"]
}
Variables have types (string, number, bool, list, map, object), descriptions, defaults, and validation rules. Good variable design makes your configurations self-documenting and prevents common mistakes.
Use variables in your resources:
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-12345678"
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
tags = {
Name = "web-${var.environment}-${count.index + 1}"
Environment = var.environment
}
}
Providing Variable Values
There are several ways to set variable values, and Terraform has a specific precedence order:
Command line flags (highest precedence):
terraform apply -var="environment=prod" -var="instance_count=3"
Variable files:
# terraform.tfvars
environment = "staging"
instance_count = 2
allowed_cidr_blocks = ["10.1.0.0/16", "10.2.0.0/16"]
Environment variables:
export TF_VAR_environment="dev"
export TF_VAR_instance_count=1
terraform apply
Interactive prompts (lowest precedence): Terraform will ask for values if they’re not provided elsewhere.
I recommend using .tfvars
files for each environment and keeping them in version control (except for sensitive values).
Outputs and Data Sharing
Outputs let you extract information from your Terraform configuration and share it with other systems or Terraform configurations.
output "instance_ips" {
description = "Public IP addresses of web instances"
value = aws_instance.web[*].public_ip
}
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = aws_lb.main.dns_name
sensitive = false
}
output "database_password" {
description = "Database password"
value = aws_db_instance.main.password
sensitive = true
}
Outputs appear when you run terraform apply
, and you can query them later:
# See all outputs
terraform output
# Get a specific output
terraform output instance_ips
# Get output in JSON format
terraform output -json
Sensitive outputs are hidden by default but can be revealed with the -json
flag or by setting sensitive = false
.
Local Values and Computed Data
Sometimes you need to compute values or avoid repeating complex expressions. Local values help with this:
locals {
common_tags = {
Environment = var.environment
Project = "my-app"
ManagedBy = "terraform"
}
instance_name_prefix = "${var.environment}-web"
# Complex computation
subnet_cidrs = [
for i in range(var.subnet_count) :
cidrsubnet(var.vpc_cidr, 8, i)
]
}
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-12345678"
tags = merge(local.common_tags, {
Name = "${local.instance_name_prefix}-${count.index + 1}"
})
}
Locals are computed once and can reference variables, resources, and other locals. They’re perfect for complex expressions that you use multiple times.
Data Sources and External Information
Data sources let you fetch information about existing resources that weren’t created by your current Terraform configuration:
# Get information about the default VPC
data "aws_vpc" "default" {
default = true
}
# Find the latest Amazon Linux AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Use data source values in resources
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
subnet_id = data.aws_vpc.default.main_route_table_id
instance_type = "t3.micro"
}
Data sources are read-only and are refreshed every time you run terraform plan
or terraform apply
. They’re essential for creating configurations that adapt to existing infrastructure.
Environment-Specific Configurations
Real projects need to work across multiple environments. Here’s a pattern that scales well:
project/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ └── terraform.tfvars
└── modules/
└── web-app/
├── main.tf
├── variables.tf
└── outputs.tf
Each environment directory contains a main.tf
that calls shared modules with environment-specific variables:
# environments/prod/main.tf
module "web_app" {
source = "../../modules/web-app"
environment = "prod"
instance_count = 3
instance_type = "t3.large"
allowed_cidrs = ["10.0.0.0/8"]
}
This pattern keeps environment-specific configuration separate while sharing common logic in modules.
State Management Commands
Terraform provides several commands for managing state when things go wrong:
# List resources in state
terraform state list
# Show details about a specific resource
terraform state show aws_instance.web
# Remove a resource from state (doesn't destroy the actual resource)
terraform state rm aws_instance.web
# Import an existing resource into state
terraform import aws_instance.web i-1234567890abcdef0
# Move a resource to a different address
terraform state mv aws_instance.web aws_instance.web_server
These commands are lifesavers when you need to refactor configurations or recover from mistakes.
Handling Sensitive Data
Never put secrets directly in your Terraform configuration. Use variables with sensitive values:
variable "database_password" {
description = "Database password"
type = string
sensitive = true
}
resource "aws_db_instance" "main" {
password = var.database_password
# other configuration...
}
Provide sensitive values through environment variables or secure variable files:
export TF_VAR_database_password="super-secret-password"
For production systems, consider using external secret management systems and data sources to fetch secrets at runtime.
Common State Problems and Solutions
State drift: When someone changes infrastructure outside of Terraform. Use terraform plan
regularly to detect drift, and terraform apply
to correct it.
State corruption: Usually caused by interrupted operations or concurrent runs. Always use remote backends with locking, and keep backups.
Large state files: Can slow down operations. Consider splitting large configurations into smaller, focused ones.
Sensitive data in state: State files can contain sensitive information. Encrypt your backend storage and limit access.
What’s Next
State management and configuration patterns form the foundation for everything else in Terraform. Understanding variables, outputs, and data sources lets you create flexible, reusable configurations. Proper state management prevents disasters and enables team collaboration.
In the next part, we’ll explore resources and data sources in depth, learning how to work with different types of cloud resources, handle dependencies, and manage complex infrastructure patterns. We’ll also cover lifecycle management and how to handle resources that need special treatment.