Skip to main content

Terraform Patterns Skill

Terraform Patterns Skill

When to Use This Skill

Use this skill when implementing terraform patterns patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Infrastructure as Code best practices with Terraform including module design, state management, multi-environment configurations, and security hardening.

Project Structure

infrastructure/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ └── database/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ └── production/
├── shared/
│ ├── versions.tf
│ └── providers.tf
└── .terraform-version

Module Design

Reusable Module Template

# modules/service/main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

variable "name" {
description = "Service name"
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]*$", var.name))
error_message = "Name must be lowercase alphanumeric with hyphens."
}
}

variable "environment" {
description = "Environment (dev, staging, production)"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}

variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}

locals {
common_tags = merge(var.tags, {
Service = var.name
Environment = var.environment
ManagedBy = "terraform"
})
}

output "service_id" {
description = "Service identifier"
value = aws_ecs_service.main.id
}

Module Composition

# environments/production/main.tf
module "vpc" {
source = "../../modules/networking"

name = "main"
environment = "production"
cidr_block = "10.0.0.0/16"
}

module "database" {
source = "../../modules/database"

name = "api"
environment = "production"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids

depends_on = [module.vpc]
}

module "service" {
source = "../../modules/service"

name = "api"
environment = "production"
vpc_id = module.vpc.vpc_id
database_url = module.database.connection_string
}

State Management

Remote State Configuration

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

State Data Sources

# Reference another state
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "production/networking.tfstate"
region = "us-east-1"
}
}

resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_id
}

State Import

# Import existing resources
terraform import aws_instance.web i-1234567890abcdef0

# Generate configuration from import
terraform plan -generate-config-out=generated.tf

Multi-Environment Patterns

Terragrunt DRY Configuration

# terragrunt.hcl (root)
remote_state {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}

inputs = {
region = "us-east-1"
}
# environments/production/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}

terraform {
source = "../../modules//service"
}

inputs = {
environment = "production"
instance_count = 3
instance_type = "t3.large"
}

Workspaces Pattern

# Create and switch workspaces
terraform workspace new production
terraform workspace select production

# Apply per workspace
terraform apply -var-file="environments/${TF_WORKSPACE}.tfvars"
# Use workspace in configuration
locals {
environment = terraform.workspace

instance_counts = {
dev = 1
staging = 2
production = 3
}
}

resource "aws_instance" "app" {
count = local.instance_counts[local.environment]
instance_type = local.environment == "production" ? "t3.large" : "t3.small"
}

Security Best Practices

Sensitive Variables

variable "database_password" {
description = "Database password"
type = string
sensitive = true
}

output "connection_string" {
value = "postgres://user:${var.database_password}@${aws_db_instance.main.endpoint}/db"
sensitive = true
}

AWS Secrets Manager Integration

data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/database/password"
}

resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

IAM Least Privilege

data "aws_iam_policy_document" "lambda" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["arn:aws:logs:*:*:*"]
}

statement {
effect = "Allow"
actions = [
"s3:GetObject"
]
resources = ["${aws_s3_bucket.data.arn}/*"]
}
}

Security Scanning

# tflint - Terraform linter
tflint --init
tflint --recursive

# checkov - Security scanner
checkov -d .

# tfsec - Security scanner
tfsec .

Cloud Provider Patterns

AWS ECS Fargate

resource "aws_ecs_service" "api" {
name = "api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 3
launch_type = "FARGATE"

network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.api.id]
assign_public_ip = false
}

load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}

deployment_circuit_breaker {
enable = true
rollback = true
}
}

GCP Cloud Run

resource "google_cloud_run_service" "api" {
name = "api"
location = var.region

template {
spec {
containers {
image = "gcr.io/${var.project_id}/api:${var.image_tag}"
ports {
container_port = 8080
}
resources {
limits = {
cpu = "1000m"
memory = "512Mi"
}
}
}
}
metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = "10"
}
}
}

traffic {
percent = 100
latest_revision = true
}
}

Kubernetes Provider

provider "kubernetes" {
host = data.aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(data.aws_eks_cluster.main.certificate_authority[0].data)
token = data.aws_eks_cluster_auth.main.token
}

resource "kubernetes_deployment" "api" {
metadata {
name = "api"
namespace = "default"
}

spec {
replicas = 3

selector {
match_labels = {
app = "api"
}
}

template {
metadata {
labels = {
app = "api"
}
}

spec {
container {
name = "api"
image = "api:latest"

port {
container_port = 8080
}

resources {
limits = {
cpu = "500m"
memory = "256Mi"
}
requests = {
cpu = "250m"
memory = "128Mi"
}
}
}
}
}
}
}

Testing and Validation

Terraform Validate

terraform init -backend=false
terraform validate
terraform fmt -check -recursive

Terratest Example

package test

import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../modules/networking",
Vars: map[string]interface{}{
"name": "test",
"environment": "dev",
"cidr_block": "10.0.0.0/16",
},
}

defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)

vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}

Plan Validation

# Generate plan
terraform plan -out=tfplan

# Convert to JSON for analysis
terraform show -json tfplan > plan.json

# Check for resource deletions
jq '.resource_changes[] | select(.change.actions | contains(["delete"]))' plan.json

CI/CD Integration

GitHub Actions

name: Terraform

on:
pull_request:
paths:
- 'infrastructure/**'

jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.5.0

- run: terraform init
working-directory: infrastructure/environments/production

- run: terraform plan -no-color
working-directory: infrastructure/environments/production
continue-on-error: true

- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '```\n' + process.env.PLAN_OUTPUT + '\n```'
})

Troubleshooting

State Locking Issues

# Force unlock (use carefully)
terraform force-unlock LOCK_ID

# Debug state
terraform state list
terraform state show aws_instance.web

Provider Issues

# Clear provider cache
rm -rf .terraform

# Upgrade providers
terraform init -upgrade

Usage Examples

Design Infrastructure Module

Apply terraform-patterns skill to design reusable VPC module with public/private subnets, NAT gateways, and security groups

Multi-Environment Setup

Apply terraform-patterns skill to configure Terragrunt for dev/staging/production environments with DRY configuration

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: terraform-patterns

Completed:
- [x] Terraform modules created with proper structure (main.tf, variables.tf, outputs.tf)
- [x] Remote state backend configured (S3 + DynamoDB or equivalent)
- [x] Multi-environment configuration implemented (dev/staging/production)
- [x] Security best practices applied (sensitive variables, least privilege IAM)
- [x] terraform validate passes without errors
- [x] terraform plan reviewed and approved
- [x] State locking verified

Outputs:
- modules/*/ directories with reusable modules
- environments/*/ directories with environment-specific configs
- backend.tf with remote state configuration
- terraform.tfvars.example for variable reference
- .terraform.lock.hcl with provider version locks

Completion Checklist

Before marking this skill as complete, verify:

  • terraform init completes without errors
  • terraform validate passes
  • terraform fmt -check -recursive passes (code formatted)
  • terraform plan shows expected changes only
  • All sensitive variables marked with sensitive = true
  • IAM policies follow least privilege principle
  • State backend configured with encryption and locking
  • Provider versions pinned in required_providers block
  • Module README.md documents inputs, outputs, and usage
  • Security scanner (tfsec, checkov) passes with no critical issues

Failure Indicators

This skill has FAILED if:

  • terraform validate fails with syntax or reference errors
  • ❌ No remote state backend configured (local state in production)
  • ❌ Hardcoded secrets in .tf files
  • ❌ IAM policies grant * permissions
  • ❌ No state locking (DynamoDB table missing for S3 backend)
  • ❌ Provider versions not pinned (using unstable versions)
  • ❌ terraform plan shows unintended resource deletions
  • ❌ Security scanner reports critical vulnerabilities
  • ❌ No .gitignore for .tfstate, .terraform/, *.tfvars

When NOT to Use

Do NOT use this skill when:

  • Using CloudFormation, Pulumi, or other IaC tools (use tool-specific patterns)
  • Managing Kubernetes manifests (use Helm or Kustomize)
  • Provisioning serverless applications (use SAM or Serverless Framework)
  • One-time manual infrastructure setup (Terraform overkill for single resources)
  • Infrastructure requires extreme customization (write custom scripts)
  • Team lacks Terraform expertise (consider managed services first)
  • Quick prototyping without state management requirements

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Hardcoded valuesNot reusable across environmentsUse variables with defaults
No remote stateState conflicts, no collaborationConfigure S3 + DynamoDB backend
Monolithic configurationHard to manage, slow plansSplit into modules and environments
Wildcard provider versionsBreaking changes on updatesPin versions with ~> 5.0
No state lockingConcurrent runs corrupt stateEnable DynamoDB locking table
Inline IAM policiesHard to audit and reuseUse aws_iam_policy_document data sources
terraform apply without planDangerous, unreviewed changesAlways run plan first, review, then apply
No .gitignoreSecrets leaked in state filesIgnore .tfstate, *.tfvars, .terraform/

Principles

This skill embodies:

  • #1 Recycle → Extend → Re-Use → Create - Reusable modules over copy-paste
  • #2 First Principles - Infrastructure as Code fundamentals
  • #3 Keep It Simple - DRY configuration with Terragrunt or workspaces
  • #4 Separation of Concerns - Modules for logical infrastructure components
  • #5 Eliminate Ambiguity - Explicit variable types and validation
  • #8 No Assumptions - Always run terraform plan before apply
  • #13 Automation - Terraform enables infrastructure automation

Full Principles: CODITECT-STANDARD-AUTOMATION.md