Chào mọi người! Nếu bạn đã làm việc trong lĩnh vực DevOps hoặc Kỹ thuật Đám mây một thời gian, bạn có thể đã gặp phải rất nhiều vấn đề. Từ những tệp main.tf khổng lồ cố gắng định nghĩa một vũ trụ, đến việc sao chép mã giữa các dự án cho đến khi bạn không thể phân biệt được cụm Kubernetes nào thuộc về dev và cụm nào là prod.
Chúng ta đều đã trải qua điều đó. Bạn bắt đầu với một dự án đơn giản và nó hoạt động. Sau đó, công ty yêu cầu bạn tạo ra một môi trường staging. Tiếp theo là môi trường UAT. Ngay lập tức, bạn bị ngập trong mã sao chép, và một thay đổi đơn giản (như nâng cấp phiên bản Kubernetes) yêu cầu cập nhật năm nơi khác nhau. Điều đó không chỉ lộn xộn; nó là công thức cho thảm họa.
Trong nhiều năm qua, tôi đã học được rằng việc viết mã cơ sở hạ tầng (IaC) tốt cũng giống như viết mã ứng dụng tốt. Tất cả đều liên quan đến mẫu, tái sử dụng, và tính mô-đun. Trong loạt bài nhiều phần này, tôi sẽ hướng dẫn bạn qua các mẫu thiết kế mà tôi sử dụng để xây dựng các dự án Terraform mạnh mẽ, có thể mở rộng và dễ quản lý cho AWS.
Hôm nay, chúng ta sẽ bắt đầu với nền tảng tuyệt đối để quản lý độ phức tạp: Mẫu Module.
Điều Gì Sai Với Một Tệp main.tf Khổng Lồ?
Hãy tưởng tượng bạn đang xây dựng một động cơ ô tô. Bạn có thể cố gắng lắp ráp từng piston, dây, và bu lông trong một lần, ngay trên sàn nhà máy. Nó có thể hoạt động, nhưng điều gì sẽ xảy ra khi bạn muốn xây dựng một động cơ hơi khác cho một mẫu xe khác? Bạn sẽ phải bắt đầu từ đầu hoặc cẩn thận sao chép tác phẩm đầu tiên của mình.
Một tệp main.tf monolithic cho một cụm EKS là một sự lắp ráp hỗn loạn. Nó kết hợp cái gì (một mặt phẳng điều khiển EKS, vai trò IAM, nhóm nút, nhóm bảo mật) với đâu và tại sao (điều này dành cho môi trường dev với các nút t3.medium, điều này dành cho ứng dụng prod với các nút m5.large).
Phương pháp này có một số thiếu sót lớn:
- Không DRY (Don't Repeat Yourself): Tạo ra một môi trường mới có nghĩa là sao chép hàng trăm dòng mã IAM và mạng phức tạp.
- Tải nhận thức cao: Hiểu cấu hình cụm yêu cầu giải mã một khối mã khổng lồ, liên kết với nhau.
- Đường kính phá hủy cao: Một lỗi nhỏ trong chính sách IAM có thể làm hỏng toàn bộ cụm của bạn, và nếu bạn đã sao chép, điểm yếu đó tồn tại trong mọi môi trường.
Chúng ta có thể làm tốt hơn. Thay vì lắp ráp động cơ từng phần mỗi lần, hãy xây dựng các thành phần chế tạo sẵn, như một hệ thống phun nhiên liệu hoàn chỉnh hoặc một mô-đun đánh lửa đã được nối dây sẵn. Trong Terraform, chúng tôi gọi những điều này là modules.
Mẫu Module: Các Khối Xây Dựng IaC Của Bạn
Một module Terraform là một gói cấu hình Terraform tự chứa mà được quản lý như một nhóm. Hãy nghĩ về nó như một hàm trong một ngôn ngữ lập trình. Nó nhận một số đầu vào (biến), thực hiện một số hành động (tạo tài nguyên), và cung cấp một số đầu ra.
Đối với một cụm EKS, một module là một cứu tinh. Nó có thể đóng gói tất cả các tài nguyên boilerplate cần thiết để có một cụm hoạt động:
- Mặt phẳng điều khiển cụm EKS.
- Các vai trò và chính sách IAM phức tạp cho cụm và các nút của nó.
- Các nhóm nút được quản lý, bao gồm các mẫu khởi động và cấu hình tự động mở rộng của chúng.
Bạn sau đó gọi module này từ mã cụ thể cho môi trường của bạn, truyền vào các giá trị như tên cụm, phiên bản, và loại phiên bản.
Ví dụ Thực Tế: Một Module Cụm EKS Có Thể Tái Sử Dụng
Hãy xây dựng một phiên bản đơn giản. Một thực tiễn tốt nhất phổ biến là có một thư mục modules trong kho lưu trữ của bạn nơi bạn lưu trữ các module tùy chỉnh, có thể tái sử dụng. Cấu trúc dự án ban đầu của chúng ta trông như sau:
terraform-project/
├── modules/
│ └── aws-eks-cluster/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
└── dev/
└── main.tf
Bên trong modules/aws-eks-cluster/, chúng ta định nghĩa API của module (variables.tf), logic của nó (main.tf), và các giá trị trả về của nó (outputs.tf).
(Để tiết kiệm không gian, mã đầy đủ cho module EKS bị bỏ qua ở đây nhưng giống như trong cuộc thảo luận trước của chúng ta. Nó định nghĩa các tài nguyên như aws_eks_cluster, aws_eks_node_group, và các vai trò IAM liên quan của chúng.)
Các Mẫu Cho Biến Môi Trường
Được rồi, chúng ta đã có module EKS có thể tái sử dụng. Bây giờ phần quan trọng nhất: Làm thế nào để chúng ta cung cấp cấu hình phù hợp cho mỗi môi trường (dev, prod, v.v.) một cách sạch sẽ, có thể mở rộng và dễ quản lý?
Hãy xem sự tiến hóa của việc truyền biến, từ phương pháp cơ bản đến thực tiễn tốt nhất được khuyến nghị.
Phương pháp 1: Tham số Trực Tiếp
Cách đơn giản nhất là mã hóa cứng các giá trị trực tiếp trong khối module bên trong main.tf của môi trường bạn.
# environments/dev/main.tf
module "eks_cluster" {
source = "../../modules/aws-eks-cluster"
# --- Tham số trực tiếp ---
cluster_name = "my-app-dev-cluster"
node_group_instance_types = ["t3.medium"]
node_group_desired_size = 2
# ... các biến khác
}
Điều này là tốt cho một bài kiểm tra nhanh, nhưng nó không mở rộng. Nó trộn lẫn cấu hình với logic, làm cho tệp khó đọc và buộc bạn phải tìm kiếm các giá trị khi bạn cần thực hiện thay đổi.
Phương pháp 2: Sử dụng Tệp .tfvars (Cách Tiêu Chuẩn)
Một mẫu tốt hơn nhiều là tách biệt các giá trị cấu hình khỏi logic tài nguyên của bạn. Một tệp .tfvars là một tệp văn bản đơn giản cho các phép gán biến. Bạn tạo một tệp cho mỗi môi trường.
1. Định nghĩa Biến Gốc: Đầu tiên, trong thư mục môi trường của bạn (environments/dev/), tạo một tệp variables.tf để khai báo các biến mà môi trường này sẽ chấp nhận.
# environments/dev/variables.tf
variable "node_group_instance_types" {
description = "Các loại phiên bản cho nhóm nút EKS."
type = list(string)
}
# ... các định nghĩa biến khác
2. Tạo Tệp .tfvars cho Môi Trường: Bây giờ, tạo một tệp dev.tfvars trong cùng thư mục để cung cấp các giá trị.
# environments/dev/dev.tfvars
node_group_instance_types = ["t3.medium"]
node_group_desired_size = 2
Môi trường prod của bạn sẽ có tệp prod.tfvars riêng với các giá trị khác, chẳng hạn như ["m5.large"].
3. Cập nhật main.tf và Áp Dụng: main.tf của bạn bây giờ sử dụng các biến này, trở thành tổng quát hơn.
# environments/dev/main.tf
module "eks_cluster" {
source = "../../modules/aws-eks-cluster"
# Truyền các biến từ module gốc đến module con
node_group_instance_types = var.node_group_instance_types
# ...
}
Bạn sau đó áp dụng cấu hình cụ thể từ dòng lệnh: terraform apply -var-file="dev.tfvars". Cách này tách biệt rõ ràng giữa "cái gì" và "cách làm."
Phương pháp 3: Sử dụng locals cho Các Giá Trị Được Tính Toán
Điều gì sẽ xảy ra nếu bạn muốn đảm bảo một quy tắc đặt tên nhất quán? Thay vì định nghĩa cluster_name trong mỗi tệp .tfvars, bạn có thể tính toán nó bằng cách sử dụng locals. Locals giống như các hằng số đã đặt tên trong cấu hình của bạn.
Tạo một tệp locals.tf trong thư mục môi trường của bạn.
# environments/dev/locals.tf
locals {
# Biến cơ sở
environment = "dev"
project = "my-app"
# Giá trị tính toán để đảm bảo quy tắc đặt tên
cluster_name = "${local.project}-${local.environment}-cluster"
# Bản đồ tập hợp các thẻ trung tâm
common_tags = {
Environment = title(local.environment)
Project = local.project
}
}
Bây giờ, main.tf của bạn có thể sử dụng giá trị local này, đảm bảo tính nhất quán: cluster_name = local.cluster_name.
Kết Hợp Mọi Thứ Lại: Cấu Trúc Được Khuyến Nghị
Để có một dự án thực sự mạnh mẽ và dễ đọc, bạn nên kết hợp Phương pháp 2 và 3.
- Sử dụng các tệp
.tfvarscho các đầu vào thô mà thay đổi giữa các môi trường (kích thước phiên bản, số lượng). - Sử dụng
localsđể đảm bảo quy tắc và tính toán các giá trị (tên, thẻ).
Dưới đây là cấu trúc tệp hoàn chỉnh, được khuyến nghị và quy trình làm việc cho môi trường dev của bạn:
Cấu Trúc Dự Án:
terraform-project/
├── modules/
│ └── aws-eks-cluster/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
└── dev/
├── main.tf # Quản lý các modules
├── variables.tf # Định nghĩa biến đầu vào cho môi trường
├── locals.tf # Định nghĩa quy tắc đặt tên và thẻ chung
└── dev.tfvars # Đặt giá trị thực tế cho dev
dev.tfvars (Cấu Hình Thô):
# environments/dev/dev.tfvars
instance_types = ["t3.medium"]
node_count = 2
locals.tf (Các Quy Tắc):
# environments/dev/locals.tf
locals {
environment = "dev"
project = "my-app"
cluster_name = "${local.project}-${local.environment}-cluster"
common_tags = {
Environment = title(local.environment)
Project = local.project
ManagedBy = "Terraform"
}
}
main.tf (Người Quản Lý):
# environments/dev/main.tf
module "eks_cluster" {
source = "../../modules/aws-eks-cluster"
# Giá trị từ locals
cluster_name = local.cluster_name
tags = local.common_tags
# Giá trị từ tệp .tfvars
node_group_instance_types = var.instance_types
node_group_desired_size = var.node_count
# Các giá trị khác như VPC và subnet
vpc_id = data.aws_vpc.selected.id
subnet_ids = data.aws_subnets.private.ids
}
Phương pháp này mang lại cho bạn những điều tốt nhất của tất cả các thế giới: sự tách biệt rõ ràng giữa các mối quan tâm, tính nhất quán được đảm bảo, và cấu hình môi trường đơn giản và dễ kiểm tra.
Tiếp Theo Là Gì Trong Phần 2?
Chúng ta đã xây dựng một module có thể tái sử dụng và thiết lập một mẫu vững chắc để cấu hình nó qua nhiều môi trường. Đây là một bước lớn hướng tới IaC đạt tiêu chuẩn chuyên nghiệp.
Nhưng vẫn còn một phần quan trọng trong câu đố còn thiếu: Quản Lý Trạng Thái. Terraform lưu trữ tệp trạng thái cho mỗi môi trường ở đâu? Làm thế nào để chúng ta ngăn chặn các lập trình viên vô tình chạy các thay đổi từ dev trên trạng thái prod?
Trong Phần 2, chúng ta sẽ khám phá các mẫu cho các backend trạng thái từ xa (sử dụng S3, tất nhiên!) và giới thiệu các công cụ như Terragrunt để giữ cho cấu hình môi trường của chúng ta thậm chí còn DRY hơn.
Hãy theo dõi và chúc bạn xây dựng tốt! Hãy để lại câu hỏi của bạn trong phần bình luận, và tôi sẽ rất vui được kết nối trên LinkedIn.