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.

results matching ""

    No results matching ""