Giới thiệu
Ở bài trước chúng ta đã nói về cách triển khai trang web với Terraform. Ở bài này chúng ta sẽ làm ví dụ tạo một Virtual Private Cloud (VPC) ở trên AWS, thông qua đó chúng ta sẽ tìm hiểu về cách tổ chức code một cách hiệu quả với Terraform Module.
Tạo Virtual Private Cloud
Hạ tầng mà ta sẽ xây dựng ở bài này như hình minh họa bên dưới.
Chúng ta sẽ nói qua từng resource mà Terraform dùng để tạo AWS VPC, mình cũng sẽ giải thích sơ về lý thuyết VPC trên AWS.
Tạo một file tên là main.tf
.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.45.0"
}
}
}
provider "aws" {
region = "us-west-2"
}
Chạy câu lệnh init
.
terraform init
Giờ ta sẽ nói qua từng resource của AWS.
Virtual Private Cloud
Virtual Private Cloud (VPC) hiểu đơn giản là một mạng kín, hình minh họa.
Mặc định mỗi Region của AWS sẽ có một VPC mặc định tên là default
. Để tạo một VPC mới, ta dùng resource aws_vpc
của Terraform.
...
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
"Name" = "custom"
}
}
Ở trên ta sẽ tạo một VPC mới với CIDR là 10.0.0.0/16
và tên là custom
. CIDR của VPC sẽ có các giá trị nằm trong khoảng sau:
- 10.0.0.0/16 -> 10.0.0.0/28
- 172.16.0.0/16 -> 172.16.0.0/28
- 192.168.0.0/16 -> 192.168.0.0/28
Subnet
Subnet sẽ chia VPC của ta ra thành nhiều mạng con nhỏ hơn. Mỗi Subnet sẽ nằm trong một Availability Zones (AZ). Và các AWS Service của ta sẽ được tạo ở trong Subnet này.
Ta dùng aws_subnet
của Terraform để tạo Subnet.
...
resource "aws_subnet" "private_subnet_2a" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-2a"
tags = {
"Name" = "private-subnet"
}
}
resource "aws_subnet" "private_subnet_2b" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "us-west-2b"
tags = {
"Name" = "private-subnet"
}
}
resource "aws_subnet" "private_subnet_2c" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.3.0/24"
availability_zone = "us-west-2c"
tags = {
"Name" = "private-subnet"
}
}
Ở đoạn code trên ta sẽ tạo 3 Subnet là 10.0.1.0/24
, 10.0.2.0/24
, 10.0.3.0/24
nằm trong các AZ a, b và c. Nếu ta cần nhiều Subnet hơn thì ta có thể sao chép ra thêm một resource khác, nhưng như vậy sẽ khiến code của ta khá dài, ta có thể rút gọn code lại như sau.
...
locals {
private = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
zone = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
resource "aws_subnet" "private_subnet" {
count = length(local.private)
vpc_id = aws_vpc.vpc.id
cidr_block = local.private[count.index]
availability_zone = local.zone[count.index % length(local.zone)]
tags = {
"Name" = "private-subnet"
}
}
Ta sẽ thêm 3 Subnet nữa là 10.0.4.0/24
, 10.0.5.0/24
, 10.0.6.0/24
(vì sao tên gọi của các Subnet này là Public hay là Private mình sẽ giải thích ở dưới).
...
locals {
private = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
zone = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
resource "aws_subnet" "private_subnet" {
count = length(local.private)
vpc_id = aws_vpc.vpc.id
cidr_block = local.private[count.index]
availability_zone = local.zone[count.index % length(local.zone)]
tags = {
"Name" = "private-subnet"
}
}
resource "aws_subnet" "public_subnet" {
count = length(local.public)
vpc_id = aws_vpc.vpc.id
cidr_block = local.public[count.index]
availability_zone = local.zone[count.index % length(local.zone)]
tags = {
"Name" = "public-subnet"
}
}
Bây giờ khi AWS Service của ta được tạo bên trong các Subnet này thì chúng có thể nói chuyện với được nhau. Nhưng nếu các AWS Service này muốn nói chuyện với các thằng khác ở bên ngoài Internet thì sẽ không được và ngược lại.
Vì ta chưa có thằng nào đóng vai trò làm Router để các AWS Service của ta có thể giao tiếp được với Internet.
Internet gateway
Để các AWS Service bên trong Subnet có thể giao tiếp được với Internet, ta cần một thằng tên là Internet Gateway (IG). Và ta sẽ gán thằng IG này vào thằng Route Table. Sau đó ta gán Route Table này vào Subnet nào mà ta muốn nó có giao tiếp được với Internet bên ngoài.
Từ đây ta mới có khái niệm là Public Subnet và Private Subnet.
Public Subnet là Subnet mà các Service bên trong nó có thể tương tác với Internet bên ngoài và ngược lại thông qua IG.
Còn đối với Private Subnet thì các Service bên trong nó có thể tương tác được với bên ngoài, nhưng theo chiều ngược lại thì không. Ta dùng resource aws_internet_gateway
để tạo IG.
...
resource "aws_internet_gateway" "ig" {
vpc_id = aws_vpc.vpc.id
tags = {
"Name" = "custom"
}
}
Gán nó vào Route Table.
...
resource "aws_internet_gateway" "ig" {
vpc_id = aws_vpc.vpc.id
tags = {
"Name" = "custom"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.ig.id
}
tags = {
"Name" = "public"
}
}
Gán Route Table cho các Subnet.
...
resource "aws_route_table_association" "public_association" {
for_each = { for k, v in aws_subnet.public_subnet : k => v }
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
Bây giờ các Service của ta bên trong Public Subnet đã có thể tương tác được với bên ngoài, vậy còn các Private Subnet thì sao?
Hiện tại thì các Service trong Private Subnet không thể giao tiếp được với Internet, nhưng ta không gán IG vào Private Subnet được, vì IG là hai chiều ra vào, trong khi ta chỉ muốn 1 chiều là chiều tương tác từ bên trong Private Subnet ra bên ngoài và chiều ngược lại thì không.
NAT gateway
Đây là thằng giúp ta làm việc đó, ta sẽ triển khai NAT lên trên một Public Subnet và gán nó vào Route Table, rồi gán Route Table đó vào các Private Subnet.
Ta dùng resource aws_nat_gateway
của Terraform để tạo NAT.
...
resource "aws_eip" "nat" {
vpc = true
}
resource "aws_nat_gateway" "public" {
depends_on = [aws_internet_gateway.ig]
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_subnet[0].id
tags = {
Name = "Public NAT"
}
}
Tạo Private Route Table và gán NAT vào.
...
resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.public.id
}
tags = {
"Name" = "private"
}
}
Gán Route Table vào các Private Subnet.
...
resource "aws_route_table_association" "public_private" {
for_each = { for k, v in aws_subnet.private_subnet : k => v }
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
Toàn bộ code.
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
"Name" = "custom"
}
}
locals {
private = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
zone = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
resource "aws_subnet" "private_subnet" {
count = length(local.private)
vpc_id = aws_vpc.vpc.id
cidr_block = local.private[count.index]
availability_zone = local.zone[count.index % length(local.zone)]
tags = {
"Name" = "private-subnet"
}
}
resource "aws_subnet" "public_subnet" {
count = length(local.public)
vpc_id = aws_vpc.vpc.id
cidr_block = local.public[count.index]
availability_zone = local.zone[count.index % length(local.zone)]
tags = {
"Name" = "public-subnet"
}
}
resource "aws_internet_gateway" "ig" {
vpc_id = aws_vpc.vpc.id
tags = {
"Name" = "custom"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.ig.id
}
tags = {
"Name" = "public"
}
}
resource "aws_route_table_association" "public_association" {
for_each = { for k, v in aws_subnet.public_subnet : k => v }
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
resource "aws_eip" "nat" {
vpc = true
}
resource "aws_nat_gateway" "public" {
depends_on = [aws_internet_gateway.ig]
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_subnet[0].id
tags = {
Name = "Public NAT"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.public.id
}
tags = {
"Name" = "private"
}
}
resource "aws_route_table_association" "public_private" {
for_each = { for k, v in aws_subnet.private_subnet : k => v }
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
Vậy là ta đã viết code xong, tiếp theo ta chạy câu lệnh apply
để tạo hạ tầng 😁.
terraform apply -auto-approve
...
Plan: 18 to add, 0 to change, 0 to destroy.
...
Apply complete! Resources: 18 added, 0 changed, 0 destroyed.
Như ta thấy thì sử dụng Terraform để tạo VPC khá đơn giản. Nhưng mỗi lần ta muốn tạo một VPC khác thì ta phải sao chép đống code này qua chỗ khác sao? Câu trả lời là không!
Để giải quyết vấn đề lập code thì Terraform cung cấp cho ta một tính năng là Module, giúp ta tổ chức code thành Module và có thể sử dụng lại ở nhiều lần.
Các bạn nhớ destroy resource.
terraform destroy -auto-approve
Tới đây thì các bạn có thể thư giản và bookmark
bài này lại để tiếp tục sau, vì phần tiếp theo cũng khá nhiều 😁.
Terraform Module
Terraform Module là một tính năng của Terraform cho phép ta tổ chức code lại một chỗ và sử dụng ở nhiều chỗ khác nhau.
Khi nói về Module ta có thể nghĩ nó như là một phần nhỏ trong một bức tranh lớn, ta sẽ ghép nhiều phần nhỏ này lại với nhau để ra được bức tranh cuối cùng, như trò chơi LEGO.
Cấu trúc của một Module
Một Module cơ bản sẽ gồm 3 tệp tin sau đây:
main.tf
chứa codevariables.tf
chứa giá trị đầu vào của Moduleoutputs.tf
chưa giá trị đầu ra của Module
Ngoài ra còn một vài tệp tin khác mà không bắt buộc là providers.tf
, versions.tf
các bạn xem toàn bộ cấu trúc ở đây Standard Module Structure.
Sử dụng Module
Để sử dụng module, ta dùng resource tên là module
.
module <module_name> {
source = <source>
version = <version>
input_one = <input_one>
input_two = <input_two>
}
<source>
có thể là dường dẫn dưới máy của ta hoặc một đường dẫn trên mạng, <version>
chỉ định phiên bản của Module, <input_one>
là các giá trị đầu vào ta định nghĩa trong tệp tin variables.tf
.
Viết Module
Bây giờ ta sẽ cấu trúc lại code của ta ở trên thành Module. Trước khi viết Module ta cần phải định nghĩa trước những giá trị động ở trong Module, để khi ta sử dụng Module ta sẽ truyền giá trị đó vào để có được các resource khác nhau.
Ví dụ ở trên thì các giá trị động mà ta cần truyền vào Module Vpc của ta là:
- vpc_cidr_block
- subnet_cidr_block và zone.
Ta tạo thư mục với cấu trúc như sau.
.
├── main.tf
└── vpc
├── main.tf
├── outputs.tf
└── variables.tf
Ta định nghĩa giá trị đầu vào của Module ở trong tệp tin variables.tf
.
variable "vpc_cidr_block" {
type = string
default = "10.0.0.0/16"
}
variable "private_subnet" {
type = list(string)
}
variable "public_subnet" {
type = list(string)
}
variable "availability_zone" {
type = list(string)
}
Cập nhật code cho tệp tin main.tf
ở trong VPC.
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr_block
enable_dns_hostnames = true
tags = {
"Name" = "custom"
}
}
resource "aws_subnet" "private_subnet" {
count = length(var.private_subnet)
vpc_id = aws_vpc.vpc.id
cidr_block = var.private_subnet[count.index]
availability_zone = var.availability_zone[count.index % length(var.availability_zone)]
tags = {
"Name" = "private-subnet"
}
}
resource "aws_subnet" "public_subnet" {
count = length(var.public_subnet)
vpc_id = aws_vpc.vpc.id
cidr_block = var.public_subnet[count.index]
availability_zone = var.availability_zone[count.index % length(var.availability_zone)]
tags = {
"Name" = "public-subnet"
}
}
resource "aws_internet_gateway" "ig" {
vpc_id = aws_vpc.vpc.id
tags = {
"Name" = "custom"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.ig.id
}
tags = {
"Name" = "public"
}
}
resource "aws_route_table_association" "public_association" {
for_each = { for k, v in aws_subnet.public_subnet : k => v }
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}
resource "aws_eip" "nat" {
vpc = true
}
resource "aws_nat_gateway" "public" {
depends_on = [aws_internet_gateway.ig]
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_subnet[0].id
tags = {
Name = "Public NAT"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.public.id
}
tags = {
"Name" = "private"
}
}
resource "aws_route_table_association" "public_private" {
for_each = { for k, v in aws_subnet.private_subnet : k => v }
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}
Bây giờ khi ta xài Module VPC này ta chỉ cần truyền vào giá trị đầu vô khác ta sẽ có được VPC khác nhau. Ở trong tệp tin main.tf
ngoài cùng ta sử dụng Module như sau.
...
provider "aws" {
region = "us-west-2"
}
module "vpc" {
source = "./vpc"
vpc_cidr_block = "10.0.0.0/16"
private_subnet = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnet = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
availability_zone = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
Code của ta khi xài Module gọn hơn rất nhiều, ta chạy thử câu lệnh plan
để xem Module của ta có viết đúng hay không.
terraform plan
...
Plan: 18 to add, 0 to change, 0 to destroy.
...
Nếu nó in ra được dòng ở trên thì Module của ta đã viết đúng, các bạn có thể chạy apply
để xem.
Đẩy Module lên mạng
Tiếp theo ta sẽ đẩy Module của ta lên trên mạng để mọi người có thể sử dụng, để tạo Module thì ta cần phải có tài khoản Github và truy cập trang https://registry.terraform.io
Đăng nhập Github và tạo một Repository ở trạng thái Public, tên phải ở dạng định terraform-<PROVIDER>-<NAME>
, sau đó sao chép 3 tệp tin ở thưc mục vpc và đẩy lên Github Repository đó, ví dụ mình tạo một Repository tên là terraform-aws-vpc
.
Sau đó ta cần tạo Tag cho Repository này, Tag này sẽ tương ứng với phiên bản của Module.
Sau đó truy cập trang Registry ở trên. Khi bạn đăng nhập vào xong thì nó sẽ có Menu Publish, ta nhấn vào và chọn Module.
Sau đó nó sẽ dẫn ta qua trang chọn Module để Publish, chọn VPC.
Sau đó nhấn Publish Module thì ta sẽ thấy Module của ta.
Phía bên phải có hướng dẫn để sử dụng module này. Giờ nếu ta muốn tạo VPC thì ta sẽ sử dụng module như sau.
module "vpc" {
source = "hoalongnatsu/vpc/aws"
version = "1.0.0"
vpc_cidr_block = "10.0.0.0/16"
private_subnet = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnet = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
availability_zone = ["us-west-2a", "us-west-2b", "us-west-2c"]
}
Các Module phổ biến
Ở trên ta viết với mục đích là tìm hiểu, còn khi làm thực tế cho môi trường Production, ta nên xài những Module có sẵn trên mạng vì họ viết sẽ kĩ hơn ta nhiều và có rất nhiều trường hợp hơn so với ta phải tự viết.
Ví dụ VPC ở trên ta có thể sử dụng một Module có sẵn là terraform-aws-modules/vpc/aws.
Họ cung cấp cho ta rất nhiều trường hợp. Ví dụ tạo một VPC cho AWS Kubernetes dùng Module có sẵn.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 3.0"
name = var.cluster_name
cidr = "10.0.0.0/16"
azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
database_subnets = ["10.0.7.0/24", "10.0.8.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
one_nat_gateway_per_az = false
enable_dns_hostnames = true
// Create db subnet group and enable public access to RDS instances
create_database_subnet_group = true
create_database_subnet_route_table = true
create_database_internet_gateway_route = true
public_subnet_tags = {
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
"kubernetes.io/role/elb" = 1
}
private_subnet_tags = {
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
"kubernetes.io/role/internal-elb" = 1
}
tags = local.tags
}
Với nhiều trường hợp như trên mà ta tự code thì cũng tối mày tối mặt, chưa kể phải kiểm tra rồi làm hằng bà lằng thứ nữa rất mất thời gian 😂. Nên trước khi ta làm gì thì lên mạng kiếm coi có ai viết Module đó chưa nhé, sẽ giúp ta tiết kiệm rất nhiều thời gian.
Github repo của toàn bộ series Terraform Series.
Kết luận
Vậy là ta đã tìm hiểu xong cách viết code từ đầu và sau đó tổ chức code lại thành Module, cách Publish một Module lên trên mạng, cách sử dụng Module có sẵn. Module giúp ta sử dụng code có sẵn và tránh phải viết code đi code lại nhiều lần. Bài tiếp theo ta cũng nói tiếp về Module và sẽ đi sâu hơn thông qua ví dụ tạo VPC, Autoscaling Group và Load Balancer trên AWS.
Tác giả @Quân Huỳnh
Nếu bài viết có gì sai hoặc cần cập nhật thì liên hệ Admin.
Tham gia nhóm chat của DevOps VN tại Telegram.
Kém tiếng Anh và cần nâng cao trình độ giao tiếp: Tại sao bạn học không hiệu quả?