--- 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 ...) ```