GitOps với ArgoCD: git là nguồn sự thật

Bài trước Helm đã gói 130 file YAML thành 5 Chart gọn ghẽ. Nhưng nhìn lại pitfall số 10 ở cuối bài — nó chính là vết nứt còn lại, và là lý do bài này tồn…

13 tháng 7, 2026 · 30 phút đọc

Bài trước Helm đã gói 130 file YAML thành 5 Chart gọn ghẽ. Nhưng nhìn lại pitfall số 10 ở cuối bài — nó chính là vết nứt còn lại, và là lý do bài này tồn tại:

"Sửa trực tiếp bằng kubectl edit trên thứ Helm quản → lần helm upgrade sau ghi đè mất. Nguồn sự thật phải là Chart/values, không phải cluster."

Vấn đề sâu hơn cả Helm: bạn vẫn deploy bằng tay. Ai có kubeconfig prod là gõ được helm upgrade từ laptop. Không ai chắc chắn prod đang chạy đúng cái gì. Và một lệnh kubectl edit vội vàng lúc 2 giờ sáng sẽ âm thầm khiến cluster lệch khỏi git — mãi mãi, cho tới khi nó nổ.

GitOps lật ngược mô hình: git là nguồn sự thật duy nhất; cluster tự kéo vềtự sửa mọi sai lệch. ArgoCD là công cụ hiện thực nó. Sau bài này, "deploy" không còn là một hành động thủ công — nó là một commit.


Vấn đề: cluster trôi khỏi git

   ❌ Push-based deploy (cách bạn đang làm):

   Laptop dev  ──helm upgrade──►  Cluster prod
   Laptop khác ──kubectl apply──►      ▲
   CI runner   ──helm upgrade──────────┘

   → Ai cũng có kubeconfig prod = ai cũng deploy được (kể cả lúc say)
   → Không có "sổ cái" ai deploy gì, lúc nào
   → 2h sáng, sự cố: một người kubectl edit replicas=10 để cứu tình thế,
     rồi QUÊN cập nhật git.
        → Git nói 3 replica. Cluster chạy 10 replica.
        → CONFIGURATION DRIFT (lệch cấu hình).
   → Tuần sau ai đó helm upgrade → replicas về 3 → sự cố lặp lại,
     không ai hiểu vì sao.
   → Dựng lại cluster từ git? Không dám chắc git = thực tế.

Ba câu hỏi mà mô hình thủ công không trả lời được:

  1. Prod đang chạy đúng cái gì? (git nói một đằng, cluster có thể một nẻo)
  2. Ai đổi cái gì, lúc nào, vì sao? (không có audit trail)
  3. Cluster cháy — dựng lại từ git được không? (nếu git ≠ thực tế thì không)

GitOps là gì

GitOps: mọi trạng thái mong muốn của hệ thống được khai báo trong git; một agent trong cluster liên tục so sánh git với thực tế và tự đưa thực tế khớp git.

Nghe quen chứ? Đây chính xácreconciliation loop của K8s (Bài 01 chặng trước) — chỉ nâng lên một tầng: thay vì "controller đối chiếu Pod với spec", giờ là "ArgoCD đối chiếu cả cluster với git".

   Push-based (cũ)              Pull-based / GitOps (mới)
   ───────────────              ─────────────────────────
   Người → đẩy vào cluster       Người → commit vào GIT
                                        │
                                        ▼
                                 ArgoCD (SỐNG TRONG CLUSTER)
                                   • kéo git về
                                   • so sánh với thực tế
                                   • tự apply cho khớp
                                   • phát hiện & sửa drift

   kubeconfig prod nằm            Không ai cần kubeconfig prod
   trên nhiều laptop              → chỉ cần quyền push git

Bốn nguyên tắc GitOps:

   1. KHAI BÁO      toàn bộ hệ thống mô tả bằng khai báo (YAML/Helm), không phải script
   2. CÓ PHIÊN BẢN  git là nguồn sự thật duy nhất — có lịch sử, có review, có revert
   3. TỰ KÉO VỀ     agent trong cluster tự pull & apply (không ai push vào cluster)
   4. TỰ HÀN GẮN    liên tục đối chiếu; lệch thì tự sửa (self-heal)

→ Lợi ích cộng dồn: deploy = git push · rollback = git revert · audit = git log · dựng lại cluster = trỏ ArgoCD vào repo.


ArgoCD — kiến trúc & Application

ArgoCD là một controller chạy trong cluster, theo dõi các repo git và đồng bộ chúng vào cluster.

   ┌──────────┐   commit &   ┌──────────┐
   │   Dev    │ ──push────►  │   GIT    │  ← nguồn sự thật
   └──────────┘              └────┬─────┘
                                  │ ArgoCD PULL (mỗi 3 phút, hoặc webhook)
                                  ▼
                     ┌────────────────────────┐
                     │  ArgoCD (trong cluster) │
                     │  • so sánh git ↔ live   │
                     │  • Synced / OutOfSync?  │
                     │  • auto-sync, self-heal │
                     └───────────┬─────────────┘
                                 │ apply
                                 ▼
                     ┌────────────────────────┐
                     │  Cluster (Deployment,   │
                     │  Service, Ingress...)   │
                     └────────────────────────┘

Application — đơn vị cốt lõi

ArgoCD định nghĩa một CRD tên Application: "repo này, đường dẫn này → đồng bộ vào cluster/namespace kia".

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-prod
  namespace: argocd
spec:
  project: default

  source:                                  # NGUỒN SỰ THẬT
    repoURL: https://git.cty.vn/team/myapp-manifests.git
    targetRevision: main                   # nhánh/tag/commit
    path: charts/myapp                     # Helm Chart từ Bài 01!
    helm:
      valueFiles:
        - values-prod.yaml                 # đúng file values của môi trường prod

  destination:                             # ĐÍCH
    server: https://kubernetes.default.svc # chính cluster này
    namespace: prod

  syncPolicy:
    automated:
      prune: true          # xóa resource đã bị gỡ khỏi git
      selfHeal: true       # AI SỬA TAY TRÊN CLUSTER → ArgoCD KÉO VỀ ĐÚNG GIT
    syncOptions:
      - CreateNamespace=true

→ Để ý: ArgoCD dùng thẳng Helm Chart bạn viết ở Bài 01. Hai công cụ không cạnh tranh — Helm đóng gói, ArgoCD đồng bộ. Cặp đôi kinh điển.

Ba núm quyết định hành vi

   automated        ArgoCD TỰ sync khi git đổi (không cần bấm nút)
                    → tt = phải bấm "Sync" thủ công (hợp môi trường nhạy cảm)

   prune: true      Xóa resource khỏi git → ArgoCD XÓA khỏi cluster
                    → tt = resource bị gỡ khỏi git vẫn "mồ côi" trong cluster

   selfHeal: true   Ai sửa tay trên cluster (kubectl edit) → ArgoCD ĐÈ LẠI theo git
                    → đây là thứ GIẾT CHẾT configuration drift ⭐

selfHeal: true là linh hồn của GitOps. Bật nó lên, kubectl edit replicas=10 sẽ bị ArgoCD kéo về 3 trong vài giây. Muốn 10 replica thật? Sửa git. Không còn đường tắt — và đó chính là điều tốt.

Trạng thái Application

   SYNC STATUS               HEALTH STATUS
   ───────────               ─────────────
   Synced      git = live     Healthy     mọi thứ chạy tt
   OutOfSync   git ≠ live     Progressing đang triển khai
   Unknown     chưa so được   Degraded    có resource lỗi (Pod CrashLoop...)
                              Missing     resource chưa tn tại

⚠️ SyncedHealthy. Synced nghĩa là "cluster khớp git"; nhưng thứ trong git có thể... hỏng (image sai → Pod CrashLoopBackOffDegraded). Nhìn cả hai cột.


Deploy = commit; Rollback = revert

Đây là thay đổi lớn nhất trong đời sống hằng ngày:

   TRƯỚC (thủ công)                     SAU (GitOps)
   ────────────────                     ────────────
   Deploy:    helm upgrade từ laptop     Sửa image tag trong git → PR → merge
                                         → ArgoCD tự sync (~vài giây)
   Rollback:  helm rollback (nhớ số      git revert <commit> → push
              revision, chạy đúng máy)   → ArgoCD tự kéo về bản cũ
   Audit:     "ai deploy nhỉ?" 🤷        git log — ai, lúc nào, PR nào, review ai
   Quyền:     kubeconfig prod trên       KHÔNG AI cần kubeconfig prod;
              nhiều laptop               chỉ cần quyền merge PR

→ Deploy trở thành thứ có review, có lịch sử, có thể revert — giống hệt cách bạn quản code. Đó là toàn bộ ý tưởng.


App-of-Apps & ApplicationSet — quản nhiều app

Một Application cho một app. Có 20 app thì sao? Hai mẫu:

App-of-Apps — một Application "cha" trỏ vào thư mục chứa các Application "con":

   root-app (Application)
      └── apps/           ← thư mục trong git
           ├── frontend.yaml   (Application)
           ├── api.yaml        (Application)
           ├── redis.yaml      (Application)
           └── monitoring.yaml (Application)

   → Thêm app mới = thêm 1 file vào apps/ → ArgoCD tự nhận. Bootstrap cả cluster
     chỉ bằng cách tạo MỘT root-app.

ApplicationSet — sinh nhiều Application từ một khuôn (generator), rất hợp mô hình "cùng app, nhiều môi trường/cluster":

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: myapp-all-envs, namespace: argocd }
spec:
  generators:
    - list:
        elements:
          - env: dev
          - env: staging
          - env: prod
  template:
    metadata: { name: 'myapp-{{env}}' }
    spec:
      project: default
      source:
        repoURL: https://git.cty.vn/team/myapp-manifests.git
        targetRevision: main
        path: charts/myapp
        helm:
          valueFiles: ['values-{{env}}.yaml']    # ← đúng cấu trúc Bài 01!
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{env}}'
      syncPolicy:
        automated: { prune: true, selfHeal: true }
        syncOptions: [CreateNamespace=true]

→ Ba môi trường sinh ra từ một khuôn, ghép thẳng với values-dev/staging/prod.yaml của Helm. Đây là lúc Bài 01 và Bài 02 khớp vào nhau hoàn hảo.


Bài toán khó: Secret trong GitOps

Đây là câu hỏi ai làm GitOps cũng vấp, và nó nối thẳng về Bài 05 chặng K8s:

   GitOps nói:   "MỌI THỨ phải ở trong git."
   Bảo mật nói:  "Secret KHÔNG BAO GIỜ được ở trong git."
   → Mâu thuẫn. Giải sao?

Ba lời giải chuẩn:

   Sealed Secrets      Mã hóa Secret bằng khóa công khai của cluster
   (Bitnami)           → file MÃ HÓA an toàn để commit git
                       → controller trong cluster giảithành Secret thật
                       → đơn giản, hợp on-prem. ⭐ dễ bắt đầu nhất

   External Secrets    Secret NẰM NGOÀI (Vault, AWS Secrets Manager...)
   Operator            → git chỉ chứa "con trỏ" tới secret, không chứa giá trị
                       → chuẩn doanh nghiệp, xoay vòng khóa tốt

   SOPS + age/KMS      Mã hóa file YAML tại chỗ, commit bản mã hóa
                       → linh hoạt, hợp nhiều pipeline

⚠️ Tuyệt đối không vì tiện GitOps mà commit Secret plain — đúng cái sự cố "rò secret lên git" tôi đã kể ở Bài 05 chặng K8s.


🚀 Lab — GitOps trong 20 phút

Dùng cluster kind.

1. Cài ArgoCD

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl -n argocd rollout status deploy/argocd-server

# Lấy mật khẩu admin ban đầu
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d ; echo

# Mở UI (Bài 02 chặng K8s — port-forward!)
kubectl port-forward svc/argocd-server -n argocd 8080:443
# → https://localhost:8080  (user: admin)

2. Tạo Application trỏ vào repo mẫu

cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: guestbook
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: HEAD
    path: guestbook
  destination:
    server: https://kubernetes.default.svc
    namespace: guestbook
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: [CreateNamespace=true]
EOF

kubectl -n argocd get applications
kubectl get pods -n guestbook       # ArgoCD tự kéo git về và tạo Pod! ✨

3. Chứng kiến self-heal giết drift

# Xem trạng thái hiện tại
kubectl -n guestbook get deploy

# GIẢ VỜ "sửa tay lúc 2h sáng":
kubectl -n guestbook scale deploy guestbook-ui --replicas=5
kubectl -n guestbook get deploy --watch

# Đợi vài giây... ArgoCD phát hiện LỆCH GIT và TỰ KÉO VỀ đúng số trong git.
# → replicas quay lại giá trị trong repo. Drift bị giết ngay lập tức. ⭐

Đây là khoảnh khắc quan trọng nhất của cả bài. Bạn vừa thấy tận mắt: cluster từ chối rời xa git. Không có kubectl edit lén lút nào sống sót.

4. Xóa resource — prune bảo vệ

kubectl -n guestbook delete deploy guestbook-ui
kubectl -n guestbook get deploy --watch
# ArgoCD tạo lại — vì git nói nó phải tồn tại. Cluster tự hàn gắn.

5. Dọn

kubectl delete application guestbook -n argocd
kubectl delete namespace guestbook argocd

Câu chuyện thực tế

Sự cố khiến chúng tôi chuyển sang GitOps xảy ra vào một đêm thứ Ba.

Traffic tăng đột biến, API chậm. Một bạn trong team (rất giỏi, rất có trách nhiệm) SSH vào, chạy kubectl scale deploy api --replicas=10. Hệ thống ổn định lại trong 2 phút. Anh hùng. Rồi bạn ấy đi ngủ — và quên cập nhật git.

Mười ngày sau, một người khác helm upgrade để đổi image tag. Helm áp lại values từ repo: replicaCount: 3. Trong một giây, số Pod API từ 10 tụt xuống 3 — giữa giờ cao điểm. API nghẽn, khách lỗi. Chúng tôi mất 35 phút để hiểu chuyện gì xảy ra, vì trong đầu ai cũng nghĩ "prod đang chạy 10 replica" — trong khi git chưa bao giờ biết điều đó.

Cái sai không phải ở người scale lúc nửa đêm. Cái sai là hệ thống cho phép cluster lệch khỏi git mà không ai hay biết.

Chúng tôi triển khai ArgoCD trong 2 ngày. Kết quả đo được sau một quý:

  • Configuration drift: về 0. selfHeal kéo mọi thay đổi tay về đúng git trong vài giây. Muốn đổi thật? Sửa git — và thế là nó tự động được ghi lại, review, và tồn tại vĩnh viễn.
  • Rollback: từ ~15 phút run tay (nhớ số revision, chạy đúng máy, cầu trời) → git revert + đợi ArgoCD sync ≈ 1 phút, và ai cũng làm được.
  • Thu hồi kubeconfig prod khỏi 6 laptop. Giờ deploy prod = merge PR. Bảo mật thắng lớn, và không ai còn deploy được lúc say.
  • Onboarding: dev mới nhìn UI ArgoCD là biết chính xác prod đang chạy gì — không phải đi hỏi.

Cú tát của ArgoCD đến ở tuần đầu: chúng tôi bật prune: truequên rằng có vài resource được tạo tay từ trước (không có trong git). ArgoCD thấy chúng "không thuộc git" và... xóa sạch. Trong đó có một ConfigMap ai đó tạo tay và không ai nhớ. Mất 30 phút để dựng lại. Bài học đắng: GitOps là hợp đồng — mọi thứ phải ở trong git, hoặc nó không tồn tại. Bật prune là bạn ký vào hợp đồng đó.

Bài học:

  1. Configuration drift là sát thủ thầm lặng. Nó không gây sự cố ngay — nó cài bom hẹn giờ cho lần deploy sau. selfHeal là thuốc giải.

  2. "Sửa tay để cứu tình thế" luôn kết thúc bằng một sự cố lớn hơn. Không phải vì người sửa sai, mà vì hệ thống không ép sự thay đổi đó quay về git.

  3. Deploy = commit, rollback = revert, audit = git log. Ba câu này gói trọn GitOps. Deploy trở thành thứ có review và lịch sử — như code.

  4. Bật prune = ký hợp đồng "mọi thứ phải ở trong git". Trước khi bật, rà soát mọi resource tạo tay. Nếu không, ArgoCD sẽ dọn sạch giúp bạn (theo nghĩa đen).

  5. Pull > push cho bảo mật. Không ai cần kubeconfig prod nữa. Cluster tự kéo về; kẻ tấn công chiếm laptop dev cũng không deploy được vào prod.

  6. Helm + ArgoCD là cặp đôi, không phải đối thủ. Helm đóng gói và tham số hóa; ArgoCD đồng bộ và canh giữ. values-prod.yaml của Bài 01 chính là thứ ArgoCD trỏ vào.


Pitfalls

  1. Bật prune: true khi cluster còn resource tạo tay → ArgoCD xóa sạch chúng. Rà soát trước khi ký hợp đồng GitOps.

  2. Commit Secret plain lên git "cho đúng tinh thần GitOps". Sai nghiêm trọng. Dùng Sealed Secrets / External Secrets / SOPS.

  3. Vẫn kubectl edit trên app do ArgoCD quản → bị đè lại (nếu selfHeal) hoặc gây OutOfSync khó hiểu. Muốn đổi thì sửa git.

  4. Nhầm Synced là "ổn". Synced chỉ nghĩa "cluster khớp git" — git có thể chứa thứ hỏng. Xem thêm cột Health (Degraded?).

  5. Dùng targetRevision: HEAD/main cho prod → mọi commit vào main lập tức lên prod. Prod nên trỏ tag/commit cụ thể hoặc nhánh release, có cổng kiểm soát.

  6. Không tách repo app và repo manifest → CI build image xong sửa manifest, gây vòng lặp trigger rối. Mẫu phổ biến: repo code (build image) tách khỏi repo manifest (ArgoCD theo dõi).

  7. Auto-sync cho môi trường siêu nhạy cảm mà không có gate. Cân nhắc automated tắt (sync thủ công) hoặc dùng sync window cho prod.

  8. ArgoCD tự quản chính nó nhưng không có backup → cluster cháy, mất luôn cấu hình Application. Đưa cả manifest ArgoCD (App-of-Apps) vào git.

  9. Quên rằng ArgoCD cần quyền RBAC (Bài 04 chặng K8s) để tạo resource. Thiếu quyền → sync fail với lỗi Forbidden.

  10. Drift do resource được controller khác sửa (vd HPA đổi replicas — Bài 03 chặng K8s!) → ArgoCD tưởng là drift và kéo về. Khắc phục: bỏ replicas khỏi git khi dùng HPA, hoặc dùng ignoreDifferences.


Tóm tắt

  • GitOps: git là nguồn sự thật duy nhất; agent trong cluster tự kéo vềtự sửa sai lệch. Chính là reconciliation loop của K8s, nâng lên tầng cả cluster.
  • 4 nguyên tắc: khai báo · có phiên bản (git) · tự kéo về (pull, không push) · tự hàn gắn.
  • Pull > push: không ai cần kubeconfig prod; deploy = merge PR.
  • ArgoCD Application = (repoURL + path + targetRevision) → (cluster + namespace), kèm syncPolicy.
  • Ba núm: automated (tự sync), prune (xóa thứ đã gỡ khỏi git — ký hợp đồng), selfHeal (đè lại thay đổi tay — giết configuration drift).
  • SyncedHealthy — xem cả hai cột.
  • Deploy = commit · Rollback = git revert · Audit = git log.
  • Nhiều app: App-of-Apps (bootstrap cả cluster) và ApplicationSet (sinh app cho nhiều môi trường/cluster — ghép thẳng với values-<env>.yaml của Helm).
  • Secret: không bao giờ plain trong git → Sealed Secrets / External Secrets / SOPS.
  • Helm + ArgoCD = cặp đôi: Helm đóng gói, ArgoCD đồng bộ & canh giữ.

Từ chặng Docker (một máy) → Kubernetes (một cụm tự phục hồi) → Helm (đóng gói có phiên bản) → GitOps (git điều khiển tất cả), bạn đã đi trọn con đường từ "chạy được" tới "vận hành được ở quy mô đội nhóm".

Hai mảnh còn thiếu để khép vòng tròn hiện đại: CI/CD (pipeline tự build image → cập nhật manifest → ArgoCD lo phần còn lại) và Observability (Prometheus/Grafana/Loki — để khi có sự cố, bạn thấy được thay vì đoán). Đó là những gì đang chờ ở phía trước.

Nếu hôm nay bạn kubectl scale một Deployment và thấy ArgoCD lặng lẽ kéo nó về đúng git trong vài giây — bạn vừa chứng kiến configuration drift chết ngay trước mắt. Và bạn sẽ không muốn quay lại cách cũ nữa.

Minh Hưng

← Bài trước
Helm: trình quản lý gói cho Kubernetes
Bài tiếp theo →
Rancher: quản trị nhiều cluster từ một chỗ
Zalo tư vấn