Terraform Good Patterns
Eight strong implementation patterns that the Terraform skill uses as positive examples. Each pattern directly prevents one or more failure modes.
1. Stable Identity Map for Service Accounts
variable "service_accounts" {
type = map(object({
display_name = string
roles = set(string)
}))
}
resource "google_service_account" "app" {
for_each = var.service_accounts
account_id = each.key
display_name = each.value.display_name
}
Why this works: Key-based identity survives insertion/removal changes. Contract is strict and predictable. Prevents identity churn.
2. Cross-Variable Validation for Safe Combinations
variable "public_endpoint" {
type = bool
default = false
}
variable "allowed_cidrs" {
type = list(string)
default = []
validation {
condition = var.public_endpoint || length(var.allowed_cidrs) == 0
error_message = "allowed_cidrs must be empty unless public_endpoint is true."
}
}
Why this works: Invalid combinations fail early. Intent is encoded directly in the module interface. Prevents blast radius from misconfigured access.
3. Strong Object Typing with Optional Fields
variable "node_pool" {
type = object({
size = string
min_replicas = number
max_replicas = number
spot_instances = optional(bool, false)
})
}
Why this works: Flexible without sacrificing schema clarity. Avoids ad-hoc maps and runtime surprises.
4. Narrow, Useful Outputs
output "app_subnet_ids" {
description = "Subnet ids for application workloads"
value = values(aws_subnet.app)[*].id
}
Why this works: Downstream modules get exactly what they need. Avoids leaking full provider objects that could expose sensitive data.
5. Controlled Provider Pinning
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.40.0, < 6.0.0"
}
}
}
Why this works: Controlled upgrade window prevents accidental major-version breakage. Prevents CI drift from floating versions.
6. Dynamic Block with Typed Variable
variable "ingress_rules" {
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
}))
}
resource "aws_security_group" "app" {
name = "app-sg"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Why this works: Iterator uses ingress.value (named after the block label, not the variable). Typed input prevents runtime shape errors.
7. Provider Alias for Multi-Region
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "eu"
region = "eu-west-1"
}
resource "aws_s3_bucket" "replica" {
provider = aws.eu
bucket = "my-replica-bucket"
}
module "eu_network" {
source = "./modules/network"
providers = {
aws = aws.eu
}
}
Why this works: Explicit alias keeps region intent clear. Modules receive providers via providers map, not implicit inheritance.
8. moved Block for Safe Rename
moved {
from = aws_kms_key.logs
to = aws_kms_key.audit
}
Why this works: Keeps state continuity during naming refactors. Prevents unnecessary replacement that could destroy encryption keys.