ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

kubectl 更新容器镜像全指南:滚动更新、回滚与安全

kubectl 更新容器镜像全指南:滚动更新、回滚与安全 上周半夜两点同事在群里甩了一句线上新版本镜像推上去了谁帮忙滚一下接着三个人同时敲回车结果同一个 Deployment 被三份不同的 YAML 覆盖副本数一会儿 3 一会儿 5最惨的是有个容器跑着新旧两个版本的镜像日志里一半是新格式一半是旧格式。那次事故之后我把 kubectl 更新容器镜像这件事完整梳理了一遍从最基本的 set image 到 GitOps 流水线里自动滚更把踩过的坑都记了下来。这篇内容面向的是经常要动线上工作负载的人刚接手 K8s 集群的运维、需要发版的后端开发、以及在做 CI/CD 流水线自动化的工程师。核心关键词就是 kubectl 和容器镜像但我不会只丢几条命令给你而是把为什么这条命令会触发滚动更新为什么换个镜像名没生效为什么 apply 会报 validating 错误这些真正让人卡住的地方讲透。看完你应该能做到在任何一类工作负载上安全地换镜像、知道怎么回滚、知道怎么在流水线里把这个动作自动化并且能守住镜像安全与容器安全的基本底线。1. 更新镜像这件事远比敲一条命令复杂1.1 一次换镜像背后到底发生了什么很多人对 kubectl 更新镜像的理解停留在改个字段Pod 重新拉一遍镜像。实际链路要长得多你改动的是 Deployment/StatefulSet/DaemonSet 的 Pod 模板控制器发现模板哈希变了于是按照更新策略新建一批 Pod等新 Pod 通过就绪探针后再逐步把旧 Pod 摘掉。中间涉及 ReplicaSet 的创建与保留、Service 端点的增删、endpoint 的平滑切换。这解释了为什么镜像换了但服务没重启和服务重启了两次都是可能发生的前者往往是因为你改的字段没有落在 Pod 模板里或者新老镜像的 tag 相同导致控制器认为模板没变后者通常是因为短时间内有多个来源同时改了模板控制器连续触发了两轮滚动。还有一个容易被忽略的点是 revision。每次 Pod 模板变化Deployment 控制器都会生成一条新的 revision 记录kubectl rollout history能看到kubectl rollout undo也是靠它回退的。默认revisionHistoryLimit是 10也就是说你连续快速滚 15 次最早那几次的历史就没了想回退到第一版是回不去的。生产环境我一般把这个值调到 20 以上代价只是多留几个闲置的 ReplicaSet。1.2 为什么我不建议删 Pod 让它自己重建我见过不少人的做法是kubectl delete pod xxx指望 Deployment 拉起新 Pod 时顺手拉到新镜像。这在镜像 tag 是latest且imagePullPolicy: Always的时候确实看起来生效了但它有两个致命问题。第一Pod 模板根本没变下一次任何原因触发的重建节点驱逐、节点重启、HPA 扩容都会把旧镜像拉回来你的发布是临时的。第二删除 Pod 绕过了滚动更新的并发控制如果你的副本数只有 2删掉一个的瞬间可用容量直接减半配合就绪探针的等待时间接口错误率会明显抬头。正确的做法永远是改动 Pod 模板让控制器用它自己的节奏去滚动。删 Pod 这种操作只适合在排查阶段临时验证新镜像能不能起来验证完立刻回退决定不要把它当成发布手段。1.3 本章小结式的几个判断原则模板没变就不要指望镜像变了这是唯一需要背下来的原则。更新策略决定风险RollingUpdate是默认Recreate会先全停再全起后者只适合单副本且能容忍中断的场景。历史版本是你的保险发布前先确认revisionHistoryLimit和当前 revision 数量。2. 动手之前环境、上下文与权限自查2.1 kubeconfig 与多集群上下文别改错集群生产事故里有一半的低级错误来自改错集群。你的~/.kube/config里通常躺着测试、预发、生产三套上下文kubectl config current-context是每次操作前必须确认的第一条命令。我自己养成的习惯是把上下文重命名成带环境前缀的名字比如prod-hz、stg-hz光标悬停就能判断不用去猜那个乱码一样的长名字对应哪套集群。kubectl config get-contexts看全量列表kubectl config use-context stg-hz切换kubectl config set-context --current --namespacemyapp把默认命名空间固定下来。这三条足够日常使用。真正要小心的是自动化脚本里直接--kubeconfig指定文件而没做校验我习惯在脚本开头加一句当前上下文打印失败就直接退出避免误伤。2.2 在流水线里怎么放 kubeconfig在 CI 里执行 kubectl思路是把 kubeconfig 内容作为受保护的变量注入。以 GitLab 为例通常会把 kubeconfig 文件做一次 base64 编码放进项目的 CI/CD Variables并且在before_script里解码落到~/.kube/config同时把文件权限收紧到 600。这样做的原因是避免把凭据直接写进仓库也避免它出现在构建日志里变量勾选 Masked 之后日志里出现的都是星号。需要注意的是从集群侧拿到的 kubeconfig 里带的凭据最好换成专用的服务账号令牌而不是给你个人的管理员凭据。服务账号只授予特定命名空间的 get/list/watch/patch/update 权限这样即使变量泄露影响面也是可控的。这一步做不做决定了你的镜像更新自动化是提效还是埋雷。2.3 权限自查为什么我 patch 会 403常见的报错是deployments.apps myapp is forbidden: User cannot patch resource。这说明你缺的是 verbo不是对象。可以用kubectl auth can-i patch deployments -n myapp快速判断返回 yes/no 一目了然。kubectl auth can-i --list -n myapp则能列出当前身份在该命名空间能做的所有动作排查 RBAC 时非常省事。提示在只有只读权限的环境里kubectl edit打开编辑器后你依然能改但保存时才会报错白白浪费时间。动手前先跑一次 can-i能避免很多无意义的等待。3. 方式一kubectl set image最直接的标准动作3.1 基本用法与一次完整发布这是最贴近更新容器镜像这条命令的写法kubectl set image deployment/myapp \ appregistry.example.com/team/myapp:v1.7.3 \ -n myapp格式是容器名镜像地址。容器名不是 Deployment 名是spec.template.spec.containers[].name写错了会直接报unable to find container。不确定容器名就用kubectl get deploy myapp -o jsonpath{.spec.template.spec.containers[*].name}打印一下比翻 YAML 快。一个 Deployment 里跑边车容器比如日志采集、指标暴露时只写一个容器名镜像是安全的其他容器的镜像不受影响。要一次换多个就继续追加键值对kubectl 会一次性提交只触发一轮滚动。提交完之后跟着看滚动状态kubectl rollout status deployment/myapp -n myapp --timeout180s--timeout很重要。不加超时命令会一直挂着加上之后超时会返回非零退出码流水线里就能立刻判定这次发布失败而不是傻等。3.2 记录变更原因让 rollout history 可读默认情况下kubectl rollout history只显示 revision 编号看不出每一版改了什么回退的时候全靠猜。解决办法是打一个注解kubectl annotate deployment/myapp \ kubernetes.io/change-causerelease v1.7.3, ticket OPS-2317 --overwrite注意这条注解要加在模板变更的同一次操作附近否则它本身也会被算作一次模板变化从而触发一轮无意义的滚动。老一点的教程会推荐--record那个参数在较新版本已经标记为废弃并最终移除新集群上别再用用注解的方式等效且更明确。3.3 最容易中的坑同名 tag 不触发更新这是新手最常见的困惑——我明明推了新镜像为什么kubectl set image之后没反应。原因是你用的还是v1.7.3这个 tagkubectl 提交的模板和现有模板完全一致控制器认为无变化自然不滚。这种情况有两种解法一是每次发布都用新 tag推荐语义化版本或 commit sha二是把imagePullPolicy设为Always并且真的重启工作负载。第二种做法我不太推荐因为Always会让每次 Pod 重建都去 registry 校验一遍拉取延迟和 registry 压力都上去了。用不可变的、带唯一标识的 tag才是既直观又安全的方式。注意latest这个 tag 在生产环境基本等于给自己埋雷。它不表达版本回滚时你无法确定上一版到底是哪个镜像审计也说不清楚。4. 方式二kubectl edit 与 patch精确改动的进阶玩法4.1 edit适合临时验证不适合流水线KUBE_EDITORvim kubectl edit deployment/myapp -n myappedit 打开的是当前的线上对象你改哪一行就只影响那一行适合临时排查。习惯上我会先kubectl get deploy myapp -o yaml /tmp/myapp.yaml存个底万一改乱了可以直接对照。要记住 edit 的行为特性它先读对象并带上resourceVersion保存时用这个版本号做乐观锁。如果在这期间别人也改了同一个对象你会收到冲突报错需要重新读取再改。这个机制其实是在保护你别嫌它烦。另外 edit 里删掉某个字段和把字段改成空值不是一回事前者会让字段回到默认值后者可能触发校验失败改之前想清楚。4.2 patch脚本友好但要选对类型kubectl patch支持几种不同的合并语义选错类型是踩坑重灾区。# strategic merge patch列表按 name 合并最贴近直觉 kubectl patch deployment myapp -n myapp --typestrategic -p \ {spec:{template:{spec:{containers:[{name:app,image:registry.example.com/team/myapp:v1.7.4}]}}}} # json patch按路径替换精确但脆弱 kubectl patch deployment myapp -n myapp --typejson -p \ [{op:replace,path:/spec/template/spec/containers/0/image,value:registry.example.com/team/myapp:v1.7.4}]strategic 的优势是你能只写要改的容器其余容器原样保留json patch 的优势是指令最少但containers/0这种下标一旦有人调整了容器顺序就改错对象了。我的经验是镜像更新一律用 strategic只有做字段删除这类操作才考虑 json patch。4.3 apply 的坑为什么 apply 会报 validating 错误热词里出现的error validating报错本质上不是你权限或集群的问题而是 YAML 内容通不过 API 的校验。常见的几类原因我用表格整理一下。报错特征真实原因处理办法error validating data: ValidationError(...): unknown field字段名拼错或该字段在当前 API 版本已移除用kubectl explain deploy.spec.template.spec逐层确认合法字段no matches for kind Xxx in version ...对应的 CRD 还没安装或 apiVersion 与集群版本不匹配先装 CRD或改用kubectl api-resources里列出的版本the path ... is invalid/ 缩进相关YAML 缩进错乱或 tab 混入kubectl apply --dry-runserver -f x.yaml先校验error converting YAML to JSON多文档---分隔后某一段结构不完整拆文件逐个 apply定位到具体那一段处理这类问题最有效的一步是加--dry-runserver。它会把对象提交给 API 做一次真实校验但不落库几乎能提前暴露所有结构性问题。--validatestrict也能让 kubectl 在客户端先做一轮 schema 校验比直接怼到服务端报错要友好。养成先 dry-run、后 apply的习惯能省下大量在集群上反复试错的时间。需要提醒的是apply是声明式的它会把你 YAML 里的字段当作期望状态同时把上一次 apply 记录在last-applied-configuration注解里用来判断哪些字段该被清理。如果你之前是用set image改的镜像再用一份旧的 YAML 去apply那个旧的镜像地址会被恢复回去。混合使用命令式和声明式是很多发布后又变回去了怪现象的根源。选一种坚持用它。5. 方式三不换镜像只重启rollout restart 的定位5.1 它解决的是另一类问题kubectl rollout restart deployment/myapp -n myapp做的事情是把 Pod 模板上打一个时间戳注解从而人为制造一次变更触发滚动。镜像本身没有任何变化。它的典型用途是ConfigMap 或 Secret 更新了但环境变量是启动时注入的必须重启进程才能生效或者挂载的配置是通过 subPath 挂的不会自动热更新。很多计费类、策略类组件就靠这个动作完成配置下发。它和更新镜像的关系是两者触发的机理完全一样都是改 Pod 模板——区别只在改的字段不同。理解了这一层你就不会再有为什么改了 ConfigMap 服务没反应的困惑了。5.2 什么时候不该用它如果你的目的是让容器拉到最新的同名镜像rollout restart配合imagePullPolicy: IfNotPresent是没有效果的因为节点上缓存的镜像会被直接复用。这时候必须改 tag或者显式改成sha256摘要地址。另一个不适用场景是 StatefulSet它的滚动是有序的默认从大到小逐个更新重启会带来较长的整体不可用窗口涉及有状态服务时务必确认好主从切换逻辑再动手。同样DaemonSet 的滚动会按照maxUnavailable逐节点进行在节点数量少、每节点单副本的场景里maxUnavailable: 1就意味着某个节点上短暂没有实例。做网络插件、日志采集这类守护进程的镜像更新时这一点必须提前评估。6. 方式四改 YAML 再 apply声明式与流水线的正路6.1 文件即事实让发布可审计、可回退命令式操作快但它留下的痕迹只在集群的 revision 历史里。要看谁在什么时候把镜像改成了什么只能靠集群审计日志。声明式路线不一样镜像地址写在仓库里的 YAML 里每次变更都是一次提交有作者、有时间、有 diff、有评审记录。这是它最大的价值不是看起来更酷。落地时我一般把 Deployment、Service、HPA、PDB 拆成独立文件目录结构按服务划分而不是把所有东西塞进一个几百行的 all-in-one 文件。理由是独立文件能让你只 apply 变更的那个对象减少误伤。# 只对某一个目录做差异预览确认无误再落库 kubectl diff -f deploy/myapp/ -n myapp kubectl apply -f deploy/myapp/ -n myappkubectl diff是很多人没注意到的宝藏命令。它会告诉你 apply 之后哪些字段会变相当于一次发布预演。把 diff 接进流水线任何意外的字段漂移比如有人手工 edit 过线上对象都会在合并前暴露出来。6.2 接进 CI/CD一个可复用的流水线骨架整体思路分四步拉代码 → 渲染镜像 tag → dry-run 校验 → 执行更新并等待结果。镜像 tag 通常取 commit 短 sha这样每次构建产物天然唯一从根上避免同名 tag 不触发更新的问题。# 片段示意变量与凭据按你的平台要求注入 update_image: image: bitnami/kubectl:latest script: - kubectl config current-context - kubectl set image deployment/myapp app$IMAGE_REPO:$CI_COMMIT_SHORT_SHA -n myapp - kubectl rollout status deployment/myapp -n myapp --timeout240s - kubectl rollout history deployment/myapp -n myapp | tail -5 only: - main几个细节值得强调。第一kubectl config current-context放在第一条是防止误操作的第一道闸门。第二rollout status必须带--timeout否则流水线可能挂到超时上限才失败。第三给这条 job 加人工审批GitLab 里用when: manual或受保护环境生产发布不要无条件自动执行——即使你的自动化很完善关键版本留一个手动确认的环节成本极低收益极高。注意流水线里不要用kubectl replace -f去实现更新。replace 是整体替换对象语义会丢掉你没写在 YAML 里的字段比如 HPA 或监控组件打上的注解而且对spec.selector这类不可变字段敏感容易报错。7. 更新之后验证、观察与三种粒度的回滚7.1 更新完应该立刻看哪几个信号我固定看四样东西顺序不能乱。第一kubectl rollout status是否正常返回。第二kubectl get deploy myapp -o wide里 READY 是否等于期望副本数AVAILABLE 是否跟上。第三kubectl get pods -l appmyapp里新 Pod 的 AGE 与 READY 列确认没有反复重建。第四kubectl get events -n myapp --sort-by.lastTimestamp | tail -20事件流是最早暴露镜像拉取失败、探针失败的地方比翻容器日志快得多。容器日志层面我会重点看启动阶段有没有明确报错、有没有打出版本号。很多团队会在启动日志里打一个 build 信息发布后 grep 一下版本就能确认新镜像真的跑起来了这比对着时间戳猜要可靠。7.2 回滚的三种粒度选错会扩大故障回滚不是只有一条命令粒度选错了反而更糟。粒度从大到小分别是整体回退到上一个 revision、回退到指定 revision、只回退镜像。kubectl rollout undo deployment/myapp -n myapp kubectl rollout undo deployment/myapp -n myapp --to-revision7 kubectl set image deployment/myapp appregistry.example.com/team/myapp:v1.7.2 -n myapp第一种最快适合新版本刚上线就发现严重问题的紧急场景。第二种适合你确定某个历史版本是稳定的基线。第三种最精细代价是会产生一个新的 revision把它和上一版的关系说清楚要靠 change-cause 注解。有个细节要记住rollout undo回退之后被回退掉的那个 revision 不会消失它会变成新的一个 revision 出现在历史末尾。所以连续 undo 两次不会撤销回退只会把版本推回更早的状态这一点在慌乱中很容易搞错方向。7.3 更新卡住了怎么判断是慢还是死滚动更新最常见的两种卡死形态Pod 一直 Pending和 Pod 起来了但不 Ready。前者通常是资源不足或被亲和性/污点约束挡住kubectl describe pod的 Events 段会直接写明原因。后者基本都是探针问题——initialDelaySeconds 给得太短或者新版本真的启动更慢了。Deployment 有一个progressDeadlineSeconds默认 600 秒超时后控制器会在 status 上标记ProgressDeadlineExceeded但不会自动回滚。这点必须清楚K8s 不会替你做决定报警要靠你自己接。我一般会在监控里对这个 condition 做告警配合流水线里的 timeout 双重兜底。8. 镜像安全与容器安全更新时必须守住的那几条线8.1 用摘要而不仅是 tag把确定性钉死tag 是可以被覆盖的仓库管理员今天推一个v1.7.3明天再推一个同名镜像内容完全不同。摘要digest则不同它是内容哈希指向的就是那一份确定的层组合。对安全要求高的工作负载我会把镜像写成registry.example.com/team/myappsha256:...的形式。回滚时尤其有用因为历史记录里那个摘要永远是可信的。代价是可读性差看不出这是 v1.7.3 还是 v1.7.4。折中方案是保留 tag 便于人读同时在准入策略层面校验摘要是否来自受信任的签名把是否允许这个镜像跑起来这件事前移到集群入口而不是靠发布者自觉。8.2 更新前的检查清单下面这几项我会在每次生产发布前过一遍看起来繁琐但每一项都对应过一次真实故障。检查项为什么必须做怎么快速确认镜像来源与签名防止供应链被投毒跑起来的是别人构造的镜像检查仓库项目归属与签名验证结果漏洞扫描结果新版本可能引入新的高危组件看扫描报告里高危项是否为 0 或已评估运行身份以 root 跑容器会放大逃逸影响面确认runAsNonRoot、runAsUser已设置文件系统可写根文件系统让攻击者能落盘持久化确认readOnlyRootFilesystem与需要写目录的挂载资源限额无限额会让新版本异常时拖垮同节点其他服务确认 requests/limits 已配置权限最小化多余的 Capability 和挂载的令牌是提权入口检查securityContext与自动挂载的服务账号令牌这里我最想强调运行身份和文件系统这两项因为它们最容易被当成以后再说而永远不做。很多基础镜像默认以 root 启动业务代码不感知一直跑得好好的直到某天出现一个可以执行命令的漏洞攻击面瞬间从应用层变成节点层。更新镜像是个天然的改造窗口——既然本来就要动模板顺手把这几个字段加上成本几乎为零。8.3 别把最新镜像当成默认选项流水线里一个很常见的坏习惯是本地调试用latest忘了改就提交上去了。防止这件事最简单的办法是在准入阶段直接拒绝没有明确 tag 或没有摘要的镜像。规则一落地团队会迅速养成习惯而且它是自动执行的不依赖人的记性。这个思路用在所有靠人自觉的规范上都有效能自动化拦截的就不要写在文档里等人去读。9. 常见问题与排查技巧实录9.1 镜像拉取失败一路往下排查ImagePullBackOff/ErrImagePull是更新后最高频的报错。排查顺序我固定为镜像地址是否写错多一个斜杠、少一个项目名都会失败→ 节点能否访问 registry → 私有仓库的凭据是否存在且正确。第三步最容易被忽略imagePullSecrets必须挂在与 Pod 相同的命名空间否则控制器找不到它。用kubectl describe pod看到Failed to pull image后面的具体信息往往一句话就能定位到底是找不到还是没权限。还有一种情况是节点上的磁盘满了导致拉取失败报错信息长得像网络问题实际是本地空间不足。集群规模一大这类长得不像它真实原因的报错就多了起来所以我的建议始终是先看 Events再看细节报错原文不要急着凭经验下结论。9.2 滚动更新迟迟不结束如果rollout status长时间没有进展先确认新 Pod 到底卡在哪一步。kubectl get pods显示 Running 但 READY 是 0/1说明进程起来了但探针没过去看describe里的探针配置和容器日志。如果新 Pod 根本没出现检查maxSurge和maxUnavailable是否被设成了让控制器无法推进的组合比如两个都为 0也检查集群是否有足够的资源调度新 Pod。资源不足是隐蔽性最高的一类。Pod 状态会停在 Pending事件里写Insufficient cpu或Insufficient memory。这时候不要急着调低 requests 糊弄过去那等于把风险转移到运行时。更稳的做法是扩容节点或降低单副本资源占用或者先用Recreate策略在不增加峰值资源的前提下完成切换。9.3 一份可以直接收藏的速查表现象最可能的原因处理动作set image 后无任何变化tag 与现有镜像完全相同模板未变换成带 commit sha 的新 tag发布后过段时间镜像又变回旧的有人用旧 YAML 执行了 apply统一到声明式检查 last-applied 注解报 error validatingYAML 字段非法、API 版本不匹配、CRD 缺失加--dry-runserver定位副本数对但流量打不进来就绪探针失败导致端点未注册检查探针路径与端口回滚后问题仍在回退粒度不对或问题不在镜像本身用--to-revision指定明确版本多个容器里只有边车更新了容器名写错或只改了一个键值对用 jsonpath 打印容器名核对这张表里的每一行我都在真实环境里遇到过。最想说的一句是把发布后立刻验证变成肌肉记忆比背下所有命令都重要。绝大多数严重事故在第一个信号出现的时候都还有挽回空间只是没人看。最后分享一个我自己一直用的习惯任何一次生产镜像更新都先在同构的预发环境完整走一遍 set image 或 apply 流程包括回滚演练。演练一次的成本大概是十分钟但它能让你在真出事的时候不需要现场查文档。预发环境和生产环境的差异节点规格、镜像缓存、配置来源一定会暴露出来一些东西早暴露永远好过晚暴露。
返回列表