Creating Local Modules
สร้าง module ของตัวเอง — สำหรับ pattern ที่ใช้ซ้ำในโปรเจค
เมื่อไหร่ควรสร้าง Module?
Rule of 3:
- ครั้งที่ 1: เขียน inline (don't repeat yourself yet)
- ครั้งที่ 2: เริ่มเห็น pattern (copy-paste ได้)
- ครั้งที่ 3: extract เป็น module ⭐
อย่า premature abstract — module ก่อนเห็น pattern จริงจะ over-engineered
โครงสร้าง Module
Standard structure:
modules/web-app/
├── README.md # documentation
├── main.tf # main resources
├── variables.tf # inputs
├── outputs.tf # outputs
├── versions.tf # provider requirements
├── locals.tf # internal computations (optional)
└── examples/ # usage examples
└── basic/
└── main.tf
ตัวอย่าง: Web App Module
Step 1: ระบุ scope
"Module นี้สร้าง web application ที่ประกอบด้วย ALB + ASG + EC2"
Step 2: Define inputs
modules/web-app/variables.tf
variable "name" {
type = string
description = "Name prefix for resources"
}
variable "vpc_id" {
type = string
description = "VPC ID where resources will be deployed"
}
variable "subnet_ids" {
type = list(string)
description = "List of subnet IDs for ASG and ALB"
validation {
condition = length(var.subnet_ids) >= 2
error_message = "At least 2 subnets required for HA."
}
}
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.micro"
}
variable "min_size" {
type = number
description = "Minimum ASG size"
default = 1
}
variable "max_size" {
type = number
description = "Maximum ASG size"
default = 3
}
variable "ami_id" {
type = string
description = "AMI ID. If null, uses latest Amazon Linux 2."
default = null
}
variable "tags" {
type = map(string)
description = "Tags to apply to all resources"
default = {}
}
Step 3: Implement resources
modules/web-app/main.tf
locals {
name_prefix = var.name
common_tags = merge(var.tags, {
Module = "web-app"
ManagedBy = "terraform"
})
}
# AMI: use input or fetch latest Amazon Linux 2
data "aws_ami" "amazon_linux" {
count = var.ami_id == null ? 1 : 0
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
locals {
ami_id = var.ami_id != null ? var.ami_id : data.aws_ami.amazon_linux[0].id
}
# Security Group
resource "aws_security_group" "alb" {
name = "${local.name_prefix}-alb-sg"
description = "Allow HTTP/HTTPS from internet"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
resource "aws_security_group" "instance" {
name = "${local.name_prefix}-instance-sg"
description = "Allow traffic from ALB"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
# ALB
resource "aws_alb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.subnet_ids
tags = local.common_tags
}
resource "aws_alb_target_group" "main" {
name = "${local.name_prefix}-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
path = "/"
healthy_threshold = 2
unhealthy_threshold = 2
}
tags = local.common_tags
}
resource "aws_alb_listener" "http" {
load_balancer_arn = aws_alb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_alb_target_group.main.arn
}
}
# Launch Template
resource "aws_launch_template" "main" {
name_prefix = "${local.name_prefix}-"
image_id = local.ami_id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.instance.id]
user_data = base64encode(<<-EOF
#!/bin/bash
yum install -y httpd
systemctl start httpd
echo "Hello from $(hostname)" > /var/www/html/index.html
EOF
)
lifecycle {
create_before_destroy = true
}
tag_specifications {
resource_type = "instance"
tags = local.common_tags
}
}
# ASG
resource "aws_autoscaling_group" "main" {
name = "${local.name_prefix}-asg"
vpc_zone_identifier = var.subnet_ids
target_group_arns = [aws_alb_target_group.main.arn]
min_size = var.min_size
max_size = var.max_size
launch_template {
id = aws_launch_template.main.id
version = aws_launch_template.main.latest_version
}
dynamic "tag" {
for_each = local.common_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
Step 4: Define outputs
modules/web-app/outputs.tf
output "alb_dns_name" {
value = aws_alb.main.dns_name
description = "DNS name of the ALB"
}
output "alb_zone_id" {
value = aws_alb.main.zone_id
description = "Route53 zone ID of ALB"
}
output "alb_arn" {
value = aws_alb.main.arn
description = "ARN of the ALB"
}
output "alb_security_group_id" {
value = aws_security_group.alb.id
description = "ALB security group ID"
}
output "instance_security_group_id" {
value = aws_security_group.instance.id
description = "Instance security group ID"
}
output "asg_name" {
value = aws_autoscaling_group.main.name
description = "ASG name"
}
Step 5: Provider versions
modules/web-app/versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
Step 6: Documentation
modules/web-app/README.md
# Web App Module
Deploys a load-balanced web application with auto-scaling.
## Architecture
ALB → ASG → EC2 instances (Amazon Linux 2 + httpd)
## Usage
```hcl
module "web_app" {
source = "./modules/web-app"
name = "my-app"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnets
instance_type = "t3.small"
min_size = 2
max_size = 10
tags = {
Environment = "production"
}
}
output "url" {
value = "http://${module.web_app.alb_dns_name}"
}
```
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| name | Resource name prefix | string | - | yes |
| vpc_id | VPC ID | string | - | yes |
| subnet_ids | Subnet IDs (>= 2) | list(string) | - | yes |
| instance_type | EC2 type | string | t3.micro | no |
| min_size | ASG min | number | 1 | no |
| max_size | ASG max | number | 3 | no |
## Outputs
| Name | Description |
|------|-------------|
| alb_dns_name | ALB DNS for app URL |
| alb_zone_id | For Route53 alias |
Step 7: Example
modules/web-app/examples/basic/main.tf
provider "aws" {
region = "ap-southeast-1"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.13.0"
name = "example-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-southeast-1a", "ap-southeast-1b"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
}
module "web_app" {
source = "../../"
name = "example-app"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnets
}
output "url" {
value = "http://${module.web_app.alb_dns_name}"
}
ใช้ Module ใน Root
main.tf (root)
module "frontend" {
source = "./modules/web-app"
name = "${var.env}-frontend"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnets
instance_type = var.env == "prod" ? "t3.medium" : "t3.micro"
min_size = var.env == "prod" ? 3 : 1
max_size = var.env == "prod" ? 10 : 3
}
Module Composition
Module เรียก module อื่นได้:
modules/full-stack/main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
# ...
}
module "web" {
source = "../web-app"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnets
}
module "db" {
source = "../database"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
}
Provider ใน Module
ส่งผ่าน provider จาก root:
root/main.tf
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "tokyo"
region = "ap-northeast-1"
}
module "us_app" {
source = "./modules/web-app"
# ใช้ default provider
}
module "tokyo_app" {
source = "./modules/web-app"
providers = {
aws = aws.tokyo
}
}
modules/web-app/versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
configuration_aliases = [aws.replica] # ถ้าต้องการ
}
}
}
Testing Local Module
cd modules/web-app/examples/basic
terraform init
terraform plan
terraform apply
terraform destroy
หรือใช้ Terratest ดูใน Section 16: Testing
สรุป
- สร้าง module เมื่อเห็น pattern ใช้ซ้ำ ≥3 ครั้ง
- Standard structure:
main.tf,variables.tf,outputs.tf,versions.tf,README.md - Define inputs ครบ + validation
- Define outputs ที่ caller ต้องใช้
- Document ผ่าน README + examples folder
- Reuse ผ่าน
source = "./modules/<name>"
ต่อไป → Module Inputs/Outputs