Giới thiệu
Ở bài trước chúng ta đã tìm hiểu cơ bản về Terraform Module và cách sử dụng nó. Ở bài này chúng ta sẽ tìm hiểu sâu hơn về Module thông qua việc xây dựng hạ tầng cho một ứng dụng thực tế bao gồm AWS Application Load Balancer + Auto Scaling Group + Relational Database Service.
Với Auto Scaling Group được đùng để tạo ra một nhóm EC2 chạy Web Server ở port 80. Relational Database Service dùng để lưu trữ dữ liệu. Và người dùng sẽ truy cập tới ứng dụng của ta thông qua Load Balancer. Đây là một mô hình rất phổ biến ở trên AWS, minh họa như sau.
Ta sẽ có 3 thành phần chính trong mô hình trên là Networking, AutoScaling và Database. Từng thành phần chính sẽ được nhóm lại thành một Module như sau.
Ta sẽ viết Module cho Networking, AutoScaling và RDS. Tất cả các Module đều có quan hệ với nhau theo mô hình cây, mà thằng trên đầu được gọi là Root Module.
Root Module
Tất cả các Workspace đều có một thằng được gọi là Root Module. Ở trong Root Module đó, chúng ta có thể có một hoặc nhiều Module con. Module có thể là Local Module với code nằm ở dưới máy của ta hoặc Remote Moudle, Moudle mà được để trên mạng và ta tải xuống bằng câu lệnh terraform init
. Mô hình cây mối quan hệ giữ các Moudle.
Như ta thấy ở Moudle hình trên thì Networking, AutoScaling và RDS là Moudle con của Root Module. Và trong một Moudle nó có thể có chứa một hoặc nhiều Moudle khác, như Networking nó chứa VPC Moudle và SG (Security Group) Moudle, nếu một Moudle nằm trong một Moudle khác, ta gọi nó là Nested Moudle.
Viết code
Giờ ta sẽ tiến hành viết code, ta tạo thư mục như sau.
.
├── main.tf
└── modules
├── autoscaling
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── database
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── networking
├── main.tf
├── outputs.tf
└── variables.tf
Ở file main.tf
của Root, ta thêm vào đoạn code sau.
locals {
project = "terraform-series"
}
provider "aws" {
region = "us-west-2"
}
module "networking" {
source = "./modules/networking"
}
module "database" {
source = "./modules/database"
}
module "autoscaling" {
source = "./modules/autoscaling"
}
Networking Module
Đầu tiên ta sẽ viết code cho Networking Module, khi viết Module thì ta cần định nghĩa giá trị đầu vào và đầu ra của Module, ta có thể định nghĩa từ đầu hoặc khi ta viết Module xong ta thấy ta cần giá trị nào mà động thì ta thêm vào cũng được không nhất thiết phải định nghĩa từ ban đầu. Networking Module của ta sẽ có giá trị đầu vào và đầu ra như sau.
Cập nhật file variables.tf
của Module.
variable "project" {
type = string
}
variable "vpc_cidr" {
type = string
}
variable "private_subnets" {
type = list(string)
}
variable "public_subnets" {
type = list(string)
}
variable "database_subnets" {
type = list(string)
}
Tiếp theo cập nhật file main.tf
của Module.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.12.0"
name = "${var.project}-vpc"
cidr = var.vpc_cidr
azs = data.aws_availability_zones.available.names
private_subnets = var.private_subnets
public_subnets = var.public_subnets
database_subnets = var.database_subnets
create_database_subnet_group = true
enable_nat_gateway = true
single_nat_gateway = true
}
Đây là Remote Module mà ta sẽ dùng câu lệnh terraform init
để tải xuống, Module này sẽ tạo VPC cho ta. Với các giá trị trên thì VPC của ta khi được tạo sẽ như thế này.
Tiếp theo ta sẽ tiến hành tạo Secutiry Group cho VPC của ta, Secutiry Group của ta phải cho phép 3 thằng sau:
- Cho phép truy cập port 80 của ALB từ mọi nơi
- Cho phép truy cập port 80 của các EC2 từ ALB
- Cho phép truy cập port 5432 của RDS từ EC2
Ta thêm SG Rule vào.
...
module "alb_sg" {
source = "terraform-in-action/sg/aws"
vpc_id = module.vpc.vpc_id
ingress_rules = [
{
port = 80
cidr_blocks = ["0.0.0.0/0"]
}
]
}
module "web_sg" {
source = "terraform-in-action/sg/aws"
vpc_id = module.vpc.vpc_id
ingress_rules = [
{
port = 80
security_groups = [module.lb_sg.security_group.id]
}
]
}
module "db_sg" {
source = "terraform-in-action/sg/aws"
vpc_id = module.vpc.vpc_id
ingress_rules = [
{
port = 5432
security_groups = [module.web_sg.security_group.id]
}
]
}
Để các Module bên ngoài có thể truy cập được các giá trị của Module này, ta cần định nghĩa giá trị đầu ra cho nó. Cập nhật file outputs.tf
.
output "vpc" {
value = module.vpc
}
output "sg" {
value = {
lb = module.lb_sg.security_group.id
web = module.web_sg.security_group.id
db = module.db_sg.security_group.id
}
}
Giá trị đầu ra
Để truy cập giá trị của một Module, ta dùng cú pháp sau module.<name>.<output_value>
, ví dụ để truy cập giá trị lb_sg id
của Networking Module.
module.networking.sg.lb
Nên nhớ module.<name>
thì name
là tên ta khai báo khi ta sử dụng Module chứ không phải tên thư mục của Module nhé. Ví dụ:
module "networking" {
source = "./modules/networking"
}
module.networking.sg.lb
module "nt" {
source = "./modules/networking"
}
module.nt.sg.lb
Vậy là ta đã viết xong Module, ta sử dụng nó như sau. Cập lại file main.tf
ngoài Root.
locals {
project = "terraform-series"
}
provider "aws" {
region = "us-west-2"
}
module "networking" {
source = "./modules/networking"
project = local.project
vpc_cidr = "10.0.0.0/16"
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", "10.0.9.0/24"]
}
module "database" {
source = "./modules/database"
}
module "autoscaling" {
source = "./modules/autoscaling"
}
Database Module
Tiếp theo ta sẽ viết code cho Database Module, input và output của Database Module.
Ở trên AWS khi ta tạo RDS yêu cầu ta cần phải có một Subnet Group trước, rồi RDS mới được triển khai lên trên Subnet Group đó.
Để tạo Subnet Group bằng Terraform thì ta sẽ xài aws_db_subnet_group
resource, ví dụ.
resource "aws_db_subnet_group" "default" {
name = "main"
subnet_ids = [aws_subnet.frontend.id, aws_subnet.backend.id]
tags = {
Name = "My DB subnet group"
}
}
Ở trên khi ta xài Module VPC, thì nó đã tạo sẵn cho ta một thằng Subnet Group sẵn, nên ta mới cần truyền thằng VPC vào Module Database, để ta đỡ phải một thằng Subnet Group khác. Ta lấy giá trị Subnet Group ở trong Module vpc như sau module.networking.vpc.database_subnet_group
. Giờ ta sẽ viết code cho Module, cập nhật tệp tin variables.tf
trong Database Module.
variable "project" {
type = string
}
variable "vpc" {
type = any
}
variable "sg" {
type = any
}
Tệp tin main.tf
.
resource "aws_db_instance" "database" {
allocated_storage = 20
engine = "postgresql"
engine_version = "12.7"
instance_class = "db.t2.micro"
identifier = "${var.project}-db-instance"
name = "terraform"
username = "admin"
password = "admin"
db_subnet_group_name = var.vpc.database_subnet_group
vpc_security_group_ids = [var.sg.db]
skip_final_snapshot = true
}
Để tạo RDS trên AWS thì ta sẽ dùng aws_db_instance
resource, ở trên ta chỉ định Engine của RDS mà ta sẽ xài là PostgreSQL 12.7, với 20GB lưu trữ, giá trị Subnet Group của RDS ta lấy giá trị truyền vào từ biến VPC. Mọi thứ có vẻ ổn, nhưng bạn để ý là ở trường password
, hiện tại ta đang để cứng giá trị, nếu ta không muốn để cứng mà ta muốn giá trị này sẽ là động thì sao?
Ta sẽ dùng một resource khác trong Terraform giúp ta tạo Password động, sau đó ta sẽ truyền giá trị Password này vào Database, cập nhật code lại.
resource "random_password" "password" {
length = 16
special = true
override_special = "_%@"
}
resource "aws_db_instance" "database" {
allocated_storage = 20
engine = "postgresql"
engine_version = "12.7"
instance_class = "db.t2.micro"
identifier = "${var.project}-db-instance"
db_name = "series"
username = "series"
password = random_password.password.result
db_subnet_group_name = var.vpc.database_subnet_group
vpc_security_group_ids = [var.sg.db]
skip_final_snapshot = true
}
Lưu ý khi ta sử dụng resource này thì Password của ta sẽ được lưu ở trong tệp tin State, lúc này ai truy cập vào tệp tin State cũng có thể thấy được Password, dẫn đến việc bảo mật của ta không được tốt, ta sẽ bàn về vấn đề secutiry ở một bài khác.
Ta xuất giá trị RDS ra ngoài để bên ngoài có thể truy cập được.
output "config" {
value = {
user = aws_db_instance.database.username
password = aws_db_instance.database.password
database = aws_db_instance.database.name
hostname = aws_db_instance.database.address
port = aws_db_instance.database.port
}
}
Ya đã viết xong Database Module, ta cập nhật lại tệp tin main.tf
ở Root để có thể sử dụng được Module.
locals {
project = "terraform-series"
}
provider "aws" {
region = "us-west-2"
}
module "networking" {
source = "./modules/networking"
project = local.project
vpc_cidr = "10.0.0.0/16"
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", "10.0.9.0/24"]
}
module "database" {
source = "./modules/database"
project = local.project
vpc = module.networking.vpc
sg = module.networking.sg
}
module "autoscaling" {
source = "./modules/autoscaling"
}
Có một điểm ta cần nói là ở tệp tin khai báo biến của Database Module, hai giá trị là VPC với SG ta khai báo dữ liệu là any
.
...
variable "vpc" {
type = any
}
variable "sg" {
type = any
}
Khi ta muốn truyền một giá trị mà ta không biết nó thuộc loại dữ liệu nào thì ta sẽ khai báo kiểu dữ của nó là any
.
Autoscaling Module
Module cuối cùng mà ta sẽ viết là Autoscaling Module, đây là một Module chứa hơi nhiều thứ. Để tạo một Autoscaling Module trên AWS và khiến nó hoạt động được, ta cần một số Service phải tạo chung với nó như là Load Balancer, Launch Templates, … Trong khi Load Balancer, ta cũng cần phải tạo cho nó 3 thằng là Load Balancer + Target Group + LB Listener. Nên để tạo được ASG trên AWS ta sẽ dùng Module có sẵn thay vì phải tự viết code. Hình minh họa Autoscaling Module.
Ta định nghĩa giá trị đầu vào và đầu ra của Autoscaling Module.
Giờ ta sẽ tiến hành viết code, cập nhật tệp tin variables.tf
của Autoscaling Module.
variable "project" {
type = string
}
variable "vpc" {
type = any
}
variable "sg" {
type = any
}
variable "db_config" {
type = object(
{
user = string
password = string
database = string
hostname = string
port = string
}
)
}
Tiếp theo ta sẽ khai báo ASG, để tạo được ASG thì ta cần có một Launch Templates đi kèm với nó, ASG sẽ dùng Template này để tạo EC2.
Để tạo Launch Templates ta dùng aws_launch_template
resource, cập nhật tệp tin main.tf
của Autoscaling Module.
data "aws_ami" "ami" {
most_recent = true
filter {
name = "name"
values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"]
}
owners = ["amazon"]
}
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = data.aws_ami.ami.id
instance_type = "t2.micro"
vpc_security_group_ids = [var.sg.web]
user_data = filebase64("${path.module}/run.sh")
}
Tệp tin run.sh
.
#!/bin/bash
yum update -y
yum install -y httpd.x86_64
systemctl start httpd
systemctl enable http
echo "$(curl http://169.254.169.254/latest/meta-data/local-ipv4)" > /var/www/html/index.html
Ở trên ta dùng aws_ami
để lọc lấy ra Image ID của OS amazon-linux-2
, sau đó gán ID này vào Launch Template, thuộc tính user_data
ta định nghĩa đoạn code sẽ chạy khi EC2 của ta được tạo ra. Tiếp theo ta gán nó vào Autoscaling Group.
...
resource "aws_autoscaling_group" "web" {
name = "${var.project}-asg"
min_size = 1
max_size = 3
vpc_zone_identifier = var.vpc.private_subnets
launch_template {
id = aws_launch_template.web.id
version = aws_launch_template.web.latest_version
}
}
Tiếp theo vì RDS của ta được tạo ở chế độ Private, nên để EC2 có thể truy cập được tới DB ta phải gán IAM Role vào trong EC2 này, ở trong Terraform ta có thể cấu hinh2 nó thông qua thuộc tính iam_instance_profile
của aws_launch_template
resource. Ta cập nhật lại code như sau.
data "aws_ami" "ami" {
most_recent = true
filter {
name = "name"
values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"]
}
owners = ["amazon"]
}
module "iam_instance_profile" {
source = "terraform-in-action/iip/aws"
actions = ["logs:*", "rds:*"]
}
resource "aws_launch_template" "web" {
name_prefix = "web-"
image_id = data.aws_ami.ami.id
instance_type = "t2.micro"
vpc_security_group_ids = [var.sg.web]
user_data = filebase64("${path.module}/run.sh")
iam_instance_profile {
name = module.iam_instance_profile.name
}
}
resource "aws_autoscaling_group" "web" {
name = "${var.project}-asg"
min_size = 1
max_size = 3
vpc_zone_identifier = var.vpc.private_subnets
launch_template {
id = aws_launch_template.web.id
version = aws_launch_template.web.latest_version
}
}
Ta dùng Module terraform-in-action/iip/aws
để tạo role
với toàn bộ quyền truy cập tới logs
và RDS, sau đó ta gán náo vào aws_launch_template
. Resource mà tiếp theo ta cần khai báo là Load Balancer, để cho phép người dùng truy cập được tới ASG của ta. Ta sẽ dùng terraform-aws-modules/alb/aws
, thêm vào thêm main.tf
đoạn code của LB.
...
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "~> 6.0"
name = var.project
load_balancer_type = "application"
vpc_id = var.vpc.vpc_id
subnets = var.vpc.public_subnets
security_groups = [var.sg.lb]
http_tcp_listeners = [
{
port = 80,
protocol = "HTTP"
target_group_index = 0
}
]
target_groups = [
{
name_prefix = "web",
backend_protocol = "HTTP",
backend_port = 80
target_type = "instance"
}
]
}
resource "aws_autoscaling_group" "web" {
name = "${var.project}-asg"
min_size = 1
max_size = 3
vpc_zone_identifier = var.vpc.private_subnets
target_group_arns = module.alb.target_group_arns
launch_template {
id = aws_launch_template.web.id
version = aws_launch_template.web.latest_version
}
}
Sau khi khai báo LB thì ta cập nhật lại thuộc tính target_group_arns
của aws_autoscaling_group
với giá trị target_group_arns
được lấy ra từ Module LB. Cập nhật giá trị đầu ra của Module.
output "lb_dns" {
value = module.alb.lb_dns_name
}
Ta sử dụng Autoscaling Module ở tệp tin main.tf
như sau.
locals {
project = "terraform-series"
}
provider "aws" {
region = "us-west-2"
}
module "networking" {
source = "./modules/networking"
project = local.project
vpc_cidr = "10.0.0.0/16"
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", "10.0.9.0/24"]
}
module "database" {
source = "./modules/database"
project = local.project
vpc = module.networking.vpc
sg = module.networking.sg
}
module "autoscaling" {
source = "./modules/autoscaling"
project = local.project
vpc = module.networking.vpc
sg = module.networking.sg
db_config = module.database.config
}
Cuối cùng ta khai báo tệp tin output
cho Root Module. Tạo tệp tin outputs.tf
ở Root.
output "db_password" {
value = module.database.config.password
sensitive = true
}
output "lb_dns_name" {
value = module.autoscaling.lb_dns
}
Ta đã viết code xong, giờ ta chạy câu lệnh init
và apply
để tạo hạ tầng nào.
terraform init
terraform apply -auto-approve
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
db_password = <sensitive>
lb_dns_name = "terraform-series-1259399054.us-west-2.elb.amazonaws.com"
Sau khi Terraform chạy xong ta sẽ thấy URL của Load Balancer được in ra Terminal, ta truy cập vào nó.
curl terraform-series-1259399054.us-west-2.elb.amazonaws.com
Ta đã tạo được hạ tầng cho một giải pháp Application Load Balancer + Auto Scaling Group + Relational Database Service 😁.
Kết luận
Vậy là ta đã tìm hiểu sâu hơn một chút về cách sử dụng Module, như bạn thấy khi ta sử dụng Module thì ở tệp tin main.tf
của Root Module ta chỉ việc khai báo Module và sử dụng nó, thay vì phải viết code dài dòng trong tệp tin main.tf
. Sử dụng Module sẽ giúp ta tổ chức code theo nhóm dễ dàng hơn.
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ả?