Hướng dẫn xây dựng AWS VPC cơ bản bằng Terraform

Bài viết này sẽ hướng dẫn bạn từng bước xây dựng một AWS VPC cơ bản với Public/Private Subnet, Internet Gateway và Route Tables bằng Terraform.

1. Kiến trúc VPC mục tiêu

Trước khi bắt tay vào viết Terraform, chúng ta cần hình dung rõ kiến trúc mạng sẽ tạo ra. Trong ví dụ này, mục tiêu là xây dựng một VPC cơ bản tại region ap-southeast-1 (Singapore) với các thành phần sau:

  • 01 VPC với CIDR block 10.0.0.0/16
  • 01 Internet Gateway để cho phép các subnet public truy cập Internet
  • 03 Public Subnets (mỗi AZ một subnet)
  • 03 Private Subnets (mỗi AZ một subnet)
  • 02 Route Tables:
    • Public Route Table (liên kết với Internet Gateway)
    • Private Route Table (chỉ dùng nội bộ, không ra Internet trực tiếp)

Kiến trúc VPC

2. Kiến trúc thư mục Terraform

Để quản lý hạ tầng theo cách có tổ chức, chúng ta chia thành hai phần:

VPC/
│── environments/
│   └── prod/
│       └── prod.tfvars
│
│── modules/
│   └── vpc/
│       ├── locals.tf
│       ├── main.tf
│       ├── variables.tf
│
│── backend.tf
│── main.tf
│── provider.tf
│── variables.tf
  • environments/prod/prod.tfvars: chứa thông tin riêng của môi trường (region, CIDR, account ID, role ARN…).
  • modules/vpc/: module VPC tái sử dụng, định nghĩa VPC, subnet, route table, gateway.
  • main.tf: gọi module VPC và truyền biến.
  • provider.tf: cấu hình AWS provider, assume role nếu cần.
  • backend.tf: định nghĩa version Terraform và provider.

3. Các bước triển khai với Terraform

a. File environments/prod/prod.tfvars

### Region and Environment Configuration
region = "ap-southeast-1"
environment = "prod"
use_name_prefix = false

### Role_arn and allowed_account_ids Configuration
role_arn            = "arn:aws:iam::891612588944:role/admin"
allowed_account_ids = ["891612588944"]

### VPC CIDR Block Configuration
vpc_cidr_block = "10.0.0.0/16"

public_subnets_cidr = [
  "10.0.0.0/27",
  "10.0.0.32/27",
  "10.0.0.64/27"
]

private_subnets_cidr = [
  "10.0.0.96/27",
  "10.0.0.128/27",
  "10.0.0.160/27"
]

b. Module modules/vpc/locals.tf

Định nghĩa 3 AZ tương ứng cho Singapore (ap-southeast-1a, 1b, 1c).

locals {
  availability_zones = ["${var.region}a", "${var.region}b", "${var.region}c"]
}

c. Module modules/vpc/main.tf

# Define VPC
resource "aws_vpc" "vpc" {
  cidr_block           = var.vpc_cidr_block # Source CIDR block for the VPC 10.0.0.0/16
  enable_dns_hostnames = true               # Enable DNS hostnames and assigned to instances
  enable_dns_support   = true               # Enable DNS support for the VPC
  tags = {
    Name = "vpc" # Name of the VPC
  }
}


#Public Subnets
resource "aws_subnet" "public_subnets" {
  count = length(var.public_subnets_cidr) # Identify the number of public subnets based on the provided CIDR blocks 10.0.0.0/27, 10.0.0.32/27, 10.0.0.64/27
  vpc_id = aws_vpc.vpc.id
  cidr_block = var.public_subnets_cidr[count.index] # Use the CIDR block for each public subnet from the provided list
  # Use availability zone for each public subnet to identify AZ
  availability_zone = element(local.availability_zones, count.index)
  map_public_ip_on_launch = true # Auto assign public IPs to instances launched in this subnet

  tags = {
    Name = "subnet-public-${count.index + 1}-${element(local.availability_zones, count.index)}"
  }
}


#Private Subnets
resource "aws_subnet" "private_subnets" {
  count = length(var.private_subnets_cidr)
  vpc_id = aws_vpc.vpc.id
  cidr_block = var.private_subnets_cidr[count.index]
  availability_zone = element(local.availability_zones, count.index)

  tags = {
    Name = "subnet-private-${count.index + 1}-${element(local.availability_zones, count.index)}"
  }
  
}


#Internet Gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "igw"
  }
}

#Route Table for Public Subnets
resource "aws_route_table" "public_route_table" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "public-route-table"
  }
}


# Route for Internet Gateway
resource "aws_route" "public_internet_gateway" {
  route_table_id         = aws_route_table.public_route_table.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

# Route table associations for both Public
resource "aws_route_table_association" "public_subnet_association" {
  count          = length(aws_subnet.public_subnets) # Create associations for each public subnet
  subnet_id      = aws_subnet.public_subnets[count.index].id # Use the subnet ID for each public subnet
  route_table_id = aws_route_table.public_route_table.id # Use the public route table for associations
}

# Route Table for Private Subnets
resource "aws_route_table" "private_route_table" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name = "private-route-table"
  }
}

# Route table associations for Private Subnets
resource "aws_route_table_association" "private_subnet_association" {
  count          = length(aws_subnet.private_subnets)
  subnet_id      = aws_subnet.private_subnets[count.index].id
  route_table_id = aws_route_table.private_route_table.id
}

d. Module modules/vpc/variables.tf

variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
}

variable "public_subnets_cidr" {
  type        = list(any)
  description = "CIDR block for Public Subnet"
}

variable "private_subnets_cidr" {
  type        = list(any)
  description = "CIDR block for Private Subnet"
}

variable "region" {
  description = "AWS region to deploy resources"
  type        = string
}

e. Root files

backend.tf

terraform {
#   backend "s3" {

#   }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  required_version = ">= 1.0.0"
}

main.tf

module "vpc" {
  source         = "./modules/vpc"
  vpc_cidr_block = var.vpc_cidr_block
  public_subnets_cidr = var.public_subnets_cidr
  private_subnets_cidr = var.private_subnets_cidr
  region = var.region
}

provider.tf

provider "aws" {
  region = var.region

  assume_role {
    role_arn = var.role_arn
  }
  allowed_account_ids = var.allowed_account_ids
}


provider "aws" {
  region = var.region
  alias = "platform_account"
}

variables.tf (root)

# Variable Provider

variable "region" {
  description = "The AWS region where the VPC will be created."
  type        = string
}

variable "environment" {
  description = "Environment name (e.g., dev, prod, staging)"
  type        = string
}

variable "use_name_prefix" {
  description = "Use name prefix for security groups"
  type        = bool
  default     = false
}

variable "role_arn" {
  description = "Role ARN to assume for cross-account access"
  type        = string
}

variable "allowed_account_ids" {
  description = "List of allowed account IDs for cross-account access"
  type        = list(string)
}

#VPC
variable "vpc_cidr_block" {
  description = "CIDR block for the VPC"
  type        = string
}

variable "public_subnets_cidr" {
  type        = list(any)
  description = "CIDR block for Public Subnet"
}

variable "private_subnets_cidr" {
  type        = list(any)
  description = "CIDR block for Private Subnet"
}

4. Triển khai Terraform

Chạy các bước sau để deploy:

cd vpc
terraform init
mkdir prod
terraform plan -var-file="environments/prod/prod.tfvars" -no-color -out="prod/tf.plan"
terraform apply prod/tf.plan

terraform destroy -var-file="environments/prod/prod.tfvars" -auto-approve

5. Kết quả đạt được

Sau khi apply thành công, bạn sẽ có:

  • Một VPC 10.0.0.0/16 tại Singapore
  • 03 public subnets (mỗi subnet tại 1 AZ, có IP public auto-assign)
  • 03 private subnets (dùng cho backend DB, app server)
  • 01 Internet Gateway gắn với Public Route Table
  • 02 Route Tables: public và private

Nguyễn Tiến Trường

Mình viết về những điều nhỏ nhặt trong cuộc sống, Viết về câu chuyện những ngày không có em