容器 OTA 升级不是把新镜像推上去再重启进程那么简单。边缘设备经常运行在弱网、无人值守、算力受限的环境中,一次失败的升级可能导致设备失联,甚至需要人工到现场恢复。因此,升级方案必须围绕可中断、可校验、可回滚三个目标来设计。镜像的分层特性让差量传输成为可能,而不可变文件系统与双槽位切换则能保证设备始终处于一个确定的状态。本文会从整体架构、镜像签名、设备端原子切换以及回滚安全防线几个角度,完整拆解一套可落地的容器 OTA 升级方案。

一、容器 OTA 升级的核心架构
边缘设备的容器 OTA 升级通常涉及云端和设备端两部分。云端负责镜像构建、签名、发布元数据以及下发升级任务;设备端负责接收任务、拉取镜像、校验签名、切换版本并报告状态。把这套职责拆开后,每一层都可以独立演进,也更容易定位问题。
在云端,镜像仓库承担不可变分发的角色,签名服务负责对镜像摘要进行签名,元数据服务则维护每个设备型号的可用版本、兼容性要求和升级路径。设备端需要一个常驻的升级代理,它不直接运行业务容器,而是通过 systemd 或其他进程管理器来编排业务容器的启动与停止。存储层采用 A/B 双槽位,例如 /var/lib/ota/slot-a 和 /var/lib/ota/slot-b,当前运行的槽位通过一个符号链接 current 指向。升级时新镜像被下载到非活动槽位,校验通过后原子地改变符号链接指向,再重启业务容器。
这种双槽位方案的最大好处是切换动作非常轻量。设备不需要在升级过程中删除旧版本,也不需要担心文件复制到一半出现损坏。即使切换后新版本启动失败,也可以迅速把符号链接指回旧槽位完成回滚。对于存储空间敏感的设备,可以只保留最近两个版本,并在新版本稳定运行一段时间后清理更早的备份。
二、镜像构建、签名与版本发布
容器 OTA 升级的起点是镜像构建。生产环境中不能依赖可变标签来标识版本,因为同一个标签可能在不同时间指向不同镜像。正确做法是固定镜像摘要,例如 registry.ipipp.com/edge/app@sha256:abc123...。客户端在下载时使用摘要地址,可以确保拉取到的内容与签名时完全一致。
构建完成后需要对镜像进行签名。签名不是对标签签名,而是对镜像摘要签名。这样即使镜像被推送到不同仓库,签名仍然有效。常用的工具是 cosign,它支持使用私钥签名,并在设备端使用公钥验证。构建和签名的典型流程如下:
docker build -t registry.ipipp.com/edge/app:1.0.2 .
docker push registry.ipipp.com/edge/app:1.0.2
image_digest=$(docker inspect --format='{{index .RepoDigests 0}}' registry.ipipp.com/edge/app:1.0.2)
cosign sign --key cosign.key ${image_digest}
签名完成后,还需要发布升级元数据。元数据至少包含版本号、镜像摘要、签名公钥 ID、适用范围和升级依赖。为了防止攻击者截获元数据并回滚到旧版本,可以引入 TUF 框架或类似机制,对元数据本身也进行签名,并保证版本号单调递增。设备端在收到升级任务时,不仅要验证镜像签名,也要验证元数据签名和版本号,避免被降级到存在漏洞的旧版本。
此外,建议为每个镜像生成 SBOM 清单,记录依赖和组件版本。当某个底层库爆发漏洞时,维护人员可以快速判断哪些设备需要紧急升级,而不是盲目推送全量更新。
三、设备端下载、校验与原子切换
设备端升级代理通过 HTTP 轮询、MQTT 推送或 CoAP 等方式获取升级任务。任务中通常包含镜像摘要地址、元数据下载地址以及公钥文件版本。代理首先下载元数据并校验签名,然后使用 skopeo 或容器运行时自身能力将镜像拉取到非活动槽位。为了节省带宽,可以在设备本地缓存镜像层,只下载缺失或变化的层。
镜像下载完成后,代理使用本机预先信任的公钥验证签名。验证通过后,镜像被标记为候选版本,但不会立即切换。切换动作必须设计成原子操作。对于基于 systemd 的设备,最常见的方式是修改符号链接并重启服务。示例脚本如下:
#!/bin/bash
set -euo pipefail
IMAGE="registry.ipipp.com/edge/app@sha256:abc123..."
PUBKEY="/etc/ota/cosign.pub"
ROOT="/var/lib/ota"
SLOT="slot-b"
BUNDLE="${ROOT}/${SLOT}/bundle"
mkdir -p "${ROOT}/${SLOT}"
skopeo copy "docker://${IMAGE}" "oci:${BUNDLE}"
cosign verify --key "${PUBKEY}" "${IMAGE}"
ln -sfn "${SLOT}" "${ROOT}/current"
systemctl restart edge-app
echo "switched to ${SLOT}"
这个脚本展示了从下载到切换的完整过程。实际部署中还需要加入断点续传和超时控制。如果下载过程中网络中断,skopeo 会留下临时文件,代理应当能够识别并清理半成品。切换前还可以运行一次容器启动测试,例如使用 docker run --rm 提前验证镜像能够正常启动,但不要影响当前业务。
对于不能中断业务的场景,可以采用双容器热切换:先在新槽位启动新版本容器,健康检查通过后再把流量切过去,最后停止旧容器。这种方式对业务连续性的影响最小,但实现复杂度更高,适合网络网关或工业控制器等对停机敏感的场合。
四、回滚机制与升级安全防线
回滚是 OTA 升级中必须优先设计的能力。双槽位方案天然支持快速回滚,但自动回滚需要配合健康检查。升级代理可以在切换后启动一个健康检查脚本,如果业务服务在指定时间内未通过检查,则自动切回旧槽位并重启服务。
#!/bin/bash
for i in 1 2 3 4 5; do
if curl -fsS http://127.0.0.1:8080/health; then
exit 0
fi
sleep 2
done
current=$(readlink /var/lib/ota/current)
if [ "${current}" = "slot-a" ]; then
ln -sfn slot-b /var/lib/ota/current
else
ln -sfn slot-a /var/lib/ota/current
fi
systemctl restart edge-app
这个脚本先尝试五次健康检查,每次间隔两秒。如果检查全部失败,就读取当前槽位并切回另一个槽位。这里的关键是旧槽位必须保持完整可用,直到新版本被确认稳定。确认稳定的周期可以设置为几小时甚至几天,期间设备持续上报运行指标,云端也可以根据指标决定是否强制回滚。
安全方面,镜像签名只是第一道防线。设备需要内置可信根证书或公钥,所有升级通信应走 TLS 通道。对于版本号,设备必须拒绝低于或等于当前版本的升级任务,除非有特殊的人工授权。元数据签名也应采用多角色签名机制,避免单一密钥泄露导致全量设备被控。设备端还可以加入硬件安全模块或可信执行环境来保护私钥和签名验证逻辑,提升整体抗攻击能力。
综合来看,容器 OTA 升级方案需要把镜像分层、签名验证、原子切换和自动回滚有机组合起来。只有同时满足可中断、可校验、可回滚三个条件,才能在边缘设备的复杂环境中实现真正可靠的远程升级。