Comprehensive Claude Code guidance system with: - 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit - 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#), infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD), testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability) - 3 hooks: secret detection, auto-formatting, TDD git pre-commit - Strict TDD enforcement with 80%+ coverage requirements - Multi-model strategy: Opus for planning, Sonnet for execution (opusplan) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
557 lines
12 KiB
Markdown
557 lines
12 KiB
Markdown
---
|
|
name: terraform-aws
|
|
description: Terraform Infrastructure as Code for AWS with module patterns, state management, and best practices. Use when writing Terraform configurations or planning AWS infrastructure.
|
|
---
|
|
|
|
# Terraform AWS Skill
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
infrastructure/
|
|
├── environments/
|
|
│ ├── dev/
|
|
│ │ ├── main.tf
|
|
│ │ ├── variables.tf
|
|
│ │ ├── outputs.tf
|
|
│ │ ├── terraform.tfvars
|
|
│ │ └── backend.tf
|
|
│ ├── staging/
|
|
│ └── prod/
|
|
├── modules/
|
|
│ ├── vpc/
|
|
│ │ ├── main.tf
|
|
│ │ ├── variables.tf
|
|
│ │ ├── outputs.tf
|
|
│ │ └── README.md
|
|
│ ├── ecs-service/
|
|
│ ├── rds/
|
|
│ ├── lambda/
|
|
│ └── s3-bucket/
|
|
└── shared/
|
|
└── providers.tf
|
|
```
|
|
|
|
## Backend Configuration
|
|
|
|
### S3 Backend (recommended for AWS)
|
|
```hcl
|
|
# backend.tf
|
|
terraform {
|
|
backend "s3" {
|
|
bucket = "mycompany-terraform-state"
|
|
key = "environments/dev/terraform.tfstate"
|
|
region = "eu-west-2"
|
|
encrypt = true
|
|
dynamodb_table = "terraform-state-lock"
|
|
}
|
|
}
|
|
```
|
|
|
|
### State Lock Table
|
|
```hcl
|
|
# One-time setup for state locking
|
|
resource "aws_dynamodb_table" "terraform_lock" {
|
|
name = "terraform-state-lock"
|
|
billing_mode = "PAY_PER_REQUEST"
|
|
hash_key = "LockID"
|
|
|
|
attribute {
|
|
name = "LockID"
|
|
type = "S"
|
|
}
|
|
|
|
tags = {
|
|
Name = "Terraform State Lock"
|
|
Environment = "shared"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Provider Configuration
|
|
|
|
```hcl
|
|
# providers.tf
|
|
terraform {
|
|
required_version = ">= 1.6.0"
|
|
|
|
required_providers {
|
|
aws = {
|
|
source = "hashicorp/aws"
|
|
version = "~> 5.0"
|
|
}
|
|
}
|
|
}
|
|
|
|
provider "aws" {
|
|
region = var.aws_region
|
|
|
|
default_tags {
|
|
tags = {
|
|
Environment = var.environment
|
|
Project = var.project_name
|
|
ManagedBy = "terraform"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Module Patterns
|
|
|
|
### VPC Module
|
|
```hcl
|
|
# modules/vpc/variables.tf
|
|
variable "name" {
|
|
description = "Name prefix for VPC resources"
|
|
type = string
|
|
}
|
|
|
|
variable "cidr_block" {
|
|
description = "CIDR block for the VPC"
|
|
type = string
|
|
default = "10.0.0.0/16"
|
|
}
|
|
|
|
variable "availability_zones" {
|
|
description = "List of availability zones"
|
|
type = list(string)
|
|
}
|
|
|
|
variable "enable_nat_gateway" {
|
|
description = "Enable NAT Gateway for private subnets"
|
|
type = bool
|
|
default = true
|
|
}
|
|
|
|
variable "single_nat_gateway" {
|
|
description = "Use single NAT Gateway (cost saving for non-prod)"
|
|
type = bool
|
|
default = false
|
|
}
|
|
|
|
# modules/vpc/main.tf
|
|
locals {
|
|
az_count = length(var.availability_zones)
|
|
|
|
public_subnets = [
|
|
for i, az in var.availability_zones :
|
|
cidrsubnet(var.cidr_block, 8, i)
|
|
]
|
|
|
|
private_subnets = [
|
|
for i, az in var.availability_zones :
|
|
cidrsubnet(var.cidr_block, 8, i + local.az_count)
|
|
]
|
|
}
|
|
|
|
resource "aws_vpc" "main" {
|
|
cidr_block = var.cidr_block
|
|
enable_dns_hostnames = true
|
|
enable_dns_support = true
|
|
|
|
tags = {
|
|
Name = var.name
|
|
}
|
|
}
|
|
|
|
resource "aws_internet_gateway" "main" {
|
|
vpc_id = aws_vpc.main.id
|
|
|
|
tags = {
|
|
Name = "${var.name}-igw"
|
|
}
|
|
}
|
|
|
|
resource "aws_subnet" "public" {
|
|
count = local.az_count
|
|
vpc_id = aws_vpc.main.id
|
|
cidr_block = local.public_subnets[count.index]
|
|
availability_zone = var.availability_zones[count.index]
|
|
map_public_ip_on_launch = true
|
|
|
|
tags = {
|
|
Name = "${var.name}-public-${var.availability_zones[count.index]}"
|
|
Type = "public"
|
|
}
|
|
}
|
|
|
|
resource "aws_subnet" "private" {
|
|
count = local.az_count
|
|
vpc_id = aws_vpc.main.id
|
|
cidr_block = local.private_subnets[count.index]
|
|
availability_zone = var.availability_zones[count.index]
|
|
|
|
tags = {
|
|
Name = "${var.name}-private-${var.availability_zones[count.index]}"
|
|
Type = "private"
|
|
}
|
|
}
|
|
|
|
resource "aws_eip" "nat" {
|
|
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : local.az_count) : 0
|
|
domain = "vpc"
|
|
|
|
tags = {
|
|
Name = "${var.name}-nat-eip-${count.index}"
|
|
}
|
|
|
|
depends_on = [aws_internet_gateway.main]
|
|
}
|
|
|
|
resource "aws_nat_gateway" "main" {
|
|
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : local.az_count) : 0
|
|
allocation_id = aws_eip.nat[count.index].id
|
|
subnet_id = aws_subnet.public[count.index].id
|
|
|
|
tags = {
|
|
Name = "${var.name}-nat-${count.index}"
|
|
}
|
|
}
|
|
|
|
# modules/vpc/outputs.tf
|
|
output "vpc_id" {
|
|
description = "ID of the VPC"
|
|
value = aws_vpc.main.id
|
|
}
|
|
|
|
output "public_subnet_ids" {
|
|
description = "IDs of public subnets"
|
|
value = aws_subnet.public[*].id
|
|
}
|
|
|
|
output "private_subnet_ids" {
|
|
description = "IDs of private subnets"
|
|
value = aws_subnet.private[*].id
|
|
}
|
|
```
|
|
|
|
### Module Usage
|
|
```hcl
|
|
# environments/dev/main.tf
|
|
module "vpc" {
|
|
source = "../../modules/vpc"
|
|
|
|
name = "${var.project_name}-${var.environment}"
|
|
cidr_block = "10.0.0.0/16"
|
|
availability_zones = ["eu-west-2a", "eu-west-2b", "eu-west-2c"]
|
|
enable_nat_gateway = true
|
|
single_nat_gateway = true # Cost saving for dev
|
|
}
|
|
|
|
module "ecs_cluster" {
|
|
source = "../../modules/ecs-cluster"
|
|
|
|
name = "${var.project_name}-${var.environment}"
|
|
vpc_id = module.vpc.vpc_id
|
|
private_subnet_ids = module.vpc.private_subnet_ids
|
|
|
|
depends_on = [module.vpc]
|
|
}
|
|
```
|
|
|
|
## Security Best Practices
|
|
|
|
### Security Groups
|
|
```hcl
|
|
resource "aws_security_group" "app" {
|
|
name = "${var.name}-app-sg"
|
|
description = "Security group for application servers"
|
|
vpc_id = var.vpc_id
|
|
|
|
# Only allow inbound from load balancer
|
|
ingress {
|
|
description = "HTTP from ALB"
|
|
from_port = var.app_port
|
|
to_port = var.app_port
|
|
protocol = "tcp"
|
|
security_groups = [aws_security_group.alb.id]
|
|
}
|
|
|
|
# Allow all outbound
|
|
egress {
|
|
description = "Allow all outbound"
|
|
from_port = 0
|
|
to_port = 0
|
|
protocol = "-1"
|
|
cidr_blocks = ["0.0.0.0/0"]
|
|
}
|
|
|
|
tags = {
|
|
Name = "${var.name}-app-sg"
|
|
}
|
|
}
|
|
|
|
# NEVER do this - open to world
|
|
# ingress {
|
|
# from_port = 22
|
|
# to_port = 22
|
|
# protocol = "tcp"
|
|
# cidr_blocks = ["0.0.0.0/0"] # BAD!
|
|
# }
|
|
```
|
|
|
|
### IAM Roles with Least Privilege
|
|
```hcl
|
|
data "aws_iam_policy_document" "ecs_task_assume_role" {
|
|
statement {
|
|
actions = ["sts:AssumeRole"]
|
|
|
|
principals {
|
|
type = "Service"
|
|
identifiers = ["ecs-tasks.amazonaws.com"]
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_iam_role" "ecs_task_role" {
|
|
name = "${var.name}-ecs-task-role"
|
|
assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role.json
|
|
}
|
|
|
|
# Specific permissions only
|
|
data "aws_iam_policy_document" "ecs_task_policy" {
|
|
statement {
|
|
sid = "AllowS3Access"
|
|
effect = "Allow"
|
|
actions = [
|
|
"s3:GetObject",
|
|
"s3:PutObject",
|
|
]
|
|
resources = [
|
|
"${aws_s3_bucket.app_data.arn}/*"
|
|
]
|
|
}
|
|
|
|
statement {
|
|
sid = "AllowSecretsAccess"
|
|
effect = "Allow"
|
|
actions = [
|
|
"secretsmanager:GetSecretValue"
|
|
]
|
|
resources = [
|
|
aws_secretsmanager_secret.app_secrets.arn
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Secrets Management
|
|
```hcl
|
|
# NEVER hardcode secrets
|
|
# BAD:
|
|
# resource "aws_db_instance" "main" {
|
|
# password = "mysecretpassword" # NEVER!
|
|
# }
|
|
|
|
# GOOD: Use AWS Secrets Manager
|
|
resource "aws_secretsmanager_secret" "db_password" {
|
|
name = "${var.name}/db-password"
|
|
recovery_window_in_days = 7
|
|
}
|
|
|
|
resource "aws_secretsmanager_secret_version" "db_password" {
|
|
secret_id = aws_secretsmanager_secret.db_password.id
|
|
secret_string = jsonencode({
|
|
password = random_password.db.result
|
|
})
|
|
}
|
|
|
|
resource "random_password" "db" {
|
|
length = 32
|
|
special = true
|
|
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
}
|
|
|
|
# Reference in RDS
|
|
resource "aws_db_instance" "main" {
|
|
# ...
|
|
password = random_password.db.result
|
|
|
|
lifecycle {
|
|
ignore_changes = [password]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Variables and Validation
|
|
|
|
```hcl
|
|
variable "environment" {
|
|
description = "Environment name"
|
|
type = string
|
|
|
|
validation {
|
|
condition = contains(["dev", "staging", "prod"], var.environment)
|
|
error_message = "Environment must be dev, staging, or prod."
|
|
}
|
|
}
|
|
|
|
variable "instance_type" {
|
|
description = "EC2 instance type"
|
|
type = string
|
|
default = "t3.micro"
|
|
|
|
validation {
|
|
condition = can(regex("^t[23]\\.", var.instance_type))
|
|
error_message = "Only t2 and t3 instance types are allowed."
|
|
}
|
|
}
|
|
|
|
variable "tags" {
|
|
description = "Additional tags for resources"
|
|
type = map(string)
|
|
default = {}
|
|
}
|
|
```
|
|
|
|
## Data Sources
|
|
|
|
```hcl
|
|
# Get latest Amazon Linux 2 AMI
|
|
data "aws_ami" "amazon_linux_2" {
|
|
most_recent = true
|
|
owners = ["amazon"]
|
|
|
|
filter {
|
|
name = "name"
|
|
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
|
|
}
|
|
|
|
filter {
|
|
name = "virtualization-type"
|
|
values = ["hvm"]
|
|
}
|
|
}
|
|
|
|
# Get current AWS account ID
|
|
data "aws_caller_identity" "current" {}
|
|
|
|
# Get current region
|
|
data "aws_region" "current" {}
|
|
|
|
# Reference in resources
|
|
resource "aws_instance" "example" {
|
|
ami = data.aws_ami.amazon_linux_2.id
|
|
instance_type = var.instance_type
|
|
|
|
tags = {
|
|
Name = "example-${data.aws_region.current.name}"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Outputs
|
|
|
|
```hcl
|
|
output "vpc_id" {
|
|
description = "ID of the VPC"
|
|
value = module.vpc.vpc_id
|
|
}
|
|
|
|
output "alb_dns_name" {
|
|
description = "DNS name of the Application Load Balancer"
|
|
value = aws_lb.main.dns_name
|
|
}
|
|
|
|
output "db_endpoint" {
|
|
description = "Endpoint of the RDS instance"
|
|
value = aws_db_instance.main.endpoint
|
|
sensitive = false
|
|
}
|
|
|
|
output "db_password_secret_arn" {
|
|
description = "ARN of the database password secret"
|
|
value = aws_secretsmanager_secret.db_password.arn
|
|
sensitive = true # Hide in logs
|
|
}
|
|
```
|
|
|
|
## Lifecycle Rules
|
|
|
|
```hcl
|
|
resource "aws_instance" "example" {
|
|
ami = data.aws_ami.amazon_linux_2.id
|
|
instance_type = var.instance_type
|
|
|
|
lifecycle {
|
|
# Prevent accidental destruction
|
|
prevent_destroy = true
|
|
|
|
# Create new before destroying old
|
|
create_before_destroy = true
|
|
|
|
# Ignore changes to tags made outside Terraform
|
|
ignore_changes = [
|
|
tags["LastModified"],
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Initialize
|
|
terraform init
|
|
terraform init -upgrade # Upgrade providers
|
|
|
|
# Planning
|
|
terraform plan # Preview changes
|
|
terraform plan -out=tfplan # Save plan
|
|
terraform plan -target=module.vpc # Plan specific resource
|
|
|
|
# Applying
|
|
terraform apply # Apply changes
|
|
terraform apply tfplan # Apply saved plan
|
|
terraform apply -auto-approve # Skip confirmation (CI/CD only!)
|
|
|
|
# State management
|
|
terraform state list # List resources
|
|
terraform state show aws_vpc.main # Show specific resource
|
|
terraform import aws_vpc.main vpc-123 # Import existing resource
|
|
|
|
# Workspace (for multiple environments)
|
|
terraform workspace list
|
|
terraform workspace new staging
|
|
terraform workspace select dev
|
|
|
|
# Formatting and validation
|
|
terraform fmt -check # Check formatting
|
|
terraform fmt -recursive # Format all files
|
|
terraform validate # Validate configuration
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
```hcl
|
|
# BAD: Hardcoded values
|
|
resource "aws_instance" "web" {
|
|
ami = "ami-12345678" # Will break across regions
|
|
instance_type = "t2.micro" # No flexibility
|
|
}
|
|
|
|
# GOOD: Use data sources and variables
|
|
resource "aws_instance" "web" {
|
|
ami = data.aws_ami.amazon_linux_2.id
|
|
instance_type = var.instance_type
|
|
}
|
|
|
|
|
|
# BAD: No state locking
|
|
terraform {
|
|
backend "s3" {
|
|
bucket = "my-state"
|
|
key = "terraform.tfstate"
|
|
# Missing dynamodb_table!
|
|
}
|
|
}
|
|
|
|
|
|
# BAD: Secrets in tfvars
|
|
# terraform.tfvars
|
|
db_password = "mysecret" # NEVER!
|
|
|
|
# GOOD: Use secrets manager or environment variables
|
|
# export TF_VAR_db_password=$(aws secretsmanager get-secret-value ...)
|
|
```
|