Write your first reusable Terraform module: a simple S3 static-website example

Writing your first Terraform module is like learning a new song: you start with a simple melody, learn the chords, then compose variations that fit different keys. For infrastructure, that “melody” is a small, well-scoped module you can reuse across projects. In this article we’ll pick a beginner-friendly, practical example — a reusable module that creates an S3 bucket configured to host a static website — and walk through the structure, the essential files, and the reasoning behind decisions beginners miss. HashiCorp’s tutorials use an S3 static-site example for exactly this purpose, so it’s a relevant place to start. (developer.hashicorp.com)

Why this module?

Module design principles (short list)

What your module will do

Directory layout A minimal, standard module layout:

This mirrors the recommended structure in Terraform docs: keep only Terraform files in the module root and include examples and docs for users. (cloud.google.com)

Key files and snippets

1) variables.tf — only what callers need Expose a small, opinionated surface:

variable "bucket_name" {
  type        = string
  description = "Name of the S3 bucket to create (must be globally unique)"
}

variable "enable_public_read" {
  type        = bool
  description = "Whether to add a public-read bucket policy (optional)"
  default     = false
}

variable "versioning" {
  type        = bool
  description = "Enable S3 versioning"
  default     = true
}

variable "index_document" {
  type        = string
  description = "Index document for website hosting"
  default     = "index.html"
}

Keep defaults sensible (e.g., versioning enabled) so callers don’t have to configure everything.

2) main.tf — the implementation Keep resources clear and annotated.

resource "aws_s3_bucket" "site" {
  bucket = var.bucket_name

  acl    = var.enable_public_read ? "public-read" : "private"

  website {
    index_document = var.index_document
    error_document = "error.html"
  }

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }

  versioning {
    enabled = var.versioning
  }
}

resource "aws_s3_bucket_policy" "public" {
  count  = var.enable_public_read ? 1 : 0
  bucket = aws_s3_bucket.site.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = "*"
      Action = "s3:GetObject"
      Resource = "${aws_s3_bucket.site.arn}/*"
    }]
  })
}

Notes:

3) outputs.tf — what callers will use

output "bucket_name" {
  value       = aws_s3_bucket.site.id
  description = "The name of the S3 bucket"
}

output "website_endpoint" {
  value       = aws_s3_bucket.site.website_endpoint
  description = "The S3 website endpoint to use in DNS or redirects"
}

4) README.md and examples Include a short example showing how to call the module from a root configuration. Good examples massively reduce friction for module consumers.

Example caller (modules referenced locally for the tutorial):

module "docs_site" {
  source = "../modules/s3-static-website"

  bucket_name        = "my-company-docs-example"
  enable_public_read = true
}

Module publishing and versioning When your module is ready to share, consider tagging releases and publishing to a registry (public or private). Declare required providers and Terraform version inside the module only when necessary for the module’s implementation; this helps with reproducibility and avoids surprises when modules get consumed. Terraform’s module and provider guidance explains how modules are distributed and versioned. (developer.hashicorp.com)

Testing, formatting, and CI Treat a module like any other piece of code:

Design trade-offs and tips

Common beginner mistakes

Analogies and mental models Think of the module like a small song arrangement: you decide the core melody (the S3 bucket), set consistent tempo and key (encryption, versioning), and allow players to choose the instrument (public vs private) via a few knobs (variables). When the arrangement is clean, you can drop it into any performance (project) and it sounds right.

References and further reading

Parting note Start small, document clearly, and keep the module focused. You’ll learn more about what to expose and what to hide as you reuse the module across projects — the same way a musician sharpens arrangements by playing them in different venues. With this S3 static-site module you get a compact, real-world example that teaches the essential module patterns without overwhelming complexity.