terraform

Terraform AWS S3 Bucket

Terraform configuration for an AWS S3 bucket with versioning, encryption, lifecycle rules, and access policies.

Overview

A Terraform configuration for provisioning an AWS S3 bucket with production-ready defaults. Includes server-side encryption, versioning, lifecycle policies for cost management, public access blocking, and logging.

Configuration

# main.tf

terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"              # Use latest 5.x release
    }
  }

  # Remote state storage (recommended for teams)
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "s3-bucket/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
      Project     = var.project_name
    }
  }
}

# ── S3 Bucket ──
resource "aws_s3_bucket" "main" {
  bucket = "${var.project_name}-${var.environment}-${var.bucket_suffix}"

  # Prevent accidental deletion of bucket with data
  force_destroy = false

  tags = {
    Name = "${var.project_name}-${var.environment}"
  }
}

# ── Versioning ──
resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id

  versioning_configuration {
    status = "Enabled"                 # Track all object versions
  }
}

# ── Server-Side 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 = "aws:kms"        # KMS encryption (or "AES256" for S3-managed)
    }
    bucket_key_enabled = true          # Reduce KMS API calls and cost
  }
}

# ── Block All Public Access ──
resource "aws_s3_bucket_public_access_block" "main" {
  bucket = aws_s3_bucket.main.id

  block_public_acls       = true       # Block public ACLs
  block_public_policy     = true       # Block public bucket policies
  ignore_public_acls      = true       # Ignore existing public ACLs
  restrict_public_buckets = true       # Restrict public bucket policies
}

# ── Lifecycle Rules ──
resource "aws_s3_bucket_lifecycle_configuration" "main" {
  bucket = aws_s3_bucket.main.id

  rule {
    id     = "transition-to-cheaper-storage"
    status = "Enabled"

    transition {
      days          = 30               # Move to IA after 30 days
      storage_class = "STANDARD_IA"
    }

    transition {
      days          = 90               # Move to Glacier after 90 days
      storage_class = "GLACIER"
    }

    noncurrent_version_expiration {
      noncurrent_days = 90             # Delete old versions after 90 days
    }
  }

  rule {
    id     = "abort-incomplete-uploads"
    status = "Enabled"

    abort_incomplete_multipart_upload {
      days_after_initiation = 7        # Clean up failed uploads after 7 days
    }
  }
}
# variables.tf

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "project_name" {
  description = "Project name used in resource naming"
  type        = string
}

variable "bucket_suffix" {
  description = "Unique suffix for the bucket name"
  type        = string
  default     = "assets"
}
# outputs.tf

output "bucket_id" {
  description = "The name of the bucket"
  value       = aws_s3_bucket.main.id
}

output "bucket_arn" {
  description = "The ARN of the bucket"
  value       = aws_s3_bucket.main.arn
}

output "bucket_domain_name" {
  description = "The domain name of the bucket"
  value       = aws_s3_bucket.main.bucket_domain_name
}

Key Options Explained

  • force_destroy = false — Prevents terraform destroy from deleting a bucket that still contains objects. Set to true only for ephemeral/dev buckets.
  • aws:kms encryption — Uses AWS KMS for server-side encryption, providing audit trails via CloudTrail. AES256 is simpler but has no key management.
  • bucket_key_enabled — Reduces the number of KMS API calls by using a bucket-level key, cutting KMS costs by up to 99%.
  • Public access block — All four flags set to true ensures no path to public access, even if a misconfigured policy is applied later.
  • Lifecycle transitions — Automatically moves data to cheaper storage classes as it ages: Standard to IA (30 days) to Glacier (90 days).
  • noncurrent_version_expiration — Cleans up old object versions created by versioning, preventing unbounded storage growth.

Common Modifications

  • Static website hosting: Add aws_s3_bucket_website_configuration with index_document and error_document for static site hosting.
  • CloudFront distribution: Pair with a CloudFront distribution using an Origin Access Control (OAC) for CDN delivery.
  • CORS: Add aws_s3_bucket_cors_configuration for cross-origin access from web applications.
  • Replication: Add aws_s3_bucket_replication_configuration for cross-region disaster recovery.
  • Notifications: Add aws_s3_bucket_notification to trigger Lambda functions or SQS queues on object events.