DevOps: Helm, Kustomize và GitOps với Argo CD (Intermediate++)
Khi số lượng service và môi trường tăng lên, kubectl apply -f không còn đủ nữa. Bài này đi vào cách quản lý Kubernetes config ở quy mô thực sự: Helm để package hoá, Kustomize để overlay theo môi trường, và Argo CD để GitOps — biến Git thành nguồn sự thật duy nhất của cluster.
1. Helm — Package Manager cho Kubernetes
1.1 Tại sao cần Helm?
Hãy tưởng tượng bạn deploy cùng một service lên 3 môi trường: dev, staging, production. Mỗi môi trường có:
- Image tag khác nhau.
- Replica count khác nhau.
- Resource limits khác nhau.
- Domain và secret khác nhau.
Không dùng Helm → copy-paste YAML, sửa từng dòng, dễ nhầm. Dùng Helm → một bộ template + một file values per môi trường.
1.2 Cấu trúc Helm chart
payment-api/
├── Chart.yaml # Metadata: name, version, dependencies
├── values.yaml # Giá trị mặc định
├── values-staging.yaml # Override cho staging
├── values-prod.yaml # Override cho production
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── configmap.yaml
│ ├── _helpers.tpl # Shared template helpers (functions)
│ └── NOTES.txt # In ra sau khi install
└── charts/ # Sub-charts (dependencies)
Chart.yaml:
apiVersion: v2
name: payment-api
description: Payment API service
type: application
version: 0.5.2 # Chart version (SemVer)
appVersion: "1.23.0" # App version được deploy
dependencies:
- name: postgresql
version: "13.2.0"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # Chỉ install khi enabled
1.3 Template language — Go templates + Sprig
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "payment-api.fullname" . }} # Dùng helper từ _helpers.tpl
labels:
{{- include "payment-api.labels" . | nindent 4 }} # nindent: indent + newline
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "payment-api.selectorLabels" . | nindent 6 }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }} # Nhúng nguyên YAML block
{{- if .Values.env }}
env:
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
{{- end }}
# templates/_helpers.tpl
{{- define "payment-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "payment-api.labels" -}}
helm.sh/chart: {{ include "payment-api.chart" . }}
{{ include "payment-api.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
1.4 Values — thiết kế tốt
# values.yaml — defaults (mọi môi trường đều kế thừa)
replicaCount: 1
image:
repository: ghcr.io/myorg/payment-api
pullPolicy: IfNotPresent
tag: "" # Trống → dùng Chart.AppVersion
service:
type: ClusterIP
port: 8080
ingress:
enabled: false # Feature flag — off by default
className: nginx
annotations: {}
hosts: []
tls: []
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 60
postgresql:
enabled: false # Dependency disabled by default
env: [] # Extra env vars
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
serviceAccount:
create: true
name: ""
# values-prod.yaml — chỉ override những gì khác
replicaCount: 3
image:
tag: "1.23.0" # Pinned version cho production
ingress:
enabled: true
hosts:
- host: api.payment.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: payment-api-tls
hosts:
- api.payment.example.com
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 50
1.5 Helm commands workflow
# Render template cục bộ (không deploy) — để debug
helm template payment-api ./payment-api --values values-prod.yaml
# Dry-run với server validation
helm install payment-api ./payment-api \
--values values-prod.yaml \
--namespace payments \
--dry-run --debug
# Install / upgrade (idempotent với --install)
helm upgrade --install payment-api ./payment-api \
--namespace payments \
--create-namespace \
--values values-prod.yaml \
--set image.tag=${GIT_SHA} \ # Override inline từ CI
--wait \ # Chờ deployment ready
--timeout 5m \
--atomic # Rollback tự động nếu fail
# Xem lịch sử release
helm history payment-api -n payments
# Rollback về revision cũ
helm rollback payment-api 3 -n payments
# Diff trước khi apply (cần plugin helm-diff)
helm diff upgrade payment-api ./payment-api --values values-prod.yaml -n payments
1.6 Helm hooks
Hooks cho phép chạy Job tại các điểm trong lifecycle của release:
# templates/db-migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "payment-api.fullname" . }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install # Chạy TRƯỚC khi deploy
"helm.sh/hook-weight": "-5" # Thứ tự (số nhỏ hơn = chạy trước)
"helm.sh/hook-delete-policy": hook-succeeded # Xoá job sau khi thành công
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "up"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: payment-api-secret
key: database-url
1.7 Helm tests
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "payment-api.fullname" . }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox
command: ['wget', '--spider', '--timeout=5',
'{{ include "payment-api.fullname" . }}:{{ .Values.service.port }}/healthz']
helm test payment-api -n payments
2. Kustomize — Config Management không template
2.1 Triết lý: patch, không template
Kustomize không dùng Go templates như Helm. Thay vào đó, bạn viết YAML thuần và dùng patches để override theo môi trường. Không có {{ }}, không có logic.
k8s/
├── base/ # Config gốc, không có secret nào
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── hpa.yaml
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── deployment-patch.yaml
└── production/
├── kustomization.yaml
├── deployment-patch.yaml
└── hpa-patch.yaml
base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- hpa.yaml
commonLabels:
app: payment-api
team: payments
images:
- name: payment-api # Tên trong deployment
newName: ghcr.io/myorg/payment-api
newTag: latest
base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
spec:
replicas: 1
selector:
matchLabels:
app: payment-api
template:
spec:
containers:
- name: payment-api
image: payment-api # Kustomize sẽ thay thế
resources:
requests:
cpu: 100m
memory: 128Mi
overlays/production/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payments-prod
bases:
- ../../base
resources:
- ingress.yaml # Thêm resource chỉ có ở production
- network-policy.yaml
patches:
- path: deployment-patch.yaml
target:
kind: Deployment
name: payment-api
images:
- name: payment-api
newName: ghcr.io/myorg/payment-api
newTag: "1.23.0" # Pinned version
replicas:
- name: payment-api
count: 3
configMapGenerator:
- name: payment-api-config
literals:
- LOG_LEVEL=warn
- ENVIRONMENT=production
secretGenerator: # Tạo secret với hash suffix (tự động rollout khi thay đổi)
- name: payment-api-secret
files:
- secret.env # File local, không commit
options:
disableNameSuffixHash: false
overlays/production/deployment-patch.yaml (Strategic Merge Patch):
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
spec:
template:
spec:
containers:
- name: payment-api
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
env:
- name: ENVIRONMENT
valueFrom:
configMapKeyRef:
name: payment-api-config
key: ENVIRONMENT
2.2 JSON Patch — thay đổi cụ thể hơn
# Xoá một field, thêm một element cụ thể trong array
patches:
- target:
kind: Deployment
name: payment-api
patch: |-
- op: replace
path: /spec/template/spec/containers/0/imagePullPolicy
value: Always
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: NEW_VAR
value: new-value
2.3 Helm vs Kustomize — khi nào dùng gì?
| Tiêu chí | Helm | Kustomize |
|---|---|---|
| Logic phức tạp (if/else, loops) | ✅ Go templates | ❌ Không có |
| Packaging và distribution | ✅ OCI registry, helm repo | ❌ Không có |
| YAML thuần, dễ đọc | ❌ Template syntax phức tạp | ✅ YAML gốc |
| Kế thừa config | Khó (phải dùng sub-chart) | ✅ base + overlays |
| Tích hợp kubectl | Cần cài thêm | ✅ Built-in từ 1.14 |
| Secret management | Helm-secrets plugin | secretGenerator |
| CI/CD phổ biến | ✅ Rất phổ biến | ✅ Phổ biến hơn trong GitOps |
Kết hợp cả hai: Dùng Helm chart của third-party (postgres, redis, ingress-nginx) kèm Kustomize để manage app của mình là pattern khá phổ biến.
3. GitOps — Git là nguồn sự thật
3.1 Nguyên tắc GitOps
GitOps (coined bởi Weaveworks, 2017) định nghĩa 4 nguyên tắc:
- Declarative: Toàn bộ hệ thống được mô tả bằng config khai báo (không phải script imperative).
- Versioned và immutable: Config lưu trong Git — mọi thay đổi có lịch sử, có thể rollback.
- Pulled automatically: Agent trong cluster pull từ Git và apply, không phải CI push vào.
- Continuously reconciled: Agent liên tục so sánh desired state (Git) và actual state (cluster), tự sửa nếu lệch.
Sự khác biệt với CI/CD truyền thống:
Push model (CI/CD truyền thống):
Code commit → CI build → CI kubectl apply → Cluster
Vấn đề: CI cần credentials vào cluster, khó audit, dễ bị drift
Pull model (GitOps):
Code commit → CI build → CI push image tag vào Git repo config
↑
Cluster ←── Argo CD pull config ──┘
Ưu điểm: Cluster không expose ra ngoài, Git là audit trail đầy đủ
3.2 Argo CD — GitOps operator phổ biến nhất
Kiến trúc Argo CD:
Git Repo (config)
│
│ poll/webhook
▼
┌─────────────────────────────────────┐
│ Argo CD │
│ ┌──────────┐ ┌────────────────┐ │
│ │ Repo │ │ Application │ │
│ │ Server │ │ Controller │ │ ← reconcile loop
│ └──────────┘ └────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ API Server (UI/CLI) │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
│ kubectl apply
▼
Kubernetes Cluster
Application CRD — đơn vị triển khai:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-api
namespace: argocd # Argo CD luôn chạy trong namespace riêng
finalizers:
- resources-finalizer.argocd.argoproj.io # Xoá app → xoá resources
spec:
project: payments-project # AppProject để phân quyền
source:
repoURL: https://github.com/myorg/k8s-config
targetRevision: HEAD # Branch/tag/commit
path: overlays/production # Kustomize path
# Hoặc với Helm:
# chart: payment-api
# helm:
# valueFiles:
# - values-prod.yaml
# set:
# - name: image.tag
# value: "1.23.0"
destination:
server: https://kubernetes.default.svc # In-cluster
namespace: payments-prod
syncPolicy:
automated:
prune: true # Xoá resource không còn trong Git
selfHeal: true # Rollback nếu có người sửa tay trong cluster
allowEmpty: false # Không sync nếu sẽ xoá tất cả
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- ApplyOutOfSyncOnly=true # Chỉ apply resource bị drift, không apply toàn bộ
retry:
limit: 3
backoff:
duration: 30s
factor: 2
maxDuration: 5m
AppProject — phân quyền theo team:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments-project
namespace: argocd
spec:
description: "Payment team applications"
sourceRepos:
- "https://github.com/myorg/k8s-config" # Chỉ cho phép pull từ repo này
destinations:
- namespace: payments-* # Wildcard: payments-prod, payments-staging
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace # Chỉ cho phép tạo Namespace (không phải ClusterRole)
namespaceResourceWhitelist:
- group: "apps"
kind: Deployment
- group: ""
kind: Service
# ... các resource cho phép
roles:
- name: developer
description: "Sync but no delete"
policies:
- p, proj:payments-project:developer, applications, get, payments-project/*, allow
- p, proj:payments-project:developer, applications, sync, payments-project/*, allow
3.3 App of Apps pattern
Khi số Application nhiều, quản lý từng cái thủ công rất vất vả. App of Apps giải quyết bằng cách để một "root" Application quản lý các Application khác:
argocd/
├── root-app.yaml # Application quản lý namespace "argocd"
└── apps/
├── payment-api.yaml # Application object
├── user-service.yaml
├── notification-service.yaml
└── infrastructure/
├── ingress-nginx.yaml
└── cert-manager.yaml
# argocd/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
source:
repoURL: https://github.com/myorg/k8s-config
path: argocd/apps # Trỏ đến thư mục chứa các Application YAML
targetRevision: HEAD
destination:
server: https://kubernetes.default.svc
namespace: argocd # Apply Application objects vào namespace argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Bây giờ chỉ cần đồng bộ một App → tất cả App con tự động được tạo và đồng bộ.
3.4 ApplicationSet — scale lên nhiều cluster
ApplicationSet controller (built-in từ Argo CD 2.0) tự động tạo Application từ template:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payment-api-multicluster
spec:
generators:
- clusters: # Tạo Application cho từng cluster đã đăng ký
selector:
matchLabels:
environment: production
template:
metadata:
name: "payment-api-{{name}}" # {{name}} = cluster name
spec:
source:
repoURL: https://github.com/myorg/k8s-config
path: "overlays/{{metadata.labels.region}}" # Overlay per region
targetRevision: HEAD
destination:
server: "{{server}}" # cluster server URL
namespace: payments-prod
3.5 Image Updater — tự động cập nhật image tag
Argo CD Image Updater tự động commit image tag mới vào Git khi CI push image mới lên registry:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-api
annotations:
argocd-image-updater.argoproj.io/image-list: |
payment-api=ghcr.io/myorg/payment-api
argocd-image-updater.argoproj.io/payment-api.update-strategy: semver
argocd-image-updater.argoproj.io/payment-api.allow-tags: regexp:^1\.\d+\.\d+$
argocd-image-updater.argoproj.io/write-back-method: git # Commit vào Git, không patch trực tiếp
argocd-image-updater.argoproj.io/git-branch: main
Khi CI push ghcr.io/myorg/payment-api:1.24.0:
- Image Updater detect version mới.
- Commit vào Git:
image.tag: 1.24.0. - Argo CD sync, deploy version mới.
- Toàn bộ quá trình có audit trail trong Git history.
4. GitOps workflow thực chiến
4.1 Cấu trúc repo phổ biến
Monorepo (một repo tất cả):
k8s-config/
├── apps/
│ ├── payment-api/
│ │ ├── base/
│ │ └── overlays/
│ │ ├── staging/
│ │ └── production/
│ └── user-service/
│ └── ...
├── infrastructure/
│ ├── ingress-nginx/
│ ├── cert-manager/
│ └── prometheus-stack/
└── argocd/
├── apps/
└── projects/
Polyrepo (mỗi team một repo):
- App repo: chứa code + Helm/Kustomize chart của chính app đó.
- Config repo (gitops repo): chứa chỉ các Application YAML, trỏ đến các app repo.
4.2 Promotion workflow — dev → staging → production
Feature branch → PR → Merge to main
│
CI builds image
Pushes: ghcr.io/myorg/payment-api:abc1234
│
CI creates PR vào k8s-config repo:
Sửa overlays/staging/kustomization.yaml
image.tag: abc1234
│
Auto-merge staging PR (không cần review)
│
Argo CD sync → deploy lên staging
│
QA approve → Manual PR vào production overlay
│
Review + merge production PR
│
Argo CD sync → deploy lên production
4.3 Rollback trong GitOps
Vì mọi thay đổi đều là commit trong Git, rollback = git revert hoặc git reset:
# Xem lịch sử
git log --oneline overlays/production/kustomization.yaml
# Revert commit gần nhất
git revert HEAD
git push
# Argo CD tự động sync, deploy version cũ
# Hoặc rollback trực tiếp trong Argo CD UI (sẽ tạo commit revert)
argocd app rollback payment-api --revision 5
Tóm tắt
| Công cụ | Dùng khi | Không dùng khi |
|---|---|---|
| Helm | Distribute chart, logic phức tạp, third-party apps | Config đơn giản, team ưu tiên YAML thuần |
| Kustomize | Multi-env config, YAML thuần, built-in kubectl | Cần templating phức tạp, distribute ra ngoài |
| Argo CD | GitOps pull model, multi-cluster, drift detection | Single-env đơn giản, CI push đủ dùng |
| App of Apps | Nhiều Application cần quản lý tập trung | Ít app, quản lý thủ công ổn |
| ApplicationSet | Deploy cùng app lên nhiều cluster | Single-cluster |
| Image Updater | Auto-update image tag không cần CI viết vào Git | Cần approval trước mỗi deploy |