🚀 DevOps✍️ Khoa📅 19/04/2026☕ 12 phút đọc

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:

  1. Declarative: Toàn bộ hệ thống được mô tả bằng config khai báo (không phải script imperative).
  2. Versioned và immutable: Config lưu trong Git — mọi thay đổi có lịch sử, có thể rollback.
  3. Pulled automatically: Agent trong cluster pull từ Git và apply, không phải CI push vào.
  4. 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:

  1. Image Updater detect version mới.
  2. Commit vào Git: image.tag: 1.24.0.
  3. Argo CD sync, deploy version mới.
  4. 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