ArgoCD 实战:用 GitOps 把 CI/CD 部署搬进 Kubernetes

从安装、Application 声明到 App of Apps 与 CI 配合,把 GitOps 部署体系在 Kubernetes 上落地,并复盘真实事故。

技术文章约 12 分钟86 次阅读

#部署为什么长这样:先从三个基本事实推导

先从问题推方案,而不是先学工具。部署环节有三个绕不开的基本事实:

  1. 集群的可用状态由一份「期望清单」决定:镜像版本、副本数、环境变量、网络规则。
  2. 任何人直接 kubectl apply 改过之后,都回答不了「线上现在和谁一致、是谁改的、什么时候改的」——状态不可审计、不可回放。
  3. 真实集群一定会漂移:手滑、扩缩容、临时排查都会让实际状态偏离预期。

结论不是「多写几个脚本」,而是把期望清单放进唯一事实来源(Git),再让一个常驻进程反复做三件事:读期望 → 对比现状 → 收敛差异。这个循环叫 reconciliation loop,这套做法就是 GitOps,ArgoCD 是它在 Kubernetes 上的标准实现。

  • push 模式:CI 构建完镜像,拿着 kubeconfig 直接改集群。缺点:集群权限必须下发给 CI,权限面大;发布完成后漂移无人发现。
  • pull 模式:controller 在集群内主动拉 Git,周期比对,有差异自动同步。CI 完全不需要集群权限。

分工一句话:CI 负责把镜像做好,ArgoCD 负责让集群长成 Git 里写的样子。

#0 环境准备

复现本文需要四样东西,先逐一确认,减少后面「环境问题当配置问题查」的时间:

  1. Kubernetes 集群:kubectl 能访问即可(k3s / minikube / kind / 云托管集群都可以),版本建议 ≥ 1.24。
  2. kubectl 已切到目标集群:kubectl cluster-info 能正常返回。
  3. argocd CLI:和 ArgoCD server 交互用(安装见下)。
  4. 一个 Git 仓库放部署清单:本文统一叫「manifests 仓库」,地址形如 https://github.com/<you>/manifests.git。下文所有 <you>、<PAT> 都替换成你自己的值。

安装 argocd CLI(Linux):

curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64

macOS(Apple Silicon 用 arm64,Intel 用 amd64):

curl -sSL -o argocd-darwin-arm64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-darwin-arm64
sudo install -m 555 argocd-darwin-arm64 /usr/local/bin/argocd
rm argocd-darwin-arm64

验证:

argocd version --client
kubectl cluster-info

#1 安装 ArgoCD

建命名空间并应用官方安装清单(stable 指向当前稳定版):

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

等待组件就绪(轮询或条件等待任选):

kubectl get pods -n argocd -w
# 或者
kubectl -n argocd wait --for=condition=available deployment --all --timeout=300s

全部 Running 且 READY 1/1 后再继续。装完有四个核心组件,它们的分工决定了后续排查先看谁:

  • argocd-server:对外 API / Web UI / CLI 入口,负责鉴权和接收 webhook。
  • argocd-repo-server:拉取并缓存 Git 仓库内容,Application 的渲染结果都从这里来。
  • argocd-application-controller:reconciliation loop 本体,持续比对 Git 与集群并执行同步。
  • argocd-redis:缓存状态与分布式锁,controller 和 server 共用,删了会短暂互相踩状态。

验证 Application 是 CRD 对象(后面声明的东西靠它):

kubectl get crd applications.argoproj.io

#2 登录并修改初始密码

初始密码机制:安装时生成一次性 admin 密码,放在 Secret argocd-initial-admin-secret 的 data.password(base64 编码)。第一次登录改密后,这个 Secret 会被自动删除。

取密码并暴露服务(port-forward 会占住终端,另开一个终端做登录):

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo
kubectl -n argocd port-forward svc/argocd-server 8080:443

用 CLI 登录并改密:

INITIAL_PW=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d)
argocd login 127.0.0.1:8080 --insecure --username admin --password "$INITIAL_PW"
argocd account update-password --current-password "$INITIAL_PW" --new-password '<你的新密码>'

验证登录态可用:

argocd app list

操作要点:

  • --insecure 只用于自签名证书场景(这里是 port-forward);生产环境必须走 Ingress + HTTPS,挂 TLS 证书,不能裸奔。
  • 验证下 argocd-initial-admin-secret 已消失:kubectl -n argocd get secret argocd-initial-admin-secret 应报 NotFound。若之后忘记密码,官方重置方法是删除该 Secret 后滚动重启 server:kubectl -n argocd rollout restart deployment argocd-server,会自动再生成一个一次性初始密码。
  • 生产加固(超出本文复现范围):Ingress + TLS 终结、SSO(GitHub / OIDC)、RBAC 按团队拆分、admin 平时禁用。

#3 注册 Git 仓库

原理:所有 Application 引用的仓库都由 repo-server 统一拉取和缓存,凭据不写死在配置里,而是由 argocd repo add 存成 argocd 命名空间下的 Secret(label argocd.argoproj.io/secret-type: repository)。私有仓库的凭据必须收敛到「只读」,因为 repo-server 拿到的是能读整个仓库的凭据。

公开仓库直接添加:

argocd repo add https://github.com/<you>/manifests.git

私有仓库推荐 HTTPS + 只读 PAT(GitHub PAT 只勾 Contents: Read):

argocd repo add https://github.com/<you>/manifests.git --username git --password <PAT>
argocd repo list

验证与排障:

  • argocd repo list 中该仓库 STATUS 为 Successful,才算注册成功。
  • 出现 REPO-CONNECTION-ERROR 时按顺序查:① 仓库地址与网络可达性 ② 凭据是否正确(PAT 是否过期)③ 私有仓库的 PAT 是否给了 Contents: Read。
  • 想确认凭据落点:kubectl -n argocd get secret -l argocd.argoproj.io/secret-type=repository。

#4 声明第一个 Application

Application 是一条声明:「Git 仓库的某个路径 → 某个集群的某个 namespace」。它本身是 CRD 对象(第 1 节已确认)。

先在 manifests 仓库里放一个最小应用,让整个链路真的能跑(目录 apps/demo-api/):

# apps/demo-api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-api
  template:
    metadata:
      labels:
        app: demo-api
    spec:
      containers:
        - name: demo-api
          image: ghcr.io/<you>/demo-api:v1

注意 Deployment 里不写 metadata.namespace,由 Application 的 destination.namespace 决定落到哪个命名空间,避免两处打架。

在本地写 Application 声明:

# application-demo-api.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: demo-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/<you>/manifests.git
    targetRevision: main
    path: apps/demo-api
  destination:
    server: https://kubernetes.default.svc
    namespace: demo
  syncPolicy:
    automated:
      sync: true
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

提交并观察:

kubectl apply -f application-demo-api.yaml
argocd app list               # demo-api 出现,先 OutOfSync,自动同步后变 Synced
kubectl -n demo get deploy demo-api   # 自动同步应已创建出 Deployment

三个字段拆开理解:

  • source:从哪个仓库、哪个分支、哪个路径取期望状态。
  • destination:同步到哪个集群、哪个命名空间;https://kubernetes.default.svc 就是当前集群(in-cluster)。
  • syncPolicy.automated:自动同步三件套,各自独立,职责不同:
    • sync:Git 与集群有差异就自动应用(这是「上线」)。
    • selfHeal:集群被手工改动就拉回 Git 状态(这是「防漂移」)。
    • prune:资源在 Git 里已不存在就删除(这是「防孤儿」)。

只开 sync = 能上线但防不了漂移;sync + selfHeal = 能拉平但删不掉孤儿;全开才是完整收敛。

手动操作(排查、回滚用):

argocd app get demo-api            # 看 SYNC STATUS 和 HEALTH STATUS
argocd app sync demo-api           # 手动触发一次同步
argocd app history demo-api        # 每次同步对应一个 Git revision
argocd app rollback demo-api 2     # 回滚到 history 里的第 2 条

app get 输出里两个维度要分开看:SYNC STATUS(OutOfSync / Synced)= Git 与集群是否一致;HEALTH STATUS(Healthy / Degraded)= 应用运行是否正常。两类问题不互通,OutOfSync 是声明问题,Degraded 是运行问题。

#5 App of Apps:用递归声明组织多应用

原理:Application 本身也是集群里的资源,所以它也能被另一个 Application 管理(递归)。于是「一个根 Application 管 apps/ 目录下的 Application 清单,每个子 Application 管自己的资源目录」,集群的部署拓扑就全写进 Git 了。

目录结构:

manifests/
├── apps/                  # 根 Application 管这里
│   ├── demo-api.yaml      # 第 4 节的 application-demo-api.yaml 挪到这里(保留 namespace: argocd)
│   └── monitoring.yaml
└── demo-api/              # 子 Application 管这里
    ├── deployment.yaml
    └── service.yaml

根 Application(path 指向 apps/,destination.namespace 必须是 argocd,子 Application 才能被 controller 识别):

# root-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-apps
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/<you>/manifests.git
    targetRevision: main
    path: apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      sync: true
      prune: true
      selfHeal: true

提交并验证递归是否生效:

kubectl apply -f root-apps.yaml
argocd app list
# 先出现 root-apps;根应用把 apps/demo-api.yaml 同步进 argocd 命名空间后,
# 列表应自动出现 demo-api;再等一轮,demo 命名空间里出现 Deployment
kubectl -n demo get deploy demo-api
argocd app get demo-api

关键细节:子 Application 的 metadata.namespace 必须是 argocd——controller 只 watch argocd 命名空间里的 Application。根 Application 的 destination 指向 argocd,就是在做「把子 Application 文件放进 argocd 命名空间」这一步。

新增一个应用 = 往 apps/ 里加一个 Application 文件并 push → 根应用自动同步 → 子应用自动出现。整个发布历史任何人 git log 都能读。

#6 接入 CI:CI 只改 Git,不碰集群

原则:既然集群只跟随 Git,部署权限就该收敛成「Git 变更」。CI 的最后一个动作是更新 manifest 仓库里的镜像 tag 并提交推送,之后 ArgoCD 自己完成部署。CI 从此不需要任何 kubeconfig。

GitHub Actions 完整 job(以 push 到 manifests 仓库为例):

# .github/workflows/deploy.yml(release 阶段的最后一步)
- name: Update image tag in gitops repo
  run: |
    git clone --depth 1 https://github.com/<you>/manifests.git /tmp/manifests
    cd /tmp/manifests
    sed -i "s|image: ghcr.io/<you>/demo-api:.*|image: ghcr.io/<you>/demo-api:${GITHUB_SHA}|" demo-api/deployment.yaml
    git config user.name "ci-bot"
    git config user.email "ci-bot@example.com"
    git commit -am "release demo-api ${GITHUB_SHA}"
    git push

两个前提要自检:

  • 该 workflow 的凭据对 manifests 仓库有 push 权限(GitHub Actions 常用自带 token permissions: contents: write,或独立 bot PAT)。
  • 镜像 tag 必须是不可变的:用 ${GITHUB_SHA}(或构建产物的 commit SHA),绝不用 latest。原因在原理层:ArgoCD 判断「要不要同步」的依据是 Git 内容是否变化,tag 不变 Git 就不变,镜像内容悄悄变了集群也不会更新——GitOps 的信任就塌了。

收敛速度,两种档位:

  • 默认:ArgoCD 每 3 分钟轮询一次仓库(180s),不配 webhook 也能收敛,只是慢。
  • 秒级:在 GitHub 仓库配置 webhook,URL 填 https://<argocd-server>/api/webhook,事件选 push;GitHub 域名在 ArgoCD 内置白名单内,收到 push 立即刷新对应仓库。若要在 GitHub 侧填 Secret 校验签名,先在 argocd-cm ConfigMap 里配 webhook.github.secret,GitHub webhook 里填同一个值。

验证闭环:

argocd app history demo-api    # 出现新 revision,与 push 的 commit 一一对应
kubectl -n demo get deploy demo-api -o jsonpath='{.spec.template.spec.containers[0].image}'
# 应输出 ghcr.io/<you>/demo-api:<git sha>

出问题回滚就是改 Git,或者 argocd app rollback demo-api <revision> 一条命令。

#7 常见问题定位手册

每一条都先看 argocd app get <app> 的两个状态列,再下沉到 Pod 层,按「现象 → 根因 → 定位 → 处理」操作:

1. 手工改的副本数/配置几分钟内被还原

  • 根因:selfHeal: true 在正常工作,集群里的手工改动在 GitOps 里不算数。
  • 定位:argocd app get demo-api 显示 Synced(已拉回)。
  • 处理:改动一律走 Git(改 manifest → push → 自动同步),不要直接 kubectl 改集群。

2. Git 里删了资源,集群里还在、流量还在

  • 根因:Application 没开 prune,孤儿资源无人清理。
  • 定位:kubectl get svc,deploy -n demo 确认残留资源。
  • 处理:确认该资源确实不再需要后,打开 prune: true 并 argocd app sync demo-api;若开 prune 前已想删,直接手动删除并确认 Git 里也没有对应声明。

3. HEALTH=Degraded 但 SYNC=Synced

  • 根因:声明一致但应用没跑起来,最常见是探针配错或启动参数错误,Pod 永远不就绪。
  • 定位:kubectl describe pod -n demo -l app=demo-api(看 Events 的探针失败原因)+ kubectl logs -n demo deploy/demo-api。
  • 处理:修探针或启动参数,push 后自动同步;健康状态取决于 Deployment 进展,这一步和普通排障没区别。

4. 镜像内容变了,集群里还是旧镜像

  • 根因:tag 用了 latest 这类可变 tag。Git 内容没变 → controller 认为无需同步 → 新旧镜像内容不一致。
  • 定位:argocd app diff demo-api 显示无差异,但实际运行镜像对不上。
  • 处理:改为不可变 tag(commit SHA),push 后自动同步;这是 GitOps 信任链的底线,不可妥协。

#8 卸载(复现完清理)

kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl delete crd applications.argoproj.io appprojects.argoproj.io applicationsets.argoproj.io
kubectl delete namespace argocd

第一条删除安装清单里的命名空间内对象与 RBAC;不删 CRD 的话,Application 对象还在,controller 只是停了,所以复现环境直接把 CRD 连同其管理的对象一起删掉,最后清理命名空间。

#复现自检清单

  • kubectl cluster-info 正常,argocd version --client 有版本输出
  • argocd 命名空间 4 个 Deployment 全部就绪
  • 改密后 argocd-initial-admin-secret 已消失
  • argocd repo list 显示 manifests 仓库 Successful
  • 第一个 Application 创建后自动变 Synced,kubectl -n demo get deploy demo-api 存在
  • App of Apps:apply 根应用后,子应用自动出现在 argocd app list
  • CI push 新 tag 后,argocd app history 出现新 revision,线上镜像是新 SHA
  • 卸载后 CRD、命名空间、相关资源全部清除

#生产环境上线前检查(不展开复现)

非生产环境先跑熟 sync + selfHeal + prune;webhook、SSO(GitHub/OIDC)、RBAC 配齐;admin 禁用、未授权不得开。上线前确认镜像 tag 策略是不可变 SHA、prune 全开、每一层 Application 都有对应 owner 的变更记录——等线上出问题,git log 就能回答「谁、什么时候、改了哪个文件」,可追溯性才是这套体系最大的收益。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

ArgoCD 实战:用 GitOps 把 CI/CD 部署搬进 Kubernetes · LXH·BLOG