镜像同步这个活儿,说起来不算难,但真做起来能踩不少坑。尤其是 Docker Hub 到 Azure ACR 这种跨平台复制,既要解决好网络链路的稳定性,又要保证镜像层不漏、标签不错、同步幂等不重复……团队里如果每次都靠人肉执行docker pull再docker push,等镜像多了、版本迭代快了,根本忙不过来,也特别容易出错。我目前在生产环境里跑的就是 GitHub Actions + image-syncer 这套自动化方案:用 GitHub Actions 负责触发和编排,用 image-syncer 负责底层的镜像搬运逻辑。用下来最大的感受是,这套组合把“什么时候同步、同步哪些镜像、怎么同步、失败怎么办”这些问题一次性都理顺了。
这篇分享会把整个方案从头到尾捋一遍,从为什么选这条路,到关键参数怎么定,再到完整 YAML 配置、实际跑通一次的全过程,以及我踩过的坑。适合正在做跨云镜像分发、或者想把 Docker Hub 镜像稳定复制到 Azure ACR 的 DevOps 同学参考。
1. 为什么要做 Docker Hub 到 Azure ACR 的自动同步
1.1 生产环境不能永远依赖公共注册表
很多团队刚起步的时候,部署服务都是直接从 Docker Hub 拉镜像,docker-compose 里写个 nginx、redis,服务器上一跑就行,简单直接。可一旦服务开始对外提供正式访问,公共注册表依赖就会变成一颗定时炸弹。
先说稳定性。Docker Hub 的拉取速度、可用性,不同地区、不同时间段差异很大。晚高峰、大版本发布那几天,经常出现把docker pull卡在半路上的情况。生产环境如果每次发布都赌网络没问题,迟早会被打脸。
再说安全性。公共镜像仓库不等于完全可信。你拉的镜像里有没有被塞进异常组件,版本更新后行为有没有变化,这些都不好控制。企业做合规审查的时候,里面有一条几乎必问:你们的镜像副本放在哪里,供应链上的每个环节能不能审计?如果你的答案是“直接跑 Docker Hub 上的镜像”,这一关不太好过。
于是很多团队会把镜像往自己的私有仓库搬。Azure ACR 就是典型的托管私仓方案,和 Azure 生态贴合得很紧,AKS 集群直接引用 ACR 里的镜像做部署,身份认证、访问控制都能走一套。把镜像从 Docker Hub 复制到 ACR,核心诉求其实就一句话:让业务运行的镜像源私有化、可控化、统一化。
1.2 复制镜像这件事,为什么不能靠手工
镜像不多的时候,手工同步完全可行。打开终端执行:
docker pull docker.io/library/nginx:1.25.3 docker tag docker.io/library/nginx:1.25.3 myacr.azurecr.io/mirrors/nginx:1.25.3 docker push myacr.azurecr.io/mirrors/nginx:1.25.3三步走,完事。可一旦镜像数量涨到几十个、上百个,版本从一天一更变成一天多更,这套手工流程就扛不住了。
第一个问题是漏。人不可能记住所有该同步的镜像和版本,总会有某一次发布忘了同步,等服务器上跑起来才发现用的是旧版本,排查半天,最后一句“哦,忘了推镜像了”收场。
第二个问题是慢。Docker 镜像本身是分层结构,每个标签背后可能有一堆共享层。手工操作每次都是全量拉取全量推送,同样的基础层反复下载、反复上传,网络带宽和磁盘空间都被白白浪费。
第三个问题是不可审计。同步这个动作是谁做的、什么时候做的、同步的是哪一批镜像、成没成功,全部没有记录。出了事故只能靠回忆,这在大团队里是不可接受的。
所以自动化同步不是“方便一下”,而是生产环境镜像管理的基本功。
2. 工具选型:为什么用 image-syncer
2.1 可选的方案其实不少
在决定用 image-syncer 之前,我把业界常见的做法挨个过了一遍,各有各的适用场景,也都各有短板。
最简单的方案是直接用skopeo copy。它能把一个镜像从一个 registry 直接复制到另一个地方,不需要在本地落盘。一条命令搞定一个镜像,配合 shell 脚本写个循环,也能批量跑。但它的问题在于循环逻辑要自己维护,失败重试要自己写,并发控制要自己想办法。而且每次执行都会把镜像的 manifest 和层完整拉下来再推上去,虽然没有本地磁盘存储,但网络开销并不小。
第二个选择是az acr import。这是 Azure 原生的导入命令,可以直接把指定仓库的镜像导入到 ACR。它和 Azure 的身份体系、命令行生态集成很好,如果只是偶尔手动导几次,非常顺手。可它的问题在于不适合做大批量自动化。import 一次管一个镜像,多个镜像要么写脚本循环,要么排队等任务。更关键的是,如果你同时要同步到多个硬件或者做复杂的 tag 重命名映射,它的表达能力会显得不太够。
第三个选择是自己写程序调 registry API。最灵活,但开发成本高。要处理 manifest 格式、认证协议、分页、失败重试、并发控制、digest 一致性检查……足够写一两个星期的活,而且后面没人维护的话就是技术债。
2.2 image-syncer 胜在哪
后来我把目光锁定在 image-syncer 上,原因有几个。
它是单一二进制的开源工具,Go 语言写的,没任何运行时依赖。GitHub Actions 的 runner 上拉下来就能跑,不需要装额外的包,也不需要维护一套 Docker 镜像环境。这对 CI 流水线来说非常友好,一个 step 就够了。
配置文件结构简单,auth段负责认证,images段负责同步关系,一张清单描述清楚。源仓库、目标仓库、tag 映射关系可以写在同一个 YAML 文件里,维护成本很低,谁都能看懂。
它有并发同步能力,可以一次性批量处理多个镜像,而不是像 shell 脚本那样一个镜像一个镜像地排队。并发数可以自己调,跑快了不会把网络打爆,跑慢了自己也能接受。
它天然支持“增量同步”,同 digest 的层不会被重复搬运。这一点特别重要,后面我会详细讲。
另外它对私有 registry 的支持比较全,Docker Hub、ACR、其他各类标准 registry 都能对接,认证方式也支持账号密码和 Token。GitHub Actions 里把凭据通过环境变量喂给它,就能把敏感信息挡在日志之外。
3. 方案设计:触发方式与整体流程
3.1 整体流程拆解
这套方案的基本流程其实不复杂,核心是三段式:
GitHub Actions 被触发以后,在托管的 runner 上下载 image-syncer,准备好同步清单,然后执行同步。同步的过程由工具自己完成:它遍历清单,读取源仓库的 manifest,对比目标仓库里已有的 digest,只搬运缺失的层,最后更新 tag 引用。
拆开来看,具体分成这几个环节:
第一是配置准备。同步清单和认证信息需要提前准备好。清单里的源镜像地址、目标镜像地址,同步策略、并发参数,都在这个环节确定。
第二是凭据注入。Docker Hub 需要 Token,ACR 需要账号密码或服务主体信息。这些凭据不能写死在仓库里,要放到 GitHub Actions Secrets 中,然后通过 workflow 的环境变量注入给 image-syncer。
第三是执行同步。image-syncer 按照配置逐库逐镜像同步,日志会记录每个镜像从准备、拉层、推层到完成的状态。这个过程是完全幂等的,同一个镜像重复同步也不会重复搬运数据。
第四是验证与告警。同步完成后可以做一次目标仓库的校验,或者凭 workflow 的日志判断是否成功。失败时通过通知渠道拉响告警。
3.2 触发策略设计
触发策略决定了这套自动化什么时候干活。我目前主要用三种。
第一种是手动触发,也就是workflow_dispatch。这种适合临时性的同步,比如刚从公共仓库拉了一个新版本进来,想立刻推到 ACR 给测试环境用。有人在 GitHub 仓库的 Actions 页面点一下“Run workflow”,选一下分支,任务就手动开始了。
第二种是定时触发,schedule配合 cron 表达式。适合做每日增量同步,比如每天凌晨三点,把所有公共镜像的最新版本刷一遍,保证 ACR 里的库存和 Docker Hub 保持同步。如果团队发布节奏是固定的,定时同步基本够用。
第三种是事件触发,比如镜像源仓库 push 之后触发。这种方式对代码仓库和镜像仓库之间有强关联的场景很合适——源仓库一更新,GitHub Actions 立刻执行同步,目标 ACR 几乎同一时间就能拿到新镜像。不过前提是 GitHub Actions 能感知到源仓库那边的变化事件,如果源仓库不在 GitHub 上,就得走 webhook 中转一下。
3.3 同步清单怎么维护
同步清单是整个方案的核心配置,相当于一张“搬移地图”。它不能随手完事后就丢在那里,最好和代码仓库一起维护,版本化、可回溯。
我习惯把同步任务按业务域拆,比如基础组件一张清单,中间件一张清单,业务镜像一张清单。每张清单里只写源地址和目标地址,不写认证信息,这样谁都能 review,也不用担心把密钥带进代码。
tag 的维护需要提前定好规则。比较常见的是全量同步和选择性同步两种。全量同步简单省心,把源仓库的所有 tag 一股脑同步过去,适合镜像数量不大、tag 数量增长也不快的仓库。选择性同步更适合需要精确定制的场景,比如只同步1.25.3、1.26.0这种指定版本,或者只同步带stable后缀的标签。image-syncer 对这两种方式都支持,关键在于把目标地址里的 tag 写清楚。
我个人的习惯是,对稳定运行的中间件镜像做“固定版本 + 关键版本”同步,不要把乱七八糟的latest直接搬过去。latest是漂移的,今天同步了一个内容,明天 Docker Hub 更新了,ACR 里还是旧的那份,反而会导致两边不一致,排查起来更费劲。
4. 准备工作:注册表、凭据与密钥
4.1 创建 Azure ACR 实例
在动 workflow 之前,先把 ACR 实例建出来。这里我用 Azure CLI 操作,下面是一个最基础的创建命令:
az group create --name my-rg --location eastasia az acr create --resource-group my-rg --name myacr --sku Standard--sku这里我用的是 Standard,它支持存储冗余和一些高级特性。如果你只需要最基础的功能,Basic 也能用,但如果计划长期作为生产镜像的主要宿主,建议至少 Standard,给后期留一点余地。
ACR 实例名有严格的全局唯一性要求,必须是字母数字且不含连字符或下划线,而且在所选区域的范围内必须全局唯一。我见过不少人在这里翻车,起名字的时候多花点时间查一查,别一边敲命令一边现想。
创建完成后,可以用一条命令确认状态:
az acr show --resource-group my-rg --name myacr --output table如果provisioningState显示Succeeded,说明 ACR 已经可以用了。登录用的登录服务器地址是myacr.azurecr.io,这个地址后面会反复用到。
4.2 准备 Docker Hub 访问凭据
Docker Hub 这边,强烈建议不要直接用账号密码,而是创建一个只读的访问令牌。登录 Docker Hub 官网,到账号设置里找到 Personal Access Tokens,新建一个 Token,权限只需要Read就行。它是用来让 image-syncer 读镜像的,不需要任何写权限。
这个 Token 的使用场景很明确:image-syncer 访问docker.io源仓库时认证用。把它放到 GitHub Actions Secrets 里,命名为DH_TOKEN,用户名也一并存进去,命名为DH_USERNAME。
关于 Docker Hub 的匿名访问我得提醒一句:虽然公共镜像在大多数时候不用认证也能拉,但 Docker Hub 对匿名拉取有很严格的速率限制,尤其是来自共享 IP 的流量。跑自动同步任务时一旦触发限流,拉取就会断断续续。提前把 Token 配好,能避开这个坑。
4.3 ACR 的认证方式选哪种
ACR 这边的认证方式,主要有两种思路。
第一种是直接用 ACR 的 Admin 账号。创建 ACR 时可以开启 Admin 账号,它有一对用户名和密码,可以直接用于docker login或 registry 鉴权。这种方式配置最简单,但有两个问题:账号权限范围是整个 ACR,没有任何细粒度控制;密码是常驻的,一旦泄露影响面很大。所以我不建议把它作为长期方案。
第二种是用服务主体(Service Principal)。Azure 的服务主体本质上是一个“机器人账号”,专门给程序和自动化任务使用。你可以给它分配一个最小区间权限的角色,比如只允许拉取或只允许推送,这样即使某个环境里的凭据泄露了,攻击者能做的事情也极其有限。
创建服务主体并分配权限,可以参考下面的 Azure CLI 命令:
az ad sp create-for-rbac \ --name http://acr-sync-sp \ --role AcrPush \ --scopes /subscriptions/<subscription-id>/resourceGroups/my-rg/providers/Microsoft.ContainerRegistry/registries/myacr执行成功后会输出appId和password,这两个值就是 ACR 侧认证用的用户名和密码,分别对应后续配置里的ACR_USERNAME和ACR_PASSWORD。
AcrPush角色允许推送镜像,是同步任务的最低必要权限。如果还有从 ACR 拉镜像的需求,可以考虑同时赋予AcrPull。尽量别直接给Contributor或更高角色,权限面收得越窄越好。
4.4 把密钥写进 GitHub Actions Secrets
GitHub Actions Secrets 是 workflow 读取凭据的安全入口。在仓库的 Settings → Secrets and variables → Actions 页面,把下面这几个变量依次新增进去:
DH_USERNAME:Docker Hub 用户名。DH_TOKEN:Docker Hub 访问令牌。ACR_USERNAME:服务主体的appId。ACR_PASSWORD:服务主体的密码。
这些 key 在 workflow 里可以通过${{ secrets.DH_USERNAME }}这种语法引用。GitHub 会自动对 Secrets 中的值做打码处理,在日志里显示成***,所以不用担心运行时不小心打印出来。
有一点特别重要:不要把这些值直接写进 workflow 文件或仓库里的任何配置文件。哪怕仓库是私有的,把密钥放进代码里也是坏习惯。密钥应该只存在于 GitHub Secrets 这个加密存储层里,被 workflow 引用时才注入到环境变量中。
5. 完整配置与首次同步
5.1 image-syncer 配置详解
image-syncer 的配置格式支持 JSON 和 YAML,我习惯用 YAML,结构更清晰一点。先来看最核心的同步清单长什么样:
auth: docker.io: username: "$DH_USERNAME" password: "$DH_TOKEN" myacr.azurecr.io: username: "$ACR_USERNAME" password: "$ACR_PASSWORD" images: docker.io/library/nginx:1.25.3: myacr.azurecr.io/mirrors/nginx:1.25.3 docker.io/library/redis:7.2.4: myacr.azurecr.io/mirrors/redis:7.2.4auth段里以 registry 域名作为 key,下面放对应的用户名和密码。docker.io是 Docker Hub 的标准域名,ACR 的登录服务器地址就是那串myacr.azurecr.io。
注意我这里写的变量是带引号的字符串占位符,不是让 image-syncer 自己去读环境变量。我会在 workflow 里用envsubst把占位符替换成真正的凭据,再生成临时配置文件。这样配置文件本身永远不包含明文的密钥。
images段的写法是“源地址 : 目标地址”的映射关系。源地址里写完整仓库路径和 tag,目标地址里写 ACR 的仓库路径和 tag。这里的目标仓库名可以重新组织,比如我在前面统一加了一个mirrors/前缀,这样 ACR 里哪些镜像是同步来的,一目了然。
如果你想把某个源仓库的所有 tag 全部同步过去,目标地址可以不写具体 tag,比如:
images: docker.io/library/nginx: myacr.azurecr.io/mirrors/nginximage-syncer 会把源仓库里当前存在的 tag 都同步到目标仓库,目标 tag 名沿用源 tag。如果只是要同步某几个特定 tag,就把目标地址写完整。两种写法结合使用,能应付绝大多数场景。
5.2 GitHub Actions workflow 完整示例
配置文件准备好后,把它放在仓库根目录下的sync/config.tpl.yaml。然后创建 workflow 文件.github/workflows/sync-to-acr.yml,内容如下:
name: sync-to-acr "on": workflow_dispatch: schedule: - cron: "0 3 * * *" permissions: contents: read jobs: sync: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v4 - name: Download image-syncer run: | curl -fsSL -o /tmp/image-syncer "<image-syncer 的下载地址>" chmod +x /tmp/image-syncer - name: Resolve config run: | envsubst < sync/config.tpl.yaml > sync/config.yaml env: DH_USERNAME: ${{ secrets.DH_USERNAME }} DH_TOKEN: ${{ secrets.DH_TOKEN }} ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} - name: Run image-syncer run: | /tmp/image-syncer \ --config sync/config.yaml \ --proc 4 \ --log ./sync-log - name: Upload sync logs if: always() uses: actions/upload-artifact@v4 with: name: sync-log path: ./sync-log我逐段说明一下。
"on"这里的引号非常重要。在 YAML 规范里on可能被某些解析器当作布尔类型处理,加引号是为了确保 GitHub Actions 正确识别事件定义。
workflow_dispatch允许你在 Actions 页面手动点击运行;schedule定义了每天凌晨三点跑一次定时同步。时间用的是 UTC 时区,对齐到自己服务器的时间记得换算,别按本地时间理解。
permissions段把默认权限收窄到只读。这个 job 不需要往仓库写任何东西,只需要 checkout 代码和下载工具,最小化 token 权限是对的。
第三步Resolve config用envsubst把模板文件里的占位符替换成环境变量。环境变量值来源于 GitHub Secrets,在 GitHub 日志里会被自动打码。
第四步是真正的同步。--proc 4表示并发 4 个镜像同步。这个数字不是越大越好,后面我会讲。
Upload sync logs这一步加了if: always(),意思是不管前一步成功还是失败,都把同步日志上传为构建产物。这样排查问题时有据可查,不用翻 runner 的原始日志。
5.3 第一次同步的完整过程
第一次同步我建议先手动触发,别直接让它挂到 cron 上。点击 Actions 页面里的工作流,选择分支,然后运行。
运行过程中可以实时切到每一步的输出日志。依赖下载完成后,image-syncer 的日志会开始滚动,形如下面的样子:
INFO: start to sync docker.io/library/nginx:1.25.3 to myacr.azurecr.io/mirrors/nginx:1.25.3 INFO: start to download image layer ... INFO: image layer download succeeded INFO: start to push image layer ... INFO: image layer push succeeded INFO: sync success, time cost: 32s看到每个镜像末尾出现sync success,基本可以断定同步成功了。
第一次同步往往比较慢,因为源仓库的层需要全部拉一遍再推上去。但第二次再跑同样的同步,速度会快很多。image-syncer 会先对比目标仓库里已有的 digest,命中相同的层就跳过,只处理有差异的部分。这个能力在增量同步里价值巨大,基础镜像几 GB 的层,第二次跑可能只需要几秒钟。
如果中途出现失败,不要整批重跑。先看日志,确定是某个镜像的网络问题还是认证问题。单点失败不影响其他镜像,下次触发重跑时只会补齐未完成的部分,已经推上去的不会重复推送。
5.4 验证镜像同步结果
同步完成后,至少要做三层验证,别只看 workflow 成功就以为万事大吉。
第一层,确认 ACR 里的镜像列表。用 Azure CLI 查看:
az acr repository list --name myacr --output table如果mirrors/nginx和mirrors/redis都出现在列表里,说明仓库确实建好了。
第二层,确认具体 tag 存在:
az acr repository show-tags \ --name myacr \ --repository mirrors/nginx \ --detail返回结果里应该有1.25.3,并且能看到 digest 和创建时间。
第三层,直接在 ACR 上拉一次镜像做冒烟测试:
docker pull myacr.azurecr.io/mirrors/nginx:1.25.3登录 ACR 的凭据用之前准备好的服务主体,拉到镜像后跑一个简单的容器,看能不能正常启动。这一步验证的不只是镜像转移的正确性,还包括 ACR 对外提供服务的连通性。
6. 常见问题速查与排查实录
6.1 高频故障与排查要点
跑这套同步方案快一年,我把实际出现过的问题整理成了下面的速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 认证失败,401 Unauthorized | Docker Hub Token 失效或权限不足 | 检查 Token 是否过期,权限是否为只读 |
| 认证失败,403 Forbidden | ACR 服务主体没有推送权限;或 admin 账号密码错误 | 检查角色是否为AcrPush,重新生成密码 |
| 大量镜像超时 | 镜像层太大,链路不稳定 | 降低--proc,增加同步批次,考虑分仓库同步 |
| 同步成功但 tag 缺失 | 目标仓库写错 tag 或源 tag 已不存在 | 核对配置,去源仓库确认 tag 列表 |
| 同步跳过但目标仓库没有更新 | 目标仓库存在相同 digest | 确定是否需要强制覆盖,用--force参数 |
| workflow 找不到配置文件 | 路径写错,或模板文件没提交 | 查看 checkout 步骤后的文件树 |
| runner 内存耗尽 | 并发数过高,下载缓存过大 | 调低--proc,或者把--log输出到临时目录 |
排查的时候有个思路很关键:先区分是哪一端的错误。源仓库失败、目标仓库失败、还是工具本身失败。image-syncer 的日志里每行都会带 register 的域名,看到域名基本就知道是哪一端。如果是docker.io失败,先查 Docker Hub 网络;如果是myacr.azurecr.io失败,查 ACR 凭据和权限。
6.2 我踩过的坑
第一个坑是凭据长度。服务主体的password自动生成后是一长串字符,我一开始把它复制进 GitHub Secrets 时,前后不小心多了一个空格。结果就是 image-syncer 报认证失败,日志里又看不出空格问题。后来我把环境变量的值通过脚本打印成字符长度做校验,才定位到是空格在捣乱。
第二个坑是定时同步的时区。GitHub Actions 的schedule用的是 UTC,我一开始直接写了一个国内常见的凌晨两点,结果同步任务在本地时间上午十点跑了一次,完全打乱了我的预期。后来统一改成在时间表达式上一行注释,标注转换后的本地时间,避免团队里其他人也踩一遍。
第三个坑是latest标签。我最开始把nginx:latest放进了同步清单,想着保持最新版本。但latest在 Docker Hub 是会变化的,第一次同步成功之后,后面新增的版本不会再自动更新到latest上,除非我手动重跑一次。结果某一次线上需要时才发现 ACR 里的latest已经落后了很久。现在我对不确定性标签的策略就是:明确到版本号,不用漂移标签。
第四个坑跟权限有关。我一开始图省事,给服务主体配的是Reader角色,同步时发现 push 失败,日志一直提示对仓库没有写权限。后来把角色改成AcrPush才解决问题。角色分配虽然是一句话的事,但修改角色之后,服务主体的 Token 需要重新获取,不能直接复用旧值。
7. 进阶玩法:定时、多目标与失败通知
7.1 定时同步与事件驱动的组合
定时同步只是基础玩法,我比较推荐的是“定时 + 手动 + 事件”三者组合。
定时保证了每天至少有一次兜底同步,就算白天漏了任何变更,凌晨也会拉齐。手动触发用于临时加急同步,比如某个紧急版本需要立刻推送到 ACR。事件绑定最适合与代码更新强同步的场景,比如镜像源仓库一有新提交,就自动触发一次同步。
在 GitHub Actions 里,这三者是可以同时写在on字段里的:
"on": workflow_dispatch: schedule: - cron: "0 3 * * *" push: branches: - main paths: - "sync/images.yaml"这里我加了一个paths过滤,只有当sync/images.yaml这个文件发生变更时,push 事件才会触发同步。这样你更新同步清单的操作本身,就等于告诉打包机有新的同步需求了。
7.2 多目标注册表扩展
如果团队有多套环境,每个环境用不同的 ACR,甚至有的是私有 Harbor、其他云平台,image-syncer 也可以承接多目标场景。
最直接的方式是准备多份同步清单,分别对应不同目标仓库,然后在一个 workflow 里并行跑多个 job:
jobs: sync-prod: runs-on: ubuntu-latest steps: - run: image-syncer --config sync/prod.yaml --proc 4 sync-test: runs-on: ubuntu-latest steps: - run: image-syncer --config sync/test.yaml --proc 4这样两个环境同步是隔离的,一个失败不会阻塞另一个。缺点是多份配置会有重复维护的成本。如果实在想减少重复,也可以生成动态清单再执行,但可读性和排查难度都会增加。两害相权,我倾向于多份静态配置。
7.3 失败通知与审计留痕
自动化的价值在于出了问题能第一时间知道。GitHub Actions 本身有运行页面,但没人会时刻盯着页面看,所以失败通知一定要接上。
最省事的方式是给 workflow 加一个失败通知 step,调用即时通讯工具的 webhook 接口:
- name: Notify on failure if: failure() run: | curl -fsSL -X POST -H "Content-Type: application/json" \ -d '{"msg":"镜像同步任务失败,请查看日志。"}' \ https://your-webhook-endpoint.example这里的if: failure()表示只有前面步骤失败时才执行。webhook 地址建议加到一个专门的WEBHOOK_URLSecret 里,别写在配置文件中。
除了通知,日志的保存也很重要。我在 workflow 里已经加了upload-artifact上传同步日志,同时建议再开一个长期存储的位置,比如把日志推到对象存储或日志服务里。这样每次同步的增量、耗时、失败点,都能形成一条完整的审计链,后面做容量规划、网络质量评估时也有数据支撑。
8. 写在最后:我的一些操作习惯
这篇分享到这儿该收尾了,最后分享几条我在实际运维中沉淀下来的小习惯。
第一,固定 image-syncer 的版本。CI/CD 管道里最怕“不确定行为”,如果每次运行时下载的二进制版本都不一样,出问题之后很难复现。我在 workflow 里写死一个具体版本号,下载地址也指向固定 Release,而不是用latest或自动更新。
第二,第一次跑全量同步时不要开太高的并发。很多人看到--proc能开 8、10,觉得越快越好。但第一次同步时,runner 上要同时下载几十个镜像的层,内存和磁盘瞬间顶满,很可能直接把 job 杀掉。实际经验是先从--proc 2或--proc 4跑,等基础镜像都同步完了,再逐步提高并发做增量。
第三,ACR 侧我会开定期清理策略。同步过来的镜像越来越多,过期 tag 不及时清掉,会白白占存储配额。可以用 ACR 的 retention policy,或者写一个定时任务清理旧 tag。镜像同步不是终点,存储治理才是长期要坚持的事。
第四,整个同步流程不要只看一次成功。偶尔凌晨跑一次全量同步,然后到 ACR 里随机抽查一批镜像的 digest 和 tag 完整性。自动化的前提是可预期,但可预期需要靠持续验证来维持。
镜像同步这件事,本质上解决的是“供应链可控性”的问题。GitHub Actions 加 image-syncer 这套组合,胜在轻量、稳定、可解释。它不追求炫技,就是把“拉”和“推”这两个动作管得明明白白,让团队不用再拿时间和心情去赌每一次手动的稳定性。