Writing your first Terraform module is like composing a short song: you pick a clear theme, structure the parts (verse, chorus), rehearse, then share. In Terraform-land the theme is a single, well-scoped piece of infrastructure; the structure is the module file layout; rehearsal is testing; and sharing is publishing a versioned module. In this walkthrough we’ll build a small, useful Terraform module that provisions an AWS S3 bucket (with encryption, versioning, lifecycle, and logging options), and show how to test it locally using Terraform’s mock providers so you can test without touching real cloud accounts.
Why this topic is timely
What you’ll get from this article
Module scope and design
Scaffold: recommended file layout Use a consistent structure so other people (and the Terraform Registry) can inspect and publish your module:
This standard layout helps automation and registry tooling parse inputs/outputs and examples. (developer.hashicorp.com)
Minimal module code (essential parts) Below are simplified examples — copy into files and adapt.
main.tf
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
acl = var.acl
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = var.enable_sse ? "AES256" : null
}
}
}
versioning {
enabled = var.enable_versioning
}
lifecycle_rule {
id = "auto-expire"
enabled = var.lifecycle_enabled
expiration {
days = var.lifecycle_days
}
prefix = var.lifecycle_prefix
}
tags = merge(var.default_tags, var.tags)
}
resource "aws_s3_bucket_logging" "log" {
count = var.enable_logging ? 1 : 0
bucket = aws_s3_bucket.this.id
target_bucket = var.logging_target_bucket
target_prefix = "${var.bucket_name}/"
}
variables.tf
variable "bucket_name" {
type = string
description = "Bucket name (must be globally unique)"
}
variable "acl" {
type = string
default = "private"
description = "ACL for the bucket"
validation {
condition = contains(["private","log-delivery-write","public-read"], var.acl)
error_message = "ACL must be one of private, log-delivery-write, or public-read"
}
}
variable "enable_sse" {
type = bool
default = true
}
variable "enable_versioning" {
type = bool
default = false
}
variable "enable_logging" {
type = bool
default = false
}
variable "logging_target_bucket" {
type = string
description = "Bucket to store access logs (if enable_logging)"
default = ""
}
variable "lifecycle_enabled" {
type = bool
default = false
}
variable "lifecycle_days" {
type = number
default = 365
}
variable "lifecycle_prefix" {
type = string
default = ""
}
variable "default_tags" {
type = map(string)
default = {}
}
variable "tags" {
type = map(string)
default = {}
}
outputs.tf
output "bucket_id" {
value = aws_s3_bucket.this.id
description = "The bucket ID (name)."
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
description = "The bucket ARN."
}
How someone consumes the module (example)
module "data_bucket" {
source = "./" # or "my-org/terraform-aws-s3-bucket/aws" from registry
bucket_name = "my-app-data-bucket-12345"
enable_versioning = true
tags = {
Team = "Platform"
Env = "staging"
}
}
Testing locally with Terraform mocks One of the most useful modern additions is mock providers in Terraform tests. With a mock provider you can run assertions against your module without real AWS credentials or resources — tests run fast and safely. This is great for module authors who want quick feedback and CI-level checks. (hashicorp.com)
Create a test file testing/bucket_test.tftest.hcl:
mock_provider "aws" {}
run "basic_bucket" {
module {
source = "../"
variables = {
bucket_name = "test-bucket"
enable_versioning = true
}
}
assert {
condition = aws_s3_bucket.this.versioning[0].enabled == true
error_message = "versioning should be enabled"
}
assert {
condition = aws_s3_bucket.this.server_side_encryption_configuration[0].rule[0].apply_server_side_encryption_by_default[0].sse_algorithm == "AES256"
error_message = "SSE should be AES256 by default"
}
}
Run the test:
Terraform will run the test with a mocked aws provider and evaluate your assertions. The mocks can be customized to return specific computed attributes if your logic depends on them. (developer.hashicorp.com)
Why test with mocks?
Best practices and hygiene
Versioning and publishing
Common pitfalls and how to avoid them
A short CI suggestion (example)
Final thoughts (a little musical metaphor) Think of your first Terraform module as a short demo track: simple, repeatable, and tested. Play it often, iterate on it, and keep it short enough that others can remix it. With Terraform’s mocking abilities you can rehearse without renting studio time — run fast tests, get confidence, and publish a clean, versioned module that other teams will enjoy using.
Helpful references
If you want, I can: