เขียน Module ของตัวเอง

📖 text • 12 นาที

เขียน Module ของตัวเอง

Module Structure

ทุก module ควรมี 3 ไฟล์:

modules/web-server/
├── main.tf          # Resource definitions
├── variables.tf     # Input variables
└── outputs.tf       # Output values

ตัวอย่าง: Web Server Module

variables.tf — Input

variable "name" {
  description = "Name prefix for resources"
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

variable "ami_id" {
  description = "AMI ID for the instance"
  type        = string
}

variable "vpc_id" {
  description = "VPC ID to deploy into"
  type        = string
}

variable "allowed_ports" {
  description = "List of ports to open"
  type        = list(number)
  default     = [80, 443]
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}

main.tf — Resources

resource "aws_security_group" "this" {
  name        = "${var.name}-sg"
  description = "Security group for ${var.name}"
  vpc_id      = var.vpc_id

  dynamic "ingress" {
    for_each = var.allowed_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      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 = merge(var.tags, {
    Name = "${var.name}-sg"
  })
}

resource "aws_instance" "this" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.this.id]

  tags = merge(var.tags, {
    Name = var.name
  })
}

outputs.tf — Output

output "instance_id" {
  description = "EC2 Instance ID"
  value       = aws_instance.this.id
}

output "public_ip" {
  description = "Public IP address"
  value       = aws_instance.this.public_ip
}

output "security_group_id" {
  description = "Security Group ID"
  value       = aws_security_group.this.id
}

Dynamic Blocks

สร้าง block ซ้ำๆ จาก list หรือ map:

# แทนที่จะเขียน ingress ซ้ำหลายรอบ
dynamic "ingress" {
  for_each = var.allowed_ports
  content {
    from_port   = ingress.value
    to_port     = ingress.value
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

เทียบกับเขียนมือ:

# ❌ ซ้ำซ้อน
ingress {
  from_port = 80
  to_port   = 80
  protocol  = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

ingress {
  from_port = 443
  to_port   = 443
  protocol  = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

Dynamic Block จาก Map

variable "ingress_rules" {
  type = map(object({
    port        = number
    cidr_blocks = list(string)
  }))
  default = {
    http = {
      port        = 80
      cidr_blocks = ["0.0.0.0/0"]
    }
    ssh = {
      port        = 22
      cidr_blocks = ["10.0.0.0/8"]
    }
  }
}

resource "aws_security_group" "this" {
  name   = "web-sg"
  vpc_id = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      description = ingress.key          # "http", "ssh"
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

เรียกใช้ Module

# root module — main.tf
module "api_server" {
  source = "./modules/web-server"

  name          = "api-server"
  instance_type = "t3.small"
  ami_id        = data.aws_ami.amazon_linux.id
  vpc_id        = data.aws_vpc.default.id
  allowed_ports = [80, 443, 8080]

  tags = {
    Environment = "prod"
    Team        = "backend"
  }
}

module "admin_server" {
  source = "./modules/web-server"

  name          = "admin-server"
  instance_type = "t2.micro"
  ami_id        = data.aws_ami.amazon_linux.id
  vpc_id        = data.aws_vpc.default.id
  allowed_ports = [80]

  tags = {
    Environment = "prod"
    Team        = "platform"
  }
}

# ใช้ output ของ module
output "api_ip" {
  value = module.api_server.public_ip
}

output "admin_ip" {
  value = module.admin_server.public_ip
}

Code เขียนครั้งเดียว ใช้ซ้ำได้เลย — แค่เปลี่ยน input

Module Best Practices

1. ตั้งชื่อ resource เป็น this

# ✅ ใน module ที่มี resource หลักตัวเดียว
resource "aws_instance" "this" { ... }

# อ้างอิง: module.web.public_ip (ไม่ต้องมี .this ซ้อน)

2. ใส่ description ทุก variable และ output

variable "name" {
  description = "Name prefix for all resources created by this module"
  type        = string
}

3. กำหนด type ทุก variable

# ❌ ไม่มี type — ใส่อะไรก็ได้
variable "port" {}

# ✅ มี type — Terraform validate ให้
variable "port" {
  type = number
}

4. ใช้ merge() สำหรับ tags

tags = merge(var.tags, {
  Name      = var.name
  ManagedBy = "terraform"
})

User ส่ง tags เพิ่มได้ + module เพิ่ม default tags ให้

5. อย่า hardcode provider ใน module

# ❌ Module ไม่ควรมี provider block
provider "aws" {
  region = "ap-southeast-1"
}

# ✅ Provider ควรอยู่ใน root module เท่านั้น

Module จะ inherit provider จาก root module

สรุป

Concept คำอธิบาย
variables.tf Input interface ของ module
main.tf Resource definitions
outputs.tf Return values ของ module
Dynamic block สร้าง block ซ้ำจาก list/map
merge() รวม tags จาก user + default
Resource naming ใช้ this สำหรับ resource หลัก

บทถัดไปเราจะเรียนรู้การใช้ module จาก Terraform Registry