Terraform
← Tổng quan series

Xây VPC AWS bằng Terraform module

Từ VPC viết tay đến module có input/output, hiểu subnet public/private, NAT Gateway và cách tái sử dụng cấu hình mạng.

Instructor walkthrough

Bài giảng: hiểu luồng trước khi chạy lệnh

Bài toán: Từ VPC viết tay đến module có input/output, hiểu subnet public/private, NAT Gateway và cách tái sử dụng cấu hình mạng.

01Chuẩn bịkiểm tra account, region, provider và chi phí
02Đọc planđối chiếu resource address, diff và dependency
03Thực hànhapply mutation nhỏ trong sandbox, lưu evidence
04Khôi phụcre-run, failure drill, destroy hoặc rollback
Command reference · baseline
terraform fmt
terraform init
terraform validate
terraform plan
terraform apply
terraform plan -destroy
terraform destroy

VPC gồm những mảnh nào?

Một mạng ứng dụng tối thiểu thường có VPC, subnet public để đặt load balancer, subnet private cho application/database, Internet Gateway cho public route và NAT Gateway để private subnet đi ra ngoài khi cần. NAT Gateway tính phí theo giờ và dữ liệu, nên đừng tạo trong account lab rồi quên xóa.

Thay vì copy một khối dài cho dev/staging/prod, ta đóng gói thành module.

Cấu trúc module

modules/vpc/
├── main.tf
├── variables.tf
└── outputs.tf

modules/vpc/variables.tf:

variable "name" { type = string }
variable "cidr" { type = string }
variable "azs"  { type = list(string) }

Root module gọi module:

module "network" {
  source = "./modules/vpc"

  name = "demo"
  cidr = "10.30.0.0/16"
  azs  = ["ap-southeast-1a", "ap-southeast-1b"]
}

Trong module, dùng for_each để tạo subnet theo AZ thay vì copy-paste:

resource "aws_vpc" "this" {
  cidr_block           = var.cidr
  enable_dns_hostnames = true
  tags = { Name = "${var.name}-vpc" }
}

resource "aws_subnet" "private" {
  for_each = toset(var.azs)

  vpc_id            = aws_vpc.this.id
  availability_zone = each.value
  cidr_block        = cidrsubnet(var.cidr, 8, index(var.azs, each.value) + 10)
  tags              = { Name = "${var.name}-private-${each.value}" }
}

Module nên trả output rõ ràng, không bắt root module “đi xuyên” vào resource nội bộ:

output "vpc_id" {
  value = aws_vpc.this.id
}

output "private_subnet_ids" {
  value = [for subnet in aws_subnet.private : subnet.id]
}

Module local hay Registry?

Module local phù hợp khi team đang thiết kế convention riêng. Module trên Terraform Registry giúp đi nhanh nhưng phải đọc version, input, output và changelog; đừng copy module vào production chỉ vì README có một đoạn code đẹp.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 6.0"
  # inputs...
}

Bài tập

Thêm biến enable_nat_gateway, output route table và viết một test plan cho hai AZ. Kiểm tra rằng resource ứng dụng chỉ nhận subnet ID từ output, không phụ thuộc vào tên resource nội bộ.

Các quyết định mạng cần ghi lại

Module VPC không nên chỉ trả “một đống subnet ID”. Hãy ghi rõ:

  • subnet nào dành cho load balancer, app và database;
  • route nào được phép ra Internet;
  • NAT Gateway đặt một hay mỗi AZ;
  • security group nào là ingress từ ALB, security group nào là egress tới database;
  • CIDR có chừa khoảng trống cho mở rộng và kết nối on-premise không.

Một thiết kế rẻ cho dev có thể dùng một NAT Gateway; production cần cân nhắc HA theo AZ và chi phí dữ liệu. Đây là trade-off phải xuất hiện trong README/module documentation, không nên ẩn trong một biến boolean khó hiểu.

Expected state và module acceptance gate

Chạy lab với enable_nat_gateway = false trước để tránh chi phí; dùng CIDR/AZ sandbox và tag Training = terraform-05. Expected state của plan phải có đúng một VPC, subnet theo AZ/key ổn định, route table/association/IGW theo design và không có NAT resource khi flag tắt. Khi bật NAT trong một plan riêng, ghi rõ số NAT, private route targets, estimated hourly/data cost và cleanup owner.

Thực hiện các failure drills trên plan hoặc sandbox:

  1. Truyền CIDR overlap, AZ trùng/không tồn tại và danh sách AZ rỗng; validation phải fail trước apply hoặc plan phải lộ lỗi rõ, không tạo mạng nửa chừng.
  2. Đổi thứ tự azs và kiểm tra for_each key/resource address không làm subnet đổi CIDR ngoài chủ ý; dùng key ổn định thay vì index nếu identity cần giữ.
  3. Bật/tắt NAT, đọc plan route/private egress, kiểm tra không có private subnet nào trỏ nhầm IGW và không có public subnet đi qua NAT ngoài design.
  4. Tạm đổi output/module interface, chạy terraform validate và consumer plan; root chỉ dùng output contract, không tham chiếu module.network.aws_subnet.private nội bộ.

Quality gate

Pass khi module có variable validation, output contract, README decision cho public/private/DB route, CIDR/AZ assumptions, NAT cost/HA trade-off và test plan. Plan phải chứng minh topology/route đúng; không apply NAT/Internet route trên account production chỉ để demo, không xóa VPC khi còn dependency và cleanup phải chạy plan -destroy có review.