资讯动态

kubectl 更新容器镜像:Deployment 滚动更新与排查实践

发布时间:2026/9/16 23:06:26 来源:尧图企业网站定制
凌晨两点被群里叫起来说新版本没生效用户还在走老逻辑。登上去一看kubectl get deploy里显示的镜像是新的kubectl get pod里跑着的容器却是三天前那一版。这种场面我遇到过不止一次问题往往不出在改镜像这条命令本身而是改完之后镜像根本没被重新拉取或者你改的对象压根没触发滚动更新。kubectl更新容器镜像这件事表面看只有一行命令实际上至少有五六条路子每条路子的适用场景、副作用、回滚成本都不一样随手挑一条用出事是迟早的。这篇东西我按自己的使用习惯来写先把改的到底是谁身上的哪个字段讲清楚再把set image、edit、apply、patch这几条路一条条拆开最后讲镜像没换成功时怎么顺着链路查下去以及把这件事塞进 CI 流水线时要注意什么。适合已经能跑kubectl get pod、但对 Deployment 更新机制只有模糊印象的人看也适合天天在流水线里改 tag 却总被更新没生效折磨的人对照着排查。1. 先把更新镜像这件事拆开你改的到底是哪个对象的哪个字段1.1 镜像字段藏在三层嵌套里新手最容易犯的错是把kubectl set image当成一个改 Pod 镜像的命令。实际上绝大多数情况下你改的是 Deployment或者 StatefulSet、DaemonSet、CronJob的 Pod 模板路径长这样Deployment └── spec └── template - Pod 模板改这里才会产生新的 ReplicaSet └── spec ├── initContainers[] │ └── image └── containers[] └── image关键点在于spec.template是模板。你把它改掉Deployment 控制器会发现模板的哈希变了于是创建一个新的 ReplicaSet再由新旧两个 ReplicaSet 按滚动更新策略做替换。所以更新镜像本质上是一次模板变更而不是给正在跑的容器换个皮。顺带说一个冷知识Pod 对象里的image字段其实是允许原地修改的少数几个字段之一和activeDeadlineSeconds、tolerations之类一样属于可变更字段你直接kubectl set image pod/xxx cimg:new也能生效kubelet 会把容器重启一遍。但这条路基本不要走因为 Pod 的所有者ReplicaSet 之类会按模板把差异纠回来你等于是在跟自己较劲。除非你手动裸跑一个 Pod否则一律改控制器对象。1.2 更新镜像和重启 Pod是两件不同的事这是第二个高频误解。很多人改完镜像没看到效果第一反应是kubectl rollout restart结果重启之后还是旧镜像于是开始怀疑人生。这两件事的关系是这样的镜像如果变了重启是自动发生的不需要你额外操作镜像如果没变同一个 tag 覆盖推送这种那你重启一百次拉到的还是本地的旧层因为imagePullPolicy决定了 kubelet 要不要去仓库看一眼。所以要不要重启这个问题根本不该出现你该问的是我的镜像引用到底变没变。1.3 动手前的三秒检查上下文、命名空间、拉取策略我现在的习惯是在执行任何变更类命令之前先敲三条只读命令确认现场。这不是仪式感是真的踩过改到测试集群以为在改生产的坑。kubectl config current-context kubectl config view --minify -o jsonpath{.contexts[0].context.namespace} kubectl get deploy name -o jsonpath{.spec.template.spec.containers[*].image}{\n} kubectl get deploy name -o jsonpath{.spec.template.spec.containers[*].imagePullPolicy}{\n}第三条和第四条就是看当前镜像和拉取策略。如果imagePullPolicy是IfNotPresent而你的 tag 又是固定值比如v1.2.3被重复推送覆盖那后面必然会出现改了没生效提前知道这点能省掉半小时排查。2. kubectl set image一行命令改镜像以及它的边界在哪2.1 语法里最容易写错的三个位置标准写法kubectl set image deployment/deploy-name container-namenew-image -n namespace三个位置各有各的坑。资源类型和名字写错会直接报 not found这个好办真正麻烦的是容器名。容器名不是 Deployment 的名字是spec.template.spec.containers[].name那个值两者经常不一样。忘了容器名怎么办kubectl get deploy name -o jsonpath{range .spec.template.spec.containers[*]}{.name}{\n}{end}如果你的 Pod 里只有一个常规容器可以用通配符省事kubectl set image deployment/web *registry.example.com/web:1.6.0*会把containers列表里的所有容器都改成同一个镜像——这在你写 sidecar 名字写到手酸的时候确实爽但反过来想它也意味着你 Terminal 上任何多容器 Pod 都会被无差别覆盖我一般只在单容器场景用。还有一点值得单独提醒initContainer 的镜像我习惯单独确认一遍不要指望一个*就万事大吉。改完之后用kubectl get deploy -o yaml把整个 Pod 模板翻一遍比什么都靠谱。2.2 先预览再落地--dry-run 的正确用法变更类命令我强烈建议养成先 dry-run的习惯。set image支持客户端预演kubectl set image deployment/web webregistry.example.com/web:1.6.0 \ --dry-runclient -o yaml | grep -A2 image:它不会打请求到 API Server只在本地把修改后的对象渲染出来给你看。确认容器名匹配上了、镜像 tag 没打错再执行真命令。这个动作花 5 秒能挡掉容器名打错导致新起了一个同名以外的容器这类不可逆的蠢事——虽然set image遇到不存在的容器名会报错但你先看一遍总归更稳。2.3 --record 已经指望不上了老教程里经常让你加--record说这样可以记录变更原因方便kubectl rollout history里看到是谁改的。这个参数在新版本里已经废弃并移除了加了要么报错要么无声地不生效。现在要留变更记录正规做法是自己在 Pod 模板上打 annotationkubectl annotate deployment/web \ kubernetes.io/change-causerelease-1.6.0 by ci-1024 \ --overwrite注意这个 annotation 打在 Deployment 上就行不需要打进 Pod 模板。把变更来源、工单号、提交哈希写进去出问题回溯时能救命。实测下来set image这条路的优点是快、可脚本化、变更面小只动 image 字段缺点是它只能改镜像不能同时改镜像拉取策略、资源限制、环境变量。如果一次发版要动好几个字段别硬凑set image直接走apply。3. kubectl edit 与 kubectl apply看得见改动的两条路3.1 kubectl edit 适合救火不适合长期依赖kubectl edit deployment/web -n prod它会用你的KUBE_EDITOR没设就用 vi拉一份实时对象给你改存盘退出后把差异提交回 API Server。优点是所见即所得容器名、tag、imagePullPolicy都在眼前不容易写错字段路径。它的风险也全在这里你手里拿的是集群里当前的状态不是 Git 仓库里的清单。如果这时候集群和仓库本来就漂移了你基于漂移状态改一下再提交漂移就被固化了。更别说手一抖删掉两行、或者把缩进弄乱报错还好怕的是改坏了别的地方你还浑然不觉。我现在只在两种场景用它一是半夜救火需要立刻把镜像换回上一个稳定版本二是排查字段到底长什么样进去看一眼就退出不存盘。长期变更一律走文件。3.2 从 YAML 文件改镜像再 apply 才是正路标准动作# 1. 从集群导出当前清单注意去掉运行时字段 kubectl get deployment web -o yaml web.yaml # 2. 编辑 image 字段 $EDITOR web.yaml # 3. 回灌 kubectl apply -f web.yaml --dry-runserver # 先做服务端校验 kubectl apply -f web.yaml这里有两点经验值得说。第一导出后记得清掉metadata.resourceVersion、status、metadata.uid这些运行时字段虽然apply大多数时候能容忍它们但混着放容易在后续 diff 时产生噪音。第二--dry-runserver和--dry-runclient的差别很关键客户端预演只做语法和本地结构校验服务端预演会把请求真的发到 API Server 做准入admission校验但不落库。真正能拦住问题的是服务端预演。如果你的清单是用 Helm 或 Kustomize 管理的那就更不该手改渲染后的 YAML。Kustomize 有对应的命令直接改镜像引用kustomize edit set image registry.example.com/webregistry.example.com/web:1.6.0它改的是kustomization.yaml里的 images 段比全局搜索替换安全得多。3.3 apply 时报 error validating 校验失败按这个顺序查网上关于apply校验错误的提问特别多典型症状就是类似error: error validating xxx.yaml这种形式。这类报错看起来吓人其实排查路径非常固定我一般按下面这个顺序走。排查顺序检查项典型现象处理方式1YAML 语法与缩进error converting YAML to JSON用kubectl apply --dry-runclient定位行号检查缩进和制表符2字段类型是否匹配cannot unmarshal string into Go value of type int32端口、副本数这类字段不要加引号3apiVersion 与集群版本no matches for kindkubectl api-resources对比可用的组与版本4自定义资源是否已注册no matches for kind Xxx in version先确认对应的 CRD 已安装并就绪5旧的三路合并基线冲突field is immutable或字段被莫名重置清掉last-applied-configuration或改用服务端应用第三条和第四条特别容易被忽略。很多清单是从网上抄的版本比较老而你的集群已经升级过资源组名或版本号早就变了。第四条的典型场景是你先把使用了某个自定义资源的清单灌进去但负责提供这个 CRD 的组件还没装好或者还没就绪apply自然就报校验失败。解决办法很简单先装 CRD、等它 Ready再灌业务清单。同样的道理如果你在改镜像的时候顺手apply了一堆网络插件的清单报错大概率是在 CRD 或者 RBAC 那一层别死盯着 Deployment 看。还有一条隐形坑kubectl apply依赖last-applied-configuration这个 annotation 做三路合并。如果这个对象曾经被kubectl edit手改过、或者被别的控制器比如同步工具、Operator动过合并结果可能和你想的不一样——你只改了一个 image结果别的字段被回退成了旧值。遇到这种诡异现象别怀疑人生先看那个 annotation必要时用kubectl apply --server-side --force-conflicts换成服务端应用把字段所有权理清楚。4. kubectl patch脚本化和流水线里最趁手的一把刀4.1 三种 patch 类型用错了会打到别的容器上kubectl patch支持三种写法差别不是风格问题是会出错的问题。strategic merge patch不显式指定 type 时的默认值是 strategic对内置资源生效。它的特点是列表按 merge key 合并containers列表的 merge key 是namekubectl patch deployment web -n prod --typestrategic -p \ {spec:{template:{spec:{containers:[{name:web,image:registry.example.com/web:1.6.0}]}}}}注意我上面只写了name和image两个键其余字段都保留了——这就是 merge key 的好处它是按容器名字匹配的不会因为顺序变化打错目标。JSON merge patch--typemerge就不一样了它遇到列表是整体替换。上面那条 patch 如果换成--typemerge你的容器列表会被替换成一个只有 name 和 image 的残缺容器其他字段全没了。这是个非常隐蔽的坑容器能起来纯属运气好。JSON patch--typejson按数组下标操作kubectl patch deployment web --typejson -p \ [{op:replace,path:/spec/template/spec/containers/0/image,value:registry.example.com/web:1.6.0}]快是快但containers/0这种下标绑定非常脆。别人往模板里加了个 sidecar 并且排在前面你的 patch 就把 sidecar 的镜像改了业务容器纹丝不动报错都没有。除非你百分之百确定列表顺序不会变否则别用下标。4.2 镜像 tag 不变时用 annotation 强制触发滚动这是实战里使用频率最高的一个 patch 技巧。有些场景你不得不复用同一个 tag比如内部约定用stable这种浮动 tag这时候光改 image 是没用的因为模板哈希没变。做法是往 Pod 模板上打一个时间戳 annotationkubectl patch deployment web -n prod -p \ {\spec\:{\template\:{\metadata\:{\annotations\:{\kubectl.kubernetes.io/restartedAt\:\$(date -u %Y-%m-%dT%H:%M:%SZ)\}}}}而kubectl rollout restart deployment/web内部干的就是这件事只是它把时间戳格式和字段名都替你想好了所以能用这条内置命令就别手写 JSON。两者的区别是restart不会改镜像它假设镜像已经变过了或者你就是要重拉而 patch 可以一次把镜像和 annotation 都带上。顺便说restartedAt这种改模板元数据来触发更新的套路本质上是利用了模板哈希变化这个机制跟镜像本身没关系。理解这一点之后你就能明白为什么给 Pod 模板加一个无害的 annotation 也能触发完整的一轮滚动更新。4.3 patch 的幂等性和返回信息要看清patch是幂等的同样的 patch 打两次第二次的结果和第一次一样这点在重试逻辑里很省心。但它的返回信息值得每次看一眼尤其是输出里带(no change)的时候——那说明你的 patch 根本没匹配上任何字段常见原因是容器名写错了或者 annotation 的路径层级少写了一层。kubectl patch deployment web -p {spec:{template:{metadata:{annotations:{x:1}}}}} # deployment.apps/web patched - 真的改了 # deployment.apps/web patched (no change) - 没匹配上回去检查路径在流水线里我习惯把这行输出抓下来做判断出现(no change)就让流水线失败而不是当作成功继续跑。因为更新镜像这件事最怕的就是静默成功。5. 镜像没换成功顺着这条链路往下查5.1 头号嫌疑tag 没变加上 IfNotPresent前面埋了很久的伏笔现在揭晓。imagePullPolicy的默认规则是这样的如果 tag 是latest或者你根本没写 tag等同于 latest默认是Always如果写了具体 tag默认是IfNotPresent。IfNotPresent的语义是本节点上已经有这个镜像引用了就不再向仓库请求。注意它判断的是镜像引用字符串不是内容摘要。所以你把web:1.6.0这个 tag 重新推了一遍节点上还留着旧的web:1.6.0kubelet 会认为我有这个镜像了根本不去查仓库。结果就是你改了 Deployment、确实触发了滚动更新、Pod 也确实重建了但跑的还是旧代码。排查三板斧kubectl get pod pod -o jsonpath{.spec.containers[*].image}{\n} kubectl get pod pod -o jsonpath{.status.containerStatuses[*].imageID}{\n} kubectl describe pod pod | grep -A3 Image:第一条是清单里声明了什么第二条是实际运行的镜像摘要是什么。这两个对不上说明拉取环节出了问题。三种解法按推荐程度排换 tag最干净天然幂等、换成 digest 引用最严格、把imagePullPolicy改成Always最省事但每次重建都要走一次仓库会拖慢扩容和故障恢复节点多的时候还会压仓库。我自己的选择是生产环境用 digest预发用递增 tag测试环境随便。5.2 滚动更新卡住不动问题通常不在镜像kubectl rollout status deployment/web不返回或者一直显示Waiting for deployment rollout to finish这时候别急着去查镜像先看这几个常见原因。现象可能原因快速确认方式新 Pod 一直是 Pending节点资源不足、亲和性、污点kubectl describe pod看 Events新 Pod ImagePullBackOff镜像地址、私有仓库凭据检查imagePullSecrets与节点网络新 Pod CrashLoopBackOff应用启动失败、配置缺失kubectl logs --previous新 Pod Running 但一直不就绪就绪探针配置不当或依赖不可用kubectl describe pod看探针失败信息老 Pod 不退出maxUnavailable: 0加探针不通过看 Deployment 的更新策略字段滚动进度为零maxSurge: 0且maxUnavailable: 0这是个非法组合会被拒绝或卡死maxSurge: 0和maxUnavailable: 0同时设置是典型的自锁配置。前者限制不能多开 Pod后者限制不能少 Pod那新 Pod 永远没位置起滚动就停在那里了。改镜像之前顺手看一眼更新策略能避免很多命令执行成功了但没动静的诡异体验。还有两个经常被忽略的因素。一是PDBPodDisruptionBudget它管的是主动驱逐理论上不阻塞 Deployment 自己的滚动更新但在节点资源紧张、Pod 被驱逐和滚动更新交织的场景下会让你感觉更新特别慢。二是探针的 initialDelaySeconds 和 failureThreshold 乘起来太保守一个 Pod 要等三分钟才 Ready十副本滚动就是半小时你会以为卡住了。5.3 rollout status / history / undo 的正确配合这三个命令我用得最频繁值得单独说清楚kubectl rollout status deployment/web -n prod --timeout300s kubectl rollout history deployment/web -n prod kubectl rollout history deployment/web -n prod --revision3 kubectl rollout undo deployment/web -n prod --to-revision3history列出的每一行对应一个 ReplicaSet能回滚多少个取决于spec.revisionHistoryLimit默认保留 10 个。所以别以为回滚是无限的发版特别频繁的服务建议把这个值调到 20 以上。undo --to-revision才是真正的回滚到某一版不带参数只回退到上一版。这里有个隐蔽的坑回滚本身也是一次模板变更它会创建一个新的 revisionhistory 里的编号是往前增长的不是简单地把指针拨回去。理解这一点你在排查为什么回滚之后 revision 号变大了时就不会困惑。还有一个必须提醒的点rolling update 的暂停与恢复。kubectl rollout pause之后你连续改好几次镜像最后resume只会触发一次滚动。这在需要同时改镜像和配置的联调场景里非常好用能避免中间态被观察到。kubectl rollout pause deployment/web kubectl set image deployment/web webregistry.example.com/web:1.6.1 kubectl set env deployment/web FEATURE_Xon kubectl rollout resume deployment/web5.4 用 digest 固定版本顺手把镜像安全的基础动作做掉容器镜像安全这件事落到日常操作上其实很朴素别用浮动 tag别让同一个 tag 指向不同内容。最直接的做法就是引用 digestregistry.example.com/websha256:7f8e...c1a2digest 是内容寻址的内容变一点digest 就完全不同根本不存在同名 tag 被覆盖这个问题。获取 digest 的方式用容器运行时自带的工具或者专门的镜像检查命令都行例如skopeo inspect docker://registry.example.com/web:1.6.0 | grep Digest把 digest 写进 Deployment 之后imagePullPolicy用IfNotPresent反而是安全的因为 digest 变了就意味着引用字符串变了kubelet 一定会去拉。这也顺带解决了一个安全审计需求你能精确回答线上跑的是哪一次构建产物。配套的两个习惯我也一并说说。一是私有仓库的凭据用imagePullSecrets引用一个只读权限的 Secret不要图省事把长期有效的凭据写进节点级配置。二是发版用的 ServiceAccount 权限要收窄只给它需要改的那几个 Deployment 的get、patch、update权限而不是一把管理员凭据走天下。这个后面在 CI 部分还会展开。6. 把更新动作放进流水线从 kubeconfig 到漂移控制6.1 CI 里配置 kubeconfig 的三种方式与各自的坑很多人第一次在流水线里跑kubectl会卡在认证上报的无非是几个错找不到配置文件、证书过期、权限不足。以 GitLab CI 为例常见的做法有这么几种。第一种是把 kubeconfig 内容作为一个受保护的变量存起来base64 编码后存放更稳妥避免换行被吃掉在 job 里解码到临时文件然后靠KUBECONFIG环境变量指过去echo $KUBE_CONFIG_B64 | base64 -d /tmp/kubeconfig export KUBECONFIG/tmp/kubeconfig kubectl config current-context第二种是走集群提供的令牌签发机制生成短期凭据用完后自动失效安全性明显更高代价是配置步骤稍多。第三种是在集群侧部署一个常驻的同步组件由它去拉取仓库变更流水线根本不持有集群凭据。规模大一点、合规要求高一点的团队基本都会走到第三种。不管用哪种有三个细节必须注意。一是KUBECONFIG指向的文件权限CI 环境里经常是多用户共享的 runner临时文件最好放在 job 私有目录里并在结束时清理。二是每个 job 都要确认当前 context 和 namespace因为共享 runner 上可能残留环境变量你以为是生产其实打到了另一个集群。三是别用个人账号的凭据个人凭据权限大、无法审计、人一离职就废用专门的 ServiceAccount 加 RBAC 才是正路。一个收窄权限的 RBAC 示例思路是只允许它改指定 namespace 下的 DeploymentapiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: prod name: image-updater rules: - apiGroups: [apps] resources: [deployments] verbs: [get, list, watch, patch, update]6.2 直接 set image 改集群还是改仓库让同步工具去推这是流水线设计里最值得想清楚的一个取舍我把它做成表格对照着看方式变更来源优点风险流水线直接set image/patch集群实时状态见效快无需等同步周期与 Git 漂移下次同步可能被回退流水线改清单文件再apply文件变更可追溯、可评审需要管理文件仓库和合并冲突流水线只改文件由同步工具拉取文件 控制器单一事实来源最可控链路长出问题要跨系统排查我踩过的坑是这样的团队一边用同步工具盯着仓库一边又在流水线里直接set image。结果是流水线改完之后同步工具在下一个周期把集群状态按仓库内容纠了回去镜像又变回旧版本。整个过程没有任何报错只有用户投诉。所以选了一条路就别脚踏两条要么流水线只改文件要么关掉同步工具对这类资源的接管用 ignoreDifferences 之类的机制。还有一个务实的小技巧如果你们不得不在流水线里直接改集群比如临时热修至少把变更同步回仓库的清单或者在 Deployment 上打一个带工单号的 annotation给后面的人留一条线索。6.3 灰度与回滚要跟更新动作一起设计最后说一个容易被当成后续再说的问题。更新镜像这一步一旦自动化触发频率会上来灰度能力就变成了必需品。最简单的一套组合是先rollout pause改镜像、改配置resume之后观察rollout status出问题立刻undo --to-revision。再往前走一步就是把副本数拆成稳定版和金丝雀版两个 Deployment用 Service 的选择器或者流量层来做切分镜像更新只发生在金丝雀那一边。回滚能不能真的救你取决于三个配置revisionHistoryLimit是否够大、旧镜像是否还在仓库里没被清理、以及回滚所需的配置项是否也做了版本化。我见过最尴尬的情况是回滚命令执行成功Pod 也起了但因为旧镜像的 tag 已经在上周被清理工具删掉了新拉起的 Pod 卡在 ImagePullBackOff回滚变成了二次事故。镜像仓库的生命周期策略和 Deployment 的回滚窗口这两个参数是要一起看的不是各管各的。回到我自己的日常习惯现在固定这么一套预发环境用set image加递增 tag 快速验证生产环境一律走文件加同步工具的流程所有生产镜像引用都用 digest任何变更类命令之前先--dry-run看一眼执行之后必须跟一条rollout status确认到底。这套动作不复杂但确实把改完没生效这类问题从每周几次压到了几乎为零。真正省时间的从来不是那行命令而是命令前后那几秒钟的确认动作。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价