Thời gian trước, tôi có nhận một dự án, đó là tư vấn và triển khai hạ tầng cho một dịch vụ mới mà đối tác đang có kế hoạch go live trong 2 tháng tới, tên của dịch vụ này là Keditor
, một dịch vụ chuyên cung cấp giải pháp tối ưu hoá và chỉnh sửa hình ảnh, và tôi giả sử rằng dịch vụ này cần triển khai trên 3 quốc gia. Trong phạm vi bài viết này tôi sẽ chỉ quan tâm đến việc thiết kế Terraform code, các vấn đề khác như kiến trúc hệ thống, kiến trúc ứng dụng, luồng dữ liệu,... Tôi xin phép không đề cập ở đây, hẹn các bạn ở một bài viết khác.
Sau khi trao đổi với đối tác, tôi đã hình thành kiến trúc hệ thống, và các dịch vụ cần thiết để có thể chạy ứng dụng keditor
. Về cơ bản, chúng ta sẽ cần dùng đến:
ALB
: Đây là một dịch vụ của AWS phục vụ cho việc cân bằng tải lưu lượng truy cập vào ứng dụng.ASG
: Một dịch vụ cho việc tự động hoá mở rộng EC2 instances.ECS
backed EC2
: Một container service, có thể hiểu nó khá tương đồng với Kubernetes(k8s)
, nếu k8s bạn phải setup và tự mình quản lý mọi thứ. Thì ECS được triển khai và quản lý bởi AWS, nên việc vận hành cũng khá nhẹ nhàng và không tốn quá nhiều workload. Thành phần API của Keditor sẽ được run tại đây.CodeBuild
: Một CI tool, tôi dùng để build docker image và thực hiện deploy new image to ECS.Lambda
: Một công cụ serverless
, cho phép bạn run code hầu như mọi loại ứng dụng hoặc dịch vụ phụ trợ mà không cần cung cấp hoặc quản lý máy chủ. Keditor image compress, convert, resize, remove, and fill background jobs sẽ được run tại đây, mỗi job là 1 Lambda function.SQS
: Một giải pháp message queue được quản lý bởi AWS, tôi hay sử dụng trong các ứng dụng mà ở đó các thành phần yêu cầu Decouple link
. Cứ mỗi Keditor jobs từ Lambda function sẽ có 1 SQS tương ứng.DocumentDB
: Một giải pháp tương tự như MongoDB, nhưng được phát triển và quản lý bởi AWS. Tất cả dữ liệu của Keditor sẽ được lưu trữ tại đây.IAM
: Nơi bạn có thể chỉ định ai hoặc cái gì có thể truy cập các dịch vụ và tài nguyên trong AWS.ECR
: Nơi để lưu trữ các container images, và được quản lý bởi AWS, bạn có thể hiểu một dịch vụ giống với docker hub
.Pagerduty
: Chúng tôi có on-call duty rotation, và Pagerduty là công cụ giúp chúng tôi wake up khi có incidents xảy ra.CloudWatch
: Tất cả alarms liên quan đến dịch vụ trên AWS sẽ được chúng tôi quản lý tại đây.Grafana
: Nơi tôi thường xây dựng các monitoring dashboards với đầy đủ các metrics cho cả 4 levels: Business, Application, Infra, và 3rd-party.Vì dịch vụ này sẽ được triển khai trên 3 quốc gia khác nhau, và đảm bảo mỗi quốc gia cần có đủ 2 môi trường staging(stg)
và production(prd)
. Vì vậy, tôi cần xác định loại tài nguyên cho hạ tầng thông qua 3 câu hỏi:
Thông qua các dịch vụ AWS được liệt kê ở trên, tôi xác định, hầu hết tất cả các dịch vụ cần được tách riêng cho từng môi trường, cho mỗi quốc gia. Riêng IAM, ECR, PagerDuty, tôi nghĩ chúng ta có thể sử dụng chung cho tất cả quốc gia, và không cần thiết phải tách riêng cho từng môi trường, đơn giản vì tôi không thường xuyên thay đổi cấu hình cho PagerDuty. Và IAM, ECR, tôi không muốn làm phức tạp việc quản lý cũng như deployment container images cho CI/CD tools.
Mặc dù tôi biết, điều này có thể không phải là phương pháp tốt nhất cho môi trường AWS của bạn, vì tách dịch vụ và khối lượng công việc giữa các môi trường có thể giúp giảm thiểu rủi ro và cung cấp ranh giới giữa các nhóm trong tổ chức của bạn. CodeBuild sẽ là tài nguyên có thể dùng chung cho tất cả quốc gia, nhưng cần phân biệt cho từng môi trường, với mục đích, dễ dàng trong việc quản lý kịch bản triển khai và tôi cần an toàn trong khi deploy một version mới của ứng dụng.
Tôi không cố gắng để chỉ ra rằng, cấu trúc dưới đây là tốt nhất hay hợp lý nhất. Mỗi công ty sẽ có những đặc điểm và yêu cầu khác nhau, nên hay xem cấu trúc này của tôi như một ví dụ để tham khảo. Tôi không khuyến khích các bạn áp dụng nó 100%.
Đây là một gợi ý cấu trúc workspaces cho dự án Terraform:
.
├── modules/
│ ├── aws/
│ │ ├── asg/
│ │ │ ├── examples
│ │ │ ├── tests
│ │ │ ├── main.tf
│ │ │ ├── variables.tf
│ │ │ ├── outputs.tf
│ │ │ └── README.md
│ │ ├── alb
│ │ ├── ecs
│ │ ├── documentdb
│ │ ├── codebuild
│ │ ├── lambda
│ │ ├── sqs
│ │ ├── iam
│ │ ├── ecr
│ │ └── cloudwatch
│ ├── grafana
│ ├── pagerduty
│ ├── sops
│ └── jsonnet
└── services/
└── keditor/
├── modules/
│ └── main/
│ ├── examples
│ ├── tests
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── README.md
├── shared/
│ ├── general/
| | ├── secrets
│ │ ├── caller_checker.tf
│ │ ├── locals.tf
│ │ ├── main.tf
│ │ └── provider.tf
│ ├── prd
│ └── stg
├── prd/
│ ├── hk-prd/
│ │ ├── secrets
│ │ ├── caller_checker.tf
│ │ ├── config.jsonnet
│ │ ├── main.tf
│ │ └── provider.tf
│ ├── sg-prd
│ ├── tw-prd
│ └── ...
└── stg/
├── hk-stg/
│ ├── secrets
│ ├── caller_checker.tf
│ ├── config.jsonnet
│ ├── main.tf
│ └── provider.tf
├── sg-stg
├── tw-stg
└── ...
Trong đó:
modules/
là thư mục chứa các module Terraform. Mỗi module có thể chứa các tệp Terraform main.tf
, variables.tf
, outputs.tf
, examples
, tests
, và README.md
. Tôi thường nhóm các modules liên quan với nhau chung một thư mục, ví dụ aws
sẽ chứa các modules liên quan đến các dịch vụ trong AWS.examples
: Là nơi tôi thường dùng để thử nghiệm module bằng việc triển khai nó trên 1 AWS account khác, tôi gọi nó là AWS development
account. Nó cũng sẽ rất hữu ích cho bạn khi dùng terratest
sau này.tests
: Nếu bạn đã từng dùng terratest
để viết test cho terraform module, thì đây sẽ là nơi tôi dùng để chứa các Golang
code.README.md
: Tôi thích viết document, do vậy mỗi một module tôi thường viết lại một vài lời giới thiệu của module, các chức năng hỗ trợ, những hạn chế và những điều cần lưu ý, cũng như cách sử dụng module, tôi sẽ có gắng để viết một cách đầy đủ tại đây. Một công cụ tôi thường hay sử dụng để hỗ trợ cho việc này là terraform-docs
.services/
là nơi tôi sử dụng để chứa tất cả Terraform code cho mỗi dịch vụ mà chúng tôi sẽ triển khai. Nó sẽ bao gồm tất cả:services/keditor/modules
là nơi tôi dùng để chứa các module đặc biệt, thứ mà chỉ sử dụng riêng cho dịch vụ này. main
module, là module với một mục tiêu duy nhất, sẽ gọi các module khác cho các dịch vụ cần thiết như DocumentDB, ECS, Lambda, CodeBuild,.. Và tại module này, pattern terraform_remote_state
sẽ được sử dụng để truy cập các tài nguyên chia sẽ bởi từng môi trường(services/keditor/share/{stg,prd}
), hoặc các tài nguyên dùng chung(services/keditor/share/general
), và bạn cần sử dụng các thông tin chia sẽ này để cung cấp giá trị cho các module khác. Câu hỏi ở đây, sao tôi lại muốn tạo một module main
chỉ để gọi các module khác, đơn giản tôi muốn sử dụng lại Terraform code, mà không cần phải lặp lại việc gọi các module khác cho từng quốc gia và môi trường.services/keditor/share
là nơi tôi chứa các tài nguyên hạ tầng được chia sẽ giữa các quốc gia và các môi trườngservices/keditor/share/general
là nơi tôi dùng để khai báo các tài nguyên được sử dụng chung và không phân biệt giữa các môi trường. Các tệp main.tf
và provider.tf
chắc là khá quen thuộc với các bạn, tệp caller_checker.tf
tôi thường sử dụng để output
một vài thông tin, mà tôi tin các thông tin này có thể giúp tôi xác nhận là tôi đang triển khai tài nguyên đúng với AWS account, Region và IAM role/user. Và tôi hay sử dụng locals.tf
để khai báo các biến cục bộ, các biến này sẽ được sử dụng trong tệp main.tf
, vì tôi tin việc tập trung tất cả thay đổi vào một nơi, sẽ giúp tôi nhanh và dễ dàng quản lý hơn. Cuối cùng, secrets
nơi tôi chứa các thông tin nhạy cảm, và các thông tin này sẽ được mã hoá bằng sops
CLI và sẽ được giải mã trong quá trình triển khai thông qua sops
module. Một bài viết khác có tựa đề Terraform best practices
tôi sẽ nói chi tiết hơn về sops
.services/keditor/share/stg
là nơi khai báo các tài nguyên dùng chung cho tất cả quốc gia trên môi trường staging.services/keditor/share/prd
là nơi khai báo các tài nguyên dùng chung cho tất cả quốc gia trên môi trường production.services/keditor/stg
: Nơi chứa các Terraform code để triển khai tài nguyên cho từng quốc gia trên môi trường staging.services/keditor/stg/hk-stg
là nơi tôi sẽ viết terraform code cho quốc gia HongKong, tệp main.tf
sẽ gọi sops
module để giải mã các thông tin nhạy cảm được mã hoá qua sops
CLI, jssonnet
module tôi dùng để đọc tệp config.jssonet
, tệp này là nơi tôi sẽ khai báo các biến, được sử dụng trong main
module. Một câu hỏi sao phai sử dụng jssonet
mà không phải tfvar
hoặc locals
đơn giản, nếu bạn đã từng sử dụng ecspresso
để quản lý deploy ứng dụng trên ECS
, thì có thể hiểu tôi muốn sử dụng lại tệp cầu hình này trong quá trình deployment. Tương tự cho các quốc gia khác.services/keditor/prd/hk-prd
mục đích sử dụng tương tự như trên tuy nhiên tài nguyên sẽ được triển khai cho môi trường production. Các thông số cấu hình như, số lượng instances, EBS size hay loại instance,… có thể khác so với môi trường staging.Sau khi chuẩn bị xong tất cả mã Terraform cần thiết để triển khai tài nguyên hạ tầng, công việc tiếp theo của tôi là tạo một IAM role với least-privilege permissions. Ở bước này, tôi thường thực hiện 3 việc sau đây:
Tôi có một dự định để giảm thiểu thời gian cho vấn đề này. Tôi sẽ tạo ra một công cụ dựa trên terraform json output sau đó dựa trên mỗi tài nguyên tương ứng để tự động tạo IAM policy cho role. Tôi dự định sẽ viết nó trong thời gian ngắn và sẽ cập nhật cho các bạn sau khi nó hoàn thành. Hoặc nếu anh chị nào biết có công cụ nào với ý tưởng tương tự, có thể comment dưới đây, và đó là điều tuyệt vời.
Với cách thiết kế này, việc tận dụng module
và terraform_remote_state
cho phép tôi sử dụng lại mã Terraform cũng như dữ liệu từ các không gian làm việc Terraform khác.
Tôi rất tiếc không thể chia sẻ mã Terraform mẫu ở trên để mọi người xem. Vì mã mẫu đó cần được làm chính xác và chỉnh chu hơn, sau khi hoàn thành, tôi sẽ cập nhật Github repo trong bài viết này. Hoặc nếu các bạn có vấn đề gì có thể trực tiếp inbox qua Linkedin, tôi sẽ giúp đỡ trong khả năng của mình.
Như đã chia sẻ ở trên, tôi đang viết một loạt bài về Terraform, và phần các mẫu thiết kế trong Terraform tôi xin phép dừng lại ở đây. Nếu có bất kỳ cập nhật hoặc tìm thấy mẫu thiết kế mới, tôi sẽ tiếp tục viết phần 3 cho loạt bài này. Bài tiếp theo của tôi dự định sẽ viết về Terraform Best Practices, mong anh chị sẽ đón nhận bài viết này.