Bộ khung Terraform (phần 1)
Terraform là công cụ định nghĩa cơ sở hạ tầng bằng ngôn ngữ HCL khá dễ làm quen, đặt biệt là người không chuyên về lập trình như mình.
Khi mình bắt đầu, mình đã có một băn khoăn làm thế nào để tổ chức terraform và sẽ mở rộng quy mô thế nào khi nhóm làm việc của mình phát triển. Tức là ban đầu chỉ mình làm việc xây dựng hạ tầng, nhưng về sau hạ tầng trở nên phức tạp hoặc có thêm các thành viên khác tham gia và các thành viên sẻ cùng xây dựng hạ tầng song song thì phải tổ chức terraform như thế nào.
Lúc bắt đầu dùng terraform mình đã từng tổ chức code như thế này.
. ├── README.md ├── bastion.tf ├── database.tf ├── dns.tf ├── ecs.tf ├── load_balancer.tf ├── local.tfvars ├── main.tf ├── outputs.tf ├── s3.tf ├── templates │ ├── bastion │ │ ├── instance-profile-policy.json │ │ └── user-data.sh │ └── ecs │ ├── assume-role-policy.json │ ├── container-definitions.json.tpl │ ├── s3-write-policy.json.tpl │ └── task-exec-role.json ├── variables.tf └── vpc ├── eip.tf ├── gw.tf ├── outputs.tf ├── rt.tf ├── sn.tf ├── variables.tf └── vpc.tf
Vấn đề
Đối với các hạ tầng đơn giản điều này có thể không có vấn đề gì, nhưng một khi hạ tầng ngày càng mở rộng, có nhiều service hơn, nhiều tài khoản hay có nhiều môi trường hơn chẳng hạn, vòng đời của các resource cũng khác nhau tùy vào từng giai đoạn của dự án, bạn cần dọn dẹp resource như thế nào mà ích ảnh hưởng đến các resource khác(mình có đề cập ở bài viết này). bạn sẽ làm như thế nào.
Có thể nó sẻ trông như thế này. Mỗi môi trường sẽ là một thư mục, nhưng code sẽ bị lặp lại nhiều.
terraform/ dev/ bootstrap/ bootstrap.tf terraform.tfstate staging/ bootstrap/ bootstrap.tf terraform.tfstate prod/ bootstrap/ bootstrap.tf terraform.tfstate
Giải pháp
Giải pháp được nghĩ đến đầu tiên là mình sẻ dùng git, mỗi môi trường sẻ tương ứng với mỗi branch, những service nào được dùng chung cho mỗi môi trường thì sẽ module hóa nó lại để hạn chế việc lặp lại code, mình có đề cập ở đây
Vẫn còn nhiều thứ để cải thiện như code vẫn còn bị lặp lại, chưa có cái nhìn tổng quan về toàn bộ hạ tầng, tổ chức repo gần như phẳng, không phân cấp.
Terragrunt
Mục đích ban đầu của Terragrunt là lấp đầy một vài khoảng trống trong chức năng của Terraform, và nó đã liên tục mở rộng với các tính năng mới. Mặc dù Terraform đã phát triển để hỗ trợ các bộ tính năng nâng cao hơn, nhưng nó vẫn còn chỗ để cải thiện. Terragrunt mang đến bộ tính năng phong phú sau đây.
1. Phụ thuộc rõ ràng: Chia sẻ state của bạn một cách dễ dàng.
Trước đây để định nghĩa sự phụ thuộc giữa hai module. Và cùng tùy thuộc vào phiên bản terraform, nhưng đôi lúc phát sinh ra issue. Từ phiên bản 0.13 có thêm chức năng chia sẽ trạng thái, nhưng đối với terragrunt thì sẽ trông như thế này.
dependency "vpc" { config_path = "../../infrastructure/vpc" } dependency "external_alb" { config_path = "../../infrastructure/load-balancer/external-alb" } dependency "external_security_group" { config_path = "../../infrastructure/load-balancer/external-security-group" } vpc_id = depedency.vpc.outputs.vpc_id private_subnet_ids = depedency.vpc.outputs.private_subnet_ids load_balancer_arn = depedency.external_alb.outputs.lb_arn security_group_id = depedency.external_security_group.outputs.sg_id
Trong Terraform, vì trạng thái chỉ khả dụng sau khi một mô-đun đã chạy, thứ tự mà các mô-đun được chạy rất quan trọng (và Terraform không biết thứ tự đó). Người vận hành cần ghi lại thứ tự để chạy đúng.
Terragrunt tạo một cây phụ thuộc và chạy tất cả các lệnh theo thứ tự thích hợp để tất cả các phụ thuộc cần thiết đều có sẵn tại thời điểm thực thi.
2. Hỗ trợ biến môi trường
Triết lý của Terraform không phải là các biến môi trường là xấu, nhưng chúng phải được đặt rõ ràng và chỉ có sẵn cho các mô-đun cấp cao nhất. Vì Terragrunt là một trình bao bọc chỉ xử lý các mô-đun gốc nên nó có thể và hỗ trợ các biến môi trường.
3. Generate: tránh lặp lại code.
với terragrunt, bạn có thể sử dụng generate các block để thêm tự động code terraform vào các module trước khi bạn dùng lệnh plan hoặc apply.
# Creates provider.tf generate "providers" { path = "providers.tf" if_exists = "overwrite" contents = <<EOF provider "aws" { region = var.aws_region } variable "aws_region" { description = "AWS region to create infrastructure in" type = string } terraform { backend "s3" { } } EOF }
4. read_terragrunt_config: Loại bỏ mã Terragrunt lặp lại.
đây làm một tính năng giúp hạn chế việc lặp lại mã của terragrunt.
Tổng kết
Trong phần này mình nêu ra một vài lý do để mình quyết định sử dụng terragrunt, cũng như khái quát về một vài tính năng của terragrunt, trong phần sau mình sẻ đi chi tiết hơn về cách mà mình đã apply terragrunt vào dự án.
Bộ khung Terraform (phần 2)
Tiếp nối phần trước, trong phần này mình cũng sẻ đưa thêm các lợi ích mà Terragrunt mang lại.
5. Quản lý trạng thái từ xa động
Trong Terraform, để lưu trữ remote state, bạn cần tạo cấu hình back-end như sau:
terraform { backend "s3" { bucket = "my-unique-bucket-name" key = "account-name/region/service/tf.tfstate" region = "eu-west-1" acl = "bucket-owner-full-control" } }
Đối với mỗi lần triển khai, bạn sẽ cần cấu hình ở trên và sửa đổi key và làm thế nào để quản lý cấu hình này trong Git. Giả sử bạn đang triển khai cùng một Terraform cho môi trường prod và trường stg. Bạn sẽ cần tạo 2 repo git, mỗi repo cho mỗi môi trường (hoặc có thể sử dụng các nhánh khác nhau). Lý tưởng nhất là bạn muốn tách biệt code Terraform khỏi cấu hình remote state.
Để giảm sự trùng lặp, người ta có thể xem xét việc chuyển cấu hình trong đối tượng phụ trợ bằng cách sử dụng các biến. tuy nhiên nó bị hạn chế vì nó không hỗ trợ các đầu vào động như biến hoặc biểu thức. Do đó, mã sau sẽ không hoạt động:
terraform { backend "s3" { bucket = var.terraform_remote_state_bucket key = "account-name/region/${var.service_name}/tf.tfstate" region = var.aws_region acl = var.acl } }
Nhưng đối với Terragrunt nó cho phép tạo ra các đường dẫn Key động.
remote_state { backend = "s3" config = { bucket = "medium-terragrunt-example" key = "terragrunted/${path_relative_to_include()}.tfstate" region = "eu-west-1" encrypt = true } }
6. Tổ chức code Terraform
Vì cấu hình state có thể quản lý một cách động nên bạn có thể tạo và deplop các module với sự liên kết rời rạc, tạo sự thuận tiện cho việc tổ chức code Terraform.
Giờ đây code Terraform có thể được tổ chức như thế này.
. │ └── eu-west-1 │ ├── infrastructure │ │ ├── load-balancer │ │ │ ├── external-alb │ │ │ │ ├── main.tf │ │ │ │ ├── outputs.tf │ │ │ │ ├── providers.tf │ │ │ │ └── terraform.tf │ │ │ └── external-security-group │ │ │ ├── main.tf │ │ │ ├── outputs.tf │ │ │ ├── providers.tf │ │ │ └── terraform.tf │ │ └── vpc │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── providers.tf │ │ └── terraform.tf │ └── services │ └── web-server │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── terraform.tf └── modules ├── infrastructure │ ├── load-balancer │ │ ├── external-alb │ │ │ ├── main.tf │ │ │ └── outputs.tf │ │ └── external-security-group │ │ ├── main.tf │ │ └── outputs.tf │ └── vpc │ ├── main.tf │ └── outputs.tf └── web-server ├── lib │ └── web_server.sh.tpl ├── main.tf └── outputs.tf
Hoặc như thế này
. ├── CHANGELOG.md ├── README.md ├── deployment │ ├── common.terragrunt.hcl │ ├── infra │ │ ├── main │ │ │ ├── account.terragrunt.hcl │ │ │ └── us-west-1 │ │ │ ├── prod │ │ │ │ └── terragrunt.hcl │ │ │ ├── region.terragrunt.hcl │ │ │ └── stage │ │ │ └── terragrunt.hcl │ │ └── secondary │ │ ├── account.terragrunt.hcl │ │ └── us-west-1 │ │ ├── dev │ │ │ ├── environment.terragrunt.hcl │ │ │ └── stack │ │ │ └── terragrunt.hcl │ │ └── region.terragrunt.hcl │ ├── root.hcl │ └── tenancy │ └── main │ └── us-east-2 │ └── prod │ └── bastion └── modules ├── README.md ├── bastion.tf ├── database.tf ├── dns.tf ├── ecs.tf ├── load_balancer.tf ├── outputs.tf ├── s3.tf ├── templates │ ├── bastion │ │ ├── instance-profile-policy.json │ │ └── user-data.sh │ └── ecs │ ├── assume-role-policy.json │ ├── container-definitions.json.tpl │ ├── s3-write-policy.json.tpl │ └── task-exec-role.json ├── variables.tf └── vpc └── terragrunt.hcl
7. Thực thi các lệnh Terraform trên nhiều mô-đun cùng một lúc
Thông thường bạn sẻ có rất nhiều modules, bạn sẽ phải chạy thủ công terraform apply trong từng thư mục con, đợi quá trình hoàn tất và lặp lại cho đến khi tất cả các module được triển khai.
Với Terragrunt trình tốn thời gian này được rút ngắn lệnh terragrunt run-all
Tổng kết
Trong phần này mình nêu ra một vài lợi ích khi sử dụng Terrafrunt, có thể còn thiếu sót, mình sẻ cập nhật nếu phát hiện thêm điều gì, trong phần tiếp theo mình sẻ chia sẻ về bộ khung Terraform, các mà mình tổ chức code Terraform cho nhiều account, nhiều môi trường.
Bộ khung Terraform (phần 3)
Trong bài đăng này, mình s giới thiệu về bộ khung terraform mà mình đã build sẳn.
Cấu trúc 2 lớp
Mình đặt tên là cấu trúc 2 lớp vì mình sẽ chia ra thành hai thư mục chín là modules và deployment.
. ├── CHANGELOG.md ├── README.md ├── deployment │ ├── common.terragrunt.hcl │ └── tenancy │ ├── main │ │ ├── account.terragrunt.hcl │ │ └── us-east-2 │ │ ├── prod │ │ │ ├── bastion │ │ │ │ └── terragrunt.hcl │ │ │ ├── environment.terragrunt.hcl │ │ │ └── network │ │ │ ├── README.md │ │ │ └── terragrunt.hcl │ │ ├── region.terragrunt.hcl │ │ └── stage │ │ └── environment.terragrunt.hcl │ └── secondary │ ├── account.terragrunt.hcl │ └── us-west-1 │ ├── dev │ │ ├── environment.terragrunt.hcl │ │ └── stack │ │ └── terragrunt.hcl │ └── region.terragrunt.hcl ├── modules │ ├── README.md │ ├── bastion │ │ ├── bastion.tf │ │ ├── outputs.tf │ │ ├── templates │ │ │ └── bastion │ │ │ ├── instance-profile-policy.json │ │ │ └── user-data.sh │ │ ├── terragrunt.hcl │ │ └── variables.tf │ └── vpc │ ├── README.md │ └── vpc.hcl
Terragrunt file
Bản chất của Terragrunt là một trình bao bọc mỏng bên ngoài Terraform, có thể tạm hiểu là Terraform không hổ trợ phân lớp code Terraform, nó chỉ đơn giản là hổ trợ module, với cách tổ chức code Terraform như trên thì Terragrunt sẽ thực thi trước sắp xếp lại code Terraform và chạy nó.
Modules
Thư mục này sẽ chứa toàn bộ module của bạn, lý tưởng nhất là bạn sẽ module hóa toàn bộ hạ tầng của bạn, mỗi thành viên trong nhóm sẽ đảm nhận một module, modules có thể là remote module hoặc local module.
như trong ví dụ này mình sử dụng cả hai loại là remote và local module.
modules/vpc/vpc.hcl
Module naỳ mình lấy từ remote về để sử dụng, cấu trúc file này cũng đơn giản, bạn chỉ cần khai báo block terraform.
terraform { source = "git::https://gitlab.com/t3774/module_aws_vpc.git?ref=refactoring" }
modules/bastion/terragrunt.hcl
File này chỉ là một file trống, vì module này là module local, nên file này chỉ có tác dụng đánh dấu, khai báo sự tồn taị của module mà thôi.
Deployment
Thư mục này là nơi mà bạn sẻ tổ chức code Terraform theo kiểu phân cấp.
. ├── common.terragrunt.hcl └── tenancy ├── main │ ├── account.terragrunt.hcl │ └── us-east-2 │ ├── prod │ │ ├── bastion │ │ │ └── terragrunt.hcl │ │ ├── environment.terragrunt.hcl │ │ └── network │ │ ├── README.md │ │ └── terragrunt.hcl │ ├── region.terragrunt.hcl │ └── stage │ └── environment.terragrunt.hcl
common.terragrunt.hcl
Trong thưc mục này mình sẽ khai báo một file common, file này chứa những cái chung nhất như block remote state, global env… sau này khi khởi tạo một module sẽ kết thừa lại file này.
locals { account = read_terragrunt_config(find_in_parent_folders("account.terragrunt.hcl")) region = read_terragrunt_config(find_in_parent_folders("region.terragrunt.hcl")) environment = read_terragrunt_config(find_in_parent_folders("environment.terragrunt.hcl")) prefix = "demo" } # Create bucket store state file remote_state { backend = "s3" disable_dependency_optimization = true config = { encrypt = true region = local.region.locals.aws_region bucket = "${local.prefix}-${local.account.locals.aws_account_id}-${local.region.locals.aws_region}-${local.environment.locals.env}-terraform-state" key = "${path_relative_to_include()}/terraform.tfstate" dynamodb_table = "${local.prefix}-${local.account.locals.aws_account_id}-${local.region.locals.aws_region}-${local.environment.locals.env}-terraform-locks" } } # Generate an backend block generate "backend" { path = "backend.tf" if_exists = "overwrite_terragrunt" contents = <<EOF terraform { backend "s3" {} } EOF } # Generate an AWS required providers block generate "required_providers" { path = "required_providers.tf" if_exists = "overwrite_terragrunt" contents = <<EOF terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 2.54.0" } } } EOF } # Generate an AWS provider block generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOF provider "aws" { region = "${local.region.locals.aws_region}" # Only these AWS Account IDs may be operated on by this template allowed_account_ids = ["${local.account.locals.aws_account_id}"] } EOF } # GLOBAL PARAMETERS inputs = { prefix = "${local.prefix}" common_tags = { Project = "demo-terragrunt" Owner = "quannhm" ManagedBy = "Terragrunt" contact = "quannhm@fpt.com" } }
tenancy
Thư mục này sẽ mô tả về hạ tầng của bạn, và nó có thể mở rộng trong tương lai,
. ├── main │ ├── account.terragrunt.hcl │ └── us-east-2 │ ├── prod │ │ ├── bastion │ │ │ └── terragrunt.hcl │ │ ├── environment.terragrunt.hcl │ │ └── network │ │ ├── README.md │ │ └── terragrunt.hcl │ ├── region.terragrunt.hcl │ └── stage │ └── environment.terragrunt.hcl └── secondary ├── account.terragrunt.hcl └── us-west-1 ├── dev │ ├── environment.terragrunt.hcl │ └── stack │ └── terragrunt.hcl └── region.terragrunt.hcl
trong ví dụ này minh có hai tenancy là main và secondary
main
tong thư mục main sẽ chia thành nhiều cấp bậc.
. ├── account.terragrunt.hcl └── us-east-2 ├── prod │ ├── bastion │ │ └── terragrunt.hcl │ ├── environment.terragrunt.hcl │ └── network │ ├── README.md │ └── terragrunt.hcl ├── region.terragrunt.hcl └── stage └── environment.terragrunt.hcl
Cấp tài khoảng: là cấp cao nhất trong thư mục main, và được khai báo trong file account.terragrunt.hcl.
locals { account_name = "main" aws_account_id = get_env("AWS_ACCOUNT_ID") aws_profile = "default" }
Cấp khu vực: là cấp độ thứ hai, cấp này sẻ được đặt trong một thư mục, và có thể mở rộng, trong ví dụ này mình đang dựng hạ tầng của mình ở khu vực us-east-2 và đươqcj khai báo trong file region.terragrunt.hcl.
locals { aws_region = "us-east-2" }
Cấp môi trường: đây là cấp độ thức ba, thường thì ở tenancy main chỉ có môi trường prod, bạn có thể mở rộng tùy vào nhu cầu của bạn và được khai báo trong file environment.terragrunt.hcl.
locals { env = "prod" }
Tương tự với các tenancy còn lại.
Tổng kết
Bạn có thể hình dung cách tổ chức code Terraform của mình như thế này, cách tổ chức code này sẽ giúp bạ có cái nhình tổng quan về toàn bộ hạ tầng, nó khác biệt hoàn taonf so với cách cách tổ chức code thông thường, nhưng bù lại nó sẽ phức tạp và tốn nhiều thời gian đẻ xây dựng ban đầu.
Trong phần này mình đã giớ thiệu tổng quang về cách tổ chức code Terraform bằng cách sử dụng Terragrunt, trong phần tiếp theo mình sẽ nói chi tiết hơn về cách sử dụng bộ khung này.
Bộ khung Terraform (phần 4)
Sau khi đi qua cả ba phần của bài viết, chắc bạn đọc cũng đã hình dung được về cách tổ chức và quản lý code Terraform của mình, nó giống như kiểu bạn tạo ra những Class (lớp chứa các thuộc tính) và sau đó mỗi khi bạn tạo một module mới bạn sẽ kế thừa các thuộc tính đó tại nơi mà bạn muốn deploy module đó.
Sự kế thừa sẽ trông như thế này. Nhìn có vẻ trừ trượng nhưng mình sẽ giải thích, cũng dễ hiểu thôi.
trong hình sẻ có hai phần.
phần module(hình chủ nhật màu anh bên tay trái), bên tron có chứa hai modules là vpc và basiton, tất cả các module sẽ được khai báo ở đây, nếu bạn có nhiêu hơn bạn vẫn sẽ khai báo ở đây.
phần deployment: phần này sẽ được phân thành nhiều cấp như account, region, envi tương ứng với mỗi hình chử nhật chồng lên nhau, mỗi cấp sẽ khai báo một file hcl tương ứng.
mỗi khi bạn muốn deploy một module bất kì lên một môi trường nào đó, bạn cần chỉ ra bạn cần deploy lên account nào, region nào và môi trường nào, để làm được điều đó bạn sẽ dùng cú pháp của Terrafrunt để kế thừa lại các thuộc tính như đã liệt kê ở trên.
đường mũi tên màu đỏ và màu xanh bắt nguồn từ nơi mà module vpc/bastion sẽ được deploy, nó thể hiện sự kế thừa đối với các file Terragrunt xung quanh.
bản chất của Terragrunt sẽ public các biến Terraform thành biến môi trường(biến trong OS), cũng như hỗ trợ một số hàm build-in để trỏ đường dẫn đến file HCl muốn kế thừa.
Ví dụ
Để deploy module vpc, mình sẽ tạo một file teragrunt.hcl với nội dung như sau.
khai báo region (ké thừa lại tệp region)
locals {
region = read_terragrunt_config(find_in_parent_folders("region.terragrunt.hcl"))
}
Khai báo backend state, khai báo common tag
include "common" {
path = find_in_parent_folders("common.terragrunt.hcl")
}
khai báo module dùng để deploy
include "module_aws_vpc" {
path = "${dirname(find_in_parent_folders("common.terragrunt.hcl"))}/../modules/vpc/vpc.hcl"
# . equivalent to get_parent_terragrunt_dir() . . . .
}
khai báo param cho module
# LOCAL PARAMETERS
inputs = {
cidr_block = "10.1.0.0/16"
cidr_publish_subnet = ["10.1.1.0/24", "10.1.2.0/24"]
cidr_private_subnet = ["10.1.10.0/24", "10.1.11.0/24"]
availability_zone = ["${local.region.locals.aws_region}a", "${local.region.locals.aws_region}b"]
nat_gateway_enabled = false
}
trổng thể file sẽ trông như thế này
locals {
region = read_terragrunt_config(find_in_parent_folders("region.terragrunt.hcl"))
}
include "common" {
path = find_in_parent_folders("common.terragrunt.hcl")
}
include "module_aws_vpc" {
path = "${dirname(find_in_parent_folders("common.terragrunt.hcl"))}/../modules/vpc/vpc.hcl"
# . equivalent to get_parent_terragrunt_dir() . . . .
}
# LOCAL PARAMETERS
inputs = {
cidr_block = "10.1.0.0/16"
cidr_publish_subnet = ["10.1.1.0/24", "10.1.2.0/24"]
cidr_private_subnet = ["10.1.10.0/24", "10.1.11.0/24"]
availability_zone = ["${local.region.locals.aws_region}a", "${local.region.locals.aws_region}b"]
nat_gateway_enabled = false
}
Tương tư với modules bastion còn lại, như khác biệt ở chổ module bastion sẽ phụ thuộc vào module vpc( nếu không có vpc thì sao tạo đc ec2 đúng không nào)
để kahi báo phụ thuộc, bạn làm như sau
dependency "vpc" {
config_path = "${get_terragrunt_dir()}/../network"
mock_outputs = {
vpc_id = "vpc-0b65210cb46184d23"
subnet_publish = ["subnet-01ccd045746e8339b"]
}
}
ơ thế mock_output là gì? cái này dùng để tạo ra output giả, khi bạn chạy Terragrunt plan, Terragrunt sẽ tạo ra một cây phụ thuộc, vì lúc đó vpc chưa được tạo nên bastion sẽ bị lỗi vì thiếu param, mock_output sẽ khắc phụ điều này.