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

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

Standard module structure (quick)

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

Testing and validation tips

Summary checklist

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)