Module Mạng Thực Tế: VPC, Subnet, và EC2

K
Kai··5 min read

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.