on
Build your first Terraform module: a secure, reusable S3 bucket
Writing your first Terraform module is a great way to turn repeating infrastructure patterns into a single, well-scoped unit you can reuse, test, and version. For many teams, a secure S3 bucket is one of the first sensible modules: it’s small, useful across projects, and benefits from sane defaults (encryption, versioning, and public-access controls). This article walks through the design and minimal code for a beginner-friendly S3 module and explains the decisions that make it reusable and safe.
Why make a module for S3 buckets?
- Reuse: capture the same security defaults across many buckets so you don’t forget them.
- Encapsulation: callers provide only what must change (name, tags, retention), not the implementation details.
- Easier reviews & upgrades: a single module change can improve security everywhere.
HashiCorp documents a small set of module conventions (main.tf, variables.tf, outputs.tf, README) and tooling that expects a standard layout — following these conventions makes your module easier to adopt and publish. (developer.hashicorp.com)
What this module will enforce
This example module focuses on a conservative, production-friendly baseline:
- Block public access at the bucket level
- Enable bucket versioning
- Require server-side encryption (SSE‑KMS or SSE‑S3)
- Provide lifecycle rules for noncurrent versions / expired objects
- Expose a small set of inputs and outputs so the module is reusable
These defaults map to current AWS guidance for securing S3: block unintended public access and enforce encryption, versioning, and lifecycle controls for important data. (docs.aws.amazon.com)
Module layout (recommended)
Follow the standard structure:
- modules/s3-bucket/
- main.tf # resources
- variables.tf # inputs
- outputs.tf # outputs
- README.md # short usage explanation
HashiCorp’s tutorials and guidance describe this pattern and common module workflows (create locally, call from root module, iterate and publish). (developer.hashicorp.com)
Minimal example: code snippets
Below are compact, focused snippets that illustrate the core of the module. They intentionally leave room for extension (ACLs, logging, replication).
main.tf
resource "aws_s3_bucket" "this" {
bucket = var.bucket
tags = var.tags
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = var.versioning ? "Enabled" : "Suspended"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
dynamic "rule" {
for_each = var.kms_key_arn != "" ? [1] : []
content {
apply_server_side_encryption_by_default {
kms_master_key_id = var.kms_key_arn
sse_algorithm = "aws:kms"
}
}
}
# Fallback to SSE-S3 if no KMS key provided (provider resource may vary)
}
variables.tf
variable "bucket" {
type = string
description = "Name of the S3 bucket"
}
variable "tags" {
type = map(string)
default = {}
}
variable "versioning" {
type = bool
default = true
}
variable "kms_key_arn" {
type = string
default = ""
description = "Optional KMS key ARN to use for SSE-KMS. If empty, use SSE-S3."
}
outputs.tf
output "bucket_id" {
value = aws_s3_bucket.this.id
description = "The bucket name / id"
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
description = "ARN of the bucket"
}
This minimal module enforces block public access and versioning and supports optional SSE-KMS. A full production module would add lifecycle rules (to limit costs and keep only necessary historical versions), logging/metrics, and configurable policies for cross-account access.
Community-maintained modules (for example the widely used terraform-aws-modules/s3-bucket) expose many more options and are good references for advanced features like replication, object lock, or ACL controls. Looking at these mature modules helps you see how to evolve your module without re-inventing complex edge cases. (registry.terraform.io)
Design tips for a beginner-friendly module
- Keep scope narrow: one module per logical responsibility (e.g., “S3 bucket” vs “S3 bucket plus lifecycle + replication”). Narrow scope makes composition easier. (docs.hashicorp.com)
- Favor explicit inputs with sensible defaults: let callers override name, tags, retention days, KMS key, but default to secure choices (public access blocked, versioning on).
- Keep outputs minimal and stable: expose ARN and name; avoid leaking internal resource addresses that callers shouldn’t depend on.
- Document behavior and constraints in README: especially any non-obvious defaults (e.g., when KMS key is empty the module uses SSE‑S3).
- Add tests if possible: simple unit-style tests or integration tests (Terratest, kitchen-terraform) catch provider or API changes early.
Common pitfalls
- Bucket name uniqueness: S3 bucket names are global — either accept names provided by callers or generate names carefully (and document collisions). Many teams prefer callers to pass a name that includes account/region to avoid surprises.
- Changing encryption configuration: switching encryption types can cause object accessibility issues. Make changes deliberately and test for compatibility with consumers.
- State and imports: if you’re converting an existing bucket to be managed by Terraform, import the resource before applying to avoid destruction.
Where to look next
- HashiCorp’s module guidance and example tutorials explain structure and workflows for creating and iterating on modules. (developer.hashicorp.com)
- AWS S3 security and best-practice documentation covers blocking public access, encryption, and lifecycle recommendations in detail. (docs.aws.amazon.com)
- Review community modules for patterns and trade-offs (for example terraform-aws-modules/s3-bucket on the Terraform Registry). (registry.terraform.io)
Summary
A first Terraform module should be small, well-documented, and opinionated about safe defaults. The S3 bucket module above provides a compact example that enforces block public access, versioning, and encryption while remaining easy to extend. Following the standard structure (main.tf, variables.tf, outputs.tf, README) keeps the module approachable for others and compatible with Terraform tooling. (developer.hashicorp.com)