When this skill is activated, always start your first response with the 🧢 emoji.
Terraform Infrastructure as Code
Terraform is the de-facto standard for declarative infrastructure provisioning. This skill covers the complete lifecycle - project setup, module design, remote state management, multi-environment strategy, and keeping real infrastructure aligned with declared configuration. Designed for engineers who know basic Terraform and need opinionated guidance on structure, safety, and production practices.
When to use this skill
Trigger this skill when the user:
- Writes or reviews Terraform HCL for any cloud provider (AWS, GCP, Azure)
- Designs reusable Terraform modules or a module registry structure
- Sets up or migrates remote state backends (S3, GCS, Terraform Cloud)
- Manages multiple environments (dev/staging/prod) with Terraform
- Diagnoses drift between actual infrastructure and Terraform state
- Runs or interprets
terraform plan,terraform apply, orterraform import - Handles state operations:
state mv,state rm,taint,untaint
Do NOT trigger this skill for:
- Kubernetes manifest authoring (use a kubernetes/helm skill instead)
- Application-level configuration management (Ansible, Chef, Puppet)
Key principles
- Declarative over imperative - Describe the desired end state, not the
steps to get there. If you find yourself writing null_resource with provisioners to run shell scripts, stop and ask whether the provider has a proper resource for this.
- Modules for every reusable pattern - Any configuration block you copy
between environments or projects is a module waiting to be written. Extract early; the cost of refactoring into a module grows with usage.
- Remote state always - Local state is only acceptable for throwaway
experiments. Production state lives in a versioned, locked backend (S3 + DynamoDB, GCS, or Terraform Cloud) from day one. State is your source of truth.
- Plan before apply, in CI -
terraform applywithout a reviewed plan is
the infrastructure equivalent of deploying untested code. Always run terraform plan -out=tfplan and review the diff before applying. Automate this in CI pipelines.
- Least privilege for providers - The IAM role or service account Terraform
uses must have only the permissions needed for that specific configuration. Never use AdministratorAccess or Owner roles for provider credentials.
Core concepts
Providers - Plugins that translate HCL into API calls for a cloud or service. Always pin provider versions in required_providers. Unpinned providers break on provider releases.
Resources - The fundamental unit. Each resource block declares one infrastructure object (aws_vpc, google_container_cluster, etc.).
Data sources - Read-only lookups of existing infrastructure not managed by this configuration. Use data blocks to reference shared resources (AMIs, existing VPCs, DNS zones) without importing them into state.
Modules - Containers for multiple resources that are used together. A module is a directory with .tf files. Modules accept variable inputs and expose output values to callers.
State - A JSON file that maps declared resources to real infrastructure objects. Terraform uses state to calculate diffs. Never edit state manually - use terraform state commands.
Workspaces - Named state instances within a single backend configuration. Useful for short-lived feature environments; not recommended for long-lived environment separation (use separate root modules instead).
Backends - Configuration for where and how state is stored and locked. Locking prevents concurrent applies from corrupting state.
Common tasks
Set up a project with S3 backend
Structure every Terraform project with these three foundational files before writing any resources.
versions.tf - Pin everything. Unpinned versions cause silent breakage.
terraform {
required_version = ">= 1.6.0, < 2.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-org-terraform-state"
key = "services/my-service/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
providers.tf - One provider block, no credentials hardcoded.
provider "aws" {
region = var.aws_region
default_tags {
tags = {
ManagedBy = "terraform"
Environment = var.environment
Service = var.service_name
}
}
}
variables.tf - Declare all inputs with descriptions and sensible defaults.
variable "aws_region" {
description = "AWS region to deploy into"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Deployment environment (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod"
}
}
variable "service_name" {
description = "Name of the service owning this infrastructure"
type = string
}
Create the S3 bucket and DynamoDB table for the backend manually (or with a
separate bootstrap Terraform config) before running terraform init. You
cannot manage the state backend with the same configuration that uses it.
Write a reusable module
A module is a directory with main.tf, variables.tf, and outputs.tf. Modules should express one cohesive infrastructure concern. All inputs are declared with descriptions in variables.tf; all outputs expose only what callers need in outputs.tf.
Calling a module from a root configuration:
module "vpc" {
source = "../../modules/vpc"
name = "my-service-${var.environment}"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
}
See references/module-patterns.md for complete module templates, versioning, and monorepo layout.
Manage environments with workspaces
Workspaces share a single backend and configuration. Use them for ephemeral feature environments; prefer separate state files (separate key paths) for permanent environments like staging and prod.
# Create and switch to a feature workspace
terraform workspace new feature-xyz
terraform workspace select feature-xyz
# Reference workspace name in configuration to vary resource names/sizes
resource "aws_instance" "app" {
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
tags = { Environment = terraform.workspace }
}
# Clean up the workspace when done
terraform workspace select default
terraform destroy
terraform workspace delete feature-xyz
For prod/staging: use separate backend key paths or separate AWS accounts
with separate root modules. Workspaces with a single state key per environment
mean a bad apply in one workspace can corrupt state for others.
Import existing resources into state
When infrastructure was created outside Terraform and you need to manage it.
# Terraform 1.5+: use import blocks (preferred, reviewable in plan)
# Add this to your .tf file temporarily:
import {
to = aws_s3_bucket.my_bucket
id = "my-existing-bucket-name"
}
# Run plan to preview what will be generated
terraform plan -generate-config-out=generated.tf
# Review generated.tf, copy the resource block into your main config, remove
# the import block, then apply
terraform apply
For older Terraform versions (pre-1.5), use the CLI:
terraform import aws_s3_bucket.my_bucket my-existing-bucket-name
After importing, always run terraform plan to verify zero