← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202617 phút đọc14 min read

GitOpsGitOps

Thuộc bộ tài liệu kiến thức theo DevOps Roadmap.

Tổng quan

GitOps là mô hình vận hành cho việc triển khai hạ tầng và ứng dụng, trong đó Git là nguồn chân lý duy nhất (single source of truth) cho trạng thái mong muốn của hệ thống. Mọi thứ định nghĩa môi trường runtime — manifest Kubernetes, Helm values, Kustomize overlays, định nghĩa hạ tầng — đều nằm trong Git. Một agent tự động chạy trong cluster liên tục so sánh những gì đang thực sự chạy với những gì Git khai báo phải chạy, và reconcile phần chênh lệch. Deploy trở thành git merge; rollback trở thành git revert.

Mô hình này mang các bảo đảm của công nghệ phần mềm vào vận hành: mọi thay đổi là một pull request được review, toàn bộ lịch sử có thể audit, và môi trường có thể dựng lại từ repository bất cứ lúc nào. Vì controller pull thay đổi về thay vì pipeline push lên, credential của cluster không bao giờ phải rời khỏi cluster — một cải thiện bảo mật lớn so với deployment do CI điều khiển kiểu cũ.

GitOps xây trực tiếp trên các chủ đề trước trong roadmap: container cho bạn artifact bất biến, Kubernetes cung cấp API khai báo và reconciliation, còn GitOps khép kín vòng lặp bằng cách đưa trạng thái mong muốn vào quản lý phiên bản. Argo CDFlux là hai triển khai thống trị, đều đã graduated trong CNCF.

Một mô hình tư duy

GitOps là vòng lặp reconciliation của Kubernetes (spec vs status), mở rộng ra để bản thân spec nằm trong Git thay vì chỉ trong etcd. Agent biến repository Git thành một kho trạng thái mong muốn thứ hai và liên tục đưa cluster về khớp với nó. Hai hệ quả trực tiếp:

GitOps không chỉ là “Kubernetes + Git”. Cùng mẫu này áp dụng cho bất cứ thứ gì có API khai báo và một reconciler — state của Terraform, tài nguyên cloud do Crossplane quản lý, các add-on cluster — nhưng Kubernetes là nơi nó trưởng thành nhất.

Kiến thức nền tảng

Bốn nguyên tắc GitOps (OpenGitOps)

  1. Khai báo (Declarative) — toàn bộ trạng thái mong muốn được biểu diễn kiểu khai báo (manifest YAML, không phải script gồm các bước mệnh lệnh).
  2. Có phiên bản và bất biến — trạng thái mong muốn được lưu ở nơi bảo đảm tính bất biến và đầy đủ lịch sử phiên bản (Git, với commit được ký, chỉ nối thêm).
  3. Được pull tự động — các software agent tự động pull trạng thái mong muốn từ nguồn, thay vì con người hay pipeline push vào.
  4. Reconcile liên tục — agent liên tục quan sát trạng thái thực tế và cố gắng đưa nó về trạng thái mong muốn, tự sửa drift mà không cần con người can thiệp.

Mô hình deploy push vs pull

Khía cạnhPush (CI/CD kiểu cũ)Pull (GitOps)
Ai thực hiện deployPipeline CI chạy kubectl apply / helm upgradeAgent trong cluster pull từ Git
Credential của clusterLưu trong hệ thống CI (rủi ro lộ lọt)Không bao giờ rời cluster
Xử lý driftKhông có — thay đổi thủ công không ai biếtĐược phát hiện và (tùy chọn) tự sửa
Dấu vết auditRải rác trong log pipelineĐầy đủ trong lịch sử Git
Khôi phục / DRChạy lại pipeline theo thứ tựTrỏ agent vào repo; trạng thái tự hội tụ
Onboard cluster mớiCấu hình credential pipeline cho từng clusterCài agent, trỏ vào repo
Phạm vi ảnh hưởng nếu CI bị chiếmQuyền ghi trực tiếp vào cluster prodCI chỉ mở được PR; merge vẫn được gác
Chiều tác độngCI → cluster (đẩy ra nhiều cluster)Cluster → Git (mỗi cluster tự pull phần của mình)

Thực tế CI vẫn tồn tại trong GitOps — nó build, test và push image, rồi cập nhật image tag trong config repo (trực tiếp hoặc qua image automation controller). Phần CD hoàn toàn do GitOps agent đảm nhiệm. Sự phân chia gọn gàng là: CI tạo ra artifact và đề xuất trạng thái mong muốn; CD (agent) hiện thực hóa nó.

Cấu trúc repository

Mẫu phổ biến là tách source code ứng dụng khỏi cấu hình deployment:

app-repo/                    # source code + Dockerfile; CI build và push image
config-repo/
├── apps/
│   └── web/
│       ├── base/                    # manifest dùng chung (Kustomize base)
│       └── overlays/
│           ├── dev/kustomization.yaml
│           ├── staging/
│           └── prod/
└── clusters/
    ├── dev/
    │   └── apps.yaml                # Argo CD Applications / Flux Kustomizations
    └── prod/
        └── apps.yaml

Tách repo giúp tránh vòng lặp CI (commit bump image lại kích hoạt build), cho phép phân quyền khác nhau (developer trên app-repo, platform/SRE trên config-repo), và giữ lịch sử audit sạch sẽ. Cây clusters/ giữ các định nghĩa gốc mà agent của từng cluster trỏ vào; apps/ giữ manifest workload thực sự. Một số team thay vào đó dùng monorepo với phân quyền chặt theo path — cách nào cũng được, nhưng hãy giữ source app và config đã render tách bạch về mặt logic.

Những gì GitOps không tự giải quyết

Khái niệm chính

Argo CD vs Flux

Khía cạnhArgo CDFlux
Trạng thái CNCFGraduatedGraduated
UIWeb UI tích hợp mạnh (trạng thái sync, cây resource, diff trực tiếp)Ưu tiên CLI; UI qua Weave GitOps / Headlamp / Capacitor
Abstraction cốt lõiCRD ApplicationApplicationSetCRD GitRepository, Kustomization, HelmRelease, OCIRepository (GitOps Toolkit)
Kiến trúcapi-server, repo-server, application-controller, redisBộ controller chuyên biệt (source, kustomize, helm, notification, image)
Multi-tenancyProjects, RBAC, SSO tích hợp sẵnRBAC thuần Kubernetes, phạm vi namespace, cô lập tenant qua impersonation
Tự động cập nhật imageDự án riêng (argocd-image-updater)Tích hợp sẵn (image-reflector + image-automation controller)
Progressive deliveryArgo Rollouts (dự án anh em)Flagger (dự án anh em)
RenderHelm, Kustomize, YAML thuần, jsonnet, config pluginHelm, Kustomize, YAML thuần
Phù hợp vớiTeam cần trực quan/UI và workflow lấy app làm trung tâmPlatform team cần khối xây dựng nhẹ, kết hợp linh hoạt, thuần Kubernetes

Cả hai đều theo dõi Git, render manifest (Helm/Kustomize/YAML thuần), diff với cluster và reconcile theo chu kỳ hoặc webhook. Chọn giữa hai chủ yếu là muốn một UI lấy app làm trung tâm có định hướng (Argo CD) hay một bộ công cụ controller kết hợp linh hoạt (Flux); nhiều tổ chức thành công với cả hai.

Chi tiết vòng lặp reconciliation

  1. Fetch — agent clone/pull revision đích (một branch, tag hoặc commit SHA) từ Git (Argo CD mặc định polling ~3 phút kèm webhook; Flux dùng interval theo từng source).
  2. Render — chạy kustomize build / helm template để sinh ra manifest mong muốn cụ thể.
  3. Diff — so sánh trạng thái mong muốn đã render với trạng thái sống của cluster (diff phía server, bỏ qua các field không nên quản lý).
  4. Sync — áp phần chênh lệch: tạo, cập nhật và (nếu bật prune) xóa các resource đã bị gỡ khỏi Git.
  5. Report — hiển thị trạng thái: Synced/OutOfSync, Healthy/Degraded, và event cho con người và cảnh báo.

Ví dụ manifest Argo CD Application

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-prod
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io   # xóa cascade các resource được quản lý khi xóa App
spec:
  project: default
  source:
    repoURL: https://github.com/example/config-repo.git
    targetRevision: main
    path: apps/web/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: web
  syncPolicy:
    automated:
      prune: true        # xóa resource đã bị gỡ khỏi Git
      selfHeal: true     # hoàn tác thay đổi thủ công trong cluster
    syncOptions:
      - CreateNamespace=true
      - ApplyOutOfSyncOnly=true
    retry:
      limit: 3
      backoff: { duration: 5s, factor: 2, maxDuration: 3m }

Với selfHeal: true, một lệnh kubectl edit thủ công sẽ bị hoàn tác trong vài giây — cluster luôn khớp với Git. Với prune: true, xóa một manifest khỏi Git sẽ xóa resource khỏi cluster. Cả hai đều mạnh và nguy hiểm; chỉ bật cho production khi kỷ luật review và rollback của bạn đã vững.

App-of-apps và ApplicationSet

Để quản lý nhiều ứng dụng, hãy định nghĩa một Application gốc mà source path của nó chứa các manifest Application khác (mẫu app-of-apps) — bootstrap cả cluster chỉ còn là một lệnh kubectl apply của App gốc. ApplicationSet tổng quát hóa mẫu này với các generator sinh hàng loạt Application từ một template duy nhất:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: web-all-envs, namespace: argocd }
spec:
  generators:
    - list:
        elements:
          - { env: dev,     cluster: https://kubernetes.default.svc }
          - { env: staging, cluster: https://kubernetes.default.svc }
          - { env: prod,    cluster: https://kubernetes.default.svc }
  template:
    metadata: { name: 'web-{{env}}' }
    spec:
      project: default
      source:
        repoURL: https://github.com/example/config-repo.git
        targetRevision: main
        path: 'apps/web/overlays/{{env}}'
      destination: { server: '{{cluster}}', namespace: web }
      syncPolicy: { automated: { prune: true, selfHeal: true } }

Promotion giữa các môi trường

Promotion là một thao tác Git, không phải một stage của pipeline:

  1. CI build web:1.5.0, push nó, và cập nhật tag trong overlays/dev.
  2. Sau khi kiểm chứng ở dev, một PR chép version bump sang overlays/staging, rồi sau khi “ngâm” ở staging, một PR nữa sang overlays/prod.
  3. Mỗi lần promotion là một commit được review và revert được, nên “cái gì đang chạy ở đâu” trả lời được bằng cách đọc Git.

Ưu tiên mỗi-môi-trường-một-thư-mục thay vì mỗi-môi-trường-một-branch: các branch dễ lệch nhau, và việc merge qua lại tạo nhiễu và vô tình promote những thay đổi không liên quan. Overlay theo thư mục làm toàn bộ state của mỗi môi trường tường minh và diff được. Các công cụ như Kargo tự động hóa promotion nhiều tầng (freight, xác minh, tiến bước có gác) trên nền GitOps, và PR generator của ApplicationSet cho môi trường preview theo từng PR.

Secrets trong GitOps

Tuyệt đối không được commit secret dạng plaintext — lịch sử Git là mãi mãi và repo bị clone khắp nơi. Ba cách tiếp cận tiêu chuẩn, mỗi cách giữ Git an toàn theo một kiểu khác nhau:

Cách tiếp cậnCách hoạt độngĐánh đổi
Sealed Secrets (Bitnami)kubeseal mã hóa Secret bằng public key của controller trong cluster; SealedSecret đã mã hóa an toàn để lưu trong Git và chỉ controller đó giải mã đượcĐơn giản, không cần store bên ngoài; nhưng secret bị gắn với key của một cluster, và rotation/DR cần backup private key
SOPS (+ age/KMS)Mã hóa từng giá trị ngay trong file YAML/JSON; Flux giải mã native, Argo CD qua plugin (ví dụ ksops)File vẫn diff được về cấu trúc; bạn tự lo quản lý key (age key, cloud KMS)
External Secrets OperatorGit chỉ lưu một tham chiếu ExternalSecret; operator đồng bộ giá trị thật từ Vault / AWS Secrets Manager / GCP SM / Azure Key Vault vào một Secret nativeDữ liệu secret hoàn toàn không chạm Git, và rotation diễn ra trong store; cần chạy và tin cậy một secret store bên ngoài
# ExternalSecret: Git giữ con trỏ, không giữ secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: db-credentials }
spec:
  refreshInterval: 1h
  secretStoreRef: { name: aws-sm, kind: ClusterSecretStore }
  target: { name: db-credentials }        # Secret K8s native được tạo ra
  data:
    - secretKey: password
      remoteRef: { key: prod/db, property: password }

Nguyên tắc: Sealed Secrets cho hệ nhỏ không có secret store; SOPS khi muốn mọi thứ (kể cả secret) nằm trong Git nhưng đã mã hóa; External Secrets Operator khi bạn đã chạy Vault hoặc một cloud secret manager và muốn Git không giữ chút vật liệu secret nào.

Phát hiện drift và reconciliation

Vòng lặp reconciliation của agent tính diff giữa trạng thái mong muốn đã render và trạng thái đang chạy. Drift có thể xử lý theo hai hướng:

Dù cách nào, thay đổi ngoài luồng đều thua: cluster không còn là nơi tạo ra trạng thái, chỉ là nơi trạng thái hội tụ về. Một số field phải được loại khỏi phát hiện drift (ví dụ số replica do HPA quản lý, field do webhook inject) qua ignoreDifferences để agent và các controller khác không “đánh nhau”.

Progressive delivery trên nền GitOps

GitOps lo cái gì được deploy; progressive delivery lo rollout an toàn thế nào. Argo Rollouts (với Argo CD) và Flagger (với Flux) thêm chiến lược canary và blue-green được điều khiển bởi metric thật: dịch 10% traffic, theo dõi tỉ lệ lỗi và độ trễ (Prometheus), rồi tự động promote hoặc rollback. Định nghĩa rollout vẫn nằm trong Git, nên chính sách an toàn cũng được version cùng với app.

Best Practices

  1. Tách repo source code ứng dụng và repo cấu hình deployment — nhịp release độc lập, phân quyền gọn gàng, không tạo vòng lặp CI từ các commit bump image.
  2. Biến Git thành con đường ghi duy nhất vào production — khóa quyền ghi kubectl trực tiếp; thứ gì không có trong Git thì coi như không tồn tại và không nên sống sót qua lần reconcile kế tiếp.
  3. Chỉ chuyển sang automated sync (prune + self-heal) khi đã đủ trưởng thành — bắt đầu với sync thủ công + review diff, rồi mới bật auto-sync theo từng môi trường khi kỷ luật và cảnh báo đã vững; giữ prod chặt hơn dev.
  4. Dùng mỗi-môi-trường-một-thư-mục, không phải mỗi-môi-trường-một-branch — tránh branch sống lâu bị lệch và các cuộc merge liên môi trường đau đớn, dễ sai.
  5. Ghim phiên bản ở mọi nơi — digest/tag của image, version của Helm chart, và commit SHA (không phải branch trôi nổi) cho base; khả năng tái lập chính là toàn bộ mục tiêu.
  6. Không bao giờ commit secret plaintext — chọn một chiến lược secret và chuẩn hóa — Sealed Secrets, SOPS, hoặc External Secrets Operator dùng thống nhất cho mọi team để có một con đường được audit.
  7. Bảo vệ config repo như bảo vệ production — branch protection, bắt buộc review, CODEOWNERS, signed commit; một lần merge ở đây chính là một lần deploy, nên hãy coi review là cổng phê duyệt thay đổi.
  8. Kiểm tra manifest trong CI trước khi mergekustomize build, helm template, kubeconform và policy check (OPA/Conftest/Kyverno) bắt lỗi schema và policy trước khi agent cố áp.
  9. Hiển thị diff đã render trong PR — chạy argocd app diff / flux diff kustomization như một PR check để reviewer thấy thay đổi thực sự áp lên cluster, không chỉ thấy sửa template.
  10. Cảnh báo khi sync thất bại hoặc có drift — nối Argo CD notifications / Flux alerts vào Slack hoặc công cụ incident; một OutOfSync hay Degraded âm thầm là rủi ro sự cố âm thầm.
  11. Bootstrap cluster bằng app-of-apps hoặc ApplicationSet — khôi phục cluster chỉ còn: tạo cluster, cài agent, trỏ vào repo, để nó tái hiện mọi thứ.
  12. Rollback bằng git revert, không sửa tay — giữ nguyên dấu vết audit và bảo đảm Git luôn phản ánh sự thật; sửa tay dù sao cũng bị self-heal hoàn tác.
  13. Dùng sync wave / sắp thứ tự phụ thuộc — deploy CRD và namespace trước các workload cần chúng, và database trước các app kết nối tới.
  14. Loại các field do controller quản lý khỏi drift — dùng ignoreDifferences cho số replica của HPA và field do admission inject để GitOps và các controller khác không giằng co.
  15. Thêm progressive delivery cho các service rủi ro — Argo Rollouts / Flagger cho canary gác bằng metric, để một version tệ bị bắt và rollback tự động, với chính sách nằm trong Git.
  16. Quản lý cả cấu hình của agent bằng Git — quản lý Argo CD/Flux qua GitOps (tự quản lý) để platform cũng tái lập và audit được như các app nó deploy.

Tài liệu tham khảo

Part of the DevOps Roadmap knowledge base.

Overview

GitOps is an operating model for delivering infrastructure and applications in which Git is the single source of truth for the desired state of the system. Everything that defines your runtime environment — Kubernetes manifests, Helm values, Kustomize overlays, infrastructure definitions — lives in Git. An automated agent running in the cluster continuously compares what is actually running against what Git says should be running, and reconciles the difference. Deploying becomes git merge; rolling back becomes git revert.

This model brings the guarantees of software engineering to operations: every change is a reviewed pull request, the full history is auditable, and the environment can be rebuilt from the repository at any time. Because a controller pulls changes rather than a pipeline pushing them, credentials for the cluster never need to leave the cluster — a major security improvement over classic CI-driven deployment.

GitOps builds directly on the previous roadmap topics: containers give you immutable artifacts, Kubernetes provides declarative APIs and reconciliation, and GitOps closes the loop by putting the desired state under version control. Argo CD and Flux are the two dominant CNCF-graduated implementations.

A mental model

GitOps is the reconciliation loop of Kubernetes (spec vs status), extended so that the spec itself lives in Git instead of only in etcd. The agent turns your Git repository into a second desired-state store and continuously drives the cluster toward it. Two consequences follow directly:

GitOps is not just “Kubernetes + Git.” The same pattern applies to anything with a declarative API and a reconciler — Terraform state, Crossplane-managed cloud resources, cluster add-ons — but Kubernetes is where it is most mature.

Fundamentals

The four GitOps principles (OpenGitOps)

  1. Declarative — the entire desired state is expressed declaratively (YAML manifests, not scripts of imperative steps).
  2. Versioned and immutable — the desired state is stored in a way that enforces immutability and full version history (Git, with signed, append-only commits).
  3. Pulled automatically — software agents automatically pull the desired state from the source, rather than a human or pipeline pushing it in.
  4. Continuously reconciled — agents continuously observe actual state and attempt to converge it toward the desired state, correcting drift without human intervention.

Push vs pull deployment models

AspectPush (classic CI/CD)Pull (GitOps)
Who deploysCI pipeline runs kubectl apply / helm upgradeIn-cluster agent pulls from Git
Cluster credentialsStored in the CI system (exfiltration risk)Never leave the cluster
Drift handlingNone — manual changes go unnoticedDetected and (optionally) auto-corrected
Audit trailScattered across pipeline logsComplete in Git history
Recovery / DRRe-run pipelines in orderPoint agent at repo; state converges
New cluster onboardingConfigure pipeline credentials per clusterInstall agent, point at repo
Blast radius of CI compromiseDirect write access to prod clusterCI can only open a PR; merge still gated
DirectionCI → cluster (outbound to many clusters)Cluster → Git (each cluster pulls its own)

In practice CI still exists in GitOps — it builds, tests, and pushes images, then updates an image tag in the config repo (directly or via an image automation controller). CD is fully owned by the GitOps agent. The clean split is: CI produces artifacts and proposes desired state; CD (the agent) realizes it.

Repository structure

A common pattern is separating application source from deployment configuration:

app-repo/                    # source code + Dockerfile; CI builds and pushes the image
config-repo/
├── apps/
│   └── web/
│       ├── base/                    # shared manifests (Kustomize base)
│       └── overlays/
│           ├── dev/kustomization.yaml
│           ├── staging/
│           └── prod/
└── clusters/
    ├── dev/
    │   └── apps.yaml                # Argo CD Applications / Flux Kustomizations
    └── prod/
        └── apps.yaml

Separate repos prevent CI loops (image-bump commits re-triggering builds), allow different access controls (developers on app-repo, platform/SRE on config-repo), and keep audit history clean. The clusters/ tree holds the root definitions each cluster’s agent points at; apps/ holds the actual workload manifests. Some teams instead use a monorepo with strict path-based ownership — either works, but keep app source and rendered config logically separate.

What GitOps does not solve on its own

Key Concepts

Argo CD vs Flux

AspectArgo CDFlux
CNCF statusGraduatedGraduated
UIRich built-in web UI (sync status, resource tree, live diffs)CLI-first; Weave GitOps / Headlamp / Capacitor plugins for UI
Core abstractionApplication and ApplicationSet CRDsGitRepository, Kustomization, HelmRelease, OCIRepository CRDs (GitOps Toolkit)
Architectureapi-server, repo-server, application-controller, redisSet of specialized controllers (source, kustomize, helm, notification, image)
Multi-tenancyProjects, RBAC, SSO built inKubernetes-native RBAC, namespace-scoped, tenant isolation via impersonation
Image update automationSeparate project (argocd-image-updater)Built-in (image-reflector + image-automation controllers)
Progressive deliveryArgo Rollouts (sibling project)Flagger (sibling project)
RenderingHelm, Kustomize, plain YAML, jsonnet, config pluginsHelm, Kustomize, plain YAML
Typical fitTeams wanting visibility/UI and an app-centric workflowPlatform teams wanting lightweight, composable, Kubernetes-native building blocks

Both watch Git, render manifests (Helm/Kustomize/plain YAML), diff against the cluster, and reconcile on an interval or webhook. Choosing between them is mostly about whether you want an opinionated app-centric UI (Argo CD) or a composable controller toolkit (Flux); many organizations succeed with either.

The reconciliation loop in detail

  1. Fetch — the agent clones/pulls the target revision (a branch, tag, or commit SHA) from Git (Argo CD polls ~3 min by default, plus webhooks; Flux uses a per-source interval).
  2. Render — it runs kustomize build / helm template to produce the concrete desired manifests.
  3. Diff — it compares rendered desired state against live cluster state (server-side diff, ignoring fields it shouldn’t manage).
  4. Sync — it applies the difference: create, update, and (if pruning is on) delete resources removed from Git.
  5. Report — it surfaces status: Synced/OutOfSync, Healthy/Degraded, and events for humans and alerting.

Example Argo CD Application manifest

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-prod
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io   # cascade-delete managed resources with the App
spec:
  project: default
  source:
    repoURL: https://github.com/example/config-repo.git
    targetRevision: main
    path: apps/web/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: web
  syncPolicy:
    automated:
      prune: true        # delete resources removed from Git
      selfHeal: true     # revert manual changes in the cluster
    syncOptions:
      - CreateNamespace=true
      - ApplyOutOfSyncOnly=true
    retry:
      limit: 3
      backoff: { duration: 5s, factor: 2, maxDuration: 3m }

With selfHeal: true, a manual kubectl edit is reverted within seconds — the cluster always matches Git. With prune: true, deleting a manifest from Git deletes the resource from the cluster. Both are powerful and dangerous; enable them for production only once your review and rollback discipline is solid.

App-of-apps and ApplicationSet

To manage many applications, define a root Application whose source path contains more Application manifests (the app-of-apps pattern) — bootstrapping an entire cluster becomes one kubectl apply of the root App. ApplicationSet generalizes this with generators that stamp out Applications from a single template:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata: { name: web-all-envs, namespace: argocd }
spec:
  generators:
    - list:
        elements:
          - { env: dev,     cluster: https://kubernetes.default.svc }
          - { env: staging, cluster: https://kubernetes.default.svc }
          - { env: prod,    cluster: https://kubernetes.default.svc }
  template:
    metadata: { name: 'web-{{env}}' }
    spec:
      project: default
      source:
        repoURL: https://github.com/example/config-repo.git
        targetRevision: main
        path: 'apps/web/overlays/{{env}}'
      destination: { server: '{{cluster}}', namespace: web }
      syncPolicy: { automated: { prune: true, selfHeal: true } }

Environment promotion

Promotion is a Git operation, not a pipeline stage:

  1. CI builds web:1.5.0, pushes it, and updates the tag in overlays/dev.
  2. After validation in dev, a PR copies the version bump to overlays/staging, then after staging soak, another PR to overlays/prod.
  3. Each promotion is a reviewed, revertible commit, so “what is running where” is answerable by reading Git.

Prefer directory-per-environment over branch-per-environment: branches drift, and merges between them create noise and accidental promotion of unrelated changes. Directory overlays make each environment’s full state explicit and diffable. Tools like Kargo automate multi-stage promotion (freight, verification, and gated progression) on top of GitOps, and Argo CD’s ApplicationSet PR generator gives per-PR preview environments.

Secrets in GitOps

Plaintext secrets can never be committed — Git history is forever and repos get cloned widely. Three standard approaches, each keeping Git safe in a different way:

ApproachHow it worksTrade-off
Sealed Secrets (Bitnami)kubeseal encrypts a Secret with the cluster controller’s public key; the encrypted SealedSecret is safe in Git and only that controller can decrypt itSimple, no external store; but secrets are tied to one cluster’s key, and rotation/DR needs the private key backed up
SOPS (+ age/KMS)Encrypt individual values inside YAML/JSON files; Flux decrypts natively, Argo CD via a plugin (e.g., ksops)Files remain structurally diffable; key management (age keys, cloud KMS) is on you
External Secrets OperatorGit stores only an ExternalSecret reference; the operator syncs the real value from Vault / AWS Secrets Manager / GCP SM / Azure Key Vault into a native SecretSecret data never touches Git at all, and rotation happens in the store; requires running and trusting an external secret store
# ExternalSecret: Git holds a pointer, not the secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: db-credentials }
spec:
  refreshInterval: 1h
  secretStoreRef: { name: aws-sm, kind: ClusterSecretStore }
  target: { name: db-credentials }        # the native K8s Secret that gets created
  data:
    - secretKey: password
      remoteRef: { key: prod/db, property: password }

Rule of thumb: Sealed Secrets for small setups with no secret store; SOPS when you want everything (including secrets) in Git but encrypted; External Secrets Operator when you already run Vault or a cloud secret manager and want Git to hold zero secret material.

Drift detection and reconciliation

The agent’s reconciliation loop computes a diff between rendered desired state and live state. Drift can be handled two ways:

Either way, out-of-band changes lose: the cluster is no longer a place where state is authored, only where it converges. Some fields must be excluded from drift detection (e.g., HPA-managed replica counts, webhook-injected fields) via ignoreDifferences so the agent and other controllers don’t fight over them.

Progressive delivery on top of GitOps

GitOps handles what is deployed; progressive delivery handles how safely it rolls out. Argo Rollouts (with Argo CD) and Flagger (with Flux) add canary and blue-green strategies driven by real metrics: shift 10% of traffic, watch error rate and latency (Prometheus), and automatically promote or roll back. The rollout definition still lives in Git, so the safety policy is versioned alongside the app.

Best Practices

  1. Separate application code and deployment config repos — independent release cadence, cleaner permissions, and no CI trigger loops from image-bump commits.
  2. Make Git the only write path to production — lock down direct kubectl write access; if it’s not in Git, it doesn’t exist and shouldn’t survive the next reconcile.
  3. Graduate to automated sync (prune + self-heal) only after maturity — start with manual sync and diff review, then enable auto-sync per environment once your discipline and alerting are solid; keep prod stricter than dev.
  4. Use directory-per-environment, not branch-per-environment — avoids long-lived branch drift and painful, error-prone cross-environment merges.
  5. Pin versions everywhere — image digests/tags, Helm chart versions, and Git commit SHAs (not floating branches) for bases; reproducibility is the entire point.
  6. Never commit plaintext secrets — pick one secrets strategy and standardize — Sealed Secrets, SOPS, or External Secrets Operator, applied consistently across all teams so there’s one audited path.
  7. Protect the config repo like production — branch protection, required reviews, CODEOWNERS, and signed commits; a merge here is a deployment, so treat the review as a change-approval gate.
  8. Validate manifests in CI before mergekustomize build, helm template, kubeconform, and policy checks (OPA/Conftest/Kyverno) catch schema and policy errors before the agent ever tries to apply them.
  9. Show the rendered diff in PRs — run argocd app diff / flux diff kustomization as a PR check so reviewers see the actual cluster-facing change, not just the template edit.
  10. Alert on sync failures and drift — wire Argo CD notifications / Flux alerts into Slack or your incident tooling; a silent OutOfSync or Degraded is a silent outage risk.
  11. Bootstrap clusters with app-of-apps or ApplicationSets — cluster recovery becomes: create cluster, install the agent, point it at the repo, and let it re-materialize everything.
  12. Roll back with git revert, not manual edits — this preserves the audit trail and keeps Git truthful; a manual fix will be undone by self-heal anyway.
  13. Use sync waves / dependency ordering — deploy CRDs and namespaces before the workloads that need them, and databases before the apps that connect to them.
  14. Exclude controller-managed fields from drift — use ignoreDifferences for HPA replica counts and admission-injected fields so GitOps and other controllers don’t thrash.
  15. Layer progressive delivery for risky services — Argo Rollouts / Flagger for metric-gated canaries so a bad version is caught and rolled back automatically, with the policy itself in Git.
  16. Keep the agent’s own config in Git too — manage Argo CD/Flux via GitOps (self-management) so the platform is as reproducible and auditable as the apps it deploys.

References