Files
Greg Hendrickson 6136cde9bb 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
2026-02-02 02:57:23 +00:00

440 lines
12 KiB
HCL

################################################################################
# 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"
}