Module Mạng Thực Tế: VPC, Subnet, và EC2
Ba bài Part IV cho ta cách viết module, gọi module, và nối chúng lại. Giờ ráp tất cả thành thứ thật sự dùng được: một module mạng dựng VPC hoàn chỉnh, rồi đặt một EC2 ra Internet bên trong nó. Đây là module nền điển hình — gần như mọi dự án AWS đều bắt đầu bằng một mạng kiểu này, và nó kết hợp đủ những gì đã học: for_each theo khóa, hàm cidrsubnet, data source, và composition giữa module với resource ở root.
Mục tiêu
Viết được một module mạng tái dùng (VPC, subnet nhiều AZ, internet gateway, route table) và đặt một EC2 vào mạng đó, hiểu từng mảnh nối với nhau ra sao.
💰 Chi phí
VPC, subnet, internet gateway, route table đều miễn phí. EC2 t3.micro rất rẻ và thuộc diện free-tier ở nhiều tài khoản; dù sao ta cũng destroy ngay sau khi xem kết quả nên chi phí thực tế chỉ vài cent. Lưu ý: NAT Gateway (không dùng ở bài này) mới là thứ tốn tiền đáng kể.
Mạng AWS gồm những gì
Trước khi viết, cần rõ một mạng công khai tối thiểu gồm bốn mảnh và cách chúng nối nhau:
┌─────────────────────── VPC (10.0.0.0/16) ───────────────────────┐
│ │
│ ┌── subnet public AZ-a ──┐ ┌── subnet public AZ-b ──┐ │
│ │ 10.0.0.0/24 │ │ 10.0.1.0/24 │ │
│ │ [EC2] │ │ │ │
│ └───────────┬────────────┘ └───────────┬────────────┘ │
│ │ route table: 0.0.0.0/0 ──► IGW │
│ └───────────────┬───────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
└────────────────────┤ Internet Gateway ├──────────────────────────┘
└────────┬─────────┘
▼ Internet
VPC là không gian mạng riêng. Subnet chia VPC thành các dải nhỏ, mỗi subnet nằm trong một AZ. Internet gateway là cửa ra Internet của VPC. Route table nói cho subnet biết traffic đi đâu — ở đây "mọi địa chỉ ngoài (0.0.0.0/0) đi qua internet gateway", biến subnet thành công khai.
Module network
Đầu vào (variables.tf): tên, CIDR của VPC, và một map AZ → CIDR subnet:
variable "name" { type = string }
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "public_subnets" {
type = map(string)
description = "Map: AZ -> CIDR subnet công khai"
}
Phần ruột (main.tf) dựng bốn mảnh, dùng for_each để tạo một subnet và một route-table-association cho mỗi AZ trong map:
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = var.name }
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
}
resource "aws_subnet" "public" {
for_each = var.public_subnets
vpc_id = aws_vpc.this.id
cidr_block = each.value
availability_zone = each.key
map_public_ip_on_launch = true
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
Hai chỗ đáng dừng lại. Một là for_each = var.public_subnets dùng map AZ→CIDR: khóa là AZ (each.key), giá trị là CIDR (each.value) — đúng kiểu dùng for_each theo khóa ổn định mà bài 11 khuyến nghị. Hai là association dùng for_each = aws_subnet.public — lặp thẳng trên một resource khác. Tài liệu gọi đây là quan hệ một-một giữa hai tập object, và đó là lý do for_each mạnh hơn count: nối trực tiếp được.
Đầu ra (outputs.tf) trả về id VPC và danh sách subnet để root dùng:
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = [for s in aws_subnet.public : s.id]
}
Root: gọi module và đặt EC2 vào
Root sinh CIDR subnet bằng cidrsubnet (bài 3), lấy AZ và AMI từ data source (bài 10), rồi đặt EC2 vào subnet đầu tiên của module:
data "aws_availability_zones" "available" { state = "available" }
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
}
module "network" {
source = "./modules/network"
name = "tf-series-bai14"
vpc_cidr = "10.0.0.0/16"
public_subnets = {
(data.aws_availability_zones.available.names[0]) = cidrsubnet("10.0.0.0/16", 8, 0) # 10.0.0.0/24
(data.aws_availability_zones.available.names[1]) = cidrsubnet("10.0.0.0/16", 8, 1) # 10.0.1.0/24
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = module.network.public_subnet_ids[0]
vpc_security_group_ids = [aws_security_group.web.id]
tags = { Name = "tf-series-bai14-web" }
}
cidrsubnet("10.0.0.0/16", 8, 0) cắt VPC /16 thành các /24 rồi lấy cái thứ 0 (10.0.0.0/24), cái thứ 1 (10.0.1.0/24) — không gõ tay dải mạng. EC2 nhận subnet_id = module.network.public_subnet_ids[0], đây là composition giữa module và resource: output của module nuôi input của instance.
Chạy thử
$ terraform apply -auto-approve
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
$ terraform state list
data.aws_ami.al2023
data.aws_availability_zones.available
aws_instance.web
aws_security_group.web
module.network.aws_internet_gateway.this
module.network.aws_route_table.public
module.network.aws_route_table_association.public["ap-southeast-1a"]
module.network.aws_route_table_association.public["ap-southeast-1b"]
module.network.aws_subnet.public["ap-southeast-1a"]
module.network.aws_subnet.public["ap-southeast-1b"]
module.network.aws_vpc.this
Chín resource, trong đó các resource mạng mang tiền tố module.network. và subnet/association đánh khóa theo AZ (["ap-southeast-1a"]). Output cho thấy mạng hoạt động:
$ terraform output
instance_public_ip = "13.212.x.x"
subnet_ids = ["subnet-0e8186...", "subnet-08b0a6..."]
vpc_id = "vpc-0b2f5189..."
EC2 nhận được public IP vì nằm trong subnet có map_public_ip_on_launch và route ra internet gateway — đúng như sơ đồ đầu bài.
🧹 Dọn dẹp
$ terraform destroy -auto-approve
Destroy complete! Resources: 9 destroyed.
Terraform xóa theo thứ tự ngược đồ thị: EC2 trước, rồi association, subnet, route table, internet gateway, cuối cùng VPC — đúng quan hệ phụ thuộc bài 5 đã mổ.
Tổng kết
Một module mạng thực tế gói VPC, subnet nhiều AZ (qua for_each trên map AZ→CIDR), internet gateway và route table thành một khối tái dùng, trả ra id VPC và danh sách subnet. Root sinh CIDR bằng cidrsubnet, lấy AZ/AMI từ data source, gọi module rồi đặt EC2 vào subnet của nó qua module.network.public_subnet_ids[0]. Đây là mẫu nền mà các bài sau xây tiếp lên trên.
Part IV khép lại với module gọn gàng, nhưng tất cả vẫn chạy trong một state duy nhất cho một môi trường. Thực tế cần dev, staging, prod tách biệt. Part V mở màn bằng bài 15: hai cách tổ chức nhiều môi trường — workspace và bố cục thư mục — cùng ưu nhược của mỗi cách.