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)
- Create GitHub repo:
terraform-<provider>-<name>(e.g.,terraform-aws-vpc) - Tag with semantic version:
v1.0.0 - Sign in to registry.terraform.io with GitHub
- Publish → Select repository
- Tags become module versions automatically
Private Registry (Terraform Cloud)
- Sign in to TFC
- Settings → Modules → Publish
- 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