TerraformPilot

AWS

How to Create an S3 Bucket with Terraform - Step by Step

Create a production-ready AWS S3 bucket with Terraform. Covers versioning, encryption, lifecycle rules, bucket policies, CORS, and static website hosting.

LLuca Berton1 min read

Quick Answer

#

Create an S3 bucket with aws_s3_bucket, then add versioning, encryption, and public access blocks as separate resources. Terraform 4.x split S3 configuration into individual resources for better modularity.

Prerequisites

#
  • Terraform 1.5+ installed
  • AWS CLI configured (aws configure)
  • An AWS account with S3 permissions

Basic S3 Bucket

#
provider "aws" {
  region = "us-east-1"
}
 
resource "aws_s3_bucket" "main" {
  bucket = "myapp-data-${var.environment}"
 
  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Production-Ready Configuration

#

Versioning

#
resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id
  versioning_configuration {
    status = "Enabled"
  }
}

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"
    }
    bucket_key_enabled = true
  }
}

Block Public Access

#
resource "aws_s3_bucket_public_access_block" "main" {
  bucket                  = aws_s3_bucket.main.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Lifecycle Rules

#
resource "aws_s3_bucket_lifecycle_configuration" "main" {
  bucket = aws_s3_bucket.main.id
 
  rule {
    id     = "archive-old-objects"
    status = "Enabled"
 
    transition {
      days          = 90
      storage_class = "STANDARD_IA"
    }
 
    transition {
      days          = 180
      storage_class = "GLACIER"
    }
 
    expiration {
      days = 365
    }
 
    noncurrent_version_expiration {
      noncurrent_days = 30
    }
  }
}

Bucket Policy

#
data "aws_iam_policy_document" "enforce_ssl" {
  statement {
    sid     = "EnforceSSL"
    effect  = "Deny"
    actions = ["s3:*"]
    resources = [
      aws_s3_bucket.main.arn,
      "${aws_s3_bucket.main.arn}/*"
    ]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
    condition {
      test     = "Bool"
      variable = "aws:SecureTransport"
      values   = ["false"]
    }
  }
}
 
resource "aws_s3_bucket_policy" "main" {
  bucket = aws_s3_bucket.main.id
  policy = data.aws_iam_policy_document.enforce_ssl.json
}

Complete Production Module

#
# variables.tf
variable "bucket_name" { type = string }
variable "environment" { type = string }
 
# main.tf
resource "aws_s3_bucket" "main" {
  bucket = var.bucket_name
  tags   = { Environment = var.environment, ManagedBy = "terraform" }
}
 
resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id
  versioning_configuration { status = "Enabled" }
}
 
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" }
    bucket_key_enabled = true
  }
}
 
resource "aws_s3_bucket_public_access_block" "main" {
  bucket                  = aws_s3_bucket.main.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
 
# outputs.tf
output "bucket_arn" { value = aws_s3_bucket.main.arn }
output "bucket_name" { value = aws_s3_bucket.main.id }
output "bucket_domain" { value = aws_s3_bucket.main.bucket_regional_domain_name }

Static Website Hosting

#
resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.main.id
 
  index_document { suffix = "index.html" }
  error_document { key = "error.html" }
}

Common Mistakes

#
MistakeFix
Inline versioning in aws_s3_bucketUse separate aws_s3_bucket_versioning resource
Bucket name with underscoresUse hyphens — underscores break DNS
Missing public access blockAlways add — blocks accidental public exposure
Hardcoded bucket nameAdd environment suffix for uniqueness
#

Conclusion

#

A production-ready S3 bucket needs versioning, encryption, public access blocks, and lifecycle rules — all as separate Terraform resources. Use bucket policies to enforce SSL and restrict access. Always use unique bucket names with environment prefixes.

#Terraform#AWS#Infrastructure as Code#AWS S3

Share this article