Files
terraform-foundation/terraform/05-workloads/_template/s3-bucket/main.tf
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

528 lines
14 KiB
HCL

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