一、遇到的真实坑:CI推完镜像,服务却没更新

做DevOps的同学大概率碰过这种糟心事:开发同学把代码改好,CI流水线自动打包成新镜像推到镜像仓库,按说后面ArgoCD会自动拉新配置更新服务,结果刷新服务一看——啥变化都没有!翻日志才发现,要么是新镜像的签名校验失败,要么是旧配置比新配置先到ArgoCD,导致ArgoCD直接跳过了这次更新。

我之前帮一家做电商的客户调这个链路时,他们的大促场景下这个问题特别突出:大促前一周每天要更3次服务,经常出现“明明CI显示成功,线上服务却没更新”的情况,后来才发现是链路的顺序和校验逻辑出了问题。

二、从代码到ArgoCD的完整事件链路拆解

要解决问题,得先把每一步的流程理清楚,不能只盯着ArgoCD的配置。我把整个链路拆成了5个核心环节,每个环节的顺序和逻辑都不能乱。

2.1 代码提交触发CI流水线

首先是开发同学把代码推到Git仓库,比如Gitea或者GitLab,然后CI流水线开始跑。CI流水线一般会做编译、单元测试、打包镜像、推镜像到镜像仓库(比如Harbor)这几步。

这里要注意,CI流水线是串行执行的,每一步都要等上一步成功才会继续。比如只有镜像推到Harbor成功了,才会去更新配置仓库里的镜像标签。

2.2 镜像推送成功后更新配置仓库

镜像推完之后,CI流水线会修改配置仓库里的k8s配置文件,把原来的旧镜像标签改成新的。比如原来的标签是v1.0.0,现在改成v1.0.1。

这个步骤特别关键,必须保证镜像真的推成功了才改标签。我之前见过有团队的CI流水线是先改标签再推镜像,结果推镜像失败了,配置仓库却已经更新了,导致ArgoCD拉到新配置,但是找不到镜像,服务更新失败。

2.3 配置仓库变更触发Webhook到ArgoCD

配置仓库改完之后,会通过Webhook给ArgoCD发一个通知,告诉ArgoCD“配置变了,快来拉新配置”。

这里的Webhook有个坑:Webhook是异步触发的,也就是说配置仓库改完之后,给ArgoCD发通知的动作是后台跑的,不保证通知一定会成功,也不保证通知的顺序。比如你先改了v1.0.1的标签,又改了v1.0.2的标签,结果Webhook先把v1.0.2的通知发给ArgoCD,再把v1.0.1的通知发给ArgoCD,ArgoCD就会先拉v1.0.2的配置,再拉v1.0.1的配置,最后更新的是旧配置。

2.4 ArgoCD拉取配置并校验镜像

ArgoCD收到Webhook通知后,会去拉配置仓库的最新配置,然后解析配置里的镜像标签,去镜像仓库拉取镜像并校验签名。

这里的校验逻辑是:ArgoCD会验证镜像的签名是否和配置仓库里的签名一致,如果不一致,就会拒绝更新。如果校验失败,或者拉配置的时候拉到了旧版本的配置,ArgoCD就会跳过这次更新。

2.5 ArgoCD同步配置到K8s集群

如果校验成功,ArgoCD就会把配置同步到K8s集群,更新服务的镜像。

三、核心问题分析:为什么会跳过更新?

我把常见的问题分成了两类,每类都有具体的场景和原因。

3.1 签名校验失败的原因

签名校验失败一般有三种情况: 第一种是镜像推送失败,比如Harbor的磁盘满了,或者网络波动,导致镜像没推上去,但是CI流水线却认为推成功了,改了配置仓库的标签。结果ArgoCD拉配置的时候,镜像不存在,校验失败。 第二种是配置仓库的签名和镜像的签名不一致。比如开发同学手动改了配置仓库里的签名,或者CI流水线在改标签的时候,签名更新失败了。 第三种是ArgoCD的签名校验配置有问题。比如ArgoCD用的是旧的公钥,而镜像用的是新的私钥签名,导致校验不通过。

3.2 事件顺序颠倒的原因

事件顺序颠倒的核心原因是Webhook的异步触发。比如:

  • 配置仓库先改了v1.0.1的标签,然后Webhook发通知给ArgoCD,但是网络波动,通知延迟了。
  • 然后开发同学又改了代码,CI流水线跑了新的版本v1.0.2,改了配置仓库的标签,这次Webhook的通知很快就到了ArgoCD。
  • ArgoCD先拉了v1.0.2的配置,更新了服务。
  • 过了一会儿,v1.0.1的Webhook通知到了ArgoCD,ArgoCD又拉了v1.0.1的配置,发现和当前集群的配置(v1.0.2)不一致,但是ArgoCD默认会认为新的通知是最新的,所以会回滚到v1.0.1,或者直接跳过这次更新。

四、解决方案:配置可靠的重试和顺序控制

针对上面的问题,我总结了一套完整的解决方案,包括链路优化、重试配置、校验逻辑优化三个部分。

4.1 链路优化:保证每一步的顺序

首先要保证CI流水线的每一步都是串行执行的,必须等上一步成功才会继续。比如:

  • 第一步:编译、单元测试。
  • 第二步:打包镜像、推镜像到Harbor,并且验证镜像是否真的存在(比如用curl请求Harbor的API,检查镜像是否存在)。
  • 第三步:更新配置仓库的镜像标签和签名。
  • 第四步:触发Webhook到ArgoCD。

这里我给大家一个具体的CI流水线示例,用的是GitLab CI,因为很多团队都在用。

# GitLab CI配置,所有步骤串行执行,每步都做验证
stages:
  - build_and_test
  - push_image
  - update_config
  - trigger_webhook

# 第一步:编译和单元测试
build_and_test:
  stage: build_and_test
  image: maven:3.8.5-openjdk-11
  script:
    - mvn clean package -DskipTests
    - mvn test
  only:
    - main

# 第二步:打包并推镜像到Harbor,验证镜像存在
push_image:
  stage: push_image
  image: docker:24.0.6
  services:
    - docker:dind
  script:
    # 登录Harbor
    - docker login -u $HARBOR_USER -p $HARBOR_PASSWORD $HARBOR_URL
    # 打包镜像,标签用当前Git提交的哈希值,保证唯一
    - IMAGE_TAG=$CI_COMMIT_SHORT_SHA
    - docker build -t $HARBOR_URL/$PROJECT_NAME/$APP_NAME:$IMAGE_TAG .
    # 推镜像
    - docker push $HARBOR_URL/$PROJECT_NAME/$APP_NAME:$IMAGE_TAG
    # 验证镜像是否存在:用Harbor的API检查镜像
    - |
      curl -f -s -o /dev/null -H "Authorization: Basic $(echo -n $HARBOR_USER:$HARBOR_PASSWORD | base64)" $HARBOR_URL/api/v2.0/projects/$PROJECT_NAME/repositories/$APP_NAME/artifacts/$IMAGE_TAG
      if [ $? -ne 0 ]; then
        echo "镜像推送失败,不存在"
        exit 1
      fi
  only:
    - main

# 第三步:更新配置仓库的镜像标签和签名
update_config:
  stage: update_config
  image: alpine/git:latest
  before_script:
    # 配置Git的身份信息
    - git config --global user.email "ci@example.com"
    - git config --global user.name "CI Bot"
    # 克隆配置仓库
    - git clone $CONFIG_REPO_URL config-repo
  script:
    - cd config-repo
    # 替换配置文件里的旧镜像标签为新标签
    - IMAGE_TAG=$CI_COMMIT_SHORT_SHA
    - sed -i "s|image: $HARBOR_URL/$PROJECT_NAME/$APP_NAME:.*|image: $HARBOR_URL/$PROJECT_NAME/$APP_NAME:$IMAGE_TAG|" deployment.yaml
    # 这里可以加签名更新的逻辑,比如用cosign给镜像签名,然后把签名写到配置文件里
    # 比如:cosign sign $HARBOR_URL/$PROJECT_NAME/$APP_NAME:$IMAGE_TAG
    # 然后把签名写到deployment.yaml的annotation里
    # 提交变更到配置仓库
    - git add deployment.yaml
    - git commit -m "Update $APP_NAME image to $IMAGE_TAG"
    - git push
  only:
    - main

# 第四步:触发Webhook到ArgoCD
trigger_webhook:
  stage: trigger_webhook
  image: alpine:latest
  script:
    # 调用ArgoCD的Webhook API,通知ArgoCD配置仓库有变更
    - |
      curl -f -s -o /dev/null -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $ARGOCD_TOKEN" $ARGOCD_URL/api/webhook/git
      if [ $? -ne 0 ]; then
        echo "Webhook触发失败"
        exit 1
      fi
  only:
    - main

这个CI流水线的核心是每一步都做验证,比如推完镜像之后会用API检查镜像是否存在,Webhook触发失败会直接退出流水线,保证不会出现“配置更新了,镜像不存在”的情况。

4.2 重试配置:保证Webhook和校验的可靠性

针对Webhook触发失败和校验失败的情况,我们需要配置重试机制。

4.2.1 Webhook的重试配置

ArgoCD的Webhook支持配置重试,我们可以在ArgoCD的配置里设置重试的次数和间隔。比如我们可以设置最多重试3次,每次间隔5分钟。

# ArgoCD的应用配置,配置Webhook的重试
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/config-repo.git
    targetRevision: main
    path: my-app
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  # 配置同步策略,包括重试
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    # 重试配置:失败后重试3次,每次间隔5分钟
    retry:
      limit: 3
      backoff:
        duration: 5m
        factor: 2
        maxDuration: 1h

这里的retry配置是说,如果同步失败,ArgoCD会每隔5分钟重试一次,最多重试3次。backoff的factor是2,意思是如果第一次重试失败,第二次的间隔会变成10分钟,第三次变成20分钟,最大不超过1小时。

4.2.2 镜像校验的重试配置

如果镜像校验失败,我们也可以配置重试。比如我们可以在ArgoCD的配置里设置校验失败后,隔一段时间再校验一次。

另外,我们还可以在配置仓库里加一个校验脚本,每次更新配置之前,先验证镜像的签名是否正确。比如:

# 校验脚本,验证镜像签名
#!/bin/bash
# 镜像地址
IMAGE=$1
# 公钥路径
PUBLIC_KEY=$2
# 验证签名
cosign verify --key $PUBLIC_KEY $IMAGE
if [ $? -ne 0 ]; then
  echo "镜像签名验证失败"
  exit 1
fi

然后把这个脚本加到CI流水线的update_config步骤里,在提交配置之前先运行这个脚本,验证镜像的签名是否正确。

4.3 顺序控制:保证Webhook的通知顺序

针对Webhook的顺序问题,我们可以用两种方法解决:

4.3.1 用Git的提交哈希作为版本号

我们可以把Git的提交哈希作为镜像的标签和配置仓库的版本号。比如每次提交代码,都会生成一个唯一的提交哈希,这个哈希是按时间顺序排列的,新的提交哈希比旧的大。

ArgoCD在拉配置的时候,会比较配置仓库的提交哈希和当前集群的配置的提交哈希,如果新的提交哈希比旧的大,就会更新,否则就会跳过。这样就可以保证ArgoCD只会更新到最新的版本,不会被旧的Webhook通知影响。

4.3.2 配置ArgoCD的同步策略为“仅同步最新版本”

我们可以在ArgoCD的配置里设置,只有当配置仓库的版本比当前集群的版本新的时候,才会同步。比如:

# ArgoCD的应用配置,仅同步最新版本
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/config-repo.git
    targetRevision: main
    path: my-app
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    # 配置同步的条件:只有当配置仓库的版本比当前集群的版本新的时候才同步
    syncOptions:
      - CreateNamespace=true
      - Validate=true
      # 仅同步最新版本
      - AllowEmpty=false

另外,我们还可以在配置仓库的配置文件里加一个annotation,记录当前的版本号,ArgoCD在同步的时候会比较这个版本号,只有新的版本号才会同步。

五、应用场景、优缺点和注意事项

5.1 应用场景

这套解决方案适用于所有用CI/CD流水线的团队,特别是以下场景:

  • 大促、活动前频繁更新服务的团队,需要保证每次更新都能成功。
  • 对服务可用性要求高的团队,不能因为更新失败导致服务中断。
  • 用ArgoCD做持续部署的团队,经常遇到更新跳过的问题。

5.2 技术优缺点

优点:

  • 可靠性高:每一步都做验证,配置了重试机制,保证更新不会因为网络波动、镜像推送失败等问题跳过。
  • 顺序控制:用Git的提交哈希作为版本号,保证ArgoCD只会更新到最新的版本,不会被旧的Webhook通知影响。
  • 可维护性高:链路清晰,每一步的逻辑都很明确,出问题的时候很容易排查。

缺点:

  • 配置复杂:需要配置CI流水线、ArgoCD的同步策略、重试机制等,对新手来说有一定的学习成本。
  • 增加了流水线的时间:每一步都做验证,比如验证镜像是否存在、验证签名等,会增加CI流水线的执行时间。
  • 对Git仓库的依赖高:如果Git仓库出问题,整个链路都会受影响。

5.3 注意事项

  • 要保证CI流水线的每一步都是串行执行的,不能并行。
  • 要定期更新ArgoCD的公钥和镜像的私钥,保证签名校验的安全性。
  • 要监控ArgoCD的同步状态,及时发现同步失败的情况。
  • 要定期备份配置仓库和镜像仓库,防止数据丢失。

六、总结

从代码提交到ArgoCD同步的整个链路,任何一个环节出问题都会导致更新被跳过。要解决这个问题,需要从链路优化、重试配置、顺序控制三个方面入手,保证每一步的顺序和可靠性。

首先要优化CI流水线,保证每一步都做验证,必须等上一步成功才会继续;然后要配置ArgoCD的重试机制,保证Webhook触发失败和校验失败后能自动重试;最后要控制Webhook的通知顺序,保证ArgoCD只会更新到最新的版本。

这套方案虽然配置复杂,但是能大大提高更新的可靠性,适合对服务可用性要求高的团队。