feat: initial Claude Code configuration scaffold
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>
This commit is contained in:
556
.claude/skills/infrastructure/terraform/SKILL.md
Normal file
556
.claude/skills/infrastructure/terraform/SKILL.md
Normal file
@@ -0,0 +1,556 @@
|
||||
---
|
||||
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 ...)
|
||||
```
|
||||
Reference in New Issue
Block a user