terraform-specialist
SKILL.md
Terraform Specialist
Guidelines for infrastructure automation and Terraform best practices.
Core Principles
- DRY with modules - Reusable, versioned components
- State is sacred - Always use remote state with locking
- Plan before apply - Review every change
- Lock versions - Pin providers and modules
- Use data sources - Avoid hardcoded values
Project Structure
infrastructure/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── prod/
├── modules/
│ ├── vpc/
│ ├── ecs-service/
│ └── rds/
└── global/
├── iam/
└── dns/
Provider Configuration
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "mycompany-terraform-state"
key = "environments/dev/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Project = var.project_name
}
}
}
Module Design
Module Structure
modules/vpc/
├── main.tf # Primary resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider requirements
├── locals.tf # Local values
└── README.md # Documentation
Module Example
# modules/vpc/variables.tf
variable "name" {
description = "Name prefix for all resources"
type = string
}
variable "cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "azs" {
description = "Availability zones"
type = list(string)
}
variable "enable_nat_gateway" {
description = "Enable NAT Gateway for private subnets"
type = bool
default = true
}
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = var.name
}
}
resource "aws_subnet" "public" {
count = length(var.azs)
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.cidr, 8, count.index)
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.name}-public-${var.azs[count.index]}"
Type = "public"
}
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "List of public subnet IDs"
value = aws_subnet.public[*].id
}
Using Modules
module "vpc" {
source = "../../modules/vpc"
name = "myapp-${var.environment}"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
enable_nat_gateway = var.environment == "prod"
}
# Reference outputs
resource "aws_instance" "app" {
subnet_id = module.vpc.public_subnet_ids[0]
}
Variables Best Practices
# Use validation
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
# Use object types for complex variables
variable "database_config" {
description = "Database configuration"
type = object({
instance_class = string
allocated_storage = number
engine_version = string
multi_az = bool
})
default = {
instance_class = "db.t3.micro"
allocated_storage = 20
engine_version = "14.7"
multi_az = false
}
}
# Sensitive variables
variable "db_password" {
description = "Database master password"
type = string
sensitive = true
}
Locals for Computed Values
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
# Conditional logic
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
# Dynamic subnet calculation
subnet_cidrs = [for i in range(3) : cidrsubnet(var.vpc_cidr, 8, i)]
}
Data Sources
# Look up AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
# Look up existing VPC
data "aws_vpc" "selected" {
filter {
name = "tag:Name"
values = ["main-vpc"]
}
}
# Current AWS account
data "aws_caller_identity" "current" {}
# Current region
data "aws_region" "current" {}
Lifecycle Rules
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
lifecycle {
# Prevent accidental destruction
prevent_destroy = true
# Create new before destroying old
create_before_destroy = true
# Ignore external changes
ignore_changes = [
tags["LastModified"],
user_data
]
}
}
Import Existing Resources
# 1. Write the resource block
resource "aws_s3_bucket" "existing" {
bucket = "my-existing-bucket"
}
# 2. Import into state
# terraform import aws_s3_bucket.existing my-existing-bucket
# 3. Run plan to verify
# terraform plan
# For Terraform 1.5+, use import blocks
import {
to = aws_s3_bucket.existing
id = "my-existing-bucket"
}
State Management
Remote State Data Source
# Access outputs from another state file
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "mycompany-terraform-state"
key = "vpc/terraform.tfstate"
region = "us-east-1"
}
}
# Use the outputs
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.vpc.outputs.public_subnet_ids[0]
}
State Commands
# List resources in state
terraform state list
# Show specific resource
terraform state show aws_instance.app
# Move resource (rename)
terraform state mv aws_instance.old aws_instance.new
# Remove from state (without destroying)
terraform state rm aws_instance.legacy
# Pull remote state locally
terraform state pull > backup.tfstate
Workspaces
# Create and switch workspace
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# List workspaces
terraform workspace list
# Switch workspace
terraform workspace select dev
# Use in config
resource "aws_instance" "app" {
count = terraform.workspace == "prod" ? 3 : 1
tags = {
Environment = terraform.workspace
}
}
Common Patterns
Count vs For Each
# count - for identical resources
resource "aws_subnet" "public" {
count = 3
cidr_block = cidrsubnet(var.cidr, 8, count.index)
}
# for_each - for unique resources (preferred)
resource "aws_iam_user" "users" {
for_each = toset(["alice", "bob", "carol"])
name = each.key
}
# for_each with map
variable "instances" {
default = {
web = { type = "t3.micro", az = "us-east-1a" }
api = { type = "t3.small", az = "us-east-1b" }
}
}
resource "aws_instance" "app" {
for_each = var.instances
instance_type = each.value.type
availability_zone = each.value.az
tags = { Name = each.key }
}
Dynamic Blocks
resource "aws_security_group" "this" {
name = "app-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Weekly Installs
2
Repository
arosenkranz/cla…e-configGitHub Stars
1
First Seen
Feb 27, 2026
Security Audits
Installed on
cline2
github-copilot2
codex2
kimi-cli2
gemini-cli2
cursor2