- vừa được xem lúc

Terraform Series - Bài 5 - Module In Depth: Create Multi-Tier Application

0 0 22

Người đăng: Quân Huỳnh

Theo Viblo Asia

Giới thiệu

Chào các bạn tới với series về Terraform, ở 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 Multi-Tier bao gồm AWS Application Load Balancer + Auto Scaling Group + Relational Database Service.

Với Auto Scaling Group nó sẽ tạo ra một group EC2 mà hosting web server mà chạy ở port 80, và dữ liệu ta sẽ lưu ở RDS loại Postgres, và client sẽ truy cập tới dứ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 group lại thành một module như sau.

image.png

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 top-level được gọi là root module.

Root module

Tất cả các workspace (folder ta viết code và chạy câu lệnh apply) đều có một thằng gọi là root module. Ở trong root module đó, chúng ta có thể có một hoặc nhiều child modules. Module có thể là local module với source code nằm ở máy của ta hoặc remote moudle, module 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 module.

Như ta thấy ở module hình trên thì Networking, AutoScaling và RDS là child module của root module. Và trong một module nó có thể có chứa một hoặc nhiều module khác, như Networking nó chứa VPC module và SG (Security Group) module, nếu một module nằm trong một module khác, ta gọi nó là nested modules.

Write code

Ok, 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à dynamic 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ị input và output như sau.

image.png

Cập nhật file variables.tf của networking 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 networking 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 phải cho phép 3 thằng sau:

  1. Cho phép truy cập port 80 của ALB từ mọi nơi.
  2. Cho phép truy cập port 80 của các EC2 từ ALB.
  3. 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 output nó ra. 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 }
}

Output value

Để truy cập giá trị của một module, ta dùng systax 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 folder của module nhé. Ví dụ:

module "networking" { source = "./modules/networking"
} module.networking.sg.lb
module "nt" { source = "./modules/networking"
} module.nt.sg.lb

Oke, vậy là ta đã viết xong networking 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.

image.png

Ở trên AWS, khi ta tạo RDS, yêu cầu ta cần phải có một subnet groups trước, rồi RDS mới được deploy lên trên subnet group đó.

image.png

Để 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 groups 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 file variables.tf trong database module.

variable "project" { type = string
} variable "vpc" { type = any
} variable "sg" { type = any
}

File 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 version 12.7, với size là 20GB, giá trị subnet group của RDS, ta lấy giá trị truyền vào từ biến vpc (lát ta sẽ truyền biến này vào database module). Mọi thứ có vẻ ok, nhưng bạn để ý là ở trường password, hiện tại ta đang fix cứng giá trị, nếu ta không muốn fix cứng mà ta muốn giá trị này sẽ là random thì sao?

Ta sẽ dùng một resource khác trong terraform giúp ta random passoword, 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 state file, lúc này ai truy cập vào state file cũng có thể thấy được password, việc này dẫn đến việc bảo mật của ta không được tốt lắm, ta sẽ bàn về vấn để secutiry ở một bài khác.

Ta output 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 }
}

Oke, ta đã viết xong database module, ta cập nhật lại fle 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à ở file khai báo biến của database module, hai giá trị là vpc với sg, ta khai báo type 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, ở trên vì biến vpc là any nên ta mới có thể truyền nguyên giá trị của module vpc vào database module được.

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 group 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 mà ta không dùng module có sẵn thì viết code cũng hơi nhiều, đây là lý do tại sao ta nên xài module cho nhanh gọn và tiện. Hình minh họa.

image.png

Ta định nghĩa input và output của autoscaling module.

image.png

Giờ ta sẽ tiến hành viết code, cập nhật file 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.

image.png

Để tạo Launch Templates, ta dùng resource aws_launch_template, cập nhật file 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")
}

File 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 data source aws_ami để filter lấy ra image id của OS amazon-linux-2, sau đó gán id này vào launch template , mục user_data ta định nghĩa đoạn script 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ể config 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 quyền là full access tới logs và rds, sau đó ta gán náo vào aws_launch_template. Ok, vậy là ta đã khai báo được ASG, resource mà tiếp theo ta cần khai báo là Load Balancer, để cho phép user 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 output của module.

output "lb_dns" { value = module.alb.lb_dns_name
}

Ta sử dụng autoscaling module ở file 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 file output cho root module. Tạo file outputs.tf ở root.

output "db_password" { value = module.database.config.password sensitive = true
} output "lb_dns_name" { value = module.autoscaling.lb_dns
}

Oke, 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

Oke, ta đã tạo được hạ tầng cho một solution 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ì ở file main 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 file main. Sử dụng module sẽ giúp ta tổ chức code theo nhóm dễ dàng hơn. Nếu có thắc mắc hoặc cần giải thích rõ thêm chỗ nào thì các bạn có thể hỏi dưới phần comment. Hẹn gặp mọi người ở bài tiếp theo.

Mục tìm kiếm đồng đội

Hiện tại thì bên công ty mình, là Hoàng Phúc International, với hơn 30 năm kinh nghiệm trong lĩnh vực thời trang. Và là trang thương mại điện tử về thời trang lớn nhất Việt Nam. Team công nghệ của HPI đang tìm kiếm đồng đội cho các vị trí như:

Với mục tiêu trong vòng 5 năm tới về mảng công nghệ là:

  • Sẽ có trang web nằm trong top 10 trang web nhanh nhất VN với 20 triệu lượt truy cập mỗi tháng.
  • 5 triệu loyal customers và có hơn 10 triệu transactions mỗi năm.

Team đang xây dựng một hệ thống rất lớn với rất nhiều vấn để cần giải quyết, và sẽ có rất nhiều bài toàn thú vị cho các bạn. Nếu các bạn có hứng thú trong việc xây dựng một hệ thống lớn, linh hoạt, dễ dàng mở rộng, và performance cao với kiến trúc microservices thì hãy tham gia với tụi mình.

Nếu các bạn quan tâm hãy gửi CV ở trong trang tuyển dụng của Hoàng Phúc International hoặc qua email của mình nha _@.com. Cảm ơn các bạn đã đọc.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Đề thi interview DevOps ở Châu Âu

Well. Chào mọi người, mình là Rice - một DevOps Engineers ở đâu đó tại Châu Âu.

0 0 88

- vừa được xem lúc

In calculus, love also means zero.

Mình nhớ hồi năm 2 đại học, thầy giáo môn calculus, trong một giây phút ngẫu hứng, đã đưa ra cái definition này. Lúc đấy mình cũng không nghĩ gì nhiều.

0 0 65

- vừa được xem lúc

Chuyện thay đổi

Thay đổi là một thứ gì đó luôn luôn đáng sợ. Cách đây vài tháng mình có duyên đi làm cho một banking solution tên là X.

0 0 47

- vừa được xem lúc

Pet vs Cattle - Thú cưng và gia súc

Khái niệm. Pets vs Cattle là một khái niệm cơ bản của DevOps. Bài viết này sẽ nói về sự phát triển của các mô hình dịch vụ từ cốt lõi Pets and Cattle. 1.

0 0 34

- vừa được xem lúc

Git workflow được Google và Facebook sử dụng có gì hay ho

Với developer thì Git hẳn là công cụ rất quen thuộc và không thể thiếu rồi. Thế nhưng có mấy ai thực sự hiểu được Git.

0 0 85

- vừa được xem lúc

Kubernetes - Học cách sử dụng Kubernetes Namespace cơ bản

Namespace trong Kubernetes là gì. Tại sao nên sử dụng namespace.

0 0 113