feat: Terraform Foundation - AWS Landing Zone

Enterprise-grade multi-tenant AWS cloud foundation.

Modules:
- GitHub OIDC for keyless CI/CD authentication
- IAM account settings and security baseline
- AWS Config Rules for compliance
- ABAC (Attribute-Based Access Control)
- SCPs (Service Control Policies)

Features:
- Multi-account architecture
- Cost optimization patterns
- Security best practices
- Comprehensive documentation

Tech: Terraform, AWS Organizations, IAM Identity Center
This commit is contained in:
2026-02-01 20:06:28 +00:00
commit 6136cde9bb
145 changed files with 30832 additions and 0 deletions

View File

@@ -0,0 +1,532 @@
################################################################################
# Workload: API Gateway REST API
#
# Deploys a REST API with:
# - API Gateway with stages
# - Lambda or HTTP backend integrations
# - Custom domain with ACM
# - WAF integration (optional)
# - CloudWatch logging
# - Usage plans and API keys
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<api-name>/
# Update locals
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-api/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
api_name = "${local.tenant}-${local.name}-${local.env}"
# API Type: REST or HTTP
api_type = "REST" # REST for full features, HTTP for simpler/cheaper
# Custom domain (set to null to skip)
domain_name = null # e.g., "api.example.com"
hosted_zone_id = null # Route53 zone ID
# WAF (requires waf-alb module deployed)
waf_acl_arn = null
# Stages
stages = ["prod", "staging"]
# Throttling defaults
throttle_burst_limit = 100
throttle_rate_limit = 50
# CloudWatch logging
logging_level = "INFO" # OFF, ERROR, INFO
# Lambda integrations (map of path -> lambda ARN)
lambda_integrations = {
# "GET /users" = "arn:aws:lambda:us-east-1:123456789012:function:get-users"
# "POST /users" = "arn:aws:lambda:us-east-1:123456789012:function:create-user"
# "GET /users/{id}" = "arn:aws:lambda:us-east-1:123456789012:function:get-user"
}
# HTTP proxy integrations (map of path -> HTTP endpoint)
http_integrations = {
# "GET /health" = "https://backend.example.com/health"
}
# Mock integrations for static responses
mock_integrations = {
"GET /health" = {
status_code = "200"
response = jsonencode({ status = "healthy" })
}
}
# CORS configuration
cors_enabled = true
cors_origins = ["*"]
cors_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
cors_headers = ["Content-Type", "Authorization", "X-Api-Key"]
# API Keys and Usage Plans
create_api_key = true
usage_plans = {
basic = {
quota_limit = 1000
quota_period = "MONTH"
throttle_burst = 10
throttle_rate = 5
}
premium = {
quota_limit = 100000
quota_period = "MONTH"
throttle_burst = 100
throttle_rate = 50
}
}
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# REST API
################################################################################
resource "aws_api_gateway_rest_api" "main" {
name = local.api_name
description = "REST API for ${local.tenant} ${local.name}"
endpoint_configuration {
types = ["REGIONAL"]
}
tags = { Name = local.api_name }
}
################################################################################
# CloudWatch Logging
################################################################################
resource "aws_cloudwatch_log_group" "api" {
name = "/aws/api-gateway/${local.api_name}"
retention_in_days = 30
tags = { Name = local.api_name }
}
resource "aws_api_gateway_account" "main" {
cloudwatch_role_arn = aws_iam_role.api_logging.arn
}
resource "aws_iam_role" "api_logging" {
name = "${local.api_name}-api-logging"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "apigateway.amazonaws.com" }
}]
})
tags = { Name = "${local.api_name}-api-logging" }
}
resource "aws_iam_role_policy_attachment" "api_logging" {
role = aws_iam_role.api_logging.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
}
################################################################################
# Mock Integration - Health Check
################################################################################
resource "aws_api_gateway_resource" "health" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "health"
}
resource "aws_api_gateway_method" "health_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "health_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = aws_api_gateway_method.health_get.http_method
type = "MOCK"
request_templates = {
"application/json" = jsonencode({ statusCode = 200 })
}
}
resource "aws_api_gateway_method_response" "health_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = aws_api_gateway_method.health_get.http_method
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_integration_response" "health_get" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = aws_api_gateway_method.health_get.http_method
status_code = aws_api_gateway_method_response.health_get.status_code
response_templates = {
"application/json" = jsonencode({
status = "healthy"
timestamp = "$context.requestTime"
})
}
response_parameters = {
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
depends_on = [aws_api_gateway_integration.health_get]
}
################################################################################
# CORS - OPTIONS method for health
################################################################################
resource "aws_api_gateway_method" "health_options" {
count = local.cors_enabled ? 1 : 0
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "health_options" {
count = local.cors_enabled ? 1 : 0
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = aws_api_gateway_method.health_options[0].http_method
type = "MOCK"
request_templates = {
"application/json" = jsonencode({ statusCode = 200 })
}
}
resource "aws_api_gateway_method_response" "health_options" {
count = local.cors_enabled ? 1 : 0
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = aws_api_gateway_method.health_options[0].http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true
"method.response.header.Access-Control-Allow-Methods" = true
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_integration_response" "health_options" {
count = local.cors_enabled ? 1 : 0
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.health.id
http_method = aws_api_gateway_method.health_options[0].http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'${join(",", local.cors_headers)}'"
"method.response.header.Access-Control-Allow-Methods" = "'${join(",", local.cors_methods)}'"
"method.response.header.Access-Control-Allow-Origin" = "'${join(",", local.cors_origins)}'"
}
depends_on = [aws_api_gateway_integration.health_options]
}
################################################################################
# Deployment & Stages
################################################################################
resource "aws_api_gateway_deployment" "main" {
rest_api_id = aws_api_gateway_rest_api.main.id
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.health.id,
aws_api_gateway_method.health_get.id,
aws_api_gateway_integration.health_get.id,
]))
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_integration.health_get,
aws_api_gateway_integration_response.health_get,
]
}
resource "aws_api_gateway_stage" "stages" {
for_each = toset(local.stages)
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = each.value
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
caller = "$context.identity.caller"
user = "$context.identity.user"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
status = "$context.status"
protocol = "$context.protocol"
responseLength = "$context.responseLength"
integrationLatency = "$context.integrationLatency"
})
}
tags = { Name = "${local.api_name}-${each.value}" }
}
resource "aws_api_gateway_method_settings" "stages" {
for_each = toset(local.stages)
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = aws_api_gateway_stage.stages[each.value].stage_name
method_path = "*/*"
settings {
logging_level = local.logging_level
data_trace_enabled = local.logging_level != "OFF"
metrics_enabled = true
throttling_burst_limit = local.throttle_burst_limit
throttling_rate_limit = local.throttle_rate_limit
}
}
################################################################################
# WAF Association (Optional)
################################################################################
resource "aws_wafv2_web_acl_association" "api" {
count = local.waf_acl_arn != null ? length(local.stages) : 0
resource_arn = aws_api_gateway_stage.stages[local.stages[count.index]].arn
web_acl_arn = local.waf_acl_arn
}
################################################################################
# Custom Domain (Optional)
################################################################################
resource "aws_acm_certificate" "api" {
count = local.domain_name != null ? 1 : 0
domain_name = local.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = { Name = local.domain_name }
}
resource "aws_route53_record" "cert_validation" {
for_each = local.domain_name != null ? {
for dvo in aws_acm_certificate.api[0].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
} : {}
zone_id = local.hosted_zone_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
resource "aws_acm_certificate_validation" "api" {
count = local.domain_name != null ? 1 : 0
certificate_arn = aws_acm_certificate.api[0].arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
resource "aws_api_gateway_domain_name" "main" {
count = local.domain_name != null ? 1 : 0
domain_name = local.domain_name
certificate_arn = aws_acm_certificate_validation.api[0].certificate_arn
endpoint_configuration {
types = ["REGIONAL"]
}
tags = { Name = local.domain_name }
}
resource "aws_api_gateway_base_path_mapping" "main" {
count = local.domain_name != null ? 1 : 0
api_id = aws_api_gateway_rest_api.main.id
stage_name = aws_api_gateway_stage.stages["prod"].stage_name
domain_name = aws_api_gateway_domain_name.main[0].domain_name
}
resource "aws_route53_record" "api" {
count = local.domain_name != null ? 1 : 0
zone_id = local.hosted_zone_id
name = local.domain_name
type = "A"
alias {
name = aws_api_gateway_domain_name.main[0].regional_domain_name
zone_id = aws_api_gateway_domain_name.main[0].regional_zone_id
evaluate_target_health = false
}
}
################################################################################
# API Keys & Usage Plans
################################################################################
resource "aws_api_gateway_api_key" "main" {
count = local.create_api_key ? 1 : 0
name = "${local.api_name}-key"
enabled = true
tags = { Name = "${local.api_name}-key" }
}
resource "aws_api_gateway_usage_plan" "plans" {
for_each = local.usage_plans
name = "${local.api_name}-${each.key}"
api_stages {
api_id = aws_api_gateway_rest_api.main.id
stage = aws_api_gateway_stage.stages["prod"].stage_name
}
quota_settings {
limit = each.value.quota_limit
period = each.value.quota_period
}
throttle_settings {
burst_limit = each.value.throttle_burst
rate_limit = each.value.throttle_rate
}
tags = { Name = "${local.api_name}-${each.key}" }
}
resource "aws_api_gateway_usage_plan_key" "main" {
count = local.create_api_key ? 1 : 0
key_id = aws_api_gateway_api_key.main[0].id
key_type = "API_KEY"
usage_plan_id = aws_api_gateway_usage_plan.plans["basic"].id
}
################################################################################
# Outputs
################################################################################
output "api_id" {
value = aws_api_gateway_rest_api.main.id
}
output "api_name" {
value = aws_api_gateway_rest_api.main.name
}
output "stage_urls" {
value = { for stage in local.stages : stage => aws_api_gateway_stage.stages[stage].invoke_url }
}
output "custom_domain_url" {
value = local.domain_name != null ? "https://${local.domain_name}" : null
}
output "api_key" {
value = local.create_api_key ? aws_api_gateway_api_key.main[0].value : null
sensitive = true
}
output "health_endpoint" {
value = "${aws_api_gateway_stage.stages["prod"].invoke_url}health"
}

View File

@@ -0,0 +1,473 @@
################################################################################
# Workload: Aurora Serverless v2
#
# Auto-scaling PostgreSQL/MySQL with:
# - Scale to zero (cost savings for dev)
# - Instant scaling (0.5 ACU increments)
# - Multi-AZ by default
# - IAM authentication
# - Data API (HTTP queries)
# - Secrets Manager integration
#
# Cost: ~$0.12/ACU-hour (scales 0.5-128 ACUs)
# Use cases: Variable workloads, dev/test, bursty traffic
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-aurora/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
cluster_name = "${local.tenant}-${local.name}-${local.env}"
# Engine
engine = "aurora-postgresql" # aurora-postgresql or aurora-mysql
engine_version = "15.4" # PostgreSQL 15.4 / MySQL 8.0
# Serverless v2 capacity
min_capacity = 0.5 # Minimum ACUs (0.5 = scale to near-zero)
max_capacity = 16 # Maximum ACUs (adjust based on needs)
# For true scale-to-zero (pauses after idle):
# Note: Only available in some regions
enable_pause = false
pause_after_seconds = 300 # 5 minutes idle
# Database
database_name = replace(local.name, "-", "_")
port = local.engine == "aurora-postgresql" ? 5432 : 3306
# Master credentials (stored in Secrets Manager)
master_username = "admin"
# Network (get from remote state or hardcode)
vpc_id = "" # data.terraform_remote_state.network.outputs.vpc_id
private_subnet_ids = [] # data.terraform_remote_state.network.outputs.private_subnet_ids
# Features
enable_iam_auth = true
enable_data_api = true # HTTP Data API (for Lambda/serverless)
enable_performance_insights = true
performance_insights_retention = 7 # days (7 = free tier)
# Backup
backup_retention_period = 7
preferred_backup_window = "03:00-04:00"
# Maintenance
preferred_maintenance_window = "sun:04:00-sun:05:00"
auto_minor_version_upgrade = true
# Deletion protection (enable for production)
deletion_protection = local.env == "prod"
skip_final_snapshot = local.env != "prod"
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Random Password
################################################################################
resource "random_password" "master" {
length = 32
special = false # Aurora has special char restrictions
}
################################################################################
# Secrets Manager
################################################################################
resource "aws_secretsmanager_secret" "db" {
name = "${local.tenant}/${local.env}/${local.name}/aurora"
description = "Aurora Serverless credentials for ${local.cluster_name}"
tags = { Name = "${local.cluster_name}-credentials" }
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode({
username = local.master_username
password = random_password.master.result
engine = local.engine
host = aws_rds_cluster.main.endpoint
port = local.port
dbname = local.database_name
dbClusterIdentifier = aws_rds_cluster.main.id
})
}
################################################################################
# Security Group
################################################################################
resource "aws_security_group" "aurora" {
count = length(local.vpc_id) > 0 ? 1 : 0
name = "${local.cluster_name}-aurora"
vpc_id = local.vpc_id
ingress {
description = "Database from VPC"
from_port = local.port
to_port = local.port
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"] # Adjust to your VPC CIDR
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.cluster_name}-aurora" }
}
################################################################################
# DB Subnet Group
################################################################################
resource "aws_db_subnet_group" "main" {
count = length(local.private_subnet_ids) > 0 ? 1 : 0
name = local.cluster_name
subnet_ids = local.private_subnet_ids
tags = { Name = local.cluster_name }
}
################################################################################
# Aurora Serverless v2 Cluster
################################################################################
resource "aws_rds_cluster" "main" {
cluster_identifier = local.cluster_name
engine = local.engine
engine_mode = "provisioned" # Required for Serverless v2
engine_version = local.engine_version
database_name = local.database_name
master_username = local.master_username
master_password = random_password.master.result
port = local.port
# Serverless v2 scaling
serverlessv2_scaling_configuration {
min_capacity = local.min_capacity
max_capacity = local.max_capacity
}
# Network
db_subnet_group_name = length(aws_db_subnet_group.main) > 0 ? aws_db_subnet_group.main[0].name : null
vpc_security_group_ids = length(aws_security_group.aurora) > 0 ? [aws_security_group.aurora[0].id] : []
# Storage
storage_encrypted = true
kms_key_id = null # Uses AWS managed key
# Features
enable_http_endpoint = local.enable_data_api
iam_database_authentication_enabled = local.enable_iam_auth
# Backup
backup_retention_period = local.backup_retention_period
preferred_backup_window = local.preferred_backup_window
copy_tags_to_snapshot = true
skip_final_snapshot = local.skip_final_snapshot
final_snapshot_identifier = local.skip_final_snapshot ? null : "${local.cluster_name}-final"
# Maintenance
preferred_maintenance_window = local.preferred_maintenance_window
apply_immediately = false
# Protection
deletion_protection = local.deletion_protection
tags = { Name = local.cluster_name }
lifecycle {
ignore_changes = [
master_password, # Managed in Secrets Manager
]
}
}
################################################################################
# Aurora Serverless v2 Instance
################################################################################
resource "aws_rds_cluster_instance" "main" {
count = 1 # Add more for read replicas
identifier = "${local.cluster_name}-${count.index + 1}"
cluster_identifier = aws_rds_cluster.main.id
instance_class = "db.serverless" # Required for Serverless v2
engine = local.engine
engine_version = local.engine_version
# Performance Insights
performance_insights_enabled = local.enable_performance_insights
performance_insights_retention_period = local.enable_performance_insights ? local.performance_insights_retention : null
# Maintenance
auto_minor_version_upgrade = local.auto_minor_version_upgrade
tags = { Name = "${local.cluster_name}-${count.index + 1}" }
}
################################################################################
# IAM Role for IAM Authentication
################################################################################
resource "aws_iam_policy" "db_connect" {
count = local.enable_iam_auth ? 1 : 0
name = "${local.cluster_name}-db-connect"
description = "IAM authentication to ${local.cluster_name}"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowDBConnect"
Effect = "Allow"
Action = "rds-db:connect"
Resource = "arn:aws:rds-db:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_rds_cluster.main.cluster_resource_id}/*"
}
]
})
tags = { Name = "${local.cluster_name}-db-connect" }
}
################################################################################
# Data API Access Policy
################################################################################
resource "aws_iam_policy" "data_api" {
count = local.enable_data_api ? 1 : 0
name = "${local.cluster_name}-data-api"
description = "Data API access to ${local.cluster_name}"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ExecuteStatement"
Effect = "Allow"
Action = [
"rds-data:ExecuteStatement",
"rds-data:BatchExecuteStatement",
"rds-data:BeginTransaction",
"rds-data:CommitTransaction",
"rds-data:RollbackTransaction"
]
Resource = aws_rds_cluster.main.arn
},
{
Sid = "GetSecret"
Effect = "Allow"
Action = "secretsmanager:GetSecretValue"
Resource = aws_secretsmanager_secret.db.arn
}
]
})
tags = { Name = "${local.cluster_name}-data-api" }
}
################################################################################
# CloudWatch Alarms
################################################################################
resource "aws_cloudwatch_metric_alarm" "cpu" {
alarm_name = "${local.cluster_name}-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 80
alarm_description = "Aurora CPU > 80%"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.main.id
}
tags = { Name = "${local.cluster_name}-cpu-high" }
}
resource "aws_cloudwatch_metric_alarm" "connections" {
alarm_name = "${local.cluster_name}-connections-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "DatabaseConnections"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 100
alarm_description = "Aurora connections > 100"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.main.id
}
tags = { Name = "${local.cluster_name}-connections-high" }
}
resource "aws_cloudwatch_metric_alarm" "capacity" {
alarm_name = "${local.cluster_name}-acu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "ServerlessDatabaseCapacity"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = local.max_capacity * 0.8
alarm_description = "Aurora ACU > 80% of max"
dimensions = {
DBClusterIdentifier = aws_rds_cluster.main.id
}
tags = { Name = "${local.cluster_name}-acu-high" }
}
################################################################################
# Outputs
################################################################################
output "cluster_endpoint" {
value = aws_rds_cluster.main.endpoint
description = "Writer endpoint"
}
output "reader_endpoint" {
value = aws_rds_cluster.main.reader_endpoint
description = "Reader endpoint"
}
output "cluster_arn" {
value = aws_rds_cluster.main.arn
description = "Cluster ARN"
}
output "cluster_id" {
value = aws_rds_cluster.main.id
description = "Cluster identifier"
}
output "port" {
value = local.port
description = "Database port"
}
output "database_name" {
value = local.database_name
description = "Database name"
}
output "secret_arn" {
value = aws_secretsmanager_secret.db.arn
description = "Secrets Manager ARN"
}
output "iam_auth_policy_arn" {
value = length(aws_iam_policy.db_connect) > 0 ? aws_iam_policy.db_connect[0].arn : null
description = "IAM policy for database authentication"
}
output "data_api_policy_arn" {
value = length(aws_iam_policy.data_api) > 0 ? aws_iam_policy.data_api[0].arn : null
description = "IAM policy for Data API access"
}
output "connection_string" {
value = "${local.engine == "aurora-postgresql" ? "postgresql" : "mysql"}://${local.master_username}:****@${aws_rds_cluster.main.endpoint}:${local.port}/${local.database_name}"
description = "Connection string template (password in Secrets Manager)"
sensitive = false
}
output "data_api_example" {
value = local.enable_data_api ? <<-EOF
aws rds-data execute-statement \
--resource-arn '${aws_rds_cluster.main.arn}' \
--secret-arn '${aws_secretsmanager_secret.db.arn}' \
--database '${local.database_name}' \
--sql 'SELECT NOW()'
EOF
: null
description = "Data API example command"
}
output "cost_estimate" {
value = {
acu_hour = "$0.12/ACU-hour"
min_idle = "$${local.min_capacity * 0.12 * 24 * 30}/month (${local.min_capacity} ACU 24/7)"
storage = "$0.10/GB-month"
io = "$0.20/million requests"
data_api = "$0.35/million Data API requests"
}
description = "Cost breakdown"
}

View File

@@ -0,0 +1,501 @@
################################################################################
# Workload: Cognito User Pool
#
# User authentication infrastructure:
# - User Pool with customizable password policy
# - App clients (web, mobile, machine-to-machine)
# - Identity Pool for AWS credential federation
# - Social/SAML/OIDC identity providers
# - Custom domain
# - Lambda triggers (pre/post auth, migration)
#
# Use cases: Web/mobile auth, B2C apps, admin portals
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-auth/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
pool_name = "${local.tenant}-${local.name}-${local.env}"
# Email configuration
email_sending_account = "COGNITO_DEFAULT" # COGNITO_DEFAULT or DEVELOPER
ses_email_from = null # Required if DEVELOPER
# Password policy
password_minimum_length = 12
password_require_lowercase = true
password_require_numbers = true
password_require_symbols = true
password_require_uppercase = true
temporary_password_validity_days = 7
# MFA
mfa_configuration = "OPTIONAL" # OFF, ON, OPTIONAL
mfa_methods = ["SOFTWARE_TOKEN_MFA"] # SOFTWARE_TOKEN_MFA, SMS_MFA
# Account recovery
recovery_mechanisms = [
{ name = "verified_email", priority = 1 },
{ name = "verified_phone_number", priority = 2 }
]
# User attributes
auto_verified_attributes = ["email"]
username_attributes = ["email"] # email, phone_number
alias_attributes = [] # email, phone_number, preferred_username
# Custom attributes
custom_attributes = {
# "tenant_id" = {
# type = "String"
# mutable = false
# min_length = 1
# max_length = 50
# }
}
# App clients
app_clients = {
web = {
generate_secret = false
explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]
supported_identity_providers = ["COGNITO"]
callback_urls = ["https://example.com/callback"]
logout_urls = ["https://example.com/logout"]
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = ["email", "openid", "profile"]
allowed_oauth_flows_user_pool_client = true
access_token_validity = 60 # minutes
id_token_validity = 60
refresh_token_validity = 30 # days
}
mobile = {
generate_secret = false
explicit_auth_flows = ["ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]
supported_identity_providers = ["COGNITO"]
callback_urls = ["myapp://callback"]
logout_urls = ["myapp://logout"]
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = ["email", "openid", "profile"]
allowed_oauth_flows_user_pool_client = true
access_token_validity = 60
id_token_validity = 60
refresh_token_validity = 30
}
# m2m = {
# generate_secret = true
# explicit_auth_flows = ["ALLOW_ADMIN_USER_PASSWORD_AUTH"]
# supported_identity_providers = ["COGNITO"]
# allowed_oauth_flows = ["client_credentials"]
# allowed_oauth_scopes = ["api/read", "api/write"]
# allowed_oauth_flows_user_pool_client = true
# }
}
# Custom domain (requires ACM cert in us-east-1 for CloudFront)
custom_domain = null # e.g., "auth.example.com"
custom_domain_cert = null # ACM certificate ARN
hosted_zone_id = null
# Identity Pool (for AWS credential federation)
enable_identity_pool = false
# Lambda triggers
lambda_triggers = {
# pre_sign_up = "arn:aws:lambda:..."
# post_confirmation = "arn:aws:lambda:..."
# pre_authentication = "arn:aws:lambda:..."
# post_authentication = "arn:aws:lambda:..."
# pre_token_generation = "arn:aws:lambda:..."
# user_migration = "arn:aws:lambda:..."
# custom_message = "arn:aws:lambda:..."
}
# Social identity providers
social_providers = {
# google = {
# client_id = "..."
# client_secret = "..."
# scopes = ["email", "profile", "openid"]
# }
# facebook = {
# client_id = "..."
# client_secret = "..."
# scopes = ["email", "public_profile"]
# }
}
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Cognito User Pool
################################################################################
resource "aws_cognito_user_pool" "main" {
name = local.pool_name
# Username configuration
username_attributes = local.username_attributes
alias_attributes = length(local.alias_attributes) > 0 ? local.alias_attributes : null
auto_verified_attributes = local.auto_verified_attributes
# Password policy
password_policy {
minimum_length = local.password_minimum_length
require_lowercase = local.password_require_lowercase
require_numbers = local.password_require_numbers
require_symbols = local.password_require_symbols
require_uppercase = local.password_require_uppercase
temporary_password_validity_days = local.temporary_password_validity_days
}
# MFA
mfa_configuration = local.mfa_configuration
dynamic "software_token_mfa_configuration" {
for_each = contains(local.mfa_methods, "SOFTWARE_TOKEN_MFA") && local.mfa_configuration != "OFF" ? [1] : []
content {
enabled = true
}
}
# Account recovery
account_recovery_setting {
dynamic "recovery_mechanism" {
for_each = local.recovery_mechanisms
content {
name = recovery_mechanism.value.name
priority = recovery_mechanism.value.priority
}
}
}
# Email configuration
email_configuration {
email_sending_account = local.email_sending_account
source_arn = local.email_sending_account == "DEVELOPER" ? local.ses_email_from : null
}
# User attribute verification
user_attribute_update_settings {
attributes_require_verification_before_update = ["email"]
}
# Admin create user config
admin_create_user_config {
allow_admin_create_user_only = false
invite_message_template {
email_subject = "Your ${local.pool_name} account"
email_message = "Your username is {username} and temporary password is {####}"
sms_message = "Your username is {username} and temporary password is {####}"
}
}
# Verification message
verification_message_template {
default_email_option = "CONFIRM_WITH_CODE"
email_subject = "Verify your email for ${local.pool_name}"
email_message = "Your verification code is {####}"
}
# Schema (custom attributes)
dynamic "schema" {
for_each = local.custom_attributes
content {
name = schema.key
attribute_data_type = schema.value.type
mutable = schema.value.mutable
required = false
developer_only_attribute = false
dynamic "string_attribute_constraints" {
for_each = schema.value.type == "String" ? [1] : []
content {
min_length = lookup(schema.value, "min_length", 0)
max_length = lookup(schema.value, "max_length", 2048)
}
}
dynamic "number_attribute_constraints" {
for_each = schema.value.type == "Number" ? [1] : []
content {
min_value = lookup(schema.value, "min_value", null)
max_value = lookup(schema.value, "max_value", null)
}
}
}
}
# Lambda triggers
lambda_config {
pre_sign_up = lookup(local.lambda_triggers, "pre_sign_up", null)
post_confirmation = lookup(local.lambda_triggers, "post_confirmation", null)
pre_authentication = lookup(local.lambda_triggers, "pre_authentication", null)
post_authentication = lookup(local.lambda_triggers, "post_authentication", null)
pre_token_generation = lookup(local.lambda_triggers, "pre_token_generation", null)
user_migration = lookup(local.lambda_triggers, "user_migration", null)
custom_message = lookup(local.lambda_triggers, "custom_message", null)
}
tags = { Name = local.pool_name }
}
################################################################################
# User Pool Domain
################################################################################
resource "aws_cognito_user_pool_domain" "main" {
domain = local.custom_domain != null ? local.custom_domain : local.pool_name
user_pool_id = aws_cognito_user_pool.main.id
certificate_arn = local.custom_domain_cert
}
# Route53 record for custom domain
resource "aws_route53_record" "cognito" {
count = local.custom_domain != null ? 1 : 0
zone_id = local.hosted_zone_id
name = local.custom_domain
type = "A"
alias {
name = aws_cognito_user_pool_domain.main.cloudfront_distribution_arn
zone_id = "Z2FDTNDATAQYW2" # CloudFront zone ID
evaluate_target_health = false
}
}
################################################################################
# App Clients
################################################################################
resource "aws_cognito_user_pool_client" "clients" {
for_each = local.app_clients
name = "${local.pool_name}-${each.key}"
user_pool_id = aws_cognito_user_pool.main.id
generate_secret = each.value.generate_secret
explicit_auth_flows = each.value.explicit_auth_flows
supported_identity_providers = each.value.supported_identity_providers
callback_urls = lookup(each.value, "callback_urls", null)
logout_urls = lookup(each.value, "logout_urls", null)
allowed_oauth_flows = lookup(each.value, "allowed_oauth_flows", null)
allowed_oauth_scopes = lookup(each.value, "allowed_oauth_scopes", null)
allowed_oauth_flows_user_pool_client = lookup(each.value, "allowed_oauth_flows_user_pool_client", false)
access_token_validity = lookup(each.value, "access_token_validity", 60)
id_token_validity = lookup(each.value, "id_token_validity", 60)
refresh_token_validity = lookup(each.value, "refresh_token_validity", 30)
token_validity_units {
access_token = "minutes"
id_token = "minutes"
refresh_token = "days"
}
prevent_user_existence_errors = "ENABLED"
enable_token_revocation = true
}
################################################################################
# Social Identity Providers
################################################################################
resource "aws_cognito_identity_provider" "google" {
count = contains(keys(local.social_providers), "google") ? 1 : 0
user_pool_id = aws_cognito_user_pool.main.id
provider_name = "Google"
provider_type = "Google"
provider_details = {
client_id = local.social_providers.google.client_id
client_secret = local.social_providers.google.client_secret
authorize_scopes = join(" ", local.social_providers.google.scopes)
}
attribute_mapping = {
email = "email"
username = "sub"
}
}
resource "aws_cognito_identity_provider" "facebook" {
count = contains(keys(local.social_providers), "facebook") ? 1 : 0
user_pool_id = aws_cognito_user_pool.main.id
provider_name = "Facebook"
provider_type = "Facebook"
provider_details = {
client_id = local.social_providers.facebook.client_id
client_secret = local.social_providers.facebook.client_secret
authorize_scopes = join(",", local.social_providers.facebook.scopes)
}
attribute_mapping = {
email = "email"
username = "id"
}
}
################################################################################
# Identity Pool (Optional)
################################################################################
resource "aws_cognito_identity_pool" "main" {
count = local.enable_identity_pool ? 1 : 0
identity_pool_name = replace(local.pool_name, "-", "_")
allow_unauthenticated_identities = false
cognito_identity_providers {
client_id = aws_cognito_user_pool_client.clients["web"].id
provider_name = aws_cognito_user_pool.main.endpoint
server_side_token_check = true
}
tags = { Name = local.pool_name }
}
resource "aws_iam_role" "authenticated" {
count = local.enable_identity_pool ? 1 : 0
name = "${local.pool_name}-authenticated"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = "cognito-identity.amazonaws.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"cognito-identity.amazonaws.com:aud" = aws_cognito_identity_pool.main[0].id
}
"ForAnyValue:StringLike" = {
"cognito-identity.amazonaws.com:amr" = "authenticated"
}
}
}]
})
tags = { Name = "${local.pool_name}-authenticated" }
}
resource "aws_iam_role_policy" "authenticated" {
count = local.enable_identity_pool ? 1 : 0
name = "authenticated-policy"
role = aws_iam_role.authenticated[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"mobileanalytics:PutEvents",
"cognito-sync:*",
"cognito-identity:*"
]
Resource = "*"
}]
})
}
resource "aws_cognito_identity_pool_roles_attachment" "main" {
count = local.enable_identity_pool ? 1 : 0
identity_pool_id = aws_cognito_identity_pool.main[0].id
roles = {
authenticated = aws_iam_role.authenticated[0].arn
}
}
################################################################################
# Outputs
################################################################################
output "user_pool_id" {
value = aws_cognito_user_pool.main.id
}
output "user_pool_arn" {
value = aws_cognito_user_pool.main.arn
}
output "user_pool_endpoint" {
value = aws_cognito_user_pool.main.endpoint
}
output "user_pool_domain" {
value = local.custom_domain != null ? "https://${local.custom_domain}" : "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.name}.amazoncognito.com"
}
output "client_ids" {
value = { for k, v in aws_cognito_user_pool_client.clients : k => v.id }
}
output "identity_pool_id" {
value = local.enable_identity_pool ? aws_cognito_identity_pool.main[0].id : null
}
output "hosted_ui_url" {
value = "${local.custom_domain != null ? "https://${local.custom_domain}" : "https://${aws_cognito_user_pool_domain.main.domain}.auth.${data.aws_region.current.name}.amazoncognito.com"}/login?client_id=${aws_cognito_user_pool_client.clients["web"].id}&response_type=code&redirect_uri=${urlencode(local.app_clients.web.callback_urls[0])}"
}

View File

@@ -0,0 +1,439 @@
################################################################################
# Workload: DynamoDB Table
#
# Deploys a NoSQL database table:
# - On-demand or provisioned capacity
# - Encryption at rest with KMS
# - Point-in-time recovery
# - TTL support
# - Global Secondary Indexes
# - Streams for event-driven patterns
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<table-name>/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-table/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
table_name = "${local.tenant}-${local.name}-${local.env}"
# Capacity mode: "PAY_PER_REQUEST" (on-demand) or "PROVISIONED"
billing_mode = "PAY_PER_REQUEST"
# Provisioned capacity (only used if billing_mode = "PROVISIONED")
read_capacity = 5
write_capacity = 5
# Auto-scaling for provisioned mode
enable_autoscaling = local.billing_mode == "PROVISIONED"
autoscaling_min_read = 5
autoscaling_max_read = 100
autoscaling_min_write = 5
autoscaling_max_write = 100
autoscaling_target_utilization = 70
# Primary key
hash_key = "pk" # Partition key
hash_key_type = "S" # S = String, N = Number, B = Binary
range_key = "sk" # Sort key (optional, set to null to disable)
range_key_type = "S"
# TTL (set to null to disable)
ttl_attribute = "ttl"
# Streams (set to null to disable)
# Options: KEYS_ONLY, NEW_IMAGE, OLD_IMAGE, NEW_AND_OLD_IMAGES
stream_view_type = null
# Point-in-time recovery
point_in_time_recovery = true
# Global Secondary Indexes (GSI)
global_secondary_indexes = [
# {
# name = "gsi1"
# hash_key = "gsi1pk"
# range_key = "gsi1sk"
# projection_type = "ALL" # ALL, KEYS_ONLY, or INCLUDE
# non_key_attributes = [] # Only for INCLUDE
# }
]
# Local Secondary Indexes (LSI) - must be defined at table creation
local_secondary_indexes = [
# {
# name = "lsi1"
# range_key = "lsi1sk"
# projection_type = "ALL"
# non_key_attributes = []
# }
]
# Table class: STANDARD or STANDARD_INFREQUENT_ACCESS
table_class = "STANDARD"
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# KMS Key
################################################################################
resource "aws_kms_key" "table" {
description = "KMS key for ${local.table_name} DynamoDB encryption"
deletion_window_in_days = 7
enable_key_rotation = true
tags = { Name = "${local.table_name}-dynamodb" }
}
resource "aws_kms_alias" "table" {
name = "alias/${local.table_name}-dynamodb"
target_key_id = aws_kms_key.table.key_id
}
################################################################################
# DynamoDB Table
################################################################################
resource "aws_dynamodb_table" "main" {
name = local.table_name
billing_mode = local.billing_mode
table_class = local.table_class
# Capacity (only for PROVISIONED)
read_capacity = local.billing_mode == "PROVISIONED" ? local.read_capacity : null
write_capacity = local.billing_mode == "PROVISIONED" ? local.write_capacity : null
# Primary key
hash_key = local.hash_key
range_key = local.range_key
# Key schema
attribute {
name = local.hash_key
type = local.hash_key_type
}
dynamic "attribute" {
for_each = local.range_key != null ? [1] : []
content {
name = local.range_key
type = local.range_key_type
}
}
# GSI attributes
dynamic "attribute" {
for_each = local.global_secondary_indexes
content {
name = attribute.value.hash_key
type = "S"
}
}
dynamic "attribute" {
for_each = [for gsi in local.global_secondary_indexes : gsi if gsi.range_key != null]
content {
name = attribute.value.range_key
type = "S"
}
}
# LSI attributes
dynamic "attribute" {
for_each = local.local_secondary_indexes
content {
name = attribute.value.range_key
type = "S"
}
}
# Global Secondary Indexes
dynamic "global_secondary_index" {
for_each = local.global_secondary_indexes
content {
name = global_secondary_index.value.name
hash_key = global_secondary_index.value.hash_key
range_key = lookup(global_secondary_index.value, "range_key", null)
projection_type = global_secondary_index.value.projection_type
non_key_attributes = global_secondary_index.value.projection_type == "INCLUDE" ? global_secondary_index.value.non_key_attributes : null
# Capacity for provisioned mode
read_capacity = local.billing_mode == "PROVISIONED" ? local.read_capacity : null
write_capacity = local.billing_mode == "PROVISIONED" ? local.write_capacity : null
}
}
# Local Secondary Indexes
dynamic "local_secondary_index" {
for_each = local.local_secondary_indexes
content {
name = local_secondary_index.value.name
range_key = local_secondary_index.value.range_key
projection_type = local_secondary_index.value.projection_type
non_key_attributes = local_secondary_index.value.projection_type == "INCLUDE" ? local_secondary_index.value.non_key_attributes : null
}
}
# TTL
dynamic "ttl" {
for_each = local.ttl_attribute != null ? [1] : []
content {
attribute_name = local.ttl_attribute
enabled = true
}
}
# Streams
stream_enabled = local.stream_view_type != null
stream_view_type = local.stream_view_type
# Encryption
server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.table.arn
}
# Point-in-time recovery
point_in_time_recovery {
enabled = local.point_in_time_recovery
}
# Deletion protection for prod
deletion_protection_enabled = local.env == "prod"
tags = {
Name = local.table_name
Backup = "true"
}
lifecycle {
prevent_destroy = false # Set to true for production
}
}
################################################################################
# Auto Scaling (Provisioned Mode Only)
################################################################################
resource "aws_appautoscaling_target" "read" {
count = local.enable_autoscaling ? 1 : 0
max_capacity = local.autoscaling_max_read
min_capacity = local.autoscaling_min_read
resource_id = "table/${aws_dynamodb_table.main.name}"
scalable_dimension = "dynamodb:table:ReadCapacityUnits"
service_namespace = "dynamodb"
}
resource "aws_appautoscaling_policy" "read" {
count = local.enable_autoscaling ? 1 : 0
name = "${local.table_name}-read-autoscaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.read[0].resource_id
scalable_dimension = aws_appautoscaling_target.read[0].scalable_dimension
service_namespace = aws_appautoscaling_target.read[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "DynamoDBReadCapacityUtilization"
}
target_value = local.autoscaling_target_utilization
}
}
resource "aws_appautoscaling_target" "write" {
count = local.enable_autoscaling ? 1 : 0
max_capacity = local.autoscaling_max_write
min_capacity = local.autoscaling_min_write
resource_id = "table/${aws_dynamodb_table.main.name}"
scalable_dimension = "dynamodb:table:WriteCapacityUnits"
service_namespace = "dynamodb"
}
resource "aws_appautoscaling_policy" "write" {
count = local.enable_autoscaling ? 1 : 0
name = "${local.table_name}-write-autoscaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.write[0].resource_id
scalable_dimension = aws_appautoscaling_target.write[0].scalable_dimension
service_namespace = aws_appautoscaling_target.write[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "DynamoDBWriteCapacityUtilization"
}
target_value = local.autoscaling_target_utilization
}
}
################################################################################
# CloudWatch Alarms
################################################################################
resource "aws_cloudwatch_metric_alarm" "throttled_requests" {
alarm_name = "${local.table_name}-throttled-requests"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "ThrottledRequests"
namespace = "AWS/DynamoDB"
period = 300
statistic = "Sum"
threshold = 0
alarm_description = "DynamoDB throttled requests detected"
dimensions = {
TableName = aws_dynamodb_table.main.name
}
tags = { Name = "${local.table_name}-throttled" }
}
resource "aws_cloudwatch_metric_alarm" "system_errors" {
alarm_name = "${local.table_name}-system-errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "SystemErrors"
namespace = "AWS/DynamoDB"
period = 300
statistic = "Sum"
threshold = 0
alarm_description = "DynamoDB system errors detected"
dimensions = {
TableName = aws_dynamodb_table.main.name
}
tags = { Name = "${local.table_name}-errors" }
}
################################################################################
# IAM Policy Document (for application access)
################################################################################
data "aws_iam_policy_document" "table_access" {
statement {
sid = "AllowTableOperations"
effect = "Allow"
actions = [
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:UpdateItem",
"dynamodb:DescribeTable",
]
resources = [
aws_dynamodb_table.main.arn,
"${aws_dynamodb_table.main.arn}/index/*",
]
}
statement {
sid = "AllowKMSDecrypt"
effect = "Allow"
actions = [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
]
resources = [aws_kms_key.table.arn]
}
}
################################################################################
# Outputs
################################################################################
output "table_name" {
value = aws_dynamodb_table.main.name
}
output "table_arn" {
value = aws_dynamodb_table.main.arn
}
output "table_id" {
value = aws_dynamodb_table.main.id
}
output "stream_arn" {
value = aws_dynamodb_table.main.stream_arn
}
output "kms_key_arn" {
value = aws_kms_key.table.arn
}
output "access_policy_json" {
value = data.aws_iam_policy_document.table_access.json
description = "IAM policy document for application access to this table"
}

View File

@@ -0,0 +1,400 @@
################################################################################
# Workload: ECR Repository
#
# Container registry with:
# - Image scanning on push
# - Lifecycle policies (cleanup old images)
# - Cross-account access
# - Replication to other regions
# - Immutable tags (optional)
#
# Use cases: Docker images, Lambda container images
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-ecr/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
# Multiple repositories can be created
repositories = {
api = {
description = "API service container"
}
worker = {
description = "Background worker container"
}
# Add more as needed
}
# Image scanning
scan_on_push = true
# Tag immutability (prevents overwriting tags)
image_tag_mutability = "MUTABLE" # MUTABLE or IMMUTABLE
# Encryption
encryption_type = "AES256" # AES256 or KMS
kms_key_arn = null # Set if using KMS
# Lifecycle policy - cleanup old images
lifecycle_policy = {
# Keep last N tagged images
keep_tagged_count = 30
# Delete untagged images older than N days
untagged_expiry_days = 7
# Keep images with these tag prefixes forever
keep_tag_prefixes = ["release-", "v"]
}
# Cross-account access (account IDs that can pull)
pull_access_accounts = [
# "123456789012", # Dev account
# "234567890123", # Staging account
]
# Cross-account push access
push_access_accounts = [
# "345678901234", # CI/CD account
]
# IAM principals with pull access
pull_access_principals = [
# "arn:aws:iam::123456789012:role/ecs-task-role",
]
# Replication to other regions
replication_regions = [
# "us-west-2",
# "eu-west-1",
]
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# ECR Repositories
################################################################################
resource "aws_ecr_repository" "repos" {
for_each = local.repositories
name = "${local.tenant}/${local.name}/${each.key}"
image_tag_mutability = local.image_tag_mutability
image_scanning_configuration {
scan_on_push = local.scan_on_push
}
encryption_configuration {
encryption_type = local.encryption_type
kms_key = local.kms_key_arn
}
tags = {
Name = "${local.tenant}/${local.name}/${each.key}"
Description = each.value.description
}
}
################################################################################
# Lifecycle Policies
################################################################################
resource "aws_ecr_lifecycle_policy" "repos" {
for_each = local.repositories
repository = aws_ecr_repository.repos[each.key].name
policy = jsonencode({
rules = [
# Keep tagged images with specific prefixes
{
rulePriority = 1
description = "Keep release images"
selection = {
tagStatus = "tagged"
tagPrefixList = local.lifecycle_policy.keep_tag_prefixes
countType = "imageCountMoreThan"
countNumber = 9999
}
action = {
type = "expire"
}
},
# Keep last N tagged images
{
rulePriority = 10
description = "Keep last ${local.lifecycle_policy.keep_tagged_count} tagged images"
selection = {
tagStatus = "tagged"
tagPrefixList = [""]
countType = "imageCountMoreThan"
countNumber = local.lifecycle_policy.keep_tagged_count
}
action = {
type = "expire"
}
},
# Delete old untagged images
{
rulePriority = 20
description = "Delete untagged images older than ${local.lifecycle_policy.untagged_expiry_days} days"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = local.lifecycle_policy.untagged_expiry_days
}
action = {
type = "expire"
}
}
]
})
}
################################################################################
# Repository Policies (Cross-Account Access)
################################################################################
resource "aws_ecr_repository_policy" "repos" {
for_each = length(local.pull_access_accounts) > 0 || length(local.push_access_accounts) > 0 || length(local.pull_access_principals) > 0 ? local.repositories : {}
repository = aws_ecr_repository.repos[each.key].name
policy = jsonencode({
Version = "2012-10-17"
Statement = concat(
# Cross-account pull access
length(local.pull_access_accounts) > 0 ? [{
Sid = "CrossAccountPull"
Effect = "Allow"
Principal = {
AWS = [for acct in local.pull_access_accounts : "arn:aws:iam::${acct}:root"]
}
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}] : [],
# Cross-account push access
length(local.push_access_accounts) > 0 ? [{
Sid = "CrossAccountPush"
Effect = "Allow"
Principal = {
AWS = [for acct in local.push_access_accounts : "arn:aws:iam::${acct}:root"]
}
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}] : [],
# Principal-based pull access
length(local.pull_access_principals) > 0 ? [{
Sid = "PrincipalPull"
Effect = "Allow"
Principal = {
AWS = local.pull_access_principals
}
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}] : []
)
})
}
################################################################################
# Replication Configuration
################################################################################
resource "aws_ecr_replication_configuration" "main" {
count = length(local.replication_regions) > 0 ? 1 : 0
replication_configuration {
rule {
dynamic "destination" {
for_each = local.replication_regions
content {
region = destination.value
registry_id = data.aws_caller_identity.current.account_id
}
}
repository_filter {
filter = "${local.tenant}/${local.name}/"
filter_type = "PREFIX_MATCH"
}
}
}
}
################################################################################
# IAM Policy for CI/CD
################################################################################
resource "aws_iam_policy" "push" {
name = "${local.tenant}-${local.name}-ecr-push"
description = "Push access to ${local.tenant}/${local.name} ECR repositories"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "GetAuthToken"
Effect = "Allow"
Action = "ecr:GetAuthorizationToken"
Resource = "*"
},
{
Sid = "PushImages"
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
Resource = [for repo in aws_ecr_repository.repos : repo.arn]
}
]
})
tags = { Name = "${local.tenant}-${local.name}-ecr-push" }
}
resource "aws_iam_policy" "pull" {
name = "${local.tenant}-${local.name}-ecr-pull"
description = "Pull access to ${local.tenant}/${local.name} ECR repositories"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "GetAuthToken"
Effect = "Allow"
Action = "ecr:GetAuthorizationToken"
Resource = "*"
},
{
Sid = "PullImages"
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
Resource = [for repo in aws_ecr_repository.repos : repo.arn]
}
]
})
tags = { Name = "${local.tenant}-${local.name}-ecr-pull" }
}
################################################################################
# Outputs
################################################################################
output "repository_urls" {
value = { for k, v in aws_ecr_repository.repos : k => v.repository_url }
description = "Repository URLs for docker push/pull"
}
output "repository_arns" {
value = { for k, v in aws_ecr_repository.repos : k => v.arn }
description = "Repository ARNs"
}
output "push_policy_arn" {
value = aws_iam_policy.push.arn
description = "IAM policy ARN for push access"
}
output "pull_policy_arn" {
value = aws_iam_policy.pull.arn
description = "IAM policy ARN for pull access"
}
output "docker_login_command" {
value = "aws ecr get-login-password --region ${data.aws_region.current.name} | docker login --username AWS --password-stdin ${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com"
description = "Command to authenticate Docker with ECR"
}
output "push_commands" {
value = { for k, v in aws_ecr_repository.repos : k => <<-EOF
docker build -t ${v.repository_url}:latest .
docker push ${v.repository_url}:latest
EOF
}
description = "Docker build and push commands for each repository"
}

View File

@@ -0,0 +1,701 @@
################################################################################
# Workload: ECS Fargate Service
#
# Container service with:
# - Fargate (serverless containers)
# - Auto-scaling
# - ALB integration
# - Service discovery
# - Secrets/SSM integration
# - CloudWatch logging
# - X-Ray tracing
#
# Use cases: Web services, APIs, microservices
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-ecs/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
prefix = "${local.tenant}-${local.name}-${local.env}"
# Container configuration
container = {
image = "nginx:latest" # Update to your ECR image
port = 80
protocol = "HTTP"
# Resources (Fargate valid combinations)
# CPU: 256, 512, 1024, 2048, 4096
# Memory depends on CPU
cpu = 256
memory = 512
# Health check
health_check_path = "/health"
health_check_interval = 30
# Environment variables
environment = {
LOG_LEVEL = "info"
NODE_ENV = local.env
}
# Secrets from SSM Parameter Store
secrets_ssm = {
# DATABASE_URL = "/${local.tenant}/${local.env}/${local.name}/database/url"
}
# Secrets from Secrets Manager
secrets_sm = {
# API_KEY = "myapp/api-key"
}
}
# Service configuration
service = {
desired_count = 2
min_count = 1
max_count = 10
# Deployment
deployment_max_percent = 200
deployment_min_healthy_percent = 100
# Enable execute command (for debugging)
enable_execute_command = true
}
# Network (get from remote state or hardcode)
vpc_id = "" # data.terraform_remote_state.network.outputs.vpc_id
private_subnet_ids = [] # data.terraform_remote_state.network.outputs.private_subnet_ids
public_subnet_ids = [] # data.terraform_remote_state.network.outputs.public_subnet_ids
# Load balancer
alb = {
enabled = true
internal = false
certificate_arn = "" # ACM certificate ARN for HTTPS
health_check_path = local.container.health_check_path
}
# Auto-scaling
autoscaling = {
enabled = true
# CPU-based scaling
cpu_target = 70
# Request count scaling (if ALB)
requests_target = 1000 # requests per target per minute
# Scale-in cooldown
scale_in_cooldown = 300
scale_out_cooldown = 60
}
# Service discovery (Cloud Map)
service_discovery = {
enabled = false
namespace_id = "" # Cloud Map namespace ID
dns_ttl = 10
}
# Logging
log_retention_days = 30
# X-Ray tracing
enable_xray = false
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# ECS Cluster
################################################################################
resource "aws_ecs_cluster" "main" {
name = local.prefix
setting {
name = "containerInsights"
value = "enabled"
}
tags = { Name = local.prefix }
}
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
base = 1
weight = 100
capacity_provider = "FARGATE"
}
}
################################################################################
# CloudWatch Logs
################################################################################
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${local.prefix}"
retention_in_days = local.log_retention_days
tags = { Name = local.prefix }
}
################################################################################
# IAM Roles
################################################################################
# Task execution role (ECS agent)
resource "aws_iam_role" "execution" {
name = "${local.prefix}-execution"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
tags = { Name = "${local.prefix}-execution" }
}
resource "aws_iam_role_policy_attachment" "execution" {
role = aws_iam_role.execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy" "execution_secrets" {
name = "secrets-access"
role = aws_iam_role.execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "SSMParameters"
Effect = "Allow"
Action = [
"ssm:GetParameters",
"ssm:GetParameter"
]
Resource = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${local.tenant}/*"
},
{
Sid = "SecretsManager"
Effect = "Allow"
Action = "secretsmanager:GetSecretValue"
Resource = "arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:${local.tenant}/*"
},
{
Sid = "KMSDecrypt"
Effect = "Allow"
Action = "kms:Decrypt"
Resource = "*"
}
]
})
}
# Task role (application)
resource "aws_iam_role" "task" {
name = "${local.prefix}-task"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
tags = { Name = "${local.prefix}-task" }
}
# Allow ECS exec
resource "aws_iam_role_policy" "task_exec" {
count = local.service.enable_execute_command ? 1 : 0
name = "ecs-exec"
role = aws_iam_role.task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
]
Resource = "*"
}]
})
}
# X-Ray tracing
resource "aws_iam_role_policy" "task_xray" {
count = local.enable_xray ? 1 : 0
name = "xray"
role = aws_iam_role.task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
"xray:GetSamplingStatisticSummaries"
]
Resource = "*"
}]
})
}
################################################################################
# Security Groups
################################################################################
resource "aws_security_group" "service" {
count = length(local.vpc_id) > 0 ? 1 : 0
name = "${local.prefix}-service"
vpc_id = local.vpc_id
ingress {
description = "From ALB"
from_port = local.container.port
to_port = local.container.port
protocol = "tcp"
security_groups = local.alb.enabled ? [aws_security_group.alb[0].id] : []
cidr_blocks = local.alb.enabled ? [] : ["0.0.0.0/0"]
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.prefix}-service" }
}
resource "aws_security_group" "alb" {
count = local.alb.enabled && length(local.vpc_id) > 0 ? 1 : 0
name = "${local.prefix}-alb"
vpc_id = local.vpc_id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP redirect"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "To service"
from_port = local.container.port
to_port = local.container.port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.prefix}-alb" }
}
################################################################################
# Application Load Balancer
################################################################################
resource "aws_lb" "main" {
count = local.alb.enabled && length(local.public_subnet_ids) > 0 ? 1 : 0
name = local.prefix
internal = local.alb.internal
load_balancer_type = "application"
security_groups = [aws_security_group.alb[0].id]
subnets = local.alb.internal ? local.private_subnet_ids : local.public_subnet_ids
enable_deletion_protection = local.env == "prod"
tags = { Name = local.prefix }
}
resource "aws_lb_target_group" "main" {
count = local.alb.enabled && length(local.vpc_id) > 0 ? 1 : 0
name = local.prefix
port = local.container.port
protocol = "HTTP"
vpc_id = local.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = local.container.health_check_interval
path = local.alb.health_check_path
matcher = "200-299"
}
tags = { Name = local.prefix }
}
resource "aws_lb_listener" "https" {
count = local.alb.enabled && length(local.alb.certificate_arn) > 0 && length(local.public_subnet_ids) > 0 ? 1 : 0
load_balancer_arn = aws_lb.main[0].arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = local.alb.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.main[0].arn
}
}
resource "aws_lb_listener" "http_redirect" {
count = local.alb.enabled && length(local.public_subnet_ids) > 0 ? 1 : 0
load_balancer_arn = aws_lb.main[0].arn
port = 80
protocol = "HTTP"
default_action {
type = length(local.alb.certificate_arn) > 0 ? "redirect" : "forward"
dynamic "redirect" {
for_each = length(local.alb.certificate_arn) > 0 ? [1] : []
content {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
target_group_arn = length(local.alb.certificate_arn) > 0 ? null : aws_lb_target_group.main[0].arn
}
}
################################################################################
# Task Definition
################################################################################
resource "aws_ecs_task_definition" "main" {
family = local.prefix
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = local.container.cpu
memory = local.container.memory
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode(concat(
[{
name = "app"
image = local.container.image
essential = true
portMappings = [{
containerPort = local.container.port
protocol = "tcp"
}]
environment = [
for k, v in local.container.environment : { name = k, value = v }
]
secrets = concat(
[for k, v in local.container.secrets_ssm : {
name = k
valueFrom = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${v}"
}],
[for k, v in local.container.secrets_sm : {
name = k
valueFrom = "arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:${v}"
}]
)
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "app"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:${local.container.port}${local.container.health_check_path} || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}],
local.enable_xray ? [{
name = "xray-daemon"
image = "amazon/aws-xray-daemon:latest"
essential = false
cpu = 32
memory = 256
portMappings = [{
containerPort = 2000
protocol = "udp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "xray"
}
}
}] : []
))
tags = { Name = local.prefix }
}
################################################################################
# ECS Service
################################################################################
resource "aws_ecs_service" "main" {
count = length(local.vpc_id) > 0 && length(local.private_subnet_ids) > 0 ? 1 : 0
name = local.prefix
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = local.service.desired_count
launch_type = "FARGATE"
deployment_maximum_percent = local.service.deployment_max_percent
deployment_minimum_healthy_percent = local.service.deployment_min_healthy_percent
enable_execute_command = local.service.enable_execute_command
network_configuration {
subnets = local.private_subnet_ids
security_groups = [aws_security_group.service[0].id]
assign_public_ip = false
}
dynamic "load_balancer" {
for_each = local.alb.enabled ? [1] : []
content {
target_group_arn = aws_lb_target_group.main[0].arn
container_name = "app"
container_port = local.container.port
}
}
dynamic "service_registries" {
for_each = local.service_discovery.enabled ? [1] : []
content {
registry_arn = aws_service_discovery_service.main[0].arn
}
}
tags = { Name = local.prefix }
lifecycle {
ignore_changes = [desired_count]
}
}
################################################################################
# Auto Scaling
################################################################################
resource "aws_appautoscaling_target" "main" {
count = local.autoscaling.enabled && length(local.vpc_id) > 0 ? 1 : 0
max_capacity = local.service.max_count
min_capacity = local.service.min_count
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main[0].name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
count = local.autoscaling.enabled && length(local.vpc_id) > 0 ? 1 : 0
name = "${local.prefix}-cpu"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main[0].resource_id
scalable_dimension = aws_appautoscaling_target.main[0].scalable_dimension
service_namespace = aws_appautoscaling_target.main[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = local.autoscaling.cpu_target
scale_in_cooldown = local.autoscaling.scale_in_cooldown
scale_out_cooldown = local.autoscaling.scale_out_cooldown
}
}
resource "aws_appautoscaling_policy" "requests" {
count = local.autoscaling.enabled && local.alb.enabled && length(local.vpc_id) > 0 ? 1 : 0
name = "${local.prefix}-requests"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.main[0].resource_id
scalable_dimension = aws_appautoscaling_target.main[0].scalable_dimension
service_namespace = aws_appautoscaling_target.main[0].service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ALBRequestCountPerTarget"
resource_label = "${aws_lb.main[0].arn_suffix}/${aws_lb_target_group.main[0].arn_suffix}"
}
target_value = local.autoscaling.requests_target
scale_in_cooldown = local.autoscaling.scale_in_cooldown
scale_out_cooldown = local.autoscaling.scale_out_cooldown
}
}
################################################################################
# Service Discovery
################################################################################
resource "aws_service_discovery_service" "main" {
count = local.service_discovery.enabled ? 1 : 0
name = local.name
dns_config {
namespace_id = local.service_discovery.namespace_id
dns_records {
ttl = local.service_discovery.dns_ttl
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
tags = { Name = local.prefix }
}
################################################################################
# Outputs
################################################################################
output "cluster_name" {
value = aws_ecs_cluster.main.name
description = "ECS cluster name"
}
output "service_name" {
value = length(aws_ecs_service.main) > 0 ? aws_ecs_service.main[0].name : null
description = "ECS service name"
}
output "alb_dns_name" {
value = length(aws_lb.main) > 0 ? aws_lb.main[0].dns_name : null
description = "ALB DNS name"
}
output "alb_zone_id" {
value = length(aws_lb.main) > 0 ? aws_lb.main[0].zone_id : null
description = "ALB hosted zone ID (for Route53 alias)"
}
output "task_definition_arn" {
value = aws_ecs_task_definition.main.arn
description = "Task definition ARN"
}
output "log_group" {
value = aws_cloudwatch_log_group.app.name
description = "CloudWatch log group"
}
output "exec_command" {
value = length(aws_ecs_service.main) > 0 ? "aws ecs execute-command --cluster ${aws_ecs_cluster.main.name} --task <task-id> --container app --interactive --command '/bin/sh'" : null
description = "ECS exec command for debugging"
}
output "update_command" {
value = length(aws_ecs_service.main) > 0 ? "aws ecs update-service --cluster ${aws_ecs_cluster.main.name} --service ${aws_ecs_service.main[0].name} --force-new-deployment" : null
description = "Force new deployment command"
}

View File

@@ -0,0 +1,560 @@
################################################################################
# Workload: ECS Fargate Service
#
# Deploys a containerized application on ECS Fargate:
# - ECS Service with Fargate launch type
# - Application Load Balancer (optional)
# - Auto-scaling based on CPU/Memory
# - CloudWatch logging
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<app>/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<APP>/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
app = "<APP>"
env = "prod" # prod, staging, dev
name = "${local.tenant}-${local.app}-${local.env}"
# Short name for resources with strict limits (ALB: 32 chars, TG: 32 chars)
# Uses first 10 chars of tenant + first 10 of app + env suffix
short_name = "${substr(local.tenant, 0, min(10, length(local.tenant)))}-${substr(local.app, 0, min(10, length(local.app)))}-${substr(local.env, 0, 4)}"
# Container config
container_image = "nginx:latest" # Replace with your ECR image
container_port = 8080
cpu = 256 # 0.25 vCPU
memory = 512 # MB
# Scaling
desired_count = 2
min_count = 1
max_count = 10
# Load balancer
enable_alb = true
health_check_path = "/health"
# Environment variables (non-sensitive)
environment = {
APP_ENV = local.env
LOG_LEVEL = "info"
}
# Secrets from SSM/Secrets Manager (ARNs)
secrets = {
# DATABASE_URL = "arn:aws:secretsmanager:us-east-1:123456789:secret:mydb-xxx"
}
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.app
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "02-network/terraform.tfstate"
region = var.region
}
}
data "terraform_remote_state" "tenant" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "04-tenants/${local.tenant}/terraform.tfstate"
region = var.region
}
}
data "terraform_remote_state" "bootstrap" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "00-bootstrap/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# ECS Cluster
################################################################################
resource "aws_ecs_cluster" "main" {
name = local.name
setting {
name = "containerInsights"
value = "enabled"
}
tags = { Name = local.name }
}
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
base = 1
weight = 100
capacity_provider = "FARGATE"
}
}
################################################################################
# CloudWatch Log Group
################################################################################
resource "aws_cloudwatch_log_group" "main" {
name = "/ecs/${local.name}"
retention_in_days = 30
tags = { Name = local.name }
}
################################################################################
# IAM - Task Execution Role
################################################################################
resource "aws_iam_role" "execution" {
name = "${local.name}-execution"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-execution" }
}
resource "aws_iam_role_policy_attachment" "execution" {
role = aws_iam_role.execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy" "execution_secrets" {
count = length(local.secrets) > 0 ? 1 : 0
name = "secrets-access"
role = aws_iam_role.execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = values(local.secrets)
},
{
Effect = "Allow"
Action = ["ssm:GetParameters"]
Resource = values(local.secrets)
}
]
})
}
################################################################################
# IAM - Task Role (app permissions)
################################################################################
resource "aws_iam_role" "task" {
name = "${local.name}-task"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-task" }
}
# Add app-specific permissions here
resource "aws_iam_role_policy" "task" {
name = "app-permissions"
role = aws_iam_role.task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowTaggedResources"
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "dynamodb:*"]
Resource = "*"
Condition = { StringEquals = { "aws:ResourceTag/Tenant" = local.tenant } }
}
]
})
}
################################################################################
# Task Definition
################################################################################
resource "aws_ecs_task_definition" "main" {
family = local.name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = local.cpu
memory = local.memory
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([
{
name = local.app
image = local.container_image
essential = true
portMappings = [{
containerPort = local.container_port
protocol = "tcp"
}]
environment = [
for k, v in local.environment : { name = k, value = v }
]
secrets = [
for k, v in local.secrets : { name = k, valueFrom = v }
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.main.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:${local.container_port}${local.health_check_path} || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
tags = { Name = local.name }
}
################################################################################
# Security Group - Service
################################################################################
resource "aws_security_group" "service" {
name = "${local.name}-service"
description = "ECS service for ${local.name}"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = { Name = "${local.name}-service" }
}
# Separate ingress rules to handle conditional ALB
resource "aws_security_group_rule" "service_from_alb" {
count = local.enable_alb ? 1 : 0
type = "ingress"
from_port = local.container_port
to_port = local.container_port
protocol = "tcp"
source_security_group_id = aws_security_group.alb[0].id
security_group_id = aws_security_group.service.id
description = "From ALB"
}
resource "aws_security_group_rule" "service_self" {
count = local.enable_alb ? 0 : 1
type = "ingress"
from_port = local.container_port
to_port = local.container_port
protocol = "tcp"
self = true
security_group_id = aws_security_group.service.id
description = "Self-referencing for service mesh"
}
################################################################################
# ALB
################################################################################
resource "aws_security_group" "alb" {
count = local.enable_alb ? 1 : 0
name = "${local.name}-alb"
description = "ALB for ${local.name}"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.name}-alb" }
}
resource "aws_lb" "main" {
count = local.enable_alb ? 1 : 0
name = local.short_name # ALB names max 32 chars
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb[0].id]
subnets = data.terraform_remote_state.network.outputs.public_subnet_ids
# Security: Drop invalid headers
drop_invalid_header_fields = true
# Access logging for audit trail
access_logs {
bucket = data.terraform_remote_state.bootstrap.outputs.logs_bucket
prefix = "alb/${local.name}"
enabled = true
}
tags = { Name = local.name }
}
resource "aws_lb_target_group" "main" {
count = local.enable_alb ? 1 : 0
name = local.short_name # Target group names max 32 chars
port = local.container_port
protocol = "HTTP"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = local.health_check_path
port = "traffic-port"
timeout = 5
unhealthy_threshold = 3
}
# Enable stickiness for stateful apps (disabled by default)
stickiness {
type = "lb_cookie"
enabled = false
cookie_duration = 86400
}
tags = { Name = local.name }
}
resource "aws_lb_listener" "http" {
count = local.enable_alb ? 1 : 0
load_balancer_arn = aws_lb.main[0].arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.main[0].arn
}
}
################################################################################
# ECS Service
################################################################################
resource "aws_ecs_service" "main" {
name = local.app
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = local.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = data.terraform_remote_state.network.outputs.private_subnet_ids
security_groups = [aws_security_group.service.id, data.terraform_remote_state.tenant.outputs.security_groups.base]
assign_public_ip = false
}
dynamic "load_balancer" {
for_each = local.enable_alb ? [1] : []
content {
target_group_arn = aws_lb_target_group.main[0].arn
container_name = local.app
container_port = local.container_port
}
}
lifecycle {
ignore_changes = [desired_count] # Managed by auto-scaling
}
tags = { Name = local.name }
}
################################################################################
# Auto Scaling
################################################################################
resource "aws_appautoscaling_target" "ecs" {
max_capacity = local.max_count
min_capacity = local.min_count
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${local.name}-cpu"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "memory" {
name = "${local.name}-memory"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = 80
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
################################################################################
# Outputs
################################################################################
output "cluster_name" {
value = aws_ecs_cluster.main.name
}
output "service_name" {
value = aws_ecs_service.main.name
}
output "alb_dns_name" {
value = local.enable_alb ? aws_lb.main[0].dns_name : null
}
output "alb_zone_id" {
value = local.enable_alb ? aws_lb.main[0].zone_id : null
}
output "log_group" {
value = aws_cloudwatch_log_group.main.name
}
output "task_role_arn" {
value = aws_iam_role.task.arn
}

View File

@@ -0,0 +1,968 @@
################################################################################
# Workload: EKS Cluster
#
# Deploys a managed Kubernetes cluster:
# - EKS cluster with managed node groups
# - Core addons (VPC CNI, CoreDNS, kube-proxy)
# - IRSA (IAM Roles for Service Accounts)
# - Cluster Autoscaler ready
# - AWS Load Balancer Controller ready
# - Optional Fargate profiles
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-eks/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
tls = {
source = "hashicorp/tls"
version = ">= 4.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-eks/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
env = "prod" # prod, staging, dev
name = "${local.tenant}-${local.env}"
# EKS Version
cluster_version = "1.29"
# Node Groups
node_groups = {
general = {
instance_types = ["t3.medium"]
capacity_type = "ON_DEMAND" # ON_DEMAND or SPOT
min_size = 2
max_size = 10
desired_size = 2
disk_size = 50
labels = {
role = "general"
}
taints = []
}
# Uncomment for spot instances
# spot = {
# instance_types = ["t3.medium", "t3.large", "t3a.medium"]
# capacity_type = "SPOT"
# min_size = 0
# max_size = 20
# desired_size = 0
# disk_size = 50
# labels = {
# role = "spot"
# }
# taints = [{
# key = "spot"
# value = "true"
# effect = "NO_SCHEDULE"
# }]
# }
}
# Fargate (for serverless pods)
enable_fargate = false
fargate_namespaces = ["serverless"] # Namespaces to run on Fargate
# Addons
enable_cluster_autoscaler = true
enable_aws_lb_controller = true
enable_ebs_csi_driver = true
enable_metrics_server = true
# Logging
cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
log_retention_days = 30
# Access
cluster_endpoint_public = true
cluster_endpoint_private = true
public_access_cidrs = ["0.0.0.0/0"] # Restrict in production!
# Admin access (IAM ARNs that can access cluster)
admin_arns = [
# "arn:aws:iam::123456789012:role/Admin",
# "arn:aws:iam::123456789012:user/admin",
]
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "02-network/terraform.tfstate"
region = var.region
}
}
data "terraform_remote_state" "tenant" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "04-tenants/${local.tenant}/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
data "aws_partition" "current" {}
################################################################################
# KMS Key for Secrets Encryption
################################################################################
resource "aws_kms_key" "eks" {
description = "EKS Secret Encryption Key for ${local.name}"
deletion_window_in_days = 7
enable_key_rotation = true
tags = { Name = "${local.name}-eks" }
}
resource "aws_kms_alias" "eks" {
name = "alias/${local.name}-eks"
target_key_id = aws_kms_key.eks.key_id
}
################################################################################
# CloudWatch Log Group
################################################################################
resource "aws_cloudwatch_log_group" "eks" {
name = "/aws/eks/${local.name}/cluster"
retention_in_days = local.log_retention_days
tags = { Name = "${local.name}-eks" }
}
################################################################################
# IAM - Cluster Role
################################################################################
resource "aws_iam_role" "cluster" {
name = "${local.name}-eks-cluster"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "eks.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-eks-cluster" }
}
resource "aws_iam_role_policy_attachment" "cluster_policy" {
role = aws_iam_role.cluster.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSClusterPolicy"
}
resource "aws_iam_role_policy_attachment" "cluster_vpc_policy" {
role = aws_iam_role.cluster.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSVPCResourceController"
}
################################################################################
# IAM - Node Role
################################################################################
resource "aws_iam_role" "node" {
name = "${local.name}-eks-node"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "ec2.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-eks-node" }
}
resource "aws_iam_role_policy_attachment" "node_policy" {
role = aws_iam_role.node.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_iam_role_policy_attachment" "node_cni_policy" {
role = aws_iam_role.node.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKS_CNI_Policy"
}
resource "aws_iam_role_policy_attachment" "node_ecr_policy" {
role = aws_iam_role.node.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
resource "aws_iam_role_policy_attachment" "node_ssm_policy" {
role = aws_iam_role.node.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
################################################################################
# IAM - Fargate Role
################################################################################
resource "aws_iam_role" "fargate" {
count = local.enable_fargate ? 1 : 0
name = "${local.name}-eks-fargate"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "eks-fargate-pods.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-eks-fargate" }
}
resource "aws_iam_role_policy_attachment" "fargate_policy" {
count = local.enable_fargate ? 1 : 0
role = aws_iam_role.fargate[0].name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
}
################################################################################
# Security Groups
################################################################################
resource "aws_security_group" "cluster" {
name = "${local.name}-eks-cluster"
description = "EKS cluster security group"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
tags = { Name = "${local.name}-eks-cluster" }
}
resource "aws_security_group_rule" "cluster_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.cluster.id
description = "Allow all outbound"
}
resource "aws_security_group" "node" {
name = "${local.name}-eks-node"
description = "EKS node security group"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
tags = {
Name = "${local.name}-eks-node"
"kubernetes.io/cluster/${local.name}" = "owned"
}
}
resource "aws_security_group_rule" "node_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.node.id
description = "Allow all outbound"
}
resource "aws_security_group_rule" "node_ingress_self" {
type = "ingress"
from_port = 0
to_port = 65535
protocol = "-1"
source_security_group_id = aws_security_group.node.id
security_group_id = aws_security_group.node.id
description = "Node to node"
}
resource "aws_security_group_rule" "node_ingress_cluster" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
source_security_group_id = aws_security_group.cluster.id
security_group_id = aws_security_group.node.id
description = "Cluster to node (webhooks)"
}
resource "aws_security_group_rule" "node_ingress_cluster_kubelet" {
type = "ingress"
from_port = 10250
to_port = 10250
protocol = "tcp"
source_security_group_id = aws_security_group.cluster.id
security_group_id = aws_security_group.node.id
description = "Cluster to node (kubelet)"
}
resource "aws_security_group_rule" "cluster_ingress_node" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
source_security_group_id = aws_security_group.node.id
security_group_id = aws_security_group.cluster.id
description = "Node to cluster API"
}
################################################################################
# EKS Cluster
################################################################################
resource "aws_eks_cluster" "main" {
name = local.name
version = local.cluster_version
role_arn = aws_iam_role.cluster.arn
vpc_config {
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
endpoint_private_access = local.cluster_endpoint_private
endpoint_public_access = local.cluster_endpoint_public
public_access_cidrs = local.public_access_cidrs
security_group_ids = [aws_security_group.cluster.id]
}
encryption_config {
provider {
key_arn = aws_kms_key.eks.arn
}
resources = ["secrets"]
}
enabled_cluster_log_types = local.cluster_log_types
depends_on = [
aws_iam_role_policy_attachment.cluster_policy,
aws_iam_role_policy_attachment.cluster_vpc_policy,
aws_cloudwatch_log_group.eks,
]
tags = { Name = local.name }
}
################################################################################
# EKS Addons
################################################################################
resource "aws_eks_addon" "vpc_cni" {
cluster_name = aws_eks_cluster.main.name
addon_name = "vpc-cni"
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
tags = { Name = "${local.name}-vpc-cni" }
}
resource "aws_eks_addon" "coredns" {
cluster_name = aws_eks_cluster.main.name
addon_name = "coredns"
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
depends_on = [aws_eks_node_group.main]
tags = { Name = "${local.name}-coredns" }
}
resource "aws_eks_addon" "kube_proxy" {
cluster_name = aws_eks_cluster.main.name
addon_name = "kube-proxy"
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
tags = { Name = "${local.name}-kube-proxy" }
}
resource "aws_eks_addon" "ebs_csi" {
count = local.enable_ebs_csi_driver ? 1 : 0
cluster_name = aws_eks_cluster.main.name
addon_name = "aws-ebs-csi-driver"
service_account_role_arn = aws_iam_role.ebs_csi[0].arn
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
depends_on = [aws_eks_node_group.main]
tags = { Name = "${local.name}-ebs-csi" }
}
################################################################################
# Node Groups
################################################################################
resource "aws_eks_node_group" "main" {
for_each = local.node_groups
cluster_name = aws_eks_cluster.main.name
node_group_name = "${local.name}-${each.key}"
node_role_arn = aws_iam_role.node.arn
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
instance_types = each.value.instance_types
capacity_type = each.value.capacity_type
disk_size = each.value.disk_size
scaling_config {
min_size = each.value.min_size
max_size = each.value.max_size
desired_size = each.value.desired_size
}
update_config {
max_unavailable = 1
}
labels = merge(each.value.labels, {
Tenant = local.tenant
})
dynamic "taint" {
for_each = each.value.taints
content {
key = taint.value.key
value = taint.value.value
effect = taint.value.effect
}
}
# Launch template for security hardening
launch_template {
id = aws_launch_template.node[each.key].id
version = aws_launch_template.node[each.key].latest_version
}
depends_on = [
aws_iam_role_policy_attachment.node_policy,
aws_iam_role_policy_attachment.node_cni_policy,
aws_iam_role_policy_attachment.node_ecr_policy,
]
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
tags = { Name = "${local.name}-${each.key}" }
}
################################################################################
# Launch Template for Node Security Hardening
################################################################################
resource "aws_launch_template" "node" {
for_each = local.node_groups
name = "${local.name}-${each.key}"
# IMDSv2 enforcement - critical security control
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # Enforces IMDSv2
http_put_response_hop_limit = 1 # Prevent container credential theft
instance_metadata_tags = "enabled"
}
# EBS encryption
block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = each.value.disk_size
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
}
# Monitoring
monitoring {
enabled = true
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${local.name}-${each.key}"
Tenant = local.tenant
}
}
tags = { Name = "${local.name}-${each.key}" }
}
################################################################################
# Fargate Profiles
################################################################################
resource "aws_eks_fargate_profile" "main" {
for_each = local.enable_fargate ? toset(local.fargate_namespaces) : []
cluster_name = aws_eks_cluster.main.name
fargate_profile_name = "${local.name}-${each.key}"
pod_execution_role_arn = aws_iam_role.fargate[0].arn
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
selector {
namespace = each.key
labels = {
Tenant = local.tenant
}
}
tags = { Name = "${local.name}-${each.key}" }
}
################################################################################
# OIDC Provider for IRSA
################################################################################
data "tls_certificate" "eks" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "eks" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.eks.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
tags = { Name = "${local.name}-eks-oidc" }
}
################################################################################
# IRSA - EBS CSI Driver
################################################################################
resource "aws_iam_role" "ebs_csi" {
count = local.enable_ebs_csi_driver ? 1 : 0
name = "${local.name}-ebs-csi"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Condition = {
StringEquals = {
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa"
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:aud" = "sts.amazonaws.com"
}
}
}]
})
tags = { Name = "${local.name}-ebs-csi" }
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
count = local.enable_ebs_csi_driver ? 1 : 0
role = aws_iam_role.ebs_csi[0].name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
}
################################################################################
# IRSA - Cluster Autoscaler
################################################################################
resource "aws_iam_role" "cluster_autoscaler" {
count = local.enable_cluster_autoscaler ? 1 : 0
name = "${local.name}-cluster-autoscaler"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Condition = {
StringEquals = {
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" = "system:serviceaccount:kube-system:cluster-autoscaler"
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:aud" = "sts.amazonaws.com"
}
}
}]
})
tags = { Name = "${local.name}-cluster-autoscaler" }
}
resource "aws_iam_role_policy" "cluster_autoscaler" {
count = local.enable_cluster_autoscaler ? 1 : 0
name = "cluster-autoscaler"
role = aws_iam_role.cluster_autoscaler[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeScalingActivities",
"autoscaling:DescribeTags",
"ec2:DescribeInstanceTypes",
"ec2:DescribeLaunchTemplateVersions",
"ec2:DescribeImages",
"ec2:GetInstanceTypesFromInstanceRequirements",
"eks:DescribeNodegroup"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup"
]
Resource = "*"
Condition = {
StringEquals = {
"autoscaling:ResourceTag/k8s.io/cluster-autoscaler/${local.name}" = "owned"
}
}
}
]
})
}
################################################################################
# IRSA - AWS Load Balancer Controller
################################################################################
resource "aws_iam_role" "lb_controller" {
count = local.enable_aws_lb_controller ? 1 : 0
name = "${local.name}-aws-lb-controller"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = {
Federated = aws_iam_openid_connect_provider.eks.arn
}
Condition = {
StringEquals = {
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" = "system:serviceaccount:kube-system:aws-load-balancer-controller"
"${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:aud" = "sts.amazonaws.com"
}
}
}]
})
tags = { Name = "${local.name}-aws-lb-controller" }
}
resource "aws_iam_role_policy" "lb_controller" {
count = local.enable_aws_lb_controller ? 1 : 0
name = "aws-lb-controller"
role = aws_iam_role.lb_controller[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["iam:CreateServiceLinkedRole"]
Resource = "*"
Condition = {
StringEquals = {
"iam:AWSServiceName" = "elasticloadbalancing.amazonaws.com"
}
}
},
{
Effect = "Allow"
Action = [
"ec2:DescribeAccountAttributes",
"ec2:DescribeAddresses",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeInternetGateways",
"ec2:DescribeVpcs",
"ec2:DescribeVpcPeeringConnections",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeInstances",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeTags",
"ec2:GetCoipPoolUsage",
"ec2:DescribeCoipPools",
"elasticloadbalancing:DescribeLoadBalancers",
"elasticloadbalancing:DescribeLoadBalancerAttributes",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:DescribeListenerCertificates",
"elasticloadbalancing:DescribeSSLPolicies",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetGroupAttributes",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:DescribeTags"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"cognito-idp:DescribeUserPoolClient",
"acm:ListCertificates",
"acm:DescribeCertificate",
"iam:ListServerCertificates",
"iam:GetServerCertificate",
"waf-regional:GetWebACL",
"waf-regional:GetWebACLForResource",
"waf-regional:AssociateWebACL",
"waf-regional:DisassociateWebACL",
"wafv2:GetWebACL",
"wafv2:GetWebACLForResource",
"wafv2:AssociateWebACL",
"wafv2:DisassociateWebACL",
"shield:GetSubscriptionState",
"shield:DescribeProtection",
"shield:CreateProtection",
"shield:DeleteProtection"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:RevokeSecurityGroupIngress",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:DeleteTags",
"ec2:DeleteSecurityGroup"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"elasticloadbalancing:CreateLoadBalancer",
"elasticloadbalancing:CreateTargetGroup"
]
Resource = "*"
Condition = {
Null = {
"aws:RequestTag/elbv2.k8s.aws/cluster" = "false"
}
}
},
{
Effect = "Allow"
Action = [
"elasticloadbalancing:AddTags",
"elasticloadbalancing:RemoveTags"
]
Resource = [
"arn:${data.aws_partition.current.partition}:elasticloadbalancing:*:*:targetgroup/*/*",
"arn:${data.aws_partition.current.partition}:elasticloadbalancing:*:*:loadbalancer/net/*/*",
"arn:${data.aws_partition.current.partition}:elasticloadbalancing:*:*:loadbalancer/app/*/*"
]
},
{
Effect = "Allow"
Action = [
"elasticloadbalancing:ModifyLoadBalancerAttributes",
"elasticloadbalancing:SetIpAddressType",
"elasticloadbalancing:SetSecurityGroups",
"elasticloadbalancing:SetSubnets",
"elasticloadbalancing:DeleteLoadBalancer",
"elasticloadbalancing:ModifyTargetGroup",
"elasticloadbalancing:ModifyTargetGroupAttributes",
"elasticloadbalancing:DeleteTargetGroup",
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:DeregisterTargets",
"elasticloadbalancing:CreateListener",
"elasticloadbalancing:DeleteListener",
"elasticloadbalancing:CreateRule",
"elasticloadbalancing:ModifyRule",
"elasticloadbalancing:DeleteRule",
"elasticloadbalancing:SetWebAcl",
"elasticloadbalancing:ModifyListener",
"elasticloadbalancing:AddListenerCertificates",
"elasticloadbalancing:RemoveListenerCertificates"
]
Resource = "*"
}
]
})
}
################################################################################
# EKS Access Entries (K8s 1.29+)
################################################################################
resource "aws_eks_access_entry" "admins" {
for_each = toset(local.admin_arns)
cluster_name = aws_eks_cluster.main.name
principal_arn = each.value
type = "STANDARD"
}
resource "aws_eks_access_policy_association" "admins" {
for_each = toset(local.admin_arns)
cluster_name = aws_eks_cluster.main.name
principal_arn = each.value
policy_arn = "arn:${data.aws_partition.current.partition}:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope {
type = "cluster"
}
depends_on = [aws_eks_access_entry.admins]
}
################################################################################
# Outputs
################################################################################
output "cluster_name" {
value = aws_eks_cluster.main.name
}
output "cluster_endpoint" {
value = aws_eks_cluster.main.endpoint
}
output "cluster_ca_certificate" {
value = aws_eks_cluster.main.certificate_authority[0].data
sensitive = true
}
output "cluster_version" {
value = aws_eks_cluster.main.version
}
output "cluster_security_group_id" {
value = aws_security_group.cluster.id
}
output "node_security_group_id" {
value = aws_security_group.node.id
}
output "oidc_provider_arn" {
value = aws_iam_openid_connect_provider.eks.arn
}
output "oidc_provider_url" {
value = aws_iam_openid_connect_provider.eks.url
}
output "cluster_autoscaler_role_arn" {
value = local.enable_cluster_autoscaler ? aws_iam_role.cluster_autoscaler[0].arn : null
}
output "lb_controller_role_arn" {
value = local.enable_aws_lb_controller ? aws_iam_role.lb_controller[0].arn : null
}
output "kubeconfig_command" {
value = "aws eks update-kubeconfig --region ${data.aws_region.current.name} --name ${aws_eks_cluster.main.name}"
}
output "next_steps" {
value = <<-EOT
EKS Cluster Created: ${aws_eks_cluster.main.name}
=============================================
1. Configure kubectl:
${local.enable_cluster_autoscaler ? "aws eks update-kubeconfig --region ${data.aws_region.current.name} --name ${aws_eks_cluster.main.name}" : ""}
2. Install Cluster Autoscaler (if enabled):
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
--namespace kube-system \
--set autoDiscovery.clusterName=${aws_eks_cluster.main.name} \
--set awsRegion=${data.aws_region.current.name} \
--set rbac.serviceAccount.create=true \
--set rbac.serviceAccount.name=cluster-autoscaler \
--set rbac.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${local.enable_cluster_autoscaler ? aws_iam_role.cluster_autoscaler[0].arn : "N/A"}
3. Install AWS Load Balancer Controller (if enabled):
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
--namespace kube-system \
--set clusterName=${aws_eks_cluster.main.name} \
--set serviceAccount.create=true \
--set serviceAccount.name=aws-load-balancer-controller \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=${local.enable_aws_lb_controller ? aws_iam_role.lb_controller[0].arn : "N/A"}
EOT
}

View File

@@ -0,0 +1,389 @@
################################################################################
# Workload: ElastiCache Redis
#
# Deploys a managed Redis cluster:
# - Redis cluster or replication group
# - Encryption at rest and in transit
# - Automatic failover (Multi-AZ)
# - CloudWatch alarms
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-cache/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-cache/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
app = "cache"
env = "prod" # prod, staging, dev
name = "${local.tenant}-${local.app}-${local.env}"
# Redis version
engine_version = "7.1"
# Node sizing
# cache.t3.micro - Dev/test ($0.017/hr)
# cache.t3.small - Small prod ($0.034/hr)
# cache.r6g.large - Production ($0.158/hr)
node_type = "cache.t3.micro"
# Cluster configuration
num_cache_clusters = local.env == "prod" ? 2 : 1 # 2 for Multi-AZ
automatic_failover = local.env == "prod"
multi_az_enabled = local.env == "prod"
# Memory management
maxmemory_policy = "volatile-lru" # Evict keys with TTL when memory full
# Maintenance
maintenance_window = "sun:05:00-sun:06:00"
snapshot_window = "04:00-05:00"
snapshot_retention = local.env == "prod" ? 7 : 1
# Port
port = 6379
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.app
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "02-network/terraform.tfstate"
region = var.region
}
}
data "terraform_remote_state" "tenant" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "04-tenants/${local.tenant}/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
################################################################################
# KMS Key
################################################################################
resource "aws_kms_key" "redis" {
description = "KMS key for ${local.name} Redis encryption"
deletion_window_in_days = 7
enable_key_rotation = true
tags = { Name = "${local.name}-redis" }
}
resource "aws_kms_alias" "redis" {
name = "alias/${local.name}-redis"
target_key_id = aws_kms_key.redis.key_id
}
################################################################################
# Subnet Group
################################################################################
resource "aws_elasticache_subnet_group" "main" {
name = local.name
description = "Subnet group for ${local.name}"
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
tags = { Name = local.name }
}
################################################################################
# Parameter Group
################################################################################
resource "aws_elasticache_parameter_group" "main" {
name = local.name
family = "redis7"
description = "Parameter group for ${local.name}"
parameter {
name = "maxmemory-policy"
value = local.maxmemory_policy
}
# Cluster mode disabled settings
parameter {
name = "cluster-enabled"
value = "no"
}
# Slow log for debugging
parameter {
name = "slowlog-log-slower-than"
value = "10000" # 10ms
}
parameter {
name = "slowlog-max-len"
value = "128"
}
tags = { Name = local.name }
}
################################################################################
# Security Group
################################################################################
resource "aws_security_group" "redis" {
name = "${local.name}-redis"
description = "Redis cluster ${local.name}"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
ingress {
description = "Redis from tenant"
from_port = local.port
to_port = local.port
protocol = "tcp"
security_groups = [data.terraform_remote_state.tenant.outputs.security_groups.base]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound"
}
tags = { Name = "${local.name}-redis" }
}
################################################################################
# Replication Group (Redis Cluster)
################################################################################
resource "aws_elasticache_replication_group" "main" {
replication_group_id = local.name
description = "Redis cluster for ${local.name}"
engine = "redis"
engine_version = local.engine_version
node_type = local.node_type
port = local.port
parameter_group_name = aws_elasticache_parameter_group.main.name
# Cluster configuration
num_cache_clusters = local.num_cache_clusters
automatic_failover_enabled = local.automatic_failover
multi_az_enabled = local.multi_az_enabled
# Network
subnet_group_name = aws_elasticache_subnet_group.main.name
security_group_ids = [aws_security_group.redis.id]
# Encryption
at_rest_encryption_enabled = true
kms_key_id = aws_kms_key.redis.arn
transit_encryption_enabled = true
auth_token = random_password.auth.result
# Maintenance
maintenance_window = local.maintenance_window
snapshot_window = local.snapshot_window
snapshot_retention_limit = local.snapshot_retention
auto_minor_version_upgrade = true
# Notifications
notification_topic_arn = aws_sns_topic.redis.arn
# Apply changes immediately in non-prod, during maintenance in prod
apply_immediately = local.env != "prod"
tags = {
Name = local.name
Backup = "true"
}
}
################################################################################
# Auth Token (Password)
################################################################################
resource "random_password" "auth" {
length = 64
special = false # Redis auth token doesn't support all special chars
}
resource "aws_secretsmanager_secret" "redis" {
name = "${local.name}-redis-auth"
description = "Redis auth token for ${local.name}"
recovery_window_in_days = local.env == "prod" ? 30 : 0
tags = { Name = "${local.name}-redis-auth" }
}
resource "aws_secretsmanager_secret_version" "redis" {
secret_id = aws_secretsmanager_secret.redis.id
secret_string = jsonencode({
auth_token = random_password.auth.result
host = aws_elasticache_replication_group.main.primary_endpoint_address
port = local.port
url = "rediss://:${random_password.auth.result}@${aws_elasticache_replication_group.main.primary_endpoint_address}:${local.port}"
})
}
################################################################################
# SNS Topic for Notifications
################################################################################
resource "aws_sns_topic" "redis" {
name = "${local.name}-redis-events"
tags = { Name = "${local.name}-redis-events" }
}
################################################################################
# CloudWatch Alarms
################################################################################
resource "aws_cloudwatch_metric_alarm" "cpu" {
alarm_name = "${local.name}-redis-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/ElastiCache"
period = 300
statistic = "Average"
threshold = 75
alarm_description = "Redis CPU utilization high"
dimensions = {
CacheClusterId = "${aws_elasticache_replication_group.main.id}-001"
}
alarm_actions = [aws_sns_topic.redis.arn]
tags = { Name = "${local.name}-redis-cpu" }
}
resource "aws_cloudwatch_metric_alarm" "memory" {
alarm_name = "${local.name}-redis-memory"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "DatabaseMemoryUsagePercentage"
namespace = "AWS/ElastiCache"
period = 300
statistic = "Average"
threshold = 80
alarm_description = "Redis memory usage high"
dimensions = {
CacheClusterId = "${aws_elasticache_replication_group.main.id}-001"
}
alarm_actions = [aws_sns_topic.redis.arn]
tags = { Name = "${local.name}-redis-memory" }
}
resource "aws_cloudwatch_metric_alarm" "connections" {
alarm_name = "${local.name}-redis-connections"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CurrConnections"
namespace = "AWS/ElastiCache"
period = 300
statistic = "Average"
threshold = 1000
alarm_description = "Redis connections high"
dimensions = {
CacheClusterId = "${aws_elasticache_replication_group.main.id}-001"
}
alarm_actions = [aws_sns_topic.redis.arn]
tags = { Name = "${local.name}-redis-connections" }
}
################################################################################
# Outputs
################################################################################
output "primary_endpoint" {
value = aws_elasticache_replication_group.main.primary_endpoint_address
}
output "reader_endpoint" {
value = aws_elasticache_replication_group.main.reader_endpoint_address
}
output "port" {
value = local.port
}
output "secret_arn" {
value = aws_secretsmanager_secret.redis.arn
}
output "security_group_id" {
value = aws_security_group.redis.id
}
output "connection_command" {
value = "redis-cli -h ${aws_elasticache_replication_group.main.primary_endpoint_address} -p ${local.port} --tls --askpass"
description = "Command to connect (retrieve password from Secrets Manager)"
}

View File

@@ -0,0 +1,385 @@
################################################################################
# Workload: EventBridge Event Bus
#
# Deploys an event-driven architecture component:
# - Custom event bus for tenant isolation
# - Event rules with pattern matching
# - Multiple targets (Lambda, SQS, Step Functions)
# - Dead letter queue for failed events
# - Event archiving for replay
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-events/
# Update locals and rules
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-events/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "events"
env = "prod"
bus_name = "${local.tenant}-${local.name}-${local.env}"
# Event archiving (for replay capability)
enable_archive = true
archive_retention_days = 30
# Dead letter queue for failed event delivery
enable_dlq = true
# Schema discovery (for event schema registry)
enable_schema_discovery = false
# Event rules - define your event routing here
event_rules = {
# Example: Route order events to SQS
# order-created = {
# description = "Route order.created events to processing queue"
# event_pattern = {
# source = ["${local.tenant}.orders"]
# detail-type = ["order.created"]
# }
# targets = {
# sqs = {
# type = "sqs"
# arn = "arn:aws:sqs:us-east-1:123456789012:order-processing"
# }
# }
# }
# Example: Route all events to CloudWatch Logs for debugging
all-events-log = {
description = "Log all events for debugging"
event_pattern = {
source = [{ prefix = "${local.tenant}." }]
}
targets = {
logs = {
type = "cloudwatch"
}
}
}
}
# Cross-account event sources (account IDs that can put events)
allowed_source_accounts = []
# Cross-account event targets (account IDs that can receive events)
allowed_target_accounts = []
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Event Bus
################################################################################
resource "aws_cloudwatch_event_bus" "main" {
name = local.bus_name
tags = { Name = local.bus_name }
}
################################################################################
# Event Bus Policy
################################################################################
resource "aws_cloudwatch_event_bus_policy" "main" {
count = length(local.allowed_source_accounts) > 0 ? 1 : 0
event_bus_name = aws_cloudwatch_event_bus.main.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCrossAccountPutEvents"
Effect = "Allow"
Principal = {
AWS = [for account in local.allowed_source_accounts : "arn:aws:iam::${account}:root"]
}
Action = "events:PutEvents"
Resource = aws_cloudwatch_event_bus.main.arn
}
]
})
}
################################################################################
# Event Archive
################################################################################
resource "aws_cloudwatch_event_archive" "main" {
count = local.enable_archive ? 1 : 0
name = local.bus_name
description = "Archive for ${local.bus_name}"
event_source_arn = aws_cloudwatch_event_bus.main.arn
retention_days = local.archive_retention_days
# Archive all events (can be filtered with event_pattern)
}
################################################################################
# Dead Letter Queue
################################################################################
resource "aws_sqs_queue" "dlq" {
count = local.enable_dlq ? 1 : 0
name = "${local.bus_name}-dlq"
message_retention_seconds = 1209600 # 14 days
kms_master_key_id = "alias/aws/sqs"
tags = { Name = "${local.bus_name}-dlq" }
}
resource "aws_sqs_queue_policy" "dlq" {
count = local.enable_dlq ? 1 : 0
queue_url = aws_sqs_queue.dlq[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowEventBridge"
Effect = "Allow"
Principal = {
Service = "events.amazonaws.com"
}
Action = "sqs:SendMessage"
Resource = aws_sqs_queue.dlq[0].arn
}]
})
}
################################################################################
# CloudWatch Log Group for Event Logging
################################################################################
resource "aws_cloudwatch_log_group" "events" {
name = "/aws/events/${local.bus_name}"
retention_in_days = 30
tags = { Name = local.bus_name }
}
# Resource policy to allow EventBridge to write logs
resource "aws_cloudwatch_log_resource_policy" "events" {
policy_name = "${local.bus_name}-events"
policy_document = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = ["events.amazonaws.com", "delivery.logs.amazonaws.com"]
}
Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "${aws_cloudwatch_log_group.events.arn}:*"
}]
})
}
################################################################################
# IAM Role for EventBridge Targets
################################################################################
resource "aws_iam_role" "eventbridge" {
name = "${local.bus_name}-eventbridge"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "events.amazonaws.com" }
}]
})
tags = { Name = "${local.bus_name}-eventbridge" }
}
################################################################################
# Event Rules and Targets
################################################################################
resource "aws_cloudwatch_event_rule" "rules" {
for_each = local.event_rules
name = "${local.bus_name}-${each.key}"
description = each.value.description
event_bus_name = aws_cloudwatch_event_bus.main.name
event_pattern = jsonencode(each.value.event_pattern)
state = "ENABLED"
tags = { Name = "${local.bus_name}-${each.key}" }
}
# CloudWatch Logs targets
resource "aws_cloudwatch_event_target" "logs" {
for_each = {
for k, v in local.event_rules : k => v
if contains(keys(v.targets), "logs") && v.targets.logs.type == "cloudwatch"
}
rule = aws_cloudwatch_event_rule.rules[each.key].name
event_bus_name = aws_cloudwatch_event_bus.main.name
target_id = "cloudwatch-logs"
arn = aws_cloudwatch_log_group.events.arn
dead_letter_config {
arn = local.enable_dlq ? aws_sqs_queue.dlq[0].arn : null
}
}
################################################################################
# Schema Registry (Optional)
################################################################################
resource "aws_schemas_discoverer" "main" {
count = local.enable_schema_discovery ? 1 : 0
source_arn = aws_cloudwatch_event_bus.main.arn
description = "Schema discoverer for ${local.bus_name}"
tags = { Name = local.bus_name }
}
################################################################################
# CloudWatch Alarms
################################################################################
resource "aws_cloudwatch_metric_alarm" "dlq_messages" {
count = local.enable_dlq ? 1 : 0
alarm_name = "${local.bus_name}-dlq-messages"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Sum"
threshold = 0
alarm_description = "Events failing to deliver to targets"
dimensions = {
QueueName = aws_sqs_queue.dlq[0].name
}
tags = { Name = "${local.bus_name}-dlq-alarm" }
}
resource "aws_cloudwatch_metric_alarm" "failed_invocations" {
alarm_name = "${local.bus_name}-failed-invocations"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "FailedInvocations"
namespace = "AWS/Events"
period = 300
statistic = "Sum"
threshold = 0
alarm_description = "EventBridge rule invocations failing"
dimensions = {
EventBusName = aws_cloudwatch_event_bus.main.name
}
tags = { Name = "${local.bus_name}-failed-invocations" }
}
################################################################################
# Outputs
################################################################################
output "event_bus_name" {
value = aws_cloudwatch_event_bus.main.name
}
output "event_bus_arn" {
value = aws_cloudwatch_event_bus.main.arn
}
output "archive_arn" {
value = local.enable_archive ? aws_cloudwatch_event_archive.main[0].arn : null
}
output "dlq_url" {
value = local.enable_dlq ? aws_sqs_queue.dlq[0].url : null
}
output "dlq_arn" {
value = local.enable_dlq ? aws_sqs_queue.dlq[0].arn : null
}
output "log_group" {
value = aws_cloudwatch_log_group.events.name
}
output "rule_arns" {
value = { for k, v in aws_cloudwatch_event_rule.rules : k => v.arn }
}
output "put_event_example" {
value = <<-EOF
aws events put-events --entries '[{
"EventBusName": "${aws_cloudwatch_event_bus.main.name}",
"Source": "${local.tenant}.myservice",
"DetailType": "order.created",
"Detail": "{\"orderId\": \"12345\", \"amount\": 99.99}"
}]'
EOF
description = "Example command to put an event"
}

View File

@@ -0,0 +1,422 @@
################################################################################
# Workload: EventBridge Rules
#
# Event-driven automation with:
# - Scheduled rules (cron/rate)
# - Event pattern rules (AWS service events)
# - Multiple targets (Lambda, SQS, SNS, Step Functions)
# - Dead letter queues
# - Input transformations
#
# Use cases: Scheduled jobs, event routing, service integration
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-events/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
prefix = "${local.tenant}-${local.name}"
# Use custom event bus (null = default bus)
event_bus_name = null
# Scheduled rules
scheduled_rules = {
# Daily report at 9 AM UTC
daily-report = {
description = "Generate daily report"
schedule_expression = "cron(0 9 * * ? *)"
enabled = true
target_type = "lambda"
target_arn = "" # Lambda function ARN
input = jsonencode({
report_type = "daily"
format = "pdf"
})
}
# Every 5 minutes health check
health-check = {
description = "Periodic health check"
schedule_expression = "rate(5 minutes)"
enabled = true
target_type = "lambda"
target_arn = "" # Lambda function ARN
}
# Monthly cleanup (1st of month at midnight)
monthly-cleanup = {
description = "Monthly data cleanup"
schedule_expression = "cron(0 0 1 * ? *)"
enabled = true
target_type = "step-function"
target_arn = "" # State machine ARN
input = jsonencode({
retention_days = 90
})
}
}
# Event pattern rules (react to AWS events)
event_pattern_rules = {
# EC2 instance state changes
ec2-state-change = {
description = "EC2 instance state changes"
enabled = true
event_pattern = jsonencode({
source = ["aws.ec2"]
detail-type = ["EC2 Instance State-change Notification"]
detail = {
state = ["stopped", "terminated"]
}
})
target_type = "sns"
target_arn = "" # SNS topic ARN
}
# S3 object created
s3-upload = {
description = "S3 object created in uploads bucket"
enabled = true
event_pattern = jsonencode({
source = ["aws.s3"]
detail-type = ["Object Created"]
detail = {
bucket = {
name = ["my-uploads-bucket"]
}
}
})
target_type = "lambda"
target_arn = "" # Lambda function ARN
input_transformer = {
input_paths = {
bucket = "$.detail.bucket.name"
key = "$.detail.object.key"
size = "$.detail.object.size"
}
input_template = <<-EOF
{
"bucket": <bucket>,
"key": <key>,
"size": <size>,
"timestamp": "<aws.events.event.ingestion-time>"
}
EOF
}
}
# CodePipeline state change
pipeline-failed = {
description = "CodePipeline execution failed"
enabled = true
event_pattern = jsonencode({
source = ["aws.codepipeline"]
detail-type = ["CodePipeline Pipeline Execution State Change"]
detail = {
state = ["FAILED"]
}
})
target_type = "sns"
target_arn = "" # SNS topic ARN
}
# GuardDuty findings
security-findings = {
description = "GuardDuty security findings"
enabled = false # Enable when GuardDuty is active
event_pattern = jsonencode({
source = ["aws.guardduty"]
detail-type = ["GuardDuty Finding"]
detail = {
severity = [{ numeric = [">=", 7] }] # High severity
}
})
target_type = "sns"
target_arn = "" # SNS topic ARN
}
}
# Enable DLQ for failed deliveries
enable_dlq = true
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Dead Letter Queue
################################################################################
resource "aws_sqs_queue" "dlq" {
count = local.enable_dlq ? 1 : 0
name = "${local.prefix}-events-dlq"
message_retention_seconds = 1209600 # 14 days
kms_master_key_id = "alias/aws/sqs"
tags = { Name = "${local.prefix}-events-dlq" }
}
resource "aws_sqs_queue_policy" "dlq" {
count = local.enable_dlq ? 1 : 0
queue_url = aws_sqs_queue.dlq[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowEventBridge"
Effect = "Allow"
Principal = {
Service = "events.amazonaws.com"
}
Action = "sqs:SendMessage"
Resource = aws_sqs_queue.dlq[0].arn
}]
})
}
################################################################################
# IAM Role for EventBridge
################################################################################
resource "aws_iam_role" "eventbridge" {
name = "${local.prefix}-eventbridge"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "events.amazonaws.com" }
}]
})
tags = { Name = "${local.prefix}-eventbridge" }
}
resource "aws_iam_role_policy" "eventbridge" {
name = "invoke-targets"
role = aws_iam_role.eventbridge.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "InvokeStepFunctions"
Effect = "Allow"
Action = "states:StartExecution"
Resource = "*"
},
{
Sid = "InvokeLambda"
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = "*"
},
{
Sid = "SendToSQS"
Effect = "Allow"
Action = "sqs:SendMessage"
Resource = "*"
},
{
Sid = "PublishToSNS"
Effect = "Allow"
Action = "sns:Publish"
Resource = "*"
}
]
})
}
################################################################################
# Scheduled Rules
################################################################################
resource "aws_cloudwatch_event_rule" "scheduled" {
for_each = { for k, v in local.scheduled_rules : k => v if v.target_arn != "" }
name = "${local.prefix}-${each.key}"
description = lookup(each.value, "description", "Scheduled rule ${each.key}")
schedule_expression = each.value.schedule_expression
event_bus_name = local.event_bus_name
state = each.value.enabled ? "ENABLED" : "DISABLED"
tags = { Name = "${local.prefix}-${each.key}" }
}
resource "aws_cloudwatch_event_target" "scheduled" {
for_each = { for k, v in local.scheduled_rules : k => v if v.target_arn != "" }
rule = aws_cloudwatch_event_rule.scheduled[each.key].name
event_bus_name = local.event_bus_name
target_id = each.key
arn = each.value.target_arn
role_arn = each.value.target_type == "step-function" ? aws_iam_role.eventbridge.arn : null
input = lookup(each.value, "input", null)
dynamic "dead_letter_config" {
for_each = local.enable_dlq ? [1] : []
content {
arn = aws_sqs_queue.dlq[0].arn
}
}
}
################################################################################
# Event Pattern Rules
################################################################################
resource "aws_cloudwatch_event_rule" "pattern" {
for_each = { for k, v in local.event_pattern_rules : k => v if v.target_arn != "" }
name = "${local.prefix}-${each.key}"
description = lookup(each.value, "description", "Event pattern rule ${each.key}")
event_pattern = each.value.event_pattern
event_bus_name = local.event_bus_name
state = each.value.enabled ? "ENABLED" : "DISABLED"
tags = { Name = "${local.prefix}-${each.key}" }
}
resource "aws_cloudwatch_event_target" "pattern" {
for_each = { for k, v in local.event_pattern_rules : k => v if v.target_arn != "" }
rule = aws_cloudwatch_event_rule.pattern[each.key].name
event_bus_name = local.event_bus_name
target_id = each.key
arn = each.value.target_arn
role_arn = each.value.target_type == "step-function" ? aws_iam_role.eventbridge.arn : null
input = lookup(each.value, "input", null)
dynamic "input_transformer" {
for_each = lookup(each.value, "input_transformer", null) != null ? [each.value.input_transformer] : []
content {
input_paths = input_transformer.value.input_paths
input_template = input_transformer.value.input_template
}
}
dynamic "dead_letter_config" {
for_each = local.enable_dlq ? [1] : []
content {
arn = aws_sqs_queue.dlq[0].arn
}
}
}
################################################################################
# Lambda Permissions
################################################################################
resource "aws_lambda_permission" "scheduled" {
for_each = { for k, v in local.scheduled_rules : k => v if v.target_arn != "" && v.target_type == "lambda" }
statement_id = "AllowEventBridge-${each.key}"
action = "lambda:InvokeFunction"
function_name = regex("function:([^:]+)", each.value.target_arn)[0]
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.scheduled[each.key].arn
}
resource "aws_lambda_permission" "pattern" {
for_each = { for k, v in local.event_pattern_rules : k => v if v.target_arn != "" && v.target_type == "lambda" }
statement_id = "AllowEventBridge-${each.key}"
action = "lambda:InvokeFunction"
function_name = regex("function:([^:]+)", each.value.target_arn)[0]
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.pattern[each.key].arn
}
################################################################################
# Outputs
################################################################################
output "scheduled_rule_arns" {
value = { for k, v in aws_cloudwatch_event_rule.scheduled : k => v.arn }
description = "Scheduled rule ARNs"
}
output "pattern_rule_arns" {
value = { for k, v in aws_cloudwatch_event_rule.pattern : k => v.arn }
description = "Event pattern rule ARNs"
}
output "dlq_arn" {
value = local.enable_dlq ? aws_sqs_queue.dlq[0].arn : null
description = "Dead letter queue ARN"
}
output "eventbridge_role_arn" {
value = aws_iam_role.eventbridge.arn
description = "EventBridge execution role ARN"
}
output "cron_examples" {
value = {
every_5_min = "rate(5 minutes)"
every_hour = "rate(1 hour)"
daily_9am_utc = "cron(0 9 * * ? *)"
weekdays_8am = "cron(0 8 ? * MON-FRI *)"
monthly_1st = "cron(0 0 1 * ? *)"
every_monday = "cron(0 12 ? * MON *)"
}
description = "Cron expression examples"
}

View File

@@ -0,0 +1,409 @@
################################################################################
# Workload: Lambda Function
#
# Deploys a serverless function:
# - Lambda function with VPC access (optional)
# - API Gateway HTTP API (optional)
# - CloudWatch logging & X-Ray tracing
# - EventBridge rules for scheduled invocation (optional)
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<app>/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<APP>/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
app = "<APP>"
env = "prod" # prod, staging, dev
name = "${local.tenant}-${local.app}-${local.env}"
# Lambda config
runtime = "python3.12" # python3.12, nodejs20.x, go1.x, etc.
handler = "main.handler"
memory_size = 256
timeout = 30
# Source - provide ONE of these
source_dir = null # Path to source directory (will be zipped)
s3_bucket = null # S3 bucket containing deployment package
s3_key = null # S3 key for deployment package
image_uri = null # Container image URI
# VPC - set to true for database access
enable_vpc = false
# API Gateway
enable_api = true
api_path = "/{proxy+}"
# Scheduled execution (cron or rate expression)
schedule_expression = null # "rate(5 minutes)" or "cron(0 12 * * ? *)"
# Environment variables
environment = {
APP_ENV = local.env
LOG_LEVEL = "INFO"
}
# Secrets (ARNs to SSM/Secrets Manager)
secrets = {}
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.app
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "network" {
count = local.enable_vpc ? 1 : 0
backend = "s3"
config = {
bucket = var.state_bucket
key = "02-network/terraform.tfstate"
region = var.region
}
}
data "terraform_remote_state" "tenant" {
count = local.enable_vpc ? 1 : 0
backend = "s3"
config = {
bucket = var.state_bucket
key = "04-tenants/${local.tenant}/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# CloudWatch Log Group
################################################################################
resource "aws_cloudwatch_log_group" "main" {
name = "/aws/lambda/${local.name}"
retention_in_days = 30
tags = { Name = local.name }
}
################################################################################
# IAM Role
################################################################################
resource "aws_iam_role" "lambda" {
name = "${local.name}-lambda"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-lambda" }
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy_attachment" "lambda_vpc" {
count = local.enable_vpc ? 1 : 0
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
resource "aws_iam_role_policy_attachment" "lambda_xray" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
}
resource "aws_iam_role_policy" "lambda_app" {
name = "app-permissions"
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowTaggedResources"
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "dynamodb:*", "sqs:*", "sns:Publish"]
Resource = "*"
Condition = { StringEquals = { "aws:ResourceTag/Tenant" = local.tenant } }
},
{
Sid = "SecretsAccess"
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue", "ssm:GetParameter", "ssm:GetParameters"]
Resource = length(local.secrets) > 0 ? values(local.secrets) : ["arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/${local.tenant}/*"]
}
]
})
}
################################################################################
# Security Group (VPC mode)
################################################################################
resource "aws_security_group" "lambda" {
count = local.enable_vpc ? 1 : 0
name = "${local.name}-lambda"
description = "Lambda function ${local.name}"
vpc_id = data.terraform_remote_state.network[0].outputs.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.name}-lambda" }
}
################################################################################
# Lambda Function
################################################################################
# Create zip from source directory if provided
data "archive_file" "lambda" {
count = local.source_dir != null ? 1 : 0
type = "zip"
source_dir = local.source_dir
output_path = "${path.module}/lambda.zip"
}
resource "aws_lambda_function" "main" {
function_name = local.name
description = "${local.tenant} ${local.app} function"
role = aws_iam_role.lambda.arn
# Source - exactly one must be specified
filename = local.source_dir != null ? data.archive_file.lambda[0].output_path : null
source_code_hash = local.source_dir != null ? data.archive_file.lambda[0].output_base64sha256 : null
s3_bucket = local.s3_bucket
s3_key = local.s3_key
image_uri = local.image_uri
package_type = local.image_uri != null ? "Image" : "Zip"
# Only for Zip packages
runtime = local.image_uri == null ? local.runtime : null
handler = local.image_uri == null ? local.handler : null
memory_size = local.memory_size
timeout = local.timeout
environment {
variables = merge(local.environment, {
for k, v in local.secrets : k => v
})
}
dynamic "vpc_config" {
for_each = local.enable_vpc ? [1] : []
content {
subnet_ids = data.terraform_remote_state.network[0].outputs.private_subnet_ids
security_group_ids = [
aws_security_group.lambda[0].id,
data.terraform_remote_state.tenant[0].outputs.security_groups.base
]
}
}
tracing_config {
mode = "Active"
}
depends_on = [aws_cloudwatch_log_group.main]
tags = { Name = local.name }
}
################################################################################
# API Gateway HTTP API
################################################################################
resource "aws_apigatewayv2_api" "main" {
count = local.enable_api ? 1 : 0
name = local.name
protocol_type = "HTTP"
cors_configuration {
allow_origins = ["*"]
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers = ["Content-Type", "Authorization"]
max_age = 300
}
tags = { Name = local.name }
}
resource "aws_apigatewayv2_stage" "main" {
count = local.enable_api ? 1 : 0
api_id = aws_apigatewayv2_api.main[0].id
name = "$default"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api[0].arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
routeKey = "$context.routeKey"
status = "$context.status"
responseLength = "$context.responseLength"
integrationError = "$context.integrationErrorMessage"
})
}
tags = { Name = local.name }
}
resource "aws_cloudwatch_log_group" "api" {
count = local.enable_api ? 1 : 0
name = "/aws/apigateway/${local.name}"
retention_in_days = 30
tags = { Name = "${local.name}-api" }
}
resource "aws_apigatewayv2_integration" "main" {
count = local.enable_api ? 1 : 0
api_id = aws_apigatewayv2_api.main[0].id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.main.invoke_arn
payload_format_version = "2.0"
}
resource "aws_apigatewayv2_route" "main" {
count = local.enable_api ? 1 : 0
api_id = aws_apigatewayv2_api.main[0].id
route_key = "ANY ${local.api_path}"
target = "integrations/${aws_apigatewayv2_integration.main[0].id}"
}
resource "aws_lambda_permission" "api" {
count = local.enable_api ? 1 : 0
statement_id = "AllowAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.main.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.main[0].execution_arn}/*/*"
}
################################################################################
# EventBridge Schedule
################################################################################
resource "aws_cloudwatch_event_rule" "schedule" {
count = local.schedule_expression != null ? 1 : 0
name = "${local.name}-schedule"
description = "Schedule for ${local.name}"
schedule_expression = local.schedule_expression
tags = { Name = "${local.name}-schedule" }
}
resource "aws_cloudwatch_event_target" "schedule" {
count = local.schedule_expression != null ? 1 : 0
rule = aws_cloudwatch_event_rule.schedule[0].name
target_id = "lambda"
arn = aws_lambda_function.main.arn
}
resource "aws_lambda_permission" "schedule" {
count = local.schedule_expression != null ? 1 : 0
statement_id = "AllowEventBridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.main.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.schedule[0].arn
}
################################################################################
# Outputs
################################################################################
output "function_name" {
value = aws_lambda_function.main.function_name
}
output "function_arn" {
value = aws_lambda_function.main.arn
}
output "invoke_arn" {
value = aws_lambda_function.main.invoke_arn
}
output "api_endpoint" {
value = local.enable_api ? aws_apigatewayv2_api.main[0].api_endpoint : null
}
output "log_group" {
value = aws_cloudwatch_log_group.main.name
}
output "role_arn" {
value = aws_iam_role.lambda.arn
}

View File

@@ -0,0 +1,458 @@
################################################################################
# Workload: OpenSearch (Elasticsearch)
#
# Search and analytics with:
# - Serverless or provisioned clusters
# - Fine-grained access control
# - VPC or public access
# - Cognito authentication
# - UltraWarm for cost-effective storage
# - Cross-cluster search
#
# Use cases: Log analytics, full-text search, observability
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-opensearch/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
domain_name = "${local.tenant}-${local.name}-${local.env}"
# Engine
engine_version = "OpenSearch_2.11"
# Cluster sizing
cluster = {
# Data nodes
instance_type = "t3.medium.search" # t3.small.search for dev
instance_count = 2
# Dedicated master nodes (recommended for production)
dedicated_master_enabled = local.env == "prod"
dedicated_master_type = "t3.medium.search"
dedicated_master_count = 3
# Multi-AZ
zone_awareness_enabled = local.env == "prod"
availability_zone_count = local.env == "prod" ? 2 : 1
}
# Storage
storage = {
type = "gp3"
size_gb = 100
iops = 3000
throughput = 125
}
# UltraWarm (cost-effective warm storage)
ultrawarm = {
enabled = false
type = "ultrawarm1.medium.search"
count = 2
}
# Network
# Option 1: VPC (private, more secure)
vpc_enabled = true
vpc_id = "" # data.terraform_remote_state.network.outputs.vpc_id
private_subnet_ids = [] # data.terraform_remote_state.network.outputs.private_subnet_ids
# Option 2: Public (set vpc_enabled = false)
# Uses IP-based access policy
# Access control
enable_fine_grained_access = true
master_user_name = "admin"
# Cognito authentication (optional, for Dashboards)
cognito = {
enabled = false
user_pool_id = ""
identity_pool_id = ""
role_arn = ""
}
# Encryption
encrypt_at_rest = true
node_to_node_encryption = true
# Logging
log_types = ["INDEX_SLOW_LOGS", "SEARCH_SLOW_LOGS", "ES_APPLICATION_LOGS"]
# Auto-tune
auto_tune_enabled = true
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Random Password for Master User
################################################################################
resource "random_password" "master" {
count = local.enable_fine_grained_access ? 1 : 0
length = 24
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
################################################################################
# Secrets Manager
################################################################################
resource "aws_secretsmanager_secret" "opensearch" {
count = local.enable_fine_grained_access ? 1 : 0
name = "${local.tenant}/${local.env}/${local.name}/opensearch"
description = "OpenSearch master credentials"
tags = { Name = "${local.domain_name}-credentials" }
}
resource "aws_secretsmanager_secret_version" "opensearch" {
count = local.enable_fine_grained_access ? 1 : 0
secret_id = aws_secretsmanager_secret.opensearch[0].id
secret_string = jsonencode({
username = local.master_user_name
password = random_password.master[0].result
endpoint = aws_opensearch_domain.main.endpoint
})
}
################################################################################
# CloudWatch Log Groups
################################################################################
resource "aws_cloudwatch_log_group" "opensearch" {
for_each = toset(local.log_types)
name = "/aws/opensearch/${local.domain_name}/${lower(each.key)}"
retention_in_days = 30
tags = { Name = "${local.domain_name}-${lower(each.key)}" }
}
resource "aws_cloudwatch_log_resource_policy" "opensearch" {
policy_name = "${local.domain_name}-logs"
policy_document = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "es.amazonaws.com"
}
Action = [
"logs:PutLogEvents",
"logs:CreateLogStream"
]
Resource = "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/opensearch/${local.domain_name}/*"
}]
})
}
################################################################################
# Security Group (VPC mode)
################################################################################
resource "aws_security_group" "opensearch" {
count = local.vpc_enabled && length(local.vpc_id) > 0 ? 1 : 0
name = "${local.domain_name}-opensearch"
vpc_id = local.vpc_id
ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.domain_name}-opensearch" }
}
################################################################################
# IAM Service-Linked Role
################################################################################
resource "aws_iam_service_linked_role" "opensearch" {
count = local.vpc_enabled ? 1 : 0
aws_service_name = "opensearchservice.amazonaws.com"
}
################################################################################
# OpenSearch Domain
################################################################################
resource "aws_opensearch_domain" "main" {
domain_name = local.domain_name
engine_version = local.engine_version
# Cluster configuration
cluster_config {
instance_type = local.cluster.instance_type
instance_count = local.cluster.instance_count
dedicated_master_enabled = local.cluster.dedicated_master_enabled
dedicated_master_type = local.cluster.dedicated_master_enabled ? local.cluster.dedicated_master_type : null
dedicated_master_count = local.cluster.dedicated_master_enabled ? local.cluster.dedicated_master_count : null
zone_awareness_enabled = local.cluster.zone_awareness_enabled
dynamic "zone_awareness_config" {
for_each = local.cluster.zone_awareness_enabled ? [1] : []
content {
availability_zone_count = local.cluster.availability_zone_count
}
}
# UltraWarm
warm_enabled = local.ultrawarm.enabled
warm_type = local.ultrawarm.enabled ? local.ultrawarm.type : null
warm_count = local.ultrawarm.enabled ? local.ultrawarm.count : null
}
# Storage
ebs_options {
ebs_enabled = true
volume_type = local.storage.type
volume_size = local.storage.size_gb
iops = local.storage.type == "gp3" ? local.storage.iops : null
throughput = local.storage.type == "gp3" ? local.storage.throughput : null
}
# VPC configuration
dynamic "vpc_options" {
for_each = local.vpc_enabled && length(local.private_subnet_ids) > 0 ? [1] : []
content {
subnet_ids = slice(local.private_subnet_ids, 0, local.cluster.availability_zone_count)
security_group_ids = [aws_security_group.opensearch[0].id]
}
}
# Encryption
encrypt_at_rest {
enabled = local.encrypt_at_rest
}
node_to_node_encryption {
enabled = local.node_to_node_encryption
}
domain_endpoint_options {
enforce_https = true
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
}
# Fine-grained access control
advanced_security_options {
enabled = local.enable_fine_grained_access
internal_user_database_enabled = local.enable_fine_grained_access
dynamic "master_user_options" {
for_each = local.enable_fine_grained_access ? [1] : []
content {
master_user_name = local.master_user_name
master_user_password = random_password.master[0].result
}
}
}
# Cognito authentication
dynamic "cognito_options" {
for_each = local.cognito.enabled ? [1] : []
content {
enabled = true
user_pool_id = local.cognito.user_pool_id
identity_pool_id = local.cognito.identity_pool_id
role_arn = local.cognito.role_arn
}
}
# Logging
dynamic "log_publishing_options" {
for_each = local.log_types
content {
cloudwatch_log_group_arn = aws_cloudwatch_log_group.opensearch[log_publishing_options.value].arn
log_type = log_publishing_options.value
}
}
# Auto-tune
auto_tune_options {
desired_state = local.auto_tune_enabled ? "ENABLED" : "DISABLED"
rollback_on_disable = "NO_ROLLBACK"
}
# Access policy (for non-VPC or fine-grained access)
access_policies = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = "*"
}
Action = "es:*"
Resource = "arn:aws:es:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:domain/${local.domain_name}/*"
Condition = local.vpc_enabled ? {} : {
IpAddress = {
"aws:SourceIp" = ["0.0.0.0/0"] # Restrict in production!
}
}
}
]
})
tags = { Name = local.domain_name }
depends_on = [
aws_iam_service_linked_role.opensearch,
aws_cloudwatch_log_resource_policy.opensearch
]
}
################################################################################
# IAM Policy for Application Access
################################################################################
resource "aws_iam_policy" "opensearch_access" {
name = "${local.domain_name}-access"
description = "Access to ${local.domain_name} OpenSearch domain"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "OpenSearchAccess"
Effect = "Allow"
Action = [
"es:ESHttpGet",
"es:ESHttpHead",
"es:ESHttpPost",
"es:ESHttpPut",
"es:ESHttpDelete"
]
Resource = "${aws_opensearch_domain.main.arn}/*"
}
]
})
tags = { Name = "${local.domain_name}-access" }
}
################################################################################
# Outputs
################################################################################
output "domain_endpoint" {
value = aws_opensearch_domain.main.endpoint
description = "OpenSearch domain endpoint"
}
output "dashboard_endpoint" {
value = aws_opensearch_domain.main.dashboard_endpoint
description = "OpenSearch Dashboards endpoint"
}
output "domain_arn" {
value = aws_opensearch_domain.main.arn
description = "Domain ARN"
}
output "domain_id" {
value = aws_opensearch_domain.main.domain_id
description = "Domain ID"
}
output "secret_arn" {
value = length(aws_secretsmanager_secret.opensearch) > 0 ? aws_secretsmanager_secret.opensearch[0].arn : null
description = "Secrets Manager ARN for credentials"
}
output "access_policy_arn" {
value = aws_iam_policy.opensearch_access.arn
description = "IAM policy for application access"
}
output "kibana_url" {
value = "https://${aws_opensearch_domain.main.dashboard_endpoint}/_dashboards"
description = "OpenSearch Dashboards URL"
}
output "curl_example" {
value = local.enable_fine_grained_access ? <<-EOF
# Get credentials from Secrets Manager
SECRET=$(aws secretsmanager get-secret-value --secret-id ${aws_secretsmanager_secret.opensearch[0].arn} --query SecretString --output text)
USER=$(echo $SECRET | jq -r .username)
PASS=$(echo $SECRET | jq -r .password)
# Query cluster health
curl -u "$USER:$PASS" "https://${aws_opensearch_domain.main.endpoint}/_cluster/health?pretty"
EOF
: null
description = "Example curl commands"
}

View File

@@ -0,0 +1,544 @@
################################################################################
# Workload: RDS Database
#
# Deploys a managed database:
# - RDS PostgreSQL/MySQL instance or Aurora cluster
# - Subnet group and security group
# - Parameter group with optimized settings
# - Secrets Manager for credentials
# - Optional read replica
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<app>-db/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<APP>-db/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
app = "<APP>"
env = "prod" # prod, staging, dev
name = "${local.tenant}-${local.app}-${local.env}"
# Engine - "postgres", "mysql", "aurora-postgresql", "aurora-mysql"
engine = "postgres"
engine_version = "16.3"
# Instance sizing
instance_class = "db.t3.micro" # db.t3.micro, db.t3.small, db.r6g.large, etc.
storage_gb = 20
max_storage_gb = 100 # Auto-scaling max (set to storage_gb to disable)
# High availability
multi_az = false # true for prod
read_replica = false # Create read replica
# Database config
database_name = "app"
port = 5432 # 5432 for postgres, 3306 for mysql
# Backup
backup_retention_days = 7
backup_window = "03:00-04:00" # UTC
maintenance_window = "sun:04:00-sun:05:00"
# Deletion protection (disable for dev/test)
deletion_protection = local.env == "prod"
skip_final_snapshot = local.env != "prod"
# Performance Insights (free for 7 days retention)
performance_insights = true
# IAM database authentication (recommended for apps)
iam_auth_enabled = true
# Enhanced Monitoring interval (0 to disable, 1/5/10/15/30/60 seconds)
monitoring_interval = local.env == "prod" ? 60 : 0
# Is this an Aurora cluster?
is_aurora = startswith(local.engine, "aurora-")
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.app
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "02-network/terraform.tfstate"
region = var.region
}
}
data "terraform_remote_state" "tenant" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "04-tenants/${local.tenant}/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# KMS Key for Encryption
################################################################################
resource "aws_kms_key" "db" {
description = "KMS key for ${local.name} database encryption"
deletion_window_in_days = 7
enable_key_rotation = true
tags = { Name = "${local.name}-db" }
}
resource "aws_kms_alias" "db" {
name = "alias/${local.name}-db"
target_key_id = aws_kms_key.db.key_id
}
################################################################################
# Random Password
################################################################################
resource "random_password" "master" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
################################################################################
# Secrets Manager
################################################################################
resource "aws_secretsmanager_secret" "db" {
name = "${local.name}-db-credentials"
description = "Database credentials for ${local.name}"
recovery_window_in_days = local.env == "prod" ? 30 : 0
tags = { Name = "${local.name}-db-credentials" }
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode({
username = "dbadmin"
password = random_password.master.result
engine = local.engine
host = local.is_aurora ? aws_rds_cluster.main[0].endpoint : aws_db_instance.main[0].address
port = local.port
database = local.database_name
url = local.is_aurora ? "postgresql://dbadmin:${random_password.master.result}@${aws_rds_cluster.main[0].endpoint}:${local.port}/${local.database_name}" : "postgresql://dbadmin:${random_password.master.result}@${aws_db_instance.main[0].address}:${local.port}/${local.database_name}"
})
depends_on = [aws_db_instance.main, aws_rds_cluster.main]
}
################################################################################
# Subnet Group
################################################################################
resource "aws_db_subnet_group" "main" {
name = local.name
description = "Subnet group for ${local.name}"
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
tags = { Name = local.name }
}
################################################################################
# Security Group
################################################################################
resource "aws_security_group" "db" {
name = "${local.name}-db"
description = "Database ${local.name}"
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
ingress {
description = "Database port from tenant"
from_port = local.port
to_port = local.port
protocol = "tcp"
security_groups = [data.terraform_remote_state.tenant.outputs.security_groups.base]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${local.name}-db" }
}
################################################################################
# Parameter Group
################################################################################
resource "aws_db_parameter_group" "main" {
count = local.is_aurora ? 0 : 1
name = local.name
family = "${local.engine}${split(".", local.engine_version)[0]}"
dynamic "parameter" {
for_each = local.engine == "postgres" ? [
{ name = "log_statement", value = "ddl" },
{ name = "log_min_duration_statement", value = "1000" },
{ name = "shared_preload_libraries", value = "pg_stat_statements", apply = "pending-reboot" }
] : [
{ name = "slow_query_log", value = "1" },
{ name = "long_query_time", value = "1" }
]
content {
name = parameter.value.name
value = parameter.value.value
apply_method = lookup(parameter.value, "apply", "immediate")
}
}
tags = { Name = local.name }
}
resource "aws_rds_cluster_parameter_group" "main" {
count = local.is_aurora ? 1 : 0
name = local.name
family = local.engine == "aurora-postgresql" ? "aurora-postgresql16" : "aurora-mysql8.0"
dynamic "parameter" {
for_each = local.engine == "aurora-postgresql" ? [
{ name = "log_statement", value = "ddl" },
{ name = "log_min_duration_statement", value = "1000" }
] : [
{ name = "slow_query_log", value = "1" },
{ name = "long_query_time", value = "1" }
]
content {
name = parameter.value.name
value = parameter.value.value
apply_method = lookup(parameter.value, "apply", "immediate")
}
}
tags = { Name = local.name }
}
################################################################################
# RDS Instance (non-Aurora)
################################################################################
resource "aws_db_instance" "main" {
count = local.is_aurora ? 0 : 1
identifier = local.name
engine = local.engine
engine_version = local.engine_version
instance_class = local.instance_class
allocated_storage = local.storage_gb
max_allocated_storage = local.max_storage_gb
storage_type = "gp3"
storage_encrypted = true
kms_key_id = aws_kms_key.db.arn
db_name = local.database_name
username = "dbadmin"
password = random_password.master.result
port = local.port
multi_az = local.multi_az
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.main[0].name
publicly_accessible = false
# IAM authentication for better security
iam_database_authentication_enabled = local.iam_auth_enabled
backup_retention_period = local.backup_retention_days
backup_window = local.backup_window
maintenance_window = local.maintenance_window
copy_tags_to_snapshot = true
performance_insights_enabled = local.performance_insights
performance_insights_retention_period = local.performance_insights ? 7 : null
performance_insights_kms_key_id = local.performance_insights ? aws_kms_key.db.arn : null
# Enhanced monitoring
monitoring_interval = local.monitoring_interval
monitoring_role_arn = local.monitoring_interval > 0 ? aws_iam_role.rds_monitoring[0].arn : null
deletion_protection = local.deletion_protection
skip_final_snapshot = local.skip_final_snapshot
final_snapshot_identifier = local.skip_final_snapshot ? null : "${local.name}-final"
enabled_cloudwatch_logs_exports = local.engine == "postgres" ? ["postgresql", "upgrade"] : ["error", "slowquery"]
# Require TLS connections
ca_cert_identifier = "rds-ca-rsa2048-g1"
tags = { Name = local.name }
}
################################################################################
# RDS Read Replica (non-Aurora)
################################################################################
resource "aws_db_instance" "replica" {
count = !local.is_aurora && local.read_replica ? 1 : 0
identifier = "${local.name}-replica"
replicate_source_db = aws_db_instance.main[0].identifier
instance_class = local.instance_class
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.main[0].name
publicly_accessible = false
performance_insights_enabled = local.performance_insights
performance_insights_retention_period = local.performance_insights ? 7 : null
skip_final_snapshot = true
tags = { Name = "${local.name}-replica" }
}
################################################################################
# Aurora Cluster
################################################################################
resource "aws_rds_cluster" "main" {
count = local.is_aurora ? 1 : 0
cluster_identifier = local.name
engine = local.engine
engine_version = local.engine_version
database_name = local.database_name
master_username = "dbadmin"
master_password = random_password.master.result
port = local.port
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.main[0].name
storage_encrypted = true
kms_key_id = aws_kms_key.db.arn
# IAM authentication
iam_database_authentication_enabled = local.iam_auth_enabled
backup_retention_period = local.backup_retention_days
preferred_backup_window = local.backup_window
preferred_maintenance_window = local.maintenance_window
copy_tags_to_snapshot = true
deletion_protection = local.deletion_protection
skip_final_snapshot = local.skip_final_snapshot
final_snapshot_identifier = local.skip_final_snapshot ? null : "${local.name}-final"
enabled_cloudwatch_logs_exports = local.engine == "aurora-postgresql" ? ["postgresql"] : ["error", "slowquery"]
tags = { Name = local.name }
}
resource "aws_rds_cluster_instance" "main" {
count = local.is_aurora ? (local.multi_az ? 2 : 1) : 0
identifier = "${local.name}-${count.index}"
cluster_identifier = aws_rds_cluster.main[0].id
engine = aws_rds_cluster.main[0].engine
engine_version = aws_rds_cluster.main[0].engine_version
instance_class = local.instance_class
publicly_accessible = false
performance_insights_enabled = local.performance_insights
performance_insights_retention_period = local.performance_insights ? 7 : null
tags = { Name = "${local.name}-${count.index}" }
}
################################################################################
# IAM Role for Enhanced Monitoring
################################################################################
resource "aws_iam_role" "rds_monitoring" {
count = local.monitoring_interval > 0 ? 1 : 0
name = "${local.name}-rds-monitoring"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "monitoring.rds.amazonaws.com" }
}]
})
tags = { Name = "${local.name}-rds-monitoring" }
}
resource "aws_iam_role_policy_attachment" "rds_monitoring" {
count = local.monitoring_interval > 0 ? 1 : 0
role = aws_iam_role.rds_monitoring[0].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
}
################################################################################
# CloudWatch Alarms
################################################################################
resource "aws_cloudwatch_metric_alarm" "cpu" {
alarm_name = "${local.name}-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "CPUUtilization"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 80
alarm_description = "Database CPU utilization high"
dimensions = {
DBInstanceIdentifier = local.is_aurora ? aws_rds_cluster_instance.main[0].identifier : aws_db_instance.main[0].identifier
}
tags = { Name = "${local.name}-cpu-high" }
}
resource "aws_cloudwatch_metric_alarm" "storage" {
count = local.is_aurora ? 0 : 1
alarm_name = "${local.name}-storage-low"
comparison_operator = "LessThanThreshold"
evaluation_periods = 1
metric_name = "FreeStorageSpace"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 5368709120 # 5 GB
alarm_description = "Database free storage space low"
dimensions = {
DBInstanceIdentifier = aws_db_instance.main[0].identifier
}
tags = { Name = "${local.name}-storage-low" }
}
resource "aws_cloudwatch_metric_alarm" "connections" {
alarm_name = "${local.name}-connections-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "DatabaseConnections"
namespace = "AWS/RDS"
period = 300
statistic = "Average"
threshold = 50 # Adjust based on instance class
alarm_description = "Database connections high"
dimensions = {
DBInstanceIdentifier = local.is_aurora ? aws_rds_cluster_instance.main[0].identifier : aws_db_instance.main[0].identifier
}
tags = { Name = "${local.name}-connections-high" }
}
################################################################################
# Outputs
################################################################################
output "endpoint" {
value = local.is_aurora ? aws_rds_cluster.main[0].endpoint : aws_db_instance.main[0].address
}
output "reader_endpoint" {
value = local.is_aurora ? aws_rds_cluster.main[0].reader_endpoint : (local.read_replica ? aws_db_instance.replica[0].address : null)
}
output "port" {
value = local.port
}
output "database_name" {
value = local.database_name
}
output "secret_arn" {
value = aws_secretsmanager_secret.db.arn
}
output "security_group_id" {
value = aws_security_group.db.id
}
output "connection_string_ssm" {
value = "Retrieve from: aws secretsmanager get-secret-value --secret-id ${aws_secretsmanager_secret.db.name}"
description = "Command to retrieve connection string"
}

View File

@@ -0,0 +1,527 @@
################################################################################
# Workload: S3 Bucket
#
# Multi-purpose S3 bucket with:
# - Versioning, encryption (KMS or S3)
# - Lifecycle rules (tiering, expiration)
# - Replication (cross-region DR)
# - Access logging
# - Event notifications (Lambda, SQS, SNS)
# - Object Lock (compliance/governance)
#
# Use cases: Data lake, backups, artifacts, logs, media storage
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-bucket/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
bucket_name = "${local.tenant}-${local.name}-${local.env}-${data.aws_caller_identity.current.account_id}"
# Versioning
versioning_enabled = true
# Encryption
encryption_type = "SSE-S3" # SSE-S3, SSE-KMS, or KMS ARN
kms_key_arn = null # Set if using SSE-KMS
# Public access (always blocked by default)
block_public_access = true
# Access logging
enable_logging = true
logging_bucket = null # Set to existing logging bucket, or creates one
logging_prefix = "s3-access-logs/${local.bucket_name}/"
# Lifecycle rules
lifecycle_rules = {
transition-to-ia = {
enabled = true
filter = {
prefix = ""
}
transitions = [
{
days = 30
storage_class = "STANDARD_IA"
},
{
days = 90
storage_class = "GLACIER"
}
]
expiration_days = 365
noncurrent_version_expiration_days = 90
}
}
# Cross-region replication
enable_replication = false
replication_region = "us-west-2"
replication_bucket = null # Will create if null
# Event notifications
lambda_notifications = {
# "object-created" = {
# lambda_arn = "arn:aws:lambda:..."
# events = ["s3:ObjectCreated:*"]
# prefix = "uploads/"
# suffix = ".jpg"
# }
}
sqs_notifications = {
# "new-files" = {
# queue_arn = "arn:aws:sqs:..."
# events = ["s3:ObjectCreated:*"]
# }
}
# Object Lock (for compliance - cannot be disabled once enabled)
object_lock_enabled = false
object_lock_mode = "GOVERNANCE" # GOVERNANCE or COMPLIANCE
object_lock_days = 30
# CORS (for web access)
cors_enabled = false
cors_rules = [
{
allowed_headers = ["*"]
allowed_methods = ["GET", "HEAD"]
allowed_origins = ["*"]
max_age_seconds = 3600
}
]
# Intelligent tiering
intelligent_tiering_enabled = false
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
provider "aws" {
alias = "replication"
region = local.replication_region
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# S3 Bucket
################################################################################
resource "aws_s3_bucket" "main" {
bucket = local.bucket_name
dynamic "object_lock_configuration" {
for_each = local.object_lock_enabled ? [1] : []
content {
object_lock_enabled = "Enabled"
}
}
tags = { Name = local.bucket_name }
}
################################################################################
# Versioning
################################################################################
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = local.versioning_enabled ? "Enabled" : "Suspended"
}
}
################################################################################
# Encryption
################################################################################
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = local.encryption_type == "SSE-S3" ? "AES256" : "aws:kms"
kms_master_key_id = local.encryption_type != "SSE-S3" ? (local.kms_key_arn != null ? local.kms_key_arn : null) : null
}
bucket_key_enabled = local.encryption_type != "SSE-S3"
}
}
################################################################################
# Public Access Block
################################################################################
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = local.block_public_access
block_public_policy = local.block_public_access
ignore_public_acls = local.block_public_access
restrict_public_buckets = local.block_public_access
}
################################################################################
# Access Logging
################################################################################
resource "aws_s3_bucket" "logs" {
count = local.enable_logging && local.logging_bucket == null ? 1 : 0
bucket = "${local.bucket_name}-logs"
tags = { Name = "${local.bucket_name}-logs" }
}
resource "aws_s3_bucket_versioning" "logs" {
count = local.enable_logging && local.logging_bucket == null ? 1 : 0
bucket = aws_s3_bucket.logs[0].id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "logs" {
count = local.enable_logging && local.logging_bucket == null ? 1 : 0
bucket = aws_s3_bucket.logs[0].id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "logs" {
count = local.enable_logging && local.logging_bucket == null ? 1 : 0
bucket = aws_s3_bucket.logs[0].id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
count = local.enable_logging && local.logging_bucket == null ? 1 : 0
bucket = aws_s3_bucket.logs[0].id
rule {
id = "expire-logs"
status = "Enabled"
expiration {
days = 90
}
}
}
resource "aws_s3_bucket_logging" "main" {
count = local.enable_logging ? 1 : 0
bucket = aws_s3_bucket.main.id
target_bucket = local.logging_bucket != null ? local.logging_bucket : aws_s3_bucket.logs[0].id
target_prefix = local.logging_prefix
}
################################################################################
# Lifecycle Rules
################################################################################
resource "aws_s3_bucket_lifecycle_configuration" "main" {
count = length(local.lifecycle_rules) > 0 ? 1 : 0
bucket = aws_s3_bucket.main.id
dynamic "rule" {
for_each = local.lifecycle_rules
content {
id = rule.key
status = rule.value.enabled ? "Enabled" : "Disabled"
filter {
prefix = lookup(rule.value.filter, "prefix", "")
}
dynamic "transition" {
for_each = lookup(rule.value, "transitions", [])
content {
days = transition.value.days
storage_class = transition.value.storage_class
}
}
dynamic "expiration" {
for_each = lookup(rule.value, "expiration_days", null) != null ? [1] : []
content {
days = rule.value.expiration_days
}
}
dynamic "noncurrent_version_expiration" {
for_each = lookup(rule.value, "noncurrent_version_expiration_days", null) != null ? [1] : []
content {
noncurrent_days = rule.value.noncurrent_version_expiration_days
}
}
}
}
depends_on = [aws_s3_bucket_versioning.main]
}
################################################################################
# Intelligent Tiering
################################################################################
resource "aws_s3_bucket_intelligent_tiering_configuration" "main" {
count = local.intelligent_tiering_enabled ? 1 : 0
bucket = aws_s3_bucket.main.id
name = "EntireBucket"
tiering {
access_tier = "DEEP_ARCHIVE_ACCESS"
days = 180
}
tiering {
access_tier = "ARCHIVE_ACCESS"
days = 90
}
}
################################################################################
# CORS
################################################################################
resource "aws_s3_bucket_cors_configuration" "main" {
count = local.cors_enabled ? 1 : 0
bucket = aws_s3_bucket.main.id
dynamic "cors_rule" {
for_each = local.cors_rules
content {
allowed_headers = cors_rule.value.allowed_headers
allowed_methods = cors_rule.value.allowed_methods
allowed_origins = cors_rule.value.allowed_origins
max_age_seconds = cors_rule.value.max_age_seconds
}
}
}
################################################################################
# Object Lock
################################################################################
resource "aws_s3_bucket_object_lock_configuration" "main" {
count = local.object_lock_enabled ? 1 : 0
bucket = aws_s3_bucket.main.id
rule {
default_retention {
mode = local.object_lock_mode
days = local.object_lock_days
}
}
}
################################################################################
# Event Notifications
################################################################################
resource "aws_s3_bucket_notification" "main" {
count = length(local.lambda_notifications) > 0 || length(local.sqs_notifications) > 0 ? 1 : 0
bucket = aws_s3_bucket.main.id
dynamic "lambda_function" {
for_each = local.lambda_notifications
content {
lambda_function_arn = lambda_function.value.lambda_arn
events = lambda_function.value.events
filter_prefix = lookup(lambda_function.value, "prefix", null)
filter_suffix = lookup(lambda_function.value, "suffix", null)
}
}
dynamic "queue" {
for_each = local.sqs_notifications
content {
queue_arn = queue.value.queue_arn
events = queue.value.events
filter_prefix = lookup(queue.value, "prefix", null)
filter_suffix = lookup(queue.value, "suffix", null)
}
}
}
################################################################################
# Replication
################################################################################
resource "aws_s3_bucket" "replica" {
count = local.enable_replication && local.replication_bucket == null ? 1 : 0
provider = aws.replication
bucket = "${local.bucket_name}-replica"
tags = { Name = "${local.bucket_name}-replica" }
}
resource "aws_s3_bucket_versioning" "replica" {
count = local.enable_replication && local.replication_bucket == null ? 1 : 0
provider = aws.replication
bucket = aws_s3_bucket.replica[0].id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_iam_role" "replication" {
count = local.enable_replication ? 1 : 0
name = "${local.bucket_name}-replication"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "s3.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy" "replication" {
count = local.enable_replication ? 1 : 0
name = "replication"
role = aws_iam_role.replication[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetReplicationConfiguration",
"s3:ListBucket"
]
Resource = aws_s3_bucket.main.arn
},
{
Effect = "Allow"
Action = [
"s3:GetObjectVersionForReplication",
"s3:GetObjectVersionAcl",
"s3:GetObjectVersionTagging"
]
Resource = "${aws_s3_bucket.main.arn}/*"
},
{
Effect = "Allow"
Action = [
"s3:ReplicateObject",
"s3:ReplicateDelete",
"s3:ReplicateTags"
]
Resource = "${local.replication_bucket != null ? local.replication_bucket : aws_s3_bucket.replica[0].arn}/*"
}
]
})
}
resource "aws_s3_bucket_replication_configuration" "main" {
count = local.enable_replication ? 1 : 0
bucket = aws_s3_bucket.main.id
role = aws_iam_role.replication[0].arn
rule {
id = "replicate-all"
status = "Enabled"
destination {
bucket = local.replication_bucket != null ? local.replication_bucket : aws_s3_bucket.replica[0].arn
storage_class = "STANDARD"
}
}
depends_on = [aws_s3_bucket_versioning.main]
}
################################################################################
# Outputs
################################################################################
output "bucket_name" {
value = aws_s3_bucket.main.id
}
output "bucket_arn" {
value = aws_s3_bucket.main.arn
}
output "bucket_domain_name" {
value = aws_s3_bucket.main.bucket_regional_domain_name
}
output "replica_bucket" {
value = local.enable_replication && local.replication_bucket == null ? aws_s3_bucket.replica[0].id : local.replication_bucket
}
output "logging_bucket" {
value = local.enable_logging && local.logging_bucket == null ? aws_s3_bucket.logs[0].id : local.logging_bucket
}

View File

@@ -0,0 +1,427 @@
################################################################################
# Workload: Secrets Manager
#
# Secure secret storage:
# - KMS encryption
# - Automatic rotation (RDS, Redshift, DocumentDB, custom Lambda)
# - Cross-account access policies
# - Versioning and recovery
# - Replication to other regions
#
# Use cases: DB credentials, API keys, certificates, config
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-secrets/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
prefix = "${local.tenant}/${local.env}"
# KMS encryption (null uses AWS managed key)
kms_key_arn = null
# Recovery window (days) - 0 for immediate deletion
recovery_window_days = 30
# Secrets to create
secrets = {
# Database credentials (auto-generated)
"db/main" = {
description = "Main database credentials"
generate_password = true
password_length = 32
exclude_characters = "\"@/\\"
secret_string_template = jsonencode({
username = "admin"
engine = "postgres"
host = "db.example.internal"
port = 5432
dbname = "main"
})
# RDS rotation
rotation = {
enabled = false
# lambda_arn = "arn:aws:lambda:..." # Rotation Lambda
# days = 30
}
}
# API keys - Update via console or CLI after deployment:
# aws secretsmanager put-secret-value --secret-id <arn> --secret-string '{"publishable_key":"pk_live_xxx","secret_key":"sk_live_xxx"}'
"api/stripe" = {
description = "Stripe API keys"
secret_string = jsonencode({
publishable_key = "pk_live_xxxxxxxxxxxx"
secret_key = "sk_live_xxxxxxxxxxxx"
})
}
# Generic config
"config/app" = {
description = "Application configuration"
secret_string = jsonencode({
feature_flags = {
new_checkout = true
beta_features = false
}
limits = {
max_upload_mb = 100
rate_limit_rpm = 1000
}
})
}
}
# Cross-account access
allowed_accounts = [
# "123456789012", # Dev account
# "234567890123", # Staging account
]
# IAM principals allowed to read secrets
allowed_principals = [
# "arn:aws:iam::123456789012:role/app-role",
]
# Replication to other regions
replica_regions = [
# "us-west-2",
# "eu-west-1",
]
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# KMS Key (optional - for customer managed encryption)
################################################################################
resource "aws_kms_key" "secrets" {
count = local.kms_key_arn == null ? 1 : 0
description = "KMS key for ${local.prefix} secrets"
deletion_window_in_days = 30
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM policies"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow Secrets Manager"
Effect = "Allow"
Principal = {
Service = "secretsmanager.amazonaws.com"
}
Action = [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
tags = { Name = "${local.prefix}-secrets" }
}
resource "aws_kms_alias" "secrets" {
count = local.kms_key_arn == null ? 1 : 0
name = "alias/${replace(local.prefix, "/", "-")}-secrets"
target_key_id = aws_kms_key.secrets[0].key_id
}
locals {
effective_kms_key = local.kms_key_arn != null ? local.kms_key_arn : aws_kms_key.secrets[0].arn
}
################################################################################
# Random Passwords
################################################################################
resource "random_password" "secrets" {
for_each = { for k, v in local.secrets : k => v if lookup(v, "generate_password", false) }
length = lookup(each.value, "password_length", 32)
special = true
override_special = lookup(each.value, "override_special", "!#$%&*()-_=+[]{}<>:?")
# Exclude problematic characters
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
}
################################################################################
# Secrets
################################################################################
resource "aws_secretsmanager_secret" "secrets" {
for_each = local.secrets
name = "${local.prefix}/${each.key}"
description = lookup(each.value, "description", "Secret for ${each.key}")
kms_key_id = local.effective_kms_key
recovery_window_in_days = local.recovery_window_days
# Replication
dynamic "replica" {
for_each = local.replica_regions
content {
region = replica.value
kms_key_id = null # Use default key in replica region
}
}
tags = { Name = "${local.prefix}/${each.key}" }
}
################################################################################
# Secret Values
################################################################################
resource "aws_secretsmanager_secret_version" "secrets" {
for_each = local.secrets
secret_id = aws_secretsmanager_secret.secrets[each.key].id
secret_string = lookup(each.value, "generate_password", false) ? jsonencode(merge(
jsondecode(lookup(each.value, "secret_string_template", "{}")),
{ password = random_password.secrets[each.key].result }
)) : lookup(each.value, "secret_string", "{}")
}
################################################################################
# Secret Rotation
################################################################################
resource "aws_secretsmanager_secret_rotation" "secrets" {
for_each = { for k, v in local.secrets : k => v if lookup(lookup(v, "rotation", {}), "enabled", false) }
secret_id = aws_secretsmanager_secret.secrets[each.key].id
rotation_lambda_arn = each.value.rotation.lambda_arn
rotation_rules {
automatically_after_days = lookup(each.value.rotation, "days", 30)
}
}
################################################################################
# Resource Policy (Cross-Account Access)
################################################################################
resource "aws_secretsmanager_secret_policy" "cross_account" {
for_each = length(local.allowed_accounts) > 0 || length(local.allowed_principals) > 0 ? local.secrets : {}
secret_arn = aws_secretsmanager_secret.secrets[each.key].arn
policy = jsonencode({
Version = "2012-10-17"
Statement = concat(
length(local.allowed_accounts) > 0 ? [{
Sid = "AllowCrossAccountAccess"
Effect = "Allow"
Principal = {
AWS = [for acct in local.allowed_accounts : "arn:aws:iam::${acct}:root"]
}
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = "*"
}] : [],
length(local.allowed_principals) > 0 ? [{
Sid = "AllowPrincipalAccess"
Effect = "Allow"
Principal = {
AWS = local.allowed_principals
}
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = "*"
}] : []
)
})
}
################################################################################
# IAM Policy for Reading Secrets
################################################################################
resource "aws_iam_policy" "read_secrets" {
name = "${replace(local.prefix, "/", "-")}-secrets-read"
description = "Read access to ${local.prefix} secrets"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "GetSecrets"
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
Resource = [for s in aws_secretsmanager_secret.secrets : s.arn]
},
{
Sid = "DecryptSecrets"
Effect = "Allow"
Action = [
"kms:Decrypt",
"kms:DescribeKey"
]
Resource = [local.effective_kms_key]
}
]
})
tags = { Name = "${local.prefix}-secrets-read" }
}
resource "aws_iam_policy" "write_secrets" {
name = "${replace(local.prefix, "/", "-")}-secrets-write"
description = "Write access to ${local.prefix} secrets"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ManageSecrets"
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecret"
]
Resource = [for s in aws_secretsmanager_secret.secrets : s.arn]
},
{
Sid = "EncryptDecryptSecrets"
Effect = "Allow"
Action = [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
]
Resource = [local.effective_kms_key]
}
]
})
tags = { Name = "${local.prefix}-secrets-write" }
}
################################################################################
# Outputs
################################################################################
output "secret_arns" {
value = { for k, v in aws_secretsmanager_secret.secrets : k => v.arn }
description = "Secret ARNs"
}
output "secret_names" {
value = { for k, v in aws_secretsmanager_secret.secrets : k => v.name }
description = "Secret names"
}
output "kms_key_arn" {
value = local.effective_kms_key
description = "KMS key ARN used for encryption"
}
output "read_policy_arn" {
value = aws_iam_policy.read_secrets.arn
description = "IAM policy ARN for reading secrets"
}
output "write_policy_arn" {
value = aws_iam_policy.write_secrets.arn
description = "IAM policy ARN for writing secrets"
}
output "secret_retrieval_commands" {
value = {
for k, v in aws_secretsmanager_secret.secrets : k =>
"aws secretsmanager get-secret-value --secret-id ${v.name} --query SecretString --output text | jq ."
}
description = "CLI commands to retrieve each secret"
}

View File

@@ -0,0 +1,524 @@
################################################################################
# Workload: SES Email Configuration
#
# Email sending infrastructure:
# - Domain identity with DKIM
# - Email identities for sending
# - Configuration sets with tracking
# - Event destinations (CloudWatch, SNS, Kinesis)
# - Dedicated IP pools (optional)
# - Suppression list management
#
# Use cases: Transactional email, marketing campaigns, notifications
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-email/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
config_name = "${local.tenant}-${local.name}-${local.env}"
# Domain to verify (required)
domain = "example.com"
hosted_zone_id = null # Route53 zone ID for automatic DNS verification
# Additional email identities
email_identities = [
# "noreply@example.com",
# "support@example.com",
]
# MAIL FROM domain (optional custom subdomain)
mail_from_subdomain = "mail" # Results in mail.example.com
# DMARC record
enable_dmarc = true
dmarc_policy = "none" # none, quarantine, reject
dmarc_rua = null # Aggregate report email, e.g., "mailto:dmarc@example.com"
# Configuration set (for tracking)
enable_config_set = true
# Event tracking
tracking_options = {
click = true
open = true
bounce = true
complaint = true
delivery = true
reject = true
send = true
}
# Event destinations
cloudwatch_destination = true
sns_destination = true
# Reputation metrics
reputation_metrics_enabled = true
# Sending quotas (request increase via AWS support)
# These are informational - actual limits set by AWS
# Suppression list
suppression_list_reasons = ["BOUNCE", "COMPLAINT"]
# Dedicated IPs (additional cost)
enable_dedicated_ips = false
dedicated_ip_count = 0
# IAM policy for sending
create_sending_role = true
sending_role_name = "${local.config_name}-ses-sender"
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# Domain Identity
################################################################################
resource "aws_ses_domain_identity" "main" {
domain = local.domain
}
resource "aws_ses_domain_dkim" "main" {
domain = aws_ses_domain_identity.main.domain
}
################################################################################
# DNS Records (if hosted zone provided)
################################################################################
# Domain verification
resource "aws_route53_record" "ses_verification" {
count = local.hosted_zone_id != null ? 1 : 0
zone_id = local.hosted_zone_id
name = "_amazonses.${local.domain}"
type = "TXT"
ttl = 600
records = [aws_ses_domain_identity.main.verification_token]
}
# DKIM records
resource "aws_route53_record" "dkim" {
count = local.hosted_zone_id != null ? 3 : 0
zone_id = local.hosted_zone_id
name = "${aws_ses_domain_dkim.main.dkim_tokens[count.index]}._domainkey.${local.domain}"
type = "CNAME"
ttl = 600
records = ["${aws_ses_domain_dkim.main.dkim_tokens[count.index]}.dkim.amazonses.com"]
}
# MAIL FROM domain
resource "aws_ses_domain_mail_from" "main" {
domain = aws_ses_domain_identity.main.domain
mail_from_domain = "${local.mail_from_subdomain}.${local.domain}"
}
resource "aws_route53_record" "mail_from_mx" {
count = local.hosted_zone_id != null ? 1 : 0
zone_id = local.hosted_zone_id
name = "${local.mail_from_subdomain}.${local.domain}"
type = "MX"
ttl = 600
records = ["10 feedback-smtp.${data.aws_region.current.name}.amazonses.com"]
}
resource "aws_route53_record" "mail_from_spf" {
count = local.hosted_zone_id != null ? 1 : 0
zone_id = local.hosted_zone_id
name = "${local.mail_from_subdomain}.${local.domain}"
type = "TXT"
ttl = 600
records = ["v=spf1 include:amazonses.com ~all"]
}
# DMARC record
resource "aws_route53_record" "dmarc" {
count = local.hosted_zone_id != null && local.enable_dmarc ? 1 : 0
zone_id = local.hosted_zone_id
name = "_dmarc.${local.domain}"
type = "TXT"
ttl = 600
records = [
local.dmarc_rua != null
? "v=DMARC1; p=${local.dmarc_policy}; rua=${local.dmarc_rua}"
: "v=DMARC1; p=${local.dmarc_policy}"
]
}
################################################################################
# Email Identities
################################################################################
resource "aws_ses_email_identity" "identities" {
for_each = toset(local.email_identities)
email = each.value
}
################################################################################
# Configuration Set
################################################################################
resource "aws_ses_configuration_set" "main" {
count = local.enable_config_set ? 1 : 0
name = local.config_name
reputation_metrics_enabled = local.reputation_metrics_enabled
delivery_options {
tls_policy = "REQUIRE"
}
tracking_options {
custom_redirect_domain = null
}
}
################################################################################
# SNS Topic for Events
################################################################################
resource "aws_sns_topic" "ses_events" {
count = local.sns_destination ? 1 : 0
name = "${local.config_name}-ses-events"
kms_master_key_id = "alias/aws/sns"
tags = { Name = "${local.config_name}-ses-events" }
}
resource "aws_sns_topic_policy" "ses_events" {
count = local.sns_destination ? 1 : 0
arn = aws_sns_topic.ses_events[0].arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowSES"
Effect = "Allow"
Principal = {
Service = "ses.amazonaws.com"
}
Action = "sns:Publish"
Resource = aws_sns_topic.ses_events[0].arn
Condition = {
StringEquals = {
"AWS:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}]
})
}
################################################################################
# Event Destinations
################################################################################
resource "aws_ses_event_destination" "cloudwatch" {
count = local.enable_config_set && local.cloudwatch_destination ? 1 : 0
name = "cloudwatch"
configuration_set_name = aws_ses_configuration_set.main[0].name
enabled = true
matching_types = compact([
local.tracking_options.bounce ? "bounce" : "",
local.tracking_options.complaint ? "complaint" : "",
local.tracking_options.delivery ? "delivery" : "",
local.tracking_options.send ? "send" : "",
local.tracking_options.reject ? "reject" : "",
local.tracking_options.open ? "open" : "",
local.tracking_options.click ? "click" : "",
])
cloudwatch_destination {
default_value = "default"
dimension_name = "ses:source-ip"
value_source = "messageTag"
}
}
resource "aws_ses_event_destination" "sns" {
count = local.enable_config_set && local.sns_destination ? 1 : 0
name = "sns"
configuration_set_name = aws_ses_configuration_set.main[0].name
enabled = true
matching_types = ["bounce", "complaint"]
sns_destination {
topic_arn = aws_sns_topic.ses_events[0].arn
}
}
################################################################################
# IAM Role for Sending
################################################################################
resource "aws_iam_role" "sending" {
count = local.create_sending_role ? 1 : 0
name = local.sending_role_name
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = [
"lambda.amazonaws.com",
"ecs-tasks.amazonaws.com",
"ec2.amazonaws.com"
]
}
Action = "sts:AssumeRole"
}]
})
tags = { Name = local.sending_role_name }
}
resource "aws_iam_role_policy" "sending" {
count = local.create_sending_role ? 1 : 0
name = "ses-sending"
role = aws_iam_role.sending[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "SendEmail"
Effect = "Allow"
Action = [
"ses:SendEmail",
"ses:SendRawEmail",
"ses:SendTemplatedEmail",
"ses:SendBulkTemplatedEmail"
]
Resource = "*"
Condition = {
StringEquals = {
"ses:FromAddress" = [for e in local.email_identities : e]
}
}
},
{
Sid = "UseConfigSet"
Effect = "Allow"
Action = ["ses:SendEmail", "ses:SendRawEmail"]
Resource = local.enable_config_set ? aws_ses_configuration_set.main[0].arn : "*"
}
]
})
}
################################################################################
# SMTP Credentials (for apps that use SMTP)
################################################################################
resource "aws_iam_user" "smtp" {
name = "${local.config_name}-smtp"
tags = { Name = "${local.config_name}-smtp" }
}
resource "aws_iam_user_policy" "smtp" {
name = "ses-smtp"
user = aws_iam_user.smtp.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "ses:SendRawEmail"
Resource = "*"
}]
})
}
resource "aws_iam_access_key" "smtp" {
user = aws_iam_user.smtp.name
}
################################################################################
# Email Templates (Examples)
################################################################################
resource "aws_ses_template" "welcome" {
name = "${local.config_name}-welcome"
subject = "Welcome to {{company_name}}!"
html = <<-HTML
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>Welcome, {{name}}!</h1>
<p>Thank you for signing up for {{company_name}}.</p>
<p>Click <a href="{{verification_link}}">here</a> to verify your email.</p>
</body>
</html>
HTML
text = <<-TEXT
Welcome, {{name}}!
Thank you for signing up for {{company_name}}.
Click the link below to verify your email:
{{verification_link}}
TEXT
}
resource "aws_ses_template" "password_reset" {
name = "${local.config_name}-password-reset"
subject = "Reset your {{company_name}} password"
html = <<-HTML
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>Password Reset Request</h1>
<p>Hi {{name}},</p>
<p>Click <a href="{{reset_link}}">here</a> to reset your password.</p>
<p>This link expires in {{expiry_hours}} hours.</p>
<p>If you didn't request this, please ignore this email.</p>
</body>
</html>
HTML
text = <<-TEXT
Password Reset Request
Hi {{name}},
Click the link below to reset your password:
{{reset_link}}
This link expires in {{expiry_hours}} hours.
If you didn't request this, please ignore this email.
TEXT
}
################################################################################
# Outputs
################################################################################
output "domain_identity_arn" {
value = aws_ses_domain_identity.main.arn
}
output "domain_verification_token" {
value = aws_ses_domain_identity.main.verification_token
}
output "dkim_tokens" {
value = aws_ses_domain_dkim.main.dkim_tokens
}
output "configuration_set" {
value = local.enable_config_set ? aws_ses_configuration_set.main[0].name : null
}
output "sns_topic_arn" {
value = local.sns_destination ? aws_sns_topic.ses_events[0].arn : null
}
output "sending_role_arn" {
value = local.create_sending_role ? aws_iam_role.sending[0].arn : null
}
output "smtp_credentials" {
value = {
username = aws_iam_access_key.smtp.id
password = aws_iam_access_key.smtp.ses_smtp_password_v4
endpoint = "email-smtp.${data.aws_region.current.name}.amazonaws.com"
port = 587
}
sensitive = true
}
output "dns_records_required" {
value = local.hosted_zone_id == null ? {
verification = {
name = "_amazonses.${local.domain}"
type = "TXT"
value = aws_ses_domain_identity.main.verification_token
}
dkim = [
for i, token in aws_ses_domain_dkim.main.dkim_tokens : {
name = "${token}._domainkey.${local.domain}"
type = "CNAME"
value = "${token}.dkim.amazonses.com"
}
]
mail_from_mx = {
name = "${local.mail_from_subdomain}.${local.domain}"
type = "MX"
value = "10 feedback-smtp.${data.aws_region.current.name}.amazonses.com"
}
mail_from_spf = {
name = "${local.mail_from_subdomain}.${local.domain}"
type = "TXT"
value = "v=spf1 include:amazonses.com ~all"
}
} : "DNS records created automatically"
}
output "templates" {
value = {
welcome = aws_ses_template.welcome.name
password_reset = aws_ses_template.password_reset.name
}
}

View File

@@ -0,0 +1,383 @@
################################################################################
# Workload: SNS Topic
#
# Pub/Sub messaging with:
# - Multiple subscription types (Lambda, SQS, HTTP, Email, SMS)
# - Message filtering
# - Dead letter queue
# - KMS encryption
# - Cross-account publishing
# - FIFO topics (ordered, exactly-once)
#
# Use cases: Event fan-out, notifications, decoupling services
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-sns/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
topic_name = "${local.tenant}-${local.name}-${local.env}"
# FIFO topic (ordered, exactly-once delivery)
fifo_topic = false
content_based_deduplication = false
# Encryption
kms_key_arn = null # null = AWS managed key
# Message delivery settings
delivery_policy = {
http = {
defaultHealthyRetryPolicy = {
minDelayTarget = 20
maxDelayTarget = 20
numRetries = 3
numMaxDelayRetries = 0
numNoDelayRetries = 0
numMinDelayRetries = 0
backoffFunction = "linear"
}
disableSubscriptionOverrides = false
}
}
# Subscriptions
subscriptions = {
# Lambda subscription
# "process-events" = {
# protocol = "lambda"
# endpoint = "arn:aws:lambda:us-east-1:123456789012:function:process-events"
# filter_policy = {
# event_type = ["order.created", "order.updated"]
# }
# }
# SQS subscription
# "event-queue" = {
# protocol = "sqs"
# endpoint = "arn:aws:sqs:us-east-1:123456789012:event-queue"
# raw_message_delivery = true
# }
# Email subscription
# "alerts" = {
# protocol = "email"
# endpoint = "alerts@example.com"
# }
# HTTP/HTTPS subscription
# "webhook" = {
# protocol = "https"
# endpoint = "https://api.example.com/webhook"
# filter_policy = {
# severity = ["high", "critical"]
# }
# }
}
# Cross-account publish access
publish_accounts = [
# "123456789012",
]
# Cross-account subscribe access
subscribe_accounts = [
# "234567890123",
]
# AWS service publish access
aws_service_principals = [
# "events.amazonaws.com", # EventBridge
# "cloudwatch.amazonaws.com", # CloudWatch Alarms
# "s3.amazonaws.com", # S3 Event Notifications
# "ses.amazonaws.com", # SES Notifications
]
# Dead letter queue for failed deliveries
enable_dlq = true
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# SNS Topic
################################################################################
resource "aws_sns_topic" "main" {
name = local.fifo_topic ? "${local.topic_name}.fifo" : local.topic_name
fifo_topic = local.fifo_topic
content_based_deduplication = local.fifo_topic ? local.content_based_deduplication : null
kms_master_key_id = local.kms_key_arn != null ? local.kms_key_arn : "alias/aws/sns"
delivery_policy = jsonencode(local.delivery_policy)
tags = { Name = local.topic_name }
}
################################################################################
# Dead Letter Queue
################################################################################
resource "aws_sqs_queue" "dlq" {
count = local.enable_dlq ? 1 : 0
name = local.fifo_topic ? "${local.topic_name}-dlq.fifo" : "${local.topic_name}-dlq"
fifo_queue = local.fifo_topic
content_based_deduplication = local.fifo_topic
message_retention_seconds = 1209600 # 14 days
kms_master_key_id = "alias/aws/sqs"
tags = { Name = "${local.topic_name}-dlq" }
}
resource "aws_sqs_queue_policy" "dlq" {
count = local.enable_dlq ? 1 : 0
queue_url = aws_sqs_queue.dlq[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowSNS"
Effect = "Allow"
Principal = {
Service = "sns.amazonaws.com"
}
Action = "sqs:SendMessage"
Resource = aws_sqs_queue.dlq[0].arn
Condition = {
ArnEquals = {
"aws:SourceArn" = aws_sns_topic.main.arn
}
}
}]
})
}
################################################################################
# Topic Policy
################################################################################
resource "aws_sns_topic_policy" "main" {
arn = aws_sns_topic.main.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = concat(
# Allow account root
[{
Sid = "DefaultPolicy"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = [
"sns:Publish",
"sns:Subscribe",
"sns:Receive",
"sns:ListSubscriptionsByTopic",
"sns:GetTopicAttributes"
]
Resource = aws_sns_topic.main.arn
}],
# Cross-account publish
length(local.publish_accounts) > 0 ? [{
Sid = "CrossAccountPublish"
Effect = "Allow"
Principal = {
AWS = [for acct in local.publish_accounts : "arn:aws:iam::${acct}:root"]
}
Action = "sns:Publish"
Resource = aws_sns_topic.main.arn
}] : [],
# Cross-account subscribe
length(local.subscribe_accounts) > 0 ? [{
Sid = "CrossAccountSubscribe"
Effect = "Allow"
Principal = {
AWS = [for acct in local.subscribe_accounts : "arn:aws:iam::${acct}:root"]
}
Action = "sns:Subscribe"
Resource = aws_sns_topic.main.arn
}] : [],
# AWS service access
length(local.aws_service_principals) > 0 ? [{
Sid = "AWSServicePublish"
Effect = "Allow"
Principal = {
Service = local.aws_service_principals
}
Action = "sns:Publish"
Resource = aws_sns_topic.main.arn
Condition = {
StringEquals = {
"aws:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}] : []
)
})
}
################################################################################
# Subscriptions
################################################################################
resource "aws_sns_topic_subscription" "subscriptions" {
for_each = local.subscriptions
topic_arn = aws_sns_topic.main.arn
protocol = each.value.protocol
endpoint = each.value.endpoint
filter_policy = lookup(each.value, "filter_policy", null) != null ? jsonencode(each.value.filter_policy) : null
filter_policy_scope = lookup(each.value, "filter_policy", null) != null ? "MessageAttributes" : null
raw_message_delivery = lookup(each.value, "raw_message_delivery", false)
redrive_policy = local.enable_dlq ? jsonencode({
deadLetterTargetArn = aws_sqs_queue.dlq[0].arn
}) : null
}
# Lambda permissions for SNS to invoke
resource "aws_lambda_permission" "sns" {
for_each = { for k, v in local.subscriptions : k => v if v.protocol == "lambda" }
statement_id = "AllowSNS-${each.key}"
action = "lambda:InvokeFunction"
function_name = regex("function:([^:]+)$", each.value.endpoint)[0]
principal = "sns.amazonaws.com"
source_arn = aws_sns_topic.main.arn
}
################################################################################
# IAM Policies
################################################################################
resource "aws_iam_policy" "publish" {
name = "${local.topic_name}-sns-publish"
description = "Publish to ${local.topic_name} SNS topic"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublishToTopic"
Effect = "Allow"
Action = "sns:Publish"
Resource = aws_sns_topic.main.arn
},
{
Sid = "DecryptKMS"
Effect = "Allow"
Action = [
"kms:Decrypt",
"kms:GenerateDataKey"
]
Resource = local.kms_key_arn != null ? [local.kms_key_arn] : ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:alias/aws/sns"]
}
]
})
tags = { Name = "${local.topic_name}-publish" }
}
################################################################################
# Outputs
################################################################################
output "topic_arn" {
value = aws_sns_topic.main.arn
description = "SNS topic ARN"
}
output "topic_name" {
value = aws_sns_topic.main.name
description = "SNS topic name"
}
output "dlq_arn" {
value = local.enable_dlq ? aws_sqs_queue.dlq[0].arn : null
description = "Dead letter queue ARN"
}
output "dlq_url" {
value = local.enable_dlq ? aws_sqs_queue.dlq[0].url : null
description = "Dead letter queue URL"
}
output "publish_policy_arn" {
value = aws_iam_policy.publish.arn
description = "IAM policy ARN for publishing"
}
output "subscription_arns" {
value = { for k, v in aws_sns_topic_subscription.subscriptions : k => v.arn }
description = "Subscription ARNs"
}
output "publish_example" {
value = "aws sns publish --topic-arn ${aws_sns_topic.main.arn} --message '{\"event\": \"test\"}' --message-attributes '{\"event_type\": {\"DataType\": \"String\", \"StringValue\": \"test\"}}'"
description = "Example publish command"
}

View File

@@ -0,0 +1,379 @@
################################################################################
# Workload: SQS Queue
#
# Deploys a managed message queue:
# - Main queue with DLQ (dead letter queue)
# - Server-side encryption
# - CloudWatch alarms
# - Optional FIFO support
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<queue-name>-queue/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-queue/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod" # prod, staging, dev
# Queue name (will add .fifo suffix if FIFO enabled)
queue_name = "${local.tenant}-${local.name}-${local.env}"
# FIFO queue (exactly-once processing, ordered)
fifo_queue = false
content_based_deduplication = false # Only for FIFO
# Message settings
message_retention_seconds = 1209600 # 14 days (max)
max_message_size = 262144 # 256 KB (max)
delay_seconds = 0 # Delay before message becomes visible
receive_wait_time_seconds = 20 # Long polling (cost efficient)
# Visibility timeout (should be > consumer processing time)
visibility_timeout_seconds = 300 # 5 minutes
# Dead letter queue settings
max_receive_count = 3 # Messages go to DLQ after this many failed receives
dlq_retention_days = 14
# Alarm thresholds
alarm_age_threshold = 300 # 5 minutes - message age alarm
alarm_messages_threshold = 1000 # Queue depth alarm
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "tenant" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "04-tenants/${local.tenant}/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# KMS Key
################################################################################
resource "aws_kms_key" "sqs" {
description = "KMS key for ${local.queue_name} SQS encryption"
deletion_window_in_days = 7
enable_key_rotation = true
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM User Permissions"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow SQS Service"
Effect = "Allow"
Principal = {
Service = "sqs.amazonaws.com"
}
Action = [
"kms:Decrypt",
"kms:GenerateDataKey"
]
Resource = "*"
},
{
Sid = "Allow SNS to use this key"
Effect = "Allow"
Principal = {
Service = "sns.amazonaws.com"
}
Action = [
"kms:Decrypt",
"kms:GenerateDataKey"
]
Resource = "*"
}
]
})
tags = { Name = "${local.queue_name}-sqs" }
}
resource "aws_kms_alias" "sqs" {
name = "alias/${local.queue_name}-sqs"
target_key_id = aws_kms_key.sqs.key_id
}
################################################################################
# Dead Letter Queue
################################################################################
resource "aws_sqs_queue" "dlq" {
name = local.fifo_queue ? "${local.queue_name}-dlq.fifo" : "${local.queue_name}-dlq"
fifo_queue = local.fifo_queue
message_retention_seconds = local.dlq_retention_days * 86400
kms_master_key_id = aws_kms_key.sqs.id
kms_data_key_reuse_period_seconds = 86400 # 24 hours
tags = { Name = "${local.queue_name}-dlq" }
}
################################################################################
# Main Queue
################################################################################
resource "aws_sqs_queue" "main" {
name = local.fifo_queue ? "${local.queue_name}.fifo" : local.queue_name
fifo_queue = local.fifo_queue
content_based_deduplication = local.fifo_queue ? local.content_based_deduplication : null
message_retention_seconds = local.message_retention_seconds
max_message_size = local.max_message_size
delay_seconds = local.delay_seconds
receive_message_wait_time_seconds = local.receive_wait_time_seconds
visibility_timeout_seconds = local.visibility_timeout_seconds
# Encryption
kms_master_key_id = aws_kms_key.sqs.id
kms_data_key_reuse_period_seconds = 86400 # 24 hours
# Dead letter queue
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.dlq.arn
maxReceiveCount = local.max_receive_count
})
tags = { Name = local.queue_name }
}
# Allow DLQ redrive
resource "aws_sqs_queue_redrive_allow_policy" "dlq" {
queue_url = aws_sqs_queue.dlq.id
redrive_allow_policy = jsonencode({
redrivePermission = "byQueue"
sourceQueueArns = [aws_sqs_queue.main.arn]
})
}
################################################################################
# Queue Policy
################################################################################
resource "aws_sqs_queue_policy" "main" {
queue_url = aws_sqs_queue.main.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowTenantAccess"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
Action = [
"sqs:SendMessage",
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:GetQueueUrl"
]
Resource = aws_sqs_queue.main.arn
Condition = {
StringEquals = {
"aws:PrincipalTag/Tenant" = local.tenant
}
}
},
{
Sid = "DenyInsecureTransport"
Effect = "Deny"
Principal = "*"
Action = "sqs:*"
Resource = aws_sqs_queue.main.arn
Condition = {
Bool = {
"aws:SecureTransport" = "false"
}
}
}
]
})
}
################################################################################
# SNS Topic for Alarms
################################################################################
resource "aws_sns_topic" "alarms" {
name = "${local.queue_name}-alarms"
tags = { Name = "${local.queue_name}-alarms" }
}
################################################################################
# CloudWatch Alarms
################################################################################
# Queue depth alarm
resource "aws_cloudwatch_metric_alarm" "depth" {
alarm_name = "${local.queue_name}-depth"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Average"
threshold = local.alarm_messages_threshold
alarm_description = "Queue depth high - messages may be backing up"
dimensions = {
QueueName = aws_sqs_queue.main.name
}
alarm_actions = [aws_sns_topic.alarms.arn]
ok_actions = [aws_sns_topic.alarms.arn]
tags = { Name = "${local.queue_name}-depth" }
}
# Message age alarm
resource "aws_cloudwatch_metric_alarm" "age" {
alarm_name = "${local.queue_name}-age"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "ApproximateAgeOfOldestMessage"
namespace = "AWS/SQS"
period = 300
statistic = "Maximum"
threshold = local.alarm_age_threshold
alarm_description = "Oldest message age high - consumers may be failing"
dimensions = {
QueueName = aws_sqs_queue.main.name
}
alarm_actions = [aws_sns_topic.alarms.arn]
ok_actions = [aws_sns_topic.alarms.arn]
tags = { Name = "${local.queue_name}-age" }
}
# DLQ messages alarm (critical - messages are failing)
resource "aws_cloudwatch_metric_alarm" "dlq" {
alarm_name = "${local.queue_name}-dlq"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 300
statistic = "Sum"
threshold = 0
alarm_description = "Messages in DLQ - processing failures detected"
dimensions = {
QueueName = aws_sqs_queue.dlq.name
}
alarm_actions = [aws_sns_topic.alarms.arn]
tags = { Name = "${local.queue_name}-dlq" }
}
################################################################################
# Outputs
################################################################################
output "queue_url" {
value = aws_sqs_queue.main.url
}
output "queue_arn" {
value = aws_sqs_queue.main.arn
}
output "queue_name" {
value = aws_sqs_queue.main.name
}
output "dlq_url" {
value = aws_sqs_queue.dlq.url
}
output "dlq_arn" {
value = aws_sqs_queue.dlq.arn
}
output "kms_key_arn" {
value = aws_kms_key.sqs.arn
}
output "alarm_topic_arn" {
value = aws_sns_topic.alarms.arn
}

View File

@@ -0,0 +1,343 @@
################################################################################
# Workload: SSM Parameter Store
#
# Configuration management (cheaper than Secrets Manager for non-secrets):
# - String, StringList, SecureString parameters
# - Hierarchical paths for organization
# - KMS encryption for SecureString
# - Parameter policies (expiration, notification)
# - Cross-account access
#
# Cost: Free for standard parameters, $0.05/10K API calls for advanced
# Use Secrets Manager for: rotation, cross-region replication, RDS integration
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-params/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
prefix = "/${local.tenant}/${local.env}/${local.name}"
# KMS key for SecureString (null = AWS managed key)
kms_key_arn = null
# Parameter tier: Standard (free, 4KB) or Advanced ($0.05/param/mo, 8KB)
tier = "Standard"
# Parameters to create
parameters = {
# Application config
"config/app_name" = {
type = "String"
value = local.name
description = "Application name"
}
"config/environment" = {
type = "String"
value = local.env
description = "Environment name"
}
"config/log_level" = {
type = "String"
value = "INFO"
description = "Application log level"
}
"config/feature_flags" = {
type = "String"
value = jsonencode({
new_checkout = true
dark_mode = false
beta_features = false
})
description = "Feature flags JSON"
}
# Database config (non-secret parts)
"database/host" = {
type = "String"
value = "db.example.internal"
description = "Database hostname"
}
"database/port" = {
type = "String"
value = "5432"
description = "Database port"
}
"database/name" = {
type = "String"
value = "myapp"
description = "Database name"
}
# Secure values (encrypted with KMS)
# Note: Update this value after deployment via CLI:
# aws ssm put-parameter --name "/<tenant>/<env>/<app>/secrets/api_key" --value "real-secret" --type SecureString --overwrite
"secrets/api_key" = {
type = "SecureString"
value = "initial-value-update-after-deploy"
description = "External API key"
}
# List example
"config/allowed_origins" = {
type = "StringList"
value = "https://example.com,https://app.example.com"
description = "CORS allowed origins"
}
}
# Parameters with expiration policies (Advanced tier only)
expiring_parameters = {
# "tokens/temp_token" = {
# type = "SecureString"
# value = "temp-value"
# description = "Temporary token"
# expiration = "2024-12-31T23:59:59Z"
# }
}
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# SSM Parameters
################################################################################
resource "aws_ssm_parameter" "params" {
for_each = local.parameters
name = "${local.prefix}/${each.key}"
description = lookup(each.value, "description", "Parameter ${each.key}")
type = each.value.type
value = each.value.value
tier = local.tier
key_id = each.value.type == "SecureString" ? local.kms_key_arn : null
tags = {
Name = "${local.prefix}/${each.key}"
Type = each.value.type
}
# Uncomment to prevent Terraform from updating SecureString values
# (useful when managing secrets externally via CLI/console)
# lifecycle {
# ignore_changes = [value]
# }
}
################################################################################
# Parameters with Expiration (Advanced Tier)
################################################################################
resource "aws_ssm_parameter" "expiring" {
for_each = local.expiring_parameters
name = "${local.prefix}/${each.key}"
description = lookup(each.value, "description", "Parameter ${each.key}")
type = each.value.type
value = each.value.value
tier = "Advanced" # Required for policies
overwrite = true # Allow updates to existing parameters
key_id = each.value.type == "SecureString" ? local.kms_key_arn : null
# Note: Parameter policies (expiration, notification) require AWS SDK/CLI
# Use aws ssm put-parameter with --policies flag for expiration:
# aws ssm put-parameter --name "/path/param" --policies '[{"Type":"Expiration","Version":"1.0","Attributes":{"Timestamp":"2024-12-31T23:59:59.000Z"}}]'
tags = {
Name = "${local.prefix}/${each.key}"
Type = each.value.type
Expiration = lookup(each.value, "expiration", "none")
}
}
################################################################################
# IAM Policy for Reading Parameters
################################################################################
resource "aws_iam_policy" "read" {
name = "${local.tenant}-${local.name}-ssm-read"
description = "Read access to ${local.prefix} parameters"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "DescribeParameters"
Effect = "Allow"
Action = [
"ssm:DescribeParameters"
]
Resource = "*"
},
{
Sid = "GetParameters"
Effect = "Allow"
Action = [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
]
Resource = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${local.prefix}/*"
},
{
Sid = "DecryptSecureStrings"
Effect = "Allow"
Action = [
"kms:Decrypt"
]
Resource = local.kms_key_arn != null ? [local.kms_key_arn] : ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:alias/aws/ssm"]
}
]
})
tags = { Name = "${local.tenant}-${local.name}-ssm-read" }
}
resource "aws_iam_policy" "write" {
name = "${local.tenant}-${local.name}-ssm-write"
description = "Write access to ${local.prefix} parameters"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ManageParameters"
Effect = "Allow"
Action = [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
"ssm:DescribeParameters"
]
Resource = "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter${local.prefix}/*"
},
{
Sid = "EncryptDecrypt"
Effect = "Allow"
Action = [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey"
]
Resource = local.kms_key_arn != null ? [local.kms_key_arn] : ["arn:aws:kms:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:alias/aws/ssm"]
}
]
})
tags = { Name = "${local.tenant}-${local.name}-ssm-write" }
}
################################################################################
# Outputs
################################################################################
output "parameter_arns" {
value = { for k, v in aws_ssm_parameter.params : k => v.arn }
description = "Parameter ARNs"
}
output "parameter_names" {
value = { for k, v in aws_ssm_parameter.params : k => v.name }
description = "Full parameter names (paths)"
}
output "prefix" {
value = local.prefix
description = "Parameter path prefix"
}
output "read_policy_arn" {
value = aws_iam_policy.read.arn
description = "IAM policy ARN for reading parameters"
}
output "write_policy_arn" {
value = aws_iam_policy.write.arn
description = "IAM policy ARN for writing parameters"
}
output "sdk_examples" {
value = {
get_single = "aws ssm get-parameter --name '${local.prefix}/config/app_name' --query Parameter.Value --output text"
get_secure = "aws ssm get-parameter --name '${local.prefix}/secrets/api_key' --with-decryption --query Parameter.Value --output text"
get_path = "aws ssm get-parameters-by-path --path '${local.prefix}/config' --recursive --query 'Parameters[*].[Name,Value]' --output table"
put_param = "aws ssm put-parameter --name '${local.prefix}/config/new_param' --value 'my-value' --type String --overwrite"
}
description = "Example CLI commands"
}
output "cost_estimate" {
value = {
standard_params = "Free (up to 10,000 parameters)"
advanced_params = "$0.05/parameter/month"
api_calls = "Free for standard, $0.05 per 10,000 for advanced"
note = "SecureString encryption uses KMS (may have additional costs)"
}
description = "Cost information"
}

View File

@@ -0,0 +1,450 @@
################################################################################
# Workload: Static Site (S3 + CloudFront)
#
# Deploys a static website:
# - S3 bucket for content (private, OAC access only)
# - CloudFront distribution with HTTPS
# - ACM certificate (DNS validation)
# - WAF integration (optional)
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<site-name>/
# Update locals and variables
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
# Domain (leave empty for CloudFront default domain)
domain_name = "" # e.g., "www.example.com"
hosted_zone_id = "" # Route53 hosted zone ID
create_certificate = local.domain_name != ""
# Content settings
default_root_object = "index.html"
error_page_path = "/error.html"
# Caching
default_ttl = 86400 # 1 day
min_ttl = 0
max_ttl = 31536000 # 1 year
# Price class
# PriceClass_100 = US, Canada, Europe (cheapest)
# PriceClass_200 = Above + Asia, Africa, Middle East
# PriceClass_All = All edge locations
price_class = "PriceClass_100"
# WAF (set to WAF web ACL ARN to enable)
waf_web_acl_arn = ""
# Logging
enable_logging = true
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
# ACM certificates must be in us-east-1 for CloudFront
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
################################################################################
# Data Sources
################################################################################
data "terraform_remote_state" "bootstrap" {
backend = "s3"
config = {
bucket = var.state_bucket
key = "00-bootstrap/terraform.tfstate"
region = var.region
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# S3 Bucket
################################################################################
resource "aws_s3_bucket" "site" {
bucket = "${local.tenant}-${local.name}-${local.env}-${data.aws_caller_identity.current.account_id}"
tags = { Name = "${local.tenant}-${local.name}" }
}
resource "aws_s3_bucket_versioning" "site" {
bucket = aws_s3_bucket.site.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "site" {
bucket = aws_s3_bucket.site.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "site" {
bucket = aws_s3_bucket.site.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Bucket policy for CloudFront OAC
resource "aws_s3_bucket_policy" "site" {
bucket = aws_s3_bucket.site.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCloudFrontOAC"
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.site.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.site.arn
}
}
}
]
})
}
################################################################################
# CloudFront Origin Access Control
################################################################################
resource "aws_cloudfront_origin_access_control" "site" {
name = "${local.tenant}-${local.name}"
description = "OAC for ${local.tenant}-${local.name}"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
################################################################################
# ACM Certificate (if custom domain)
################################################################################
resource "aws_acm_certificate" "site" {
count = local.create_certificate ? 1 : 0
provider = aws.us_east_1
domain_name = local.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = { Name = "${local.tenant}-${local.name}" }
}
resource "aws_route53_record" "cert_validation" {
for_each = local.create_certificate ? {
for dvo in aws_acm_certificate.site[0].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
} : {}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = local.hosted_zone_id
}
resource "aws_acm_certificate_validation" "site" {
count = local.create_certificate ? 1 : 0
provider = aws.us_east_1
certificate_arn = aws_acm_certificate.site[0].arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
################################################################################
# CloudFront Logging Bucket
################################################################################
resource "aws_s3_bucket" "logs" {
count = local.enable_logging ? 1 : 0
bucket = "${local.tenant}-${local.name}-logs-${data.aws_caller_identity.current.account_id}"
tags = { Name = "${local.tenant}-${local.name}-logs" }
}
resource "aws_s3_bucket_ownership_controls" "logs" {
count = local.enable_logging ? 1 : 0
bucket = aws_s3_bucket.logs[0].id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
resource "aws_s3_bucket_acl" "logs" {
count = local.enable_logging ? 1 : 0
depends_on = [aws_s3_bucket_ownership_controls.logs]
bucket = aws_s3_bucket.logs[0].id
acl = "private"
}
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
count = local.enable_logging ? 1 : 0
bucket = aws_s3_bucket.logs[0].id
rule {
id = "cleanup"
status = "Enabled"
expiration {
days = 90
}
}
}
################################################################################
# CloudFront Distribution
################################################################################
resource "aws_cloudfront_distribution" "site" {
enabled = true
is_ipv6_enabled = true
default_root_object = local.default_root_object
price_class = local.price_class
comment = "${local.tenant} ${local.name} static site"
aliases = local.create_certificate ? [local.domain_name] : []
origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.site.id}"
origin_access_control_id = aws_cloudfront_origin_access_control.site.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.site.id}"
viewer_protocol_policy = "redirect-to-https"
compress = true
min_ttl = local.min_ttl
default_ttl = local.default_ttl
max_ttl = local.max_ttl
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
# Security headers
response_headers_policy_id = aws_cloudfront_response_headers_policy.security.id
}
# Custom error pages
custom_error_response {
error_code = 404
response_code = 404
response_page_path = local.error_page_path
error_caching_min_ttl = 60
}
custom_error_response {
error_code = 403
response_code = 404
response_page_path = local.error_page_path
error_caching_min_ttl = 60
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = local.create_certificate ? aws_acm_certificate.site[0].arn : null
ssl_support_method = local.create_certificate ? "sni-only" : null
minimum_protocol_version = local.create_certificate ? "TLSv1.2_2021" : null
cloudfront_default_certificate = !local.create_certificate
}
dynamic "logging_config" {
for_each = local.enable_logging ? [1] : []
content {
bucket = aws_s3_bucket.logs[0].bucket_domain_name
include_cookies = false
prefix = "cloudfront/"
}
}
web_acl_id = local.waf_web_acl_arn != "" ? local.waf_web_acl_arn : null
tags = { Name = "${local.tenant}-${local.name}" }
}
################################################################################
# Security Headers Policy
################################################################################
resource "aws_cloudfront_response_headers_policy" "security" {
name = "${local.tenant}-${local.name}-security"
comment = "Security headers for ${local.tenant}-${local.name}"
security_headers_config {
content_type_options {
override = true
}
frame_options {
frame_option = "DENY"
override = true
}
referrer_policy {
referrer_policy = "strict-origin-when-cross-origin"
override = true
}
strict_transport_security {
access_control_max_age_sec = 31536000 # 1 year
include_subdomains = true
preload = true
override = true
}
xss_protection {
mode_block = true
protection = true
override = true
}
content_security_policy {
content_security_policy = "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'"
override = true
}
}
}
################################################################################
# Route53 Record (if custom domain)
################################################################################
resource "aws_route53_record" "site" {
count = local.create_certificate ? 1 : 0
zone_id = local.hosted_zone_id
name = local.domain_name
type = "A"
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
}
################################################################################
# Outputs
################################################################################
output "bucket_name" {
value = aws_s3_bucket.site.id
}
output "bucket_arn" {
value = aws_s3_bucket.site.arn
}
output "distribution_id" {
value = aws_cloudfront_distribution.site.id
}
output "distribution_domain" {
value = aws_cloudfront_distribution.site.domain_name
}
output "site_url" {
value = local.create_certificate ? "https://${local.domain_name}" : "https://${aws_cloudfront_distribution.site.domain_name}"
}
output "deploy_command" {
value = "aws s3 sync ./dist s3://${aws_s3_bucket.site.id} --delete && aws cloudfront create-invalidation --distribution-id ${aws_cloudfront_distribution.site.id} --paths '/*'"
description = "Command to deploy content"
}

View File

@@ -0,0 +1,499 @@
################################################################################
# Workload: Step Functions State Machine
#
# Deploys a serverless workflow:
# - Step Functions state machine
# - IAM role with least-privilege
# - CloudWatch logging
# - X-Ray tracing
# - EventBridge trigger (optional)
# - API Gateway trigger (optional)
#
# Usage:
# Copy this folder to 05-workloads/<tenant>-<workflow-name>/
# Update the state machine definition in definition.json
# terraform init -backend-config=../../00-bootstrap/backend.hcl
# terraform apply
################################################################################
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
backend "s3" {
key = "05-workloads/<TENANT>-<NAME>-workflow/terraform.tfstate"
}
}
################################################################################
# Configuration - UPDATE THESE
################################################################################
locals {
# Naming
tenant = "<TENANT>"
name = "<NAME>"
env = "prod"
state_machine_name = "${local.tenant}-${local.name}-${local.env}"
# State machine type: STANDARD or EXPRESS
# STANDARD: Long-running (up to 1 year), exactly-once execution
# EXPRESS: Short-duration (up to 5 min), at-least-once, cheaper
type = "STANDARD"
# Logging level: OFF, ALL, ERROR, FATAL
logging_level = "ERROR"
# X-Ray tracing
tracing_enabled = true
# EventBridge trigger (set to null to disable)
schedule_expression = null # e.g., "rate(1 hour)" or "cron(0 12 * * ? *)"
# API Gateway trigger
enable_api_trigger = false
# Lambda functions this workflow can invoke (ARNs)
lambda_arns = [
# "arn:aws:lambda:us-east-1:123456789012:function:my-function",
]
# DynamoDB tables this workflow can access (ARNs)
dynamodb_arns = [
# "arn:aws:dynamodb:us-east-1:123456789012:table/my-table",
]
# SQS queues this workflow can send to (ARNs)
sqs_arns = [
# "arn:aws:sqs:us-east-1:123456789012:my-queue",
]
# SNS topics this workflow can publish to (ARNs)
sns_arns = [
# "arn:aws:sns:us-east-1:123456789012:my-topic",
]
# S3 buckets this workflow can access (ARNs)
s3_arns = [
# "arn:aws:s3:::my-bucket/*",
]
}
################################################################################
# Variables
################################################################################
variable "region" {
type = string
default = "us-east-1"
}
variable "state_bucket" {
type = string
}
################################################################################
# Provider
################################################################################
provider "aws" {
region = var.region
default_tags {
tags = {
Tenant = local.tenant
App = local.name
Environment = local.env
ManagedBy = "terraform"
}
}
}
################################################################################
# Data Sources
################################################################################
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
################################################################################
# CloudWatch Log Group
################################################################################
resource "aws_cloudwatch_log_group" "main" {
name = "/aws/states/${local.state_machine_name}"
retention_in_days = 30
tags = { Name = local.state_machine_name }
}
################################################################################
# IAM Role
################################################################################
resource "aws_iam_role" "state_machine" {
name = "${local.state_machine_name}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "states.amazonaws.com" }
}]
})
tags = { Name = "${local.state_machine_name}-role" }
}
# CloudWatch Logs permissions
resource "aws_iam_role_policy" "logs" {
name = "cloudwatch-logs"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"logs:CreateLogDelivery",
"logs:CreateLogStream",
"logs:GetLogDelivery",
"logs:UpdateLogDelivery",
"logs:DeleteLogDelivery",
"logs:ListLogDeliveries",
"logs:PutLogEvents",
"logs:PutResourcePolicy",
"logs:DescribeResourcePolicies",
"logs:DescribeLogGroups"
]
Resource = "*"
}]
})
}
# X-Ray permissions
resource "aws_iam_role_policy" "xray" {
count = local.tracing_enabled ? 1 : 0
name = "xray"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets"
]
Resource = "*"
}]
})
}
# Lambda invocation permissions
resource "aws_iam_role_policy" "lambda" {
count = length(local.lambda_arns) > 0 ? 1 : 0
name = "lambda"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = local.lambda_arns
}]
})
}
# DynamoDB permissions
resource "aws_iam_role_policy" "dynamodb" {
count = length(local.dynamodb_arns) > 0 ? 1 : 0
name = "dynamodb"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
]
Resource = local.dynamodb_arns
}]
})
}
# SQS permissions
resource "aws_iam_role_policy" "sqs" {
count = length(local.sqs_arns) > 0 ? 1 : 0
name = "sqs"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"sqs:SendMessage",
"sqs:GetQueueUrl"
]
Resource = local.sqs_arns
}]
})
}
# SNS permissions
resource "aws_iam_role_policy" "sns" {
count = length(local.sns_arns) > 0 ? 1 : 0
name = "sns"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sns:Publish"
Resource = local.sns_arns
}]
})
}
# S3 permissions
resource "aws_iam_role_policy" "s3" {
count = length(local.s3_arns) > 0 ? 1 : 0
name = "s3"
role = aws_iam_role.state_machine.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
]
Resource = local.s3_arns
}]
})
}
################################################################################
# State Machine Definition
################################################################################
# Simple example - replace with your actual workflow
locals {
state_machine_definition = jsonencode({
Comment = "Example workflow for ${local.tenant} ${local.name}"
StartAt = "ProcessInput"
States = {
ProcessInput = {
Type = "Pass"
Parameters = {
"input.$" = "$"
"timestamp" = "$$.State.EnteredTime"
}
Next = "Success"
}
Success = {
Type = "Succeed"
}
}
})
}
################################################################################
# Step Functions State Machine
################################################################################
resource "aws_sfn_state_machine" "main" {
name = local.state_machine_name
role_arn = aws_iam_role.state_machine.arn
type = local.type
definition = local.state_machine_definition
logging_configuration {
log_destination = "${aws_cloudwatch_log_group.main.arn}:*"
include_execution_data = true
level = local.logging_level
}
tracing_configuration {
enabled = local.tracing_enabled
}
tags = { Name = local.state_machine_name }
}
################################################################################
# EventBridge Schedule Trigger
################################################################################
resource "aws_cloudwatch_event_rule" "schedule" {
count = local.schedule_expression != null ? 1 : 0
name = "${local.state_machine_name}-schedule"
description = "Trigger ${local.state_machine_name} on schedule"
schedule_expression = local.schedule_expression
tags = { Name = "${local.state_machine_name}-schedule" }
}
resource "aws_cloudwatch_event_target" "schedule" {
count = local.schedule_expression != null ? 1 : 0
rule = aws_cloudwatch_event_rule.schedule[0].name
target_id = "StepFunctions"
arn = aws_sfn_state_machine.main.arn
role_arn = aws_iam_role.eventbridge[0].arn
input = jsonencode({
source = "scheduled"
timestamp = "$.time"
})
}
resource "aws_iam_role" "eventbridge" {
count = local.schedule_expression != null ? 1 : 0
name = "${local.state_machine_name}-eventbridge"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "events.amazonaws.com" }
}]
})
tags = { Name = "${local.state_machine_name}-eventbridge" }
}
resource "aws_iam_role_policy" "eventbridge" {
count = local.schedule_expression != null ? 1 : 0
name = "start-execution"
role = aws_iam_role.eventbridge[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "states:StartExecution"
Resource = aws_sfn_state_machine.main.arn
}]
})
}
################################################################################
# API Gateway Trigger
################################################################################
resource "aws_apigatewayv2_api" "main" {
count = local.enable_api_trigger ? 1 : 0
name = local.state_machine_name
protocol_type = "HTTP"
tags = { Name = local.state_machine_name }
}
resource "aws_apigatewayv2_stage" "main" {
count = local.enable_api_trigger ? 1 : 0
api_id = aws_apigatewayv2_api.main[0].id
name = "$default"
auto_deploy = true
}
resource "aws_apigatewayv2_integration" "main" {
count = local.enable_api_trigger ? 1 : 0
api_id = aws_apigatewayv2_api.main[0].id
integration_type = "AWS_PROXY"
integration_subtype = "StepFunctions-StartExecution"
credentials_arn = aws_iam_role.api[0].arn
request_parameters = {
StateMachineArn = aws_sfn_state_machine.main.arn
Input = "$request.body"
}
}
resource "aws_apigatewayv2_route" "main" {
count = local.enable_api_trigger ? 1 : 0
api_id = aws_apigatewayv2_api.main[0].id
route_key = "POST /execute"
target = "integrations/${aws_apigatewayv2_integration.main[0].id}"
}
resource "aws_iam_role" "api" {
count = local.enable_api_trigger ? 1 : 0
name = "${local.state_machine_name}-api"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "apigateway.amazonaws.com" }
}]
})
tags = { Name = "${local.state_machine_name}-api" }
}
resource "aws_iam_role_policy" "api" {
count = local.enable_api_trigger ? 1 : 0
name = "start-execution"
role = aws_iam_role.api[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = "states:StartExecution"
Resource = aws_sfn_state_machine.main.arn
}]
})
}
################################################################################
# Outputs
################################################################################
output "state_machine_arn" {
value = aws_sfn_state_machine.main.arn
}
output "state_machine_name" {
value = aws_sfn_state_machine.main.name
}
output "role_arn" {
value = aws_iam_role.state_machine.arn
}
output "log_group" {
value = aws_cloudwatch_log_group.main.name
}
output "api_endpoint" {
value = local.enable_api_trigger ? "${aws_apigatewayv2_api.main[0].api_endpoint}/execute" : null
}
output "execution_command" {
value = "aws stepfunctions start-execution --state-machine-arn ${aws_sfn_state_machine.main.arn} --input '{\"key\": \"value\"}'"
}