Skip to main content

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