← 回文章列表

2025年2月17日

GitOps 與 Argo CD:為什麼部署要從「推」改成「拉」

GitOpsArgo CDKubernetesHelmCI/CDDevOps

Push 模式的問題

最常見的 Kubernetes 部署方式,是在 CI Pipeline 的最後一步直接部署:

Shell
helm upgrade --install order-api ./charts/order-api -f envs/prod/values.yaml

這叫 Push 模式:由 Pipeline 把變更「推」進叢集。它可以運作,但隨著叢集與服務變多,問題會一一浮現:

  • CI 系統要握有叢集的管理權限:每一個 Pipeline 都要有能改正式環境的憑證,CI 系統一旦被入侵,所有叢集都暴露
  • 叢集的實際狀態沒人知道:有人半夜手動 kubectl edit 調整了設定解決問題,事後沒有改回設定檔,下次部署時又被覆蓋回去,問題重現
  • 「現在線上是什麼版本」要去叢集裡查:沒有一個地方可以一眼看出每個環境部署了什麼
  • 回滾要重跑 Pipeline:而且要記得當時是用哪一個 commit、哪一份 values 部署的

GitOps:Git 是唯一的真相

GitOps 的核心想法是:叢集應該是什麼樣子,完全由 Git 上的檔案描述。OpenGitOps 定義了四個原則:

  1. 宣告式(Declarative):系統的期望狀態以宣告式的設定描述(Kubernetes Manifest、Helm values)
  2. 版本化且不可變(Versioned and Immutable):期望狀態存在 Git 中,每一次變更都有紀錄,可以追溯、可以回到任何一個版本
  3. 自動拉取(Pulled Automatically):由叢集內的代理程式主動從 Git 拉取期望狀態,而不是由外部推入
  4. 持續調和(Continuously Reconciled):代理程式持續比對「期望狀態」與「實際狀態」,發現不一致就修正

GitOps 與 Argo CD 流程示意圖:CI Pipeline 建置測試並把映像推到 Image Registry,再更新 Git 上 values.yaml 的 image.tag;工程師調整設定也是發 PR 到 Git;Argo CD 從 Git 拉取期望狀態,比對期望與實際,不一致就同步到 dev 與 prod 叢集;如果有人手動 kubectl scale 把 replicas 改成 8,Argo CD 會標示 OutOfSync,開啟 selfHeal 時自動改回 Git 上的 replicas 4

在 GitOps 的流程裡,CI 和 CD 被拆開了:

  • CI 只負責建置、測試、推送映像,最後更新 Git 上的映像版本號(發一個 commit 或 PR),不碰叢集
  • CD 由叢集內的 Argo CD 負責:它看到 Git 變了,就把叢集同步成 Git 描述的狀態

部署變成「改 Git」,回滾變成「revert 一個 commit」,稽核變成「看 Git log」。

Argo CD 的核心概念:Application

Argo CD 用一個 CRD Application 描述「把哪個 Git 路徑的內容,部署到哪個叢集的哪個 Namespace」:

YAML
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: order-api-prod
  namespace: argocd
spec:
  project: trading
  source:
    repoURL: https://git.example.com/platform/deploy.git
    targetRevision: main
    path: charts/order-api
    helm:
      valueFiles:
        - ../../envs/prod/order-api/values.yaml
  destination:
    server: https://prod-cluster.example.com
    namespace: order
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

每個 Application 都有兩個狀態:

  • Sync Status:叢集的實際狀態和 Git 是否一致——Synced 或 OutOfSync
  • Health Status:部署出去的資源是否健康——Deployment 的 Pod 都 Ready 了嗎?Healthy、Progressing、Degraded

同步策略:automated、prune、selfHeal

syncPolicy 決定 Argo CD 發現不一致時要做什麼:

設定作用沒開的話
automatedGit 有變更時自動同步只標示 OutOfSync,要人按下 Sync
prune: trueGit 上刪掉的資源,叢集裡也刪掉資源會留在叢集裡變成孤兒
selfHeal: true叢集被手動修改時,自動改回 Git 的狀態只標示 OutOfSync,不主動修正

selfHeal 是 GitOps 最有感的一點:有人手動 kubectl scale --replicas=8,Argo CD 幾秒內就會把它改回 Git 上的 replicas: 4。這逼著所有變更都必須走 Git——想改副本數,就去改 values 檔發 PR。

例外:和 HPA 一起使用時,副本數是由 HPA 動態決定的,Argo CD 不應該把它改回去。可以在 Application 中用 ignoreDifferences 忽略 Deployment 的 /spec/replicas,或是乾脆不在 Manifest 中設定 replicas。

正式環境要不要開 automated,各團隊的選擇不同:有些團隊在正式環境只開 selfHeal 防止漂移,同步仍然由人在確認後按下,把「合併 PR」和「真正上線」拆成兩個決策點。

Sync Wave:控制資源的部署順序

一個應用程式可能包含資料庫 Migration Job、ConfigMap、Deployment,它們之間有先後順序:Migration 要先跑完,新版的程式才能啟動。Argo CD 用 Sync Wave 控制順序,數字小的先部署,等它健康了才進下一波:

YAML
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "-1"   # Migration Job:先跑
---
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"    # Deployment:後跑(預設 0)

搭配 argocd.argoproj.io/hook: PreSync 這類 Hook,還可以定義只在同步前或同步後執行的 Job,例如部署前的資料庫備份、部署後的冒煙測試。

多叢集、多環境怎麼規劃

當環境與服務一多,Application 的數量會快速增加。常見的規劃方式:

Helm Chart 共用,環境差異只放在 values

text
deploy/
├── charts/
│   └── order-api/            ← 共用的 Chart
└── envs/
    ├── dev/order-api/values.yaml
    ├── uat/order-api/values.yaml
    └── prod/order-api/values.yaml

環境之間的差異(副本數、資源、網域、外部服務位址)集中在 values 檔,比對兩個環境的差異就是比對兩個檔案。

用 ApplicationSet 產生 Application

不需要為「每個服務 × 每個環境」手寫一份 Application。ApplicationSet 可以依 Git 目錄結構或叢集清單自動產生:

YAML
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: order-api
spec:
  generators:
    - list:
        elements:
          - env: dev
            cluster: https://dev-cluster.example.com
          - env: prod
            cluster: https://prod-cluster.example.com
  template:
    metadata:
      name: "order-api-{{env}}"
    spec:
      project: trading
      source:
        repoURL: https://git.example.com/platform/deploy.git
        targetRevision: main
        path: charts/order-api
        helm:
          valueFiles: ["../../envs/{{env}}/order-api/values.yaml"]
      destination:
        server: "{{cluster}}"
        namespace: order

新增一個環境,只要在清單中加一筆。

應用程式原始碼和部署設定分開兩個 Repo

CI 更新映像版本時會 commit 到部署設定的 Repo。如果兩者放在同一個 Repo,CI 的 commit 又會觸發一次 CI,很容易形成迴圈;權限上,能改程式碼的人也不一定該能改正式環境的設定。

使用情境:證券系統導入 GitOps

在證券系統導入 Argo CD 時,主要想解決的是「正式環境的實際狀態不透明」和「部署權限散落在各個 Pipeline」:

  • 各服務的 Helm Chart 與各環境的 values 集中在部署 Repo,正式環境的每一次變更都是一個經過審核的 PR
  • CI 只負責建置映像並更新 values 中的 image.tag,不再持有叢集的管理權限
  • 正式環境開啟 selfHeal,手動調整會被自動修正,避免設定漂移
  • 搭配 Centralized Pipeline Library,開發人員不需要自己撰寫部署腳本

一句話總結

GitOps 讓 Git 成為叢集期望狀態的唯一真相,CI 只負責產出映像並更新 Git,叢集內的 Argo CD 持續拉取、比對、同步;用 automated 自動部署、prune 清掉被刪除的資源、selfHeal 修正手動漂移,再用 Sync Wave 控制部署順序、ApplicationSet 管理多環境,部署、回滾、稽核都回到 Git 上完成。