Argo CD là công cụ khai báo phân phối liên tục GitOps cho Kubernetes, đưa trạng thái mong muốn từ Git đến trạng thái thực tế trên cluster - vòng lặp đó thường gọi là "reconciliation".
Vận hành Argo CD ở quy mô nhiều cluster cần một chiến lược nhất quán và đáng tin cậy. Mỗi tổ chức chọn một topology khác nhau, và câu hỏi cần trả lời là: topology đó phục vụ nhu cầu nào, thêm cluster phải sửa bao nhiêu chỗ, và ai vận hành phần nào.
Thông thường, để ship một application lên môi trường, Developer phải mở ticket, chờ DevOps xử lý rồi kiểm tra lại, dù chỉ đổi version, đổi port hay scale deployment. Thời gian của Developer và workload của DevOps là hai điểm nghẽn tuyến tính với nhau, làm trì trệ cả SDLC.
Thử đếm: một fleet 5 cluster trên 4 môi trường với 20 application, nếu viết tay từng file Application, bạn đang giữ 100 file lặp lại gần như y hệt nhau. Không ai làm sai gì cả, nhưng chi phí vận hành tăng theo phép nhân.
Vậy Developer cần làm gì để ship một application mới từ local lên dev, hay promote từ dev lên staging? Thường là mở ticket, chuẩn bị pre-deploy, rồi chờ trong SLA đã thống nhất. Tưởng tượng 5 ticket, 4 giờ SLA mỗi cái, trong khi DevOps đang support 10 người giống bạn.
Bài viết này giới thiệu một kiến trúc GitOps dùng environment-as-folder dạng monorepo, ship workload đến multi-cluster qua một Argo CD Hub duy nhất.
Argo CD Application và ApplicationSet
Mọi thứ Argo CD quản lý đều đi qua một CRD tên là Application. Nó trỏ tới một repo, một path và một cluster đích, tức nói cho controller biết lấy manifest ở đâu và đưa đến đâu. Ở quy mô fleet, câu hỏi vì thế không còn là Argo CD hoạt động thế nào, mà là ai sở hữu manifest nào và những Application đó từ đâu ra. Phần này trả lời cả hai, trước bằng cách phân loại manifest theo chủ sở hữu, sau bằng ApplicationSet, thứ sinh Application thay vì để bạn viết tay.
Phân loại Manifest theo chủ sở hữu
Tôi gọi chung các loại manifest do Argo CD quản là Argo Manifest. Tôi chia chúng làm hai loại theo người sở hữu, không theo loại resource, vì chủ sở hữu mới là thứ quyết định ai được thao tác trên manifest đó.
Application Manifest định nghĩa workload của sản phẩm, dịch vụ mà Developer muốn triển khai đến. Đây có thể là frontend, backend hay middleware của team A và team B, của product X và product Y nào đó.
Infrastructure Manifest định nghĩa những workload mà Operator tự quản, và DevOps team sở hữu chúng. Nhóm này rộng hơn nhóm trên, gồm ba loại khá khác nhau:
- Thành phần cấu hình chính cluster, ví dụ Argo CD, coredns, ingress controller, secret operator hay cert-manager.
- Application phục vụ việc quản trị và vận hành fleet, ví dụ dashboard, monitoring hay backup.
- Infrastructure software phục vụ Developer trong quá trình phát triển phần mềm, ví dụ memcache, message queue hay database client.
| Loại manifest | Chủ sở hữu |
| Application Manifest | Developer & DevOps team |
| Infrastructure Manifest | DevOps team |
Argo CD được xếp vào nhóm Infrastructure Manifest, và chúng sẽ tự "reconcile" sau khi lần bootstrap đầu tiên hoàn tất. Chi tiết sẽ được trình bày ở mục Ngoại lệ duy nhất là chính Argo CD.
ApplicationSet thay thế Application truyền thống như thế nào
Một Argo CD Application được định nghĩa ở dạng:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: production-backend
spec:
project: default
source:
repoURL: https://github.com/acme/apps.git
path: workloads/backend/charts/production
destination:
server: https://k8s-prod.internal:6443
namespace: shopApplication cần gắn trực tiếp spec.destination.server vào một cluster cụ thể và spec.source.path trỏ đến một Kubernetes manifest folder. Thêm một application mới nghĩa là thêm một Application, sửa lại đúng những field trên rồi apply lại.
Vấn đề sẽ rất ổn nếu bạn quản lý chỉ cần 1, 10 hoặc 30 Application files. Nhưng sẽ ra sao nếu con số là 1000? Khi đó, bạn sẽ phải rất mất thời gian và bối rối khi cần tìm chính xác Application nào đó. Với cách tiếp cận này, bạn hoàn toàn "rất khó" có thể mở rộng phạm vi quản lý Argo CD.
ApplicationSet được sinh ra để đảo ngược quan hệ đó. Bạn không cần viết Application nữa, bạn khai báo các hàm để sinh ra chúng. Đầu vào của hàm là các generator, và mỗi generator trả về một tập parameter để đổ vào template một cách tự động. Tìm hiểu thêm trong tài liệu ApplicationSet Generators.
Trong kiến trúc này, tôi sử dụng 3 loại generator gồm: Git generator, Cluster generator và Matrix generator.
Git generator
Argo CD chỉ có một git generator, với hai chế độ khác nhau là files và directories (Argo CD docs).
# Infrastructure, khớp theo thư mục
- git:
directories:
- path: workloads/*/charts/dev
# Workload, khớp theo sự có mặt của một file
- git:
files:
- path: workloads/*/charts/dev/app.yamlChế độ directories khớp mọi thư mục đúng pattern, nên hễ thư mục tồn tại là application sẽ tồn tại. Chế độ files khớp một file cụ thể, nên application chỉ tồn tại khi file đó tồn tại, và nội dung file được render vào template thành parameters. Xóa file đi thì theo mặc định Application tương ứng bị controller xóa theo, không ApplicationSet nào phải sửa.
# workloads/backend/charts/dev/app.yaml
namespace: shopĐây được xem là công tắc bật/tắt application theo môi trường, và phần cấu trúc thư mục bên dưới sẽ dùng đúng cơ chế này.
Cluster generator
Argo CD lưu thông tin cluster trong dạng Secret, và ApplicationSet controller đọc đúng những Secret đó để sinh parameter (Argo CD docs). Đăng ký một cluster mới nghĩa là tạo một Secret có label argocd.argoproj.io/secret-type: cluster.
apiVersion: v1
kind: Secret
metadata:
name: cluster-dev
namespace: argocd
labels:
argocd.argoproj.io/secret-type: cluster
env: dev
type: Opaque
stringData:
name: dev
server: https://k3d-dev-server-0:6443Cluster generator sử dụng selector theo đúng ngữ nghĩa label selector chuẩn của Kubernetes, tức matchLabels và matchExpressions kết hợp với nhau theo quan hệ AND. Muốn quan hệ OR giữa các value của cùng một key thì bạn dùng matchExpressions với operator In.
# Chọn cluster production ở châu Á hoặc Mỹ.
- clusters:
selector:
matchLabels:
env: production
matchExpressions:
- key: region
operator: In
values: [asia, us]Matrix generator
Hai generator trên trả về hai tập parameter độc lập. Matrix generator là ma trận ghép nối chúng với nhau, và mỗi cặp sẽ có đủ các field cần thiết mà một Application cần. Từ đó, Application được sinh ra tự động.
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: "dev"
- git: # sử dụng git generator với repoURL
repoURL: https://github.com/nh4ttruong/argocd-gitops-spokes.git
revision: HEAD
files:
- path: workloads/*/charts/dev/app.yamlPhần template nhận parameter của cả hai và ghép chúng thành một Application. Dựa trên path workloads/*/charts/dev/app.yaml, ta có ví dụ:
template:
metadata:
name: "{{index .path.segments 3}}-{{index .path.segments 1}}" # dev-backend
spec:
project: "{{index .path.segments 3}}-apps" # dev-apps
sources:
- repoURL: <repo spokes>
path: "{{.path.path}}" # workloads/backend/charts/dev
destination:
server: "{{.server}}" # từ cluster generator
namespace: "{{.namespace}}" # từ nội dung app.yamlMặc định ApplicationSet dùng fasttemplate, nơi bạn viết {{path[3]}}. Tài liệu ghi rõ nó sẽ bị thay bằng Go Template. Bật goTemplate: true thì cùng giá trị đó viết thành {{index .path.segments 3}}, và nhờ hàm index mà bạn ghép được tên theo quy ước ngay trong template như ví dụ trên.
Chỗ hay nhầm nhất là biến nào đến từ đâu, vì template trộn parameter của hai generator vào cùng một chỗ.
| Biến | Nguồn | Giá trị ví dụ |
{{.server}} | cluster generator | Endpoint API server của spoke |
{{.path.path}} | git generator | workloads/backend/charts/dev |
{{index .path.segments 1}} | git generator | backend |
{{index .path.segments 3}} | git generator | dev |
{{.namespace}} | git generator | namespace: shop trong app.yaml |
goTemplateOptions: ["missingkey=error"] biến việc tra một key không tồn tại thành lỗi thay vì bị bỏ qua im lặng (Argo CD docs). Với một hệ thống chạy bằng convention thì đây là thứ chặn lỗi đặt tên ngay tại chỗ. Một app.yaml thiếu key làm việc sinh Application thất bại, thay vì tạo ra một Application có namespace trống.
Tổ chức Repository: cấu trúc và quy ước
Chia repository theo chủ sở hữu, không theo môi trường
Kiến trúc này chia repo theo chủ sở hữu, không chia theo môi trường. Repo Hub thuộc team DevOps và giữ mọi thứ của Argo CD, gồm root app, Application môi trường, ApplicationSet, AppProject và chart hạ tầng. Các repo Spokes thuộc các team developer và chỉ giữ chart cùng values của sản phẩm. Lý do chia như vậy là quyền ghi trên Git cấp theo repo, nên ranh giới repo là ranh giới duy nhất mà Git cưỡng chế được.
Một hub cộng một spokes là hình dạng nhỏ nhất dùng được, còn số repo spokes thì tùy tính chất tổ chức. Mỗi team một repo, hoặc mỗi product một repo, hoặc một repo dùng chung cho các workload cross-team. Thứ duy nhất đổi trong hub là repoURL của ApplicationSet workload.
# appsets/dev/workloads/dev-payments-team-helm.yaml
- git:
repoURL: https://github.com/<org>/team-payments.git # Thay đổi duy nhất
revision: HEAD
files:
- path: workloads/*/charts/dev/app.yamlQuy ước bên trong mỗi repo spokes giữ nguyên, nên thêm một repo là thêm một ApplicationSet chứ không phải viết lại gì. Vậy có nên tách repo theo mức độ rủi ro, tức non-production và production riêng lẻ được không? Câu trả lời nằm ở mục Có nên tách Production và Non-Production.
Tự phục vụ trong khuôn khổ AppProject
Nhiều repo cho các team tự quản trị đã giải quyết phần tự phục vụ. AppProject lo phần còn lại, tức giới hạn mỗi team được đáp xuống đâu và tạo được resource gì.
# workloads/argocd/envs/hub/app-projects/dev-apps.yaml
spec:
sourceRepos:
- 'https://github.com/nh4ttruong/argocd-gitops-spokes.git'
destinations:
- namespace: '*'
server: 'https://k3d-dev-server-0:6443'
clusterResourceWhitelist:
- group: ''
kind: 'Namespace' # Chỉ Namespace ở cluster scope
namespaceResourceWhitelist:
- group: '*'
kind: '*'Ba field trên chặn ba chuyện: sourceRepos chỉ định repo nguồn, destinations.server định nghĩa cluster đích, còn clusterResourceWhitelist cho thấy phạm vi của resource. Developer vì thế toàn quyền trong namespace nhưng không tạo được ClusterRole hay CRD, kể cả khi họ merge đúng những thứ đó vào repo của mình.
Cấu trúc của Hub repo
Hub repository được trình bày dựa trên cấu trúc thư mục sau:
hub/
├── root-app-of-apps.yaml # App of Apps tầng 1, source thư mục apps/
├── apps/ # App of Apps tầng 2, mỗi file source appsets/<env>/
│ ├── hub.yaml # cũng là nơi đặt policy của từng môi trường
│ ├── dev.yaml
│ ├── staging.yaml
│ └── production.yaml
├── appsets/ # 14 ApplicationSet, một cái cho mỗi (env, engine)
│ ├── hub/infrastructure/
│ ├── dev/
│ │ ├── infrastructure/
│ │ │ ├── dev-infrastructure-helm.yaml
│ │ │ └── dev-infrastructure-kustomize.yaml
│ │ └── workloads/
│ │ ├── dev-application-helm.yaml
│ │ └── dev-application-kustomize.yaml
│ ├── staging/
│ └── production/
├── workloads/ # Infrastructure của chính platform
├── argocd/envs/hub/ # Argo CD tự quản chính nó
├── cert-manager/
│ ├── charts/<env>/Chart.yaml
│ └── values/{common.yaml,<env>/values.yaml}
├── ingress-nginx/
└── coredns/envs/<env>/
Thư mục này là sự kết hợp giữa App of Apps pattern và Application Sets & Cluster Label pattern.
root-app-of-apps.yamllà file định nghĩa chứasourcetrỏ đến thư mụcapps/.- Mỗi file trong
apps/là một environment filedev.yaml,staging.yaml..., các file này có source trỏ đếnappsets/<env>/folder. Mục đích việc chia ra là để dễ quản lý từ environment đến workload. - Thư mục
appsets/gồm cácApplicationSetđịnh nghĩa phẳng theo môi trường, và mỗi môi trường lại tách thànhinfrastructure/vớiworkloads/. Cách chia này để dễ phân biệt hai nhóm, vì nhánhinfrastructure/chứa các tập hợp infrastructure application còn nhánhworkloads/trỏ đến các spokes application. - Thư mục
workloads/chứa chính Argo CD (self-bootstrap) cùng các infrastructure software mà Cluster Hub dùng, ví dụ ingress-nginx, coredns hay cert-manager. Mỗi component ở đây theo một trong hai hình dạng thư mục,charts/<env>/cho Helm vàenvs/<env>/cho Kustomize. Tôi nói kỹ hai hình dạng đó ở mục Chuyển hóa manifest thành Application.
Bản thân root-app-of-apps.yaml cũng source chính nó qua directory.include, nên sau lần bootstrap đầu tiên thì nó nằm trong vòng reconcile như mọi manifest khác.
# root-app-of-apps.yaml
sources:
- path: .
repoURL: https://github.com/nh4ttruong/argocd-gitops-hub.git
targetRevision: HEAD
directory:
include: root-app-of-apps.yaml
- path: apps
repoURL: https://github.com/nh4ttruong/argocd-gitops-hub.git
targetRevision: HEADCấu trúc các Spoke repo
Cây spokes là bản giản lược của cây hub. Không có root-app-of-apps.yaml, không có apps/, cũng không có appsets/, chỉ còn workloads/. Lý do là developer không cần biết Argo CD, nên repo này không chứa manifest Argo CD nào ngoài app.yaml.
Mỗi ApplicationSet ở appsets/<env>/workloads/ của Hub repo trỏ vào repo này rồi khớp glob workloads/*/charts/<env>/app.yaml. Nhờ vậy Hub biết app nào đang bật ở môi trường nào mà không ai phải khai báo thêm.
Một hub phục vụ được nhiều repo dạng này. Mỗi team một repo, và mỗi repo thêm một ApplicationSet trong hub với repoURL khác, còn quy ước thư mục thì giữ nguyên. Tôi đã nói kỹ chuyện đó ở mục Chia repository theo chủ sở hữu phía trên, còn phần phân quyền đi theo nằm ở mục Tự phục vụ trong khuôn khổ AppProject.
spokes-apps/
└── workloads/
├── backend/ # App Helm, wrapper chart quanh chart thật
│ ├── charts/
│ │ ├── dev/{Chart.yaml,app.yaml}
│ │ ├── staging/{Chart.yaml,app.yaml}
│ │ └── production/{Chart.yaml,app.yaml}
│ └── values/
│ ├── common.yaml # Giá trị dùng chung cho mọi môi trường
│ ├── dev/values.yaml # Override riêng của dev
│ ├── staging/values.yaml
│ └── production/values.yaml
├── homepage/ # Cùng hình dạng, cùng namespace shop
├── webhook/
└── whoami/ # App Kustomize, base cộng overlay
├── base/{deployment.yaml,service.yaml,kustomization.yaml}
└── envs/
├── dev/{kustomization.yaml,app.yaml}
├── staging/{kustomization.yaml,app.yaml}
└── production/{kustomization.yaml,app.yaml}Repo spokes dùng đúng hai hình dạng thư mục đó, backend theo Helm còn whoami theo Kustomize. Generator vì thế biết phải render bằng engine nào.
Có thể bạn sẽ hỏi vì sao không dùng một branch cho mỗi môi trường. Lý do gọn nhất là cả Helm và Kustomize đều tách môi trường bằng file chứ không bằng branch, đúng như charts/<env>/ và envs/<env>/ trong hai cây trên. Dùng branch là đi ngược lại chính công cụ render. Lý do nặng hơn là với GitOps thì branch chính là trạng thái đang chạy, nên một lần merge không còn là đề nghị deploy mà là một lần deploy.
7 quy ước cốt lõi
Bảy quy ước dưới đây là toàn bộ thứ cần biết để hiểu được một application mới được định nghĩa sẽ nằm ở đâu.
| Việc | Quy ước | Ai quyết định |
| App Helm | workloads/<app>/charts/<env>/Chart.yaml là wrapper chart, values ghép values/common.yaml rồi values/<env>/values.yaml | Developer |
| App Kustomize | workloads/<app>/envs/<env>/kustomization.yaml là overlay của base/ | Developer |
| Bật app trong một môi trường | Đặt app.yaml cạnh chart hoặc overlay của môi trường đó | Developer |
| Namespace đích | Workload lấy từ app.yaml, infrastructure lấy tên app | Developer, hoặc suy ra với infrastructure |
Tên Application sinh ra | {env}-{app}, suy từ segment của đường dẫn | Không ai, generator tự ghép |
AppProject | Infrastructure vào {env}-infra, workload vào {env}-apps | Template trong hub |
| Chọn cluster | Label env trên cluster secret | Team DevOps |
Hãy để ý là không dòng nào trong bảng yêu cầu bạn sửa một ApplicationSet. Thêm một app, bật app đó ở một môi trường mới, hay thêm một cluster đều chỉ là thêm file hoặc thêm label, không việc nào phải cần sửa generator. Đây chính là tiêu chí tôi dùng để đánh giá một cấu trúc thư mục. Hễ một việc thường ngày lại buộc tôi sửa generator thì quy ước đã sai ở đâu đó.
Cái giá của việc chạy bằng quy ước là một đường dẫn đặt sai tên sẽ không sinh ra gì cả. Glob không khớp thì generator im lặng bỏ qua, không có Application nào xuất hiện và cũng không có lỗi nào báo lên. missingkey=error chỉ chặn được phần thiếu key, nó không biết bạn định đặt file ở đâu.
Chuyển hóa manifest thành Application
Generator quyết định Application nào tồn tại, còn engine render mới là thứ biến một thư mục thành manifest Kubernetes. Kiến trúc này dùng cả Helm và Kustomize cùng lúc, đây là lựa chọn có chủ ý.
Helm với wrapper chart
App Helm không chứa template nào. Nó là một umbrella chart chỉ khai báo dependency trỏ tới chart thật.
# workloads/backend/charts/dev/Chart.yaml
apiVersion: v2
name: backend
version: 0.1.0
appVersion: 6.14.1
dependencies:
- name: podinfo
version: 6.14.1
repository: https://stefanprodan.github.io/podinfoCách này giữ version upstream ở một dòng duy nhất, và helm dependency lo phần cập nhật remote chart. Values thì đến từ một source khác. Source thứ hai đặt ref: values để tạo biến $values trỏ về gốc repo, và biến này chỉ dùng được ở đầu đường dẫn value file (Argo CD docs). Nhờ vậy common.yaml và <env>/values.yaml là hai file tách riêng thay vì một khối lặp lại cho từng môi trường.
Thứ tự trong valueFiles là thứ quyết định thứ tự ưu tiên của giá trị sẽ dùng. Helm nạp lần lượt từ trên xuống và file sau override file trước, nên common.yaml đặt mặc định cho mọi môi trường còn <env>/values.yaml chỉ cần khai báo đúng những gì môi trường đó khác. Một key không xuất hiện ở file môi trường thì lấy giá trị từ common.yaml. Đó là lý do đảo thứ tự hai dòng này là đổi hành vi, không phải đổi hình thức.
Đến đây thì mọi mảnh đã có mặt, nên bạn đọc được một ApplicationSet hoàn chỉnh. Đây là bộ sinh workload Helm cho dev, và ba bộ còn lại khác nó đúng ở tên môi trường.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: dev-application-helm
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: "dev"
- git:
repoURL: https://github.com/nh4ttruong/argocd-gitops-spokes.git
revision: HEAD
files:
- path: workloads/*/charts/dev/app.yaml
template:
metadata:
name: "{{index .path.segments 3}}-{{index .path.segments 1}}"
spec:
project: "{{index .path.segments 3}}-apps"
sources:
- repoURL: https://github.com/nh4ttruong/argocd-gitops-spokes.git
targetRevision: HEAD
path: "{{.path.path}}"
helm:
valueFiles:
- $values/workloads/{{index .path.segments 1}}/values/common.yaml
- $values/workloads/{{index .path.segments 1}}/values/{{index .path.segments 3}}/values.yaml
ignoreMissingValueFiles: true
- repoURL: https://github.com/nh4ttruong/argocd-gitops-spokes.git
targetRevision: HEAD
ref: values
destination:
server: "{{.server}}"
namespace: "{{.namespace}}"ignoreMissingValueFiles: true cho phép một môi trường không override gì cả. Cái giá là một file values đặt sai tên bị bỏ qua im lặng thay vì báo lỗi. missingkey=error chặn được loại lỗi đó ở tầng template, nhưng ở tầng này thì không.
Kustomize với base và overlay
App Kustomize giữ manifest thật trong base/, còn mỗi môi trường là một overlay patch lên nó.
# workloads/whoami/envs/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
labels:
- pairs:
env: dev
patches:
- target:
kind: Deployment
name: whoami
patch: |-
- op: replace
path: /spec/replicas
value: 1Overlay của staging và production khác nhau đúng một con số. Đó là hình dạng lý tưởng của một overlay, và cũng là dấu hiệu bạn đang dùng đúng engine.
Vì sao dùng cả hai
Mỗi engine là một thuộc tính của application chứ không phải quyết định rằng tại sao dùng cái này mà không dùng cái kia. Vậy tại sao không dùng cả hai?
- Helm wrapper: Bạn chỉ cần một upstream chart hoặc một template, bạn cấu hình values là hoàn tất. Chúng ta dùng chúng như một engine theo chuẩn và cũng không cần phải sửa ApplicationSet.
- Kustomize: Như cái tên của nó, bạn hãy dùng nó như một engine để custom mọi thứ "không theo standard" như Helm wrapper. Kustomize với cơ chế patch sẽ biến mọi thứ phải ad-hocs thành những định nghĩa trong file YAML.
Không trường nào trong cấu hình khai báo engine, vì generator suy ra nó từ hình dạng đường dẫn như hai cây thư mục phía trên và cấu hình spec.template.spec.sources (Helm) và resources (Kustomize).
Bạn cũng có thể dùng một pattern khác thay thế là helmCharts của Kustomize, tức inflate chart rồi patch trên output đã render. Cái giá là mất đường điều khiển bằng values (3 patterns for deploying Helm charts with Argo CD), và --enable-helm phải bật ở kustomizeBuildOptions của chính Argo CD. Đúng một chỗ trong kiến trúc này trả cái giá đó.
Ngoại lệ duy nhất là chính Argo CD
Theo quy tắc trên thì Argo CD phải là Helm wrapper, vì có upstream chart sẵn từ https://argoproj.github.io/argo-helm repo. Bootstrap là quá trình khởi tạo Argo CD. Nhưng sau đó, bạn cần phải vận hành và quản trị chúng thông qua cơ chế GitOps. Ví dụ như khi thêm các ExternalSecrets, các AppProject, các cluster secrets để join các spoke cluster vào hub cluster, thêm một repository... Bạn không thể để plain token vào values.yaml của Argo CD chart được. Tất cả cần phải thêm vào dưới dạng external resources hoặc existingSecret / existingConfigMap. Đó là chỗ duy nhất helmCharts đáng dùng.
# workloads/argocd/envs/hub/kustomization.yaml
helmCharts:
- name: argo-cd
repo: https://argoproj.github.io/argo-helm
version: 10.3.3
releaseName: argocd
includeCRDs: true
namespace: argocd
valuesFile: argo-cd-values.yaml
resources:
- ../../base
- app-projects/
- in-cluster-registration.yamlTừ commit, sync đến Application
Dù đi qua bao nhiêu tầng generator, chuỗi bên dưới vẫn là toàn bộ đường đi từ một lần commit, sync manifest và render ra thành một Application. Quá trình này được gọi là Reconciliation.
Thứ chạm tới cluster ở cuối chuỗi là một Application thường. Nó có resource tree, có health, có diff, và bạn debug nó y như một app viết tay, chỉ thêm một badge ghi ApplicationSet nào đã sinh ra nó.
Promotion giữa các environment trong GitOps
Trong kiến trúc này, promotion không cần một pipeline riêng. Mỗi môi trường đã giữ trọn cấu hình trong thư mục của nó, nên đưa một bản release sang môi trường kế tiếp quy về việc thay đổi đúng phần khai báo đáng mang đi. Vậy chúng ta cần mang cái gì đi sang môi trường đích?
Chỉ version là thứ cần promote
Mở một file values thật ra là thấy ngay. Không dòng nào trong đó nên đi theo bạn sang môi trường khác.
# workloads/backend/values/production/values.yaml
podinfo:
replicaCount: 3 # của riêng production
logLevel: info # của riêng production
resources: # của riêng production
requests: {cpu: 100m, memory: 64Mi}
limits: {cpu: 500m, memory: 256Mi}Replica, log level và resource limit đều là thuộc tính của môi trường, nên mang chúng đi là sẽ vô tình làm hỏng môi trường đích. Thứ duy nhất đáng promote là version của image, và nó phải nằm riêng ra một chỗ.
Với app Helm thì đó là một file chỉ chứa đúng version, đặt cuối danh sách valueFiles.
# workloads/backend/values/production/version.yaml
podinfo:
image:
tag: 6.14.1valueFiles:
- $values/workloads/{{index .path.segments 1}}/values/common.yaml
- $values/workloads/{{index .path.segments 1}}/values/{{index .path.segments 3}}/values.yaml
- $values/workloads/{{index .path.segments 1}}/values/{{index .path.segments 3}}/version.yamlThứ tự ở đây là cơ chế, không phải trang trí. Lớp version phải nằm sau values.yaml, vì đảo lại thì version bị ghi đè mà không có thông báo nào. ignoreMissingValueFiles: true lo phần app nào chưa có version.yaml.
Với app Kustomize thì không có file tương đương, vì Kustomize không có cơ chế chồng thêm file như valueFiles. Đơn vị promote nằm trong kustomization.yaml của overlay, và tag phải rời khỏi base/ để mỗi môi trường tự khai báo.
# workloads/whoami/envs/production/kustomization.yaml
images:
- name: traefik/whoami
newTag: v1.10.1Chênh lệch này là lý do tôi khuyến khích dùng Helm wrapper cho workload ở spokes. File version.yaml cho promotion một đơn vị tách riêng, sạch và dễ review, còn Kustomize nên để dành cho những workload thật sự cần patch tự do như đã nói ở mục Vì sao dùng cả hai.
Có nhiều cách để promote dựa trên file, cp file version giữa hai thư mục, kustomize edit set image, một dòng sed, hay sửa tay đều được, miễn là thay đổi nào cũng thành một PR được review. Mục đích chỉ là đổi đúng cái tag đó.
Điểm chung mới là điều đáng giá. Đơn vị promote nằm riêng nên mỗi lần promote để lại diff đúng một dòng, và commit đó tự nói lên đang đưa version nào sang môi trường nào. Đọc git log của thư mục production là thấy lịch sử promote, không phải đi đọc lại cả file cấu hình.
Chuyển Application giữa các cluster
Chuyển một application sang một cluster khác không phải là migrate. Vì destination.server trong template lấy từ cluster generator, việc một application chạm tới cluster nào hoàn toàn do label trên cluster secret quyết định. Đổi label của một cluster từ dev sang staging thì các Application dev-* bị prune và staging-* được sinh trên chính cluster đó, không file nào trong Git thay đổi.
Điều đó cũng đúng cho việc thêm hay rút cluster. Thêm một cluster dev thứ hai là tạo một cluster secret gắn env: dev, và mọi app dev xuất hiện trên đó ở lần poll tiếp theo. Rút một cluster là xóa secret.
Chú ý rằng, một lần đổi cluster label sẽ khiến Argo CD prune hàng loạt workload đang chạy. Do đó, quyền sửa cluster secret phải được coi như quyền admin trên toàn fleet.
Có nên tách Production và Non-Production
Thông thường, những nơi như dev, staging và uat hay được gọi là môi trường non-production, tức nơi bạn được phép sai và sửa lại. Phần còn lại là production, môi trường sản phẩm và dịch vụ của bạn đang chạy, phục vụ khách hàng và business.
Non-production và production dùng chung toàn bộ cơ chế ở trên. Khác biệt duy nhất là sync policy trong template của các ApplicationSet thuộc môi trường production. Mọi Application sinh ra từ đó chỉ refresh và báo OutOfSync, còn muốn deploy thật thì DevOps team phải Manual Sync.
# appsets/production/workloads/production-application-helm.yaml
syncPolicy:
## production should not be automated
# automated:
# prune: true
# selfHeal: trueCòn file apps/production.yaml thì không cần gate như vậy. Nó chỉ giao các ApplicationSet từ hub repo, nơi DevOps team sở hữu và mọi thay đổi đã qua review khi merge, nên thêm một lần bấm sync ở đó chỉ là rào chắn thừa.
Còn chuyện có nên tách manifest ra repo riêng thì tùy quy mô, và có ba hình dạng dùng được.
| Mô hình | Dùng khi |
| Một repo duy nhất | Blast radius thấp, một team, một product. Đây là hình dạng của demo này |
Tách non-production và production | Developer team tự chủ hoàn toàn ở non-production, còn production cần kiểm duyệt và làm việc cross-team với DevOps team |
| Mỗi team hoặc mỗi product một repo | Team size lớn, hoặc nhiều team release liên tục |
Cái giá chỉ là số lượng repo, và đổi lại thì DevOps team không phải cân nhắc từng lần deploy ở môi trường non-production. Quyền review và merge trên repo non-production giao cho Developer Lead, còn team developer chỉ cần hiểu topology cùng vài khái niệm Kubernetes cơ bản là tự vận hành trơn tru. DevOps team lo policy và vận hành cluster, và anh em developer không còn phải tạo những ticket lặt vặt như ở đầu bài nữa.
Giới hạn cần biết của kiến trúc này
Đến rồi, đây là phần còn lại của bài viết này. Giới hạn của topology này.
- Chưa có progressive sync. Một thay đổi cấu hình có thể sync đồng thời mọi cluster trong một môi trường, và đó là failure mode của kiến trúc này. Giới hạn này đã được đề cập và đã có tính năng RollingSync đang ở beta từ Argo CD version 3.3. Nó cho phép chia Application thành từng nhóm và sync lần lượt.
- Hub giữ credential của mọi spoke cluster. Ai chiếm được hub là chiếm được cả fleet. Mô hình push dồn toàn bộ rủi ro security về một chỗ, nên hardening cho hub cluster là việc bắt buộc, không phải tùy chọn.
- Hub cũng là single point of failure cho việc sync. Hub chết thì fleet vẫn chạy và workload không mất. Nhưng mọi thay đổi mới ngừng được deploy, và selfHeal ngừng sửa drift cho tới khi hub sống lại.
- Label schema là một cam kết trong thiết kế. Bộ label trên cluster secret phải được chốt từ đầu. Tiêu chí không phải sửa generator chỉ đúng với những dimension đã lường trước, còn muốn thêm một chiều mới về sau, ví dụ
env,regionhaytier, thì phải sửa lại các ApplicationSet. - Cả fleet đang follow
HEADcủa một branch. Merge vào non-production là live ngay trên mọi cluster của môi trường đó. Đó là tính năng, nhưng cũng nghĩa là bạn phải thực sự cẩn thận và hiểu mình đang làm gì, vì mọi sai lầm đều phải trả giá bằng thời gian.
Demo
Toàn bộ hệ thống chạy được trên k3d, và bootstrap với hai lệnh.
kustomize build workloads/argocd/envs/hub --enable-helm \
| kubectl apply --server-side --force-conflicts -f -
kubectl apply -f root-app-of-apps.yamlHai lệnh đó đã được gói sẵn trong bootstrap.sh cùng bước chờ CRD, nên demo chạy theo ba bước, dựng cluster trước rồi gắn spoke sau.
./demo/k3d/up.sh hub dev # Dựng cluster, hub trước
./demo/k3d/bootstrap.sh # Argo CD trên hub, credential Git, root app
./demo/k3d/register-spoke.sh dev # Join cluster dev làm spokeKhoảng một phút sau khi đăng ký, chuỗi phản ứng hoàn tất và mọi app dev đều Healthy. Xem application đang được gán vào cluster nào, rồi đổi một label.
$ kubectl -n argocd get applications -l env=dev
NAME SYNC STATUS HEALTH STATUS
dev Synced Healthy
dev-backend Synced Healthy
dev-cert-manager Synced Healthy
dev-coredns Synced Healthy
dev-homepage Synced Healthy
dev-ingress-nginx Synced Healthy
dev-webhook Synced Healthy
dev-whoami Synced Healthy
$ kubectl -n argocd get applications -l env=staging
NAME SYNC STATUS HEALTH STATUS
staging Synced Healthy
# Đổi một nhãn, không sửa file nào trong Git
$ kubectl -n argocd label secret cluster-dev env=staging --overwrite
secret/cluster-dev labeledVài chục giây sau, các Application dev-* bị prune và bộ staging-* được sinh trên chính cluster đó. Cùng một cây thư mục trong Git, hai tập Application khác nhau, và khác biệt duy nhất là nhãn của cluster.
$ kubectl -n argocd get applications -l env=staging
NAME SYNC STATUS HEALTH STATUS
staging Synced Healthy
staging-backend Unknown Unknown
staging-cert-manager Unknown Unknown
staging-coredns Unknown Unknown
staging-homepage Unknown Unknown
staging-ingress-nginx Unknown Unknown
staging-webhook Unknown Unknown
staging-whoami Unknown UnknownĐể ý cột trạng thái. Các app staging-* được sinh ra nhưng đứng ở Unknown, vì AppProject staging-apps trong demo đang ghim destination là placeholder của cluster staging thật.
$ kubectl -n argocd get application staging-backend \
-o jsonpath='{.status.conditions[0].message}'
application destination server 'https://k3d-dev-server-0:6443' and namespace 'shop'
do not match any of the allowed destinations in project 'staging-apps'Generator dựa vào cluster label, còn AppProject thì không. Một cluster bị dán nhãn nhầm sẽ nhận đúng bộ Application của môi trường mới, nhưng bị chặn ngay tại ranh giới project khi cluster đó chưa được khai là destination hợp lệ. Đây là cơ chế ở mục Tự phục vụ trong khuôn khổ AppProject làm việc ngoài đời, và cũng là lớp đỡ cuối cho cảnh báo về quyền sửa cluster secret ở mục trên.
Trả nhãn về env=dev thì fleet tự hồi phục về đúng trạng thái khai báo trong Git, không cần ai bấm gì, nhờ sync policy automated của non-production.
$ kubectl -n argocd label secret cluster-dev env=dev --overwrite
secret/cluster-dev labeled
$ kubectl -n argocd get applications -l env=dev
NAME SYNC STATUS HEALTH STATUS
dev Synced Healthy
dev-backend Synced Healthy
dev-cert-manager Synced Healthy
dev-coredns Synced Healthy
dev-homepage Synced Healthy
dev-ingress-nginx Synced Healthy
dev-webhook Synced Healthy
dev-whoami Synced HealthyCode đầy đủ nằm ở hai repository, gồm argocd-gitops-hub cho control plane và argocd-gitops-spokes cho workload.
Kết luận
Vấn đề về thời gian của Developer và workload của DevOps team đang tuyến tính với nhau, vì mọi thay đổi đều phải qua tay người. Kiến trúc này gỡ nút thắt đó bằng cách đưa mọi thông tin vận hành về dạng dữ liệu máy đọc được: cluster là một nhãn, môi trường là một thư mục, workload là file YAML, và ApplicationSet chỉ còn là engine ghép chúng lại.
Developer thêm một thư mục, mở PR trong repo của mình, application xuất hiện đúng môi trường cần, không ticket, không quyền ghi vào Argo CD. DevOps team cũng nhẹ gánh workload xử lý ticket, mở rộng fleet chỉ còn là thêm spoke bằng cluster secret, và chỉ cần quan tâm đến Hub, Spokes và phân quyền.
Muốn tự kiểm chứng, hãy chạy demo ở trên và đổi một nhãn trên cluster secret. Fleet tự sắp xếp lại mà không file nào trong Git thay đổi, và đó là toàn bộ tinh thần của kiến trúc này.








