on
Writing your first Terraform module: a practical walkthrough
Modules are the building blocks of reusable Terraform — they let you package a set of resources, inputs, and outputs so other teams (or future-you) can consume them without repeating code. This guide walks through a minimal, practical first module: a small S3-like resource wrapper (conceptual) that demonstrates the standard structure, input validation, outputs, and a few maintainability touches you’ll want from the start.
Why modules matter
- Reuse: keep resource definitions in one place and share them across environments.
- Encapsulation: hide implementation details behind inputs and outputs.
- Maintainability: a well-structured module is easier to test, document, and upgrade.
Terraform’s docs define a standard module layout and tooling practices you should follow so registry tooling and other devs can understand and reuse your work. (developer.hashicorp.com)
What you’ll build
- Module name: myorg/s3-bucket
- Purpose: create a storage bucket with ACL, versioning toggle, and encrypted-by-default behavior.
- Deliverables: main.tf, variables.tf, outputs.tf, README.md, and an examples/ directory.
Standard module structure (quick)
- main.tf — resource and data definitions.
- variables.tf — all input variables with types, defaults, and validation.
- outputs.tf — the values consumers will rely on.
- README.md — short description, usage example, inputs/outputs summary.
- examples/ — one small example root module showing how to call your module.
Following this structure helps Terraform tooling and the Registry generate docs automatically. (developer.hashicorp.com)
Start with variables: types + validation Define clear variable types and use validation blocks to surface good errors early. The style guide encourages using validation rules beyond type checks — they make modules safer to use. (docs.hashicorp.com)
Example variables.tf
variable "name" {
type = string
description = "Bucket name (must be 3-63 chars, lowercase and dashes)."
validation {
condition = length(var.name) >= 3 && length(var.name) <= 63 && can(regex("^[-a-z0-9]+$", var.name))
error_message = "name must be 3-63 chars and contain only lowercase letters, numbers, and dashes."
}
}
variable "versioning_enabled" {
type = bool
default = false
description = "Enable object versioning"
}
Note: Recent Terraform releases improve variable validation capabilities; depending on your team’s Terraform version you may have additional features (for example, referencing other variables inside validation became supported in later releases). Check your installed Terraform version when using cross-variable checks. (support.hashicorp.com)
Put the resources in main.tf Keep the module focused: one logical responsibility (create a bucket). Avoid mixing provisioning for unrelated systems.
Example main.tf (AWS-style pseudocode)
resource "aws_s3_bucket" "this" {
bucket = var.name
acl = "private"
versioning {
enabled = var.versioning_enabled
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
tags = {
managed_by = "terraform"
}
}
Outputs: publish only what callers need Expose stable, useful outputs — resource IDs, ARNs, endpoints. Avoid leaking internal-only values.
Example outputs.tf
output "bucket_id" {
description = "ID of the bucket"
value = aws_s3_bucket.this.id
}
output "bucket_arn" {
description = "ARN of the bucket"
value = aws_s3_bucket.this.arn
}
README and examples Write a concise README showing a sample module call and minimal explanation. Include an examples/ directory with a tiny root module that calls your module with realistic variable values. Registry tooling and users expect examples to be runnable and copyable. (developer.hashicorp.com)
A short usage snippet for README
module "s3" {
source = "./" # or registry address when published
name = "my-app-bucket"
versioning_enabled = true
}
Provider and versioning guidance
- Pin providers and specify required Terraform versions where appropriate so users don’t get unexpected behavior. The style guide demonstrates pinning provider versions in the terraform block to help reproducibility. (docs.hashicorp.com)
- For modules intended for broad reuse, prefer publishing them in their own repository with semantic version tags (the Terraform Registry expects modules to be versioned). The module-creation guidance encourages clear boundaries and versioned repositories for nested modules. (docs.hashicorp.com)
Testing and validation tips
- Start with local
terraform initandterraform validate. - Use the examples directory as a smoke-test that a consumer can copy and run.
- Add CI to run
terraform fmt,terraform validate, and a plan against a disposable or mock backend before tagging releases.
Summary checklist
- Follow the standard file layout (main.tf, variables.tf, outputs.tf, README, examples). (developer.hashicorp.com)
- Use typed variables and validation blocks to enforce sensible inputs. (docs.hashicorp.com)
- Expose clear outputs and document them in README/examples. (developer.hashicorp.com)
- Pin providers / specify required Terraform version to prevent surprises. (docs.hashicorp.com)
- Version and publish modules from their own repo when sharing broadly. (docs.hashicorp.com)
If you follow this small pattern — organized files, clear inputs with validation, sensible outputs, and a runnable example — you’ll have a solid, reusable first module that’s easy to maintain and safe for others to adopt. (docs.hashicorp.com)