Advanced Patterns
Managing multiple environments with Terraform requires more than just copying configurations and changing a few variables. You need patterns that prevent accidents, enable safe experimentation, and scale with your team’s complexity. The techniques in this part address the challenges that emerge when Terraform moves from a personal tool to a critical part of your infrastructure workflow.
Workspaces, remote backends, and sophisticated state management aren’t just advanced features—they’re essential tools for preventing the kind of mistakes that can take down production systems. These patterns separate hobbyist Terraform usage from production-ready infrastructure management.
Terraform Workspaces
Workspaces let you manage multiple instances of the same infrastructure using a single configuration. Think of them as parallel universes for your Terraform state—same configuration, different resources.
# Create and switch to a new workspace
terraform workspace new staging
terraform workspace new production
# List workspaces
terraform workspace list
# Switch between workspaces
terraform workspace select staging
terraform workspace select production
# See current workspace
terraform workspace show
Each workspace has its own state file, so you can have identical infrastructure in different environments without conflicts:
# Use workspace name in resource naming
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
tags = {
Name = "web-${terraform.workspace}"
Environment = terraform.workspace
}
}
# Workspace-specific variables
locals {
environment_config = {
dev = {
instance_count = 1
instance_type = "t3.micro"
}
staging = {
instance_count = 2
instance_type = "t3.small"
}
production = {
instance_count = 5
instance_type = "t3.large"
}
}
config = local.environment_config[terraform.workspace]
}
resource "aws_instance" "app" {
count = local.config.instance_count
instance_type = local.config.instance_type
ami = data.aws_ami.latest.id
tags = {
Name = "app-${terraform.workspace}-${count.index + 1}"
}
}
Workspaces are great for development and testing, but many teams prefer separate configurations for production environments to avoid accidental cross-environment changes.
Remote State and Backend Configuration
Remote backends store your state file in a shared location and provide locking to prevent concurrent modifications. The S3 backend with DynamoDB locking is the most common pattern:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
But here’s the catch: you can’t use variables in backend configuration. This makes it tricky to use the same configuration across environments. Here are some solutions:
Backend configuration files:
# backend-dev.hcl
bucket = "my-terraform-state-dev"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
# backend-prod.hcl
bucket = "my-terraform-state-prod"
key = "infrastructure/terraform.tfstate"
region = "us-west-2"
# Initialize with specific backend config
terraform init -backend-config=backend-dev.hcl
terraform init -backend-config=backend-prod.hcl
Partial backend configuration:
terraform {
backend "s3" {
# Bucket and key provided during init
region = "us-west-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
terraform init \
-backend-config="bucket=my-terraform-state-prod" \
-backend-config="key=infrastructure/terraform.tfstate"
Remote State Data Sources
When you split your infrastructure into multiple Terraform configurations, you need to share data between them. Remote state data sources let you read outputs from other Terraform configurations:
# In your networking configuration
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
# In your application configuration
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "network/terraform.tfstate"
region = "us-west-2"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
vpc_security_group_ids = [aws_security_group.app.id]
# other configuration...
}
resource "aws_security_group" "app" {
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
# security group rules...
}
This pattern lets you manage different parts of your infrastructure independently while maintaining the relationships between them.
Complex Dependencies and Ordering
Sometimes Terraform’s automatic dependency detection isn’t enough. You might need explicit control over resource creation order or complex conditional logic:
# Explicit dependencies
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
# This instance depends on the database being ready
depends_on = [
aws_db_instance.main,
aws_security_group.database
]
}
# Conditional resource creation
resource "aws_db_instance" "main" {
count = var.create_database ? 1 : 0
allocated_storage = 20
storage_type = "gp2"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
db_name = "myapp"
username = "admin"
password = var.db_password
skip_final_snapshot = true
}
# Use conditional outputs
output "database_endpoint" {
value = var.create_database ? aws_db_instance.main[0].endpoint : null
}
Advanced Variable Patterns
Complex configurations often need sophisticated variable handling:
# Object variables for complex configuration
variable "applications" {
description = "Map of applications to deploy"
type = map(object({
image_tag = string
instance_type = string
min_capacity = number
max_capacity = number
environment_vars = map(string)
}))
default = {
web = {
image_tag = "v1.0.0"
instance_type = "t3.micro"
min_capacity = 2
max_capacity = 10
environment_vars = {
LOG_LEVEL = "info"
DEBUG = "false"
}
}
api = {
image_tag = "v2.1.0"
instance_type = "t3.small"
min_capacity = 3
max_capacity = 15
environment_vars = {
LOG_LEVEL = "warn"
DATABASE_URL = "mysql://..."
}
}
}
}
# Use for_each with complex objects
resource "aws_ecs_service" "apps" {
for_each = var.applications
name = each.key
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.apps[each.key].arn
desired_count = each.value.min_capacity
# Use nested values
dynamic "load_balancer" {
for_each = each.key == "web" ? [1] : []
content {
target_group_arn = aws_lb_target_group.web.arn
container_name = each.key
container_port = 80
}
}
}
Error Handling and Validation
Advanced configurations need robust error handling and validation:
# Input validation
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "cidr_block" {
description = "VPC CIDR block"
type = string
validation {
condition = can(cidrhost(var.cidr_block, 0))
error_message = "Must be a valid CIDR block."
}
}
# Preconditions and postconditions (Terraform 1.2+)
resource "aws_instance" "web" {
ami = data.aws_ami.latest.id
instance_type = var.instance_type
lifecycle {
precondition {
condition = data.aws_ami.latest.architecture == "x86_64"
error_message = "AMI must be x86_64 architecture."
}
postcondition {
condition = self.public_ip != ""
error_message = "Instance must have a public IP address."
}
}
}
Dynamic Configuration with Functions
Terraform’s built-in functions enable sophisticated configuration logic:
locals {
# Generate subnet CIDRs automatically
availability_zones = data.aws_availability_zones.available.names
subnet_cidrs = [
for i, az in local.availability_zones :
cidrsubnet(var.vpc_cidr, 8, i)
]
# Create tags with computed values
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
CreatedDate = formatdate("YYYY-MM-DD", timestamp())
}
# Conditional logic with functions
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
# Complex data transformation
security_group_rules = flatten([
for app_name, app_config in var.applications : [
for port in app_config.ports : {
app_name = app_name
port = port
protocol = "tcp"
cidr_blocks = app_config.allowed_cidrs
}
]
])
}
# Use transformed data
resource "aws_security_group_rule" "app_ingress" {
for_each = {
for rule in local.security_group_rules :
"${rule.app_name}-${rule.port}" => rule
}
type = "ingress"
from_port = each.value.port
to_port = each.value.port
protocol = each.value.protocol
cidr_blocks = each.value.cidr_blocks
security_group_id = aws_security_group.apps[each.value.app_name].id
}
State Management Strategies
Large organizations need sophisticated state management strategies:
Layered architecture: Split infrastructure into layers with dependencies:
├── 01-foundation/ # VPC, subnets, basic networking
├── 02-security/ # IAM roles, security groups
├── 03-data/ # Databases, storage
├── 04-compute/ # EC2, ECS, Lambda
└── 05-applications/ # Application-specific resources
Environment isolation: Separate state files for each environment:
├── environments/
│ ├── dev/
│ │ ├── foundation/
│ │ ├── security/
│ │ └── applications/
│ ├── staging/
│ └── production/
Team boundaries: Organize state by team ownership:
├── platform-team/ # Shared infrastructure
├── web-team/ # Web application resources
├── data-team/ # Data pipeline resources
└── security-team/ # Security and compliance
Performance Optimization
Large Terraform configurations can be slow. Here are optimization strategies:
Targeted operations:
# Apply changes to specific resources
terraform apply -target="module.database"
terraform apply -target="aws_instance.web[0]"
# Plan specific resources
terraform plan -target="module.vpc"
Parallelism control:
# Increase parallelism for faster operations
terraform apply -parallelism=20
# Decrease for rate-limited APIs
terraform apply -parallelism=5
State optimization:
# Remove unused resources from state
terraform state rm aws_instance.old_server
# Move resources between configurations
terraform state mv aws_instance.web module.web.aws_instance.server
What’s Coming Next
Advanced patterns give you the tools to handle complex, real-world infrastructure scenarios. Workspaces, remote state, and sophisticated variable handling let you build systems that scale with your organization and handle the complexity of modern cloud architectures.
In the next part, we’ll focus on production practices and security—how to implement proper access controls, secrets management, testing strategies, and the operational practices that keep Terraform-managed infrastructure secure and reliable in production environments.