Skip to main content

Module Best Practices

สรุป best practices สำหรับ design + maintain Terraform modules ใน production

Module Design Principles

1. Single Responsibility

1 module = 1 ความรับผิดชอบ

✅ Good:
modules/vpc/ # network only
modules/rds/ # database only
modules/web-app/ # web application

❌ Bad:
modules/everything/ # VPC + RDS + EC2 + S3 + IAM + ...

2. Stable Interface

Inputs/outputs ไม่ควรเปลี่ยนบ่อย — ทุก breaking change = major version bump

3. Composition over Inheritance

Module เล็ก + เอามาประกอบกัน ดีกว่า module ใหญ่เดียวที่ทำทุกอย่าง

# ✅ Composition
module "network" { source = "./modules/network" }
module "database" {
source = "./modules/database"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
}

# ❌ Monolithic
module "everything" {
source = "./modules/everything"
# 50+ variables
}

4. Sensible Defaults

Optional variables ควรมี default ที่ "production-safe" — user ไม่ต้องตั้งทุกอย่าง

variable "encryption_enabled" {
type = bool
default = true # ✅ secure by default
}

variable "backup_retention_days" {
type = number
default = 7 # ✅ production-safe default
}

Naming Conventions

Module Name

✅ Good: terraform-aws-vpc, modules/web-app, modules/eks
❌ Bad: tf-stuff, MyVPC, vpc_module

Pattern: terraform-<provider>-<purpose> (สำหรับ public) หรือ short name สำหรับ internal: vpc, web-app

Resource Names ใน Module

# ใช้ "this" หรือ "main" เมื่อมีตัวเดียว
resource "aws_vpc" "this" { ... }
resource "aws_vpc" "main" { ... }

# ใช้ descriptive name เมื่อมีหลายตัว
resource "aws_subnet" "public" { ... }
resource "aws_subnet" "private" { ... }
resource "aws_subnet" "database" { ... }

Variable / Output Names

✅ snake_case: vpc_id, instance_type, enable_dns
❌ camelCase: vpcId, instanceType
❌ kebab-case: vpc-id, instance-type

File Organization

Standard Files

modules/web-app/
├── README.md # docs (REQUIRED)
├── main.tf # primary resources
├── variables.tf # inputs
├── outputs.tf # outputs
├── versions.tf # provider requirements
├── locals.tf # internal computations (optional)
├── data.tf # data sources (optional)
└── examples/ # usage examples (RECOMMENDED)
├── basic/
├── advanced/
└── multi-region/

When Files Get Big

modules/eks-cluster/
├── main.tf # cluster
├── nodes.tf # node groups
├── irsa.tf # IAM roles for service accounts
├── networking.tf # VPC subnet associations
├── variables.tf
├── outputs.tf
└── versions.tf

แยก resources เป็นไฟล์ตาม theme — main.tf อย่ายาวเกิน 300 บรรทัด

Versioning

Semantic Versioning

  • Major (1.0.0 → 2.0.0) — breaking changes
  • Minor (1.0.0 → 1.1.0) — new features, backwards compatible
  • Patch (1.0.0 → 1.0.1) — bug fixes

Git Tag for Versions

git tag v1.2.0
git push --tags
module "vpc" {
source = "git::https://github.com/myorg/modules.git//vpc?ref=v1.2.0"
}

Breaking Changes

Document ใน CHANGELOG.md:

## [2.0.0] - 2026-05-07

### Breaking Changes
- Renamed variable `subnet_ids``subnets`
- Removed support for Terraform < 1.5
- Changed output `id``vpc_id` for clarity

### Migration Guide
See [UPGRADE.md](./UPGRADE.md) for migration from v1.x

Documentation

README.md (Required)

# Module Name

Brief description.

## Architecture

Diagram or text description.

## Usage

\`\`\`hcl
module "example" {
source = "..."
...
}
\`\`\`

## Requirements

| Name | Version |
|------|---------|
| terraform | >= 1.5 |
| aws | >= 5.0 |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|

## Outputs

| Name | Description |
|------|-------------|

## Examples

See [examples/](./examples/)

Auto-Generate with terraform-docs

brew install terraform-docs
terraform-docs markdown table --output-file README.md modules/web-app/

หรือ pre-commit:

.pre-commit-config.yaml
- repo: https://github.com/terraform-docs/terraform-docs
rev: v0.18.0
hooks:
- id: terraform-docs-system
args: [markdown, table, "--output-file", "README.md", "./modules"]

Examples Folder

modules/web-app/
└── examples/
├── basic/ # minimum config
│ ├── main.tf
│ └── README.md
├── advanced/ # all features
│ ├── main.tf
│ └── README.md
└── multi-region/ # complex use case
├── main.tf
└── README.md

แต่ละ example — ต้อง standalone (รัน terraform apply ได้เลย)

Testing

Test 1: terraform validate (ใน CI)

cd modules/web-app
terraform init -backend=false
terraform validate

Test 2: Plan against Examples

cd modules/web-app/examples/basic
terraform init
terraform plan

Test 3: Terratest (Integration)

test/web_app_test.go
package test

import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
)

func TestWebApp(t *testing.T) {
opts := &terraform.Options{
TerraformDir: "../examples/basic",
}

defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)

url := terraform.Output(t, opts, "url")
// assert URL works
}

ดูเพิ่มใน Section 16: Testing Modules

Common Anti-Patterns

❌ Module ที่ใหญ่เกินไป

modules/everything/  # 1000+ lines

→ แยกเป็น modules เล็กๆ

❌ Hard-coded Values

resource "aws_instance" "web" {
region = "us-east-1" # ❌ hard-code
}

→ ใช้ provider config + variable

❌ ลืม Outputs

# Module สร้าง resource — แต่ไม่ output อะไรเลย
# Caller ใช้งานต่อไม่ได้!

❌ ไม่มี Default

variable "some_optional_thing" {
type = bool
# ไม่มี default → user ต้องส่งทุก var เสมอ
}

❌ ใช้ count กับ for_each ปนกัน

resource "aws_instance" "web" {
count = 2
for_each = toset(["a", "b"]) # ❌ ใช้ทั้งคู่
}

❌ Module เรียกใช้ Provider โดยตรง

# modules/x/main.tf
provider "aws" {
region = "us-east-1" # ❌ ห้ามใน module — ใช้จาก root
}

Production Module Checklist

📋 Required:
- [ ] README.md with usage example
- [ ] versions.tf with provider constraints
- [ ] All variables have description
- [ ] All variables have type
- [ ] Sensitive variables marked sensitive
- [ ] All outputs have description
- [ ] Pinned to specific version

📋 Recommended:
- [ ] examples/ folder with 2+ examples
- [ ] CHANGELOG.md
- [ ] Auto-generated docs (terraform-docs)
- [ ] CI/CD: terraform validate, fmt check
- [ ] Integration tests (Terratest)
- [ ] Tagged releases (git tag)
- [ ] Security scan (Checkov, tfsec)

Module Registry Publishing

Public Registry (Terraform Registry)

  1. Create GitHub repo: terraform-<provider>-<name> (e.g., terraform-aws-vpc)
  2. Tag with semantic version: v1.0.0
  3. Sign in to registry.terraform.io with GitHub
  4. Publish → Select repository
  5. Tags become module versions automatically

Private Registry (Terraform Cloud)

  1. Sign in to TFC
  2. Settings → Modules → Publish
  3. Connect VCS

ตัวอย่าง: Mature Module Repo Structure

terraform-aws-web-app/
├── README.md
├── CHANGELOG.md
├── LICENSE
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── locals.tf
├── data.tf
├── .pre-commit-config.yaml
├── .github/
│ └── workflows/
│ ├── validate.yml
│ └── release.yml
├── examples/
│ ├── basic/
│ ├── advanced/
│ └── multi-region/
├── test/
│ └── web_app_test.go
└── docs/
└── architecture.md

สรุป

  • Single responsibility — 1 module = 1 หน้าที่
  • Stable interface — input/output เปลี่ยนน้อย, version with semver
  • Documentation — README + examples + CHANGELOG
  • Testing — validate, plan, integration test
  • Versioning — git tag, pin in caller
  • No hard-code — ใช้ variables
  • Provider config อยู่ใน root, ไม่ใช่ module

ต่อไป → Section 13: Provisioners