上一章结束时,TaskBoard 已有两个由 Deployment 管理的 Ready 副本,service/taskboard 通过两个就绪 EndpointSlice 端点提供稳定入口,toolbox-safe 可以用 DNS 名称访问它。现在运行链路稳定了,但欢迎语和功能开关仍直接写在 Deployment 的 Pod 模板里;Redis 地址尚未声明,口令也还没有合适的注入方式。
把环境差异写进镜像,会导致同一份应用代码为了开发、预发布、生产分别构建镜像;把所有值直接写进 Deployment,又会让普通配置、敏感凭据和工作负载结构混在一次变更里。更麻烦的是,即便配置对象更新成功,也不代表运行中进程已经读取了新值。
本章要把普通配置放进 ConfigMap,把 Redis 口令放进 Secret,并通过 Downward API 注入当前 Pod 身份。随后我们会有意只更新 ConfigMap,证明环境变量不会自动刷新;再通过 rollout restart 让新 Pod 读取新值。最后用 /tmp/not-ready 标记让一个 Pod 暂时不就绪,观察 readiness 怎样改变 EndpointSlice,而不会重启容器。
完成本章时,TaskBoard Deployment 会引用 taskboard-config 和 taskboard-secret,欢迎语为“你好,配置已经更新”,两个 Pod 都重新 Ready,Service 后端仍为两个。Redis 还没有部署,因此 REQUIRE_REDIS_READY 继续为 false。下一章将在这套探针和配置边界上执行滚动发布与 HPA 扩缩容。
新终端不会继承上一章的工具路径、集群配置和项目目录。本章会写入并应用 k8s/config.yaml 与 Deployment 文件,因此第一个写操作前必须先锁定三条边界:课程标记正确、kubectl 连接课程 context、相对路径从 TaskBoard 项目根目录解析。
下面的块只恢复当前 Shell 并读取 context,不会改动 Kubernetes 对象。理解每条检查后执行:
COURSE_ROOT="$HOME/welearn-kubernetes-course"
if ! { [ -d "$COURSE_ROOT" ] && \
printf 'welearn-kubernetes-course\n' | cmp -s - "$COURSE_ROOT/.course-owned"; }; then
echo '停止:课程目录不存在或归属标记不匹配' >&2
exit 1
fi
case ":$PATH:" in
*":$COURSE_ROOT/bin:"*) ;;
*) export PATH="$COURSE_ROOT/bin:$PATH" ;;
esac
export KUBECONFIG="$COURSE_ROOT/welearn-course.kubeconfig"
COURSE_CONTEXT="$(kubectl config current-context 2>/dev/null)" || { echo '停止:无法读取课程 context' >&2; exit 1; }
if [ "$COURSE_CONTEXT" != 'kind-welearn-course' ]; then
echo "停止:当前 context 不是 kind-welearn-course,而是 $COURSE_CONTEXT" >&2
exit 1
fi
CLUSTER_NAME='welearn-course'
NODE_NAME='welearn-course-control-plane'
CLUSTER_LABEL='io.x-k8s.kind.cluster=welearn-course'
CLUSTER_MARKER="$COURSE_ROOT/.kind-cluster-owned"
KIND_CLUSTERS="$(kind get clusters 2>&1)" || { echo '停止:无法读取 kind 集群列表' >&2; exit 1; }
printf '%s\n' "$KIND_CLUSTERS" | grep --fixed-strings --line-regexp --quiet "$CLUSTER_NAME" || { echo '停止:课程 kind 集群不存在' >&2; exit 1; }
LABEL_IDS="$(docker ps -aq --no-trunc --filter "label=$CLUSTER_LABEL" 2>&1)" || { echo '停止:无法按 kind 标签读取节点容器' >&2; exit 1; }
NAME_IDS="$(docker ps -aq --no-trunc --filter "name=$NODE_NAME" 2>&1)" || { echo '停止:无法按名称读取节点容器' >&2; exit 1; }
LABEL_COUNT="$(printf '%s\n' "$LABEL_IDS" | awk 'NF { count += 1 } END { print count + 0 }')"
NAME_COUNT="$(printf '%s\n' "$NAME_IDS" | awk 'NF { count += 1 } END { print count + 0 }')"
if [ "$LABEL_COUNT" -ne 1 ] || [ "$NAME_COUNT" -ne 1 ] || [ "$LABEL_IDS" != "$NAME_IDS" ]; then
echo '停止:课程节点容器的名称、标签或数量不一致' >&2
exit 1
fi
COURSE_NODE_ID="$LABEL_IDS"
case "$COURSE_NODE_ID" in
*[!0-9a-f]*|'') echo '停止:课程节点容器 ID 格式无效' >&2; exit 1 ;;
esac
[ "${#COURSE_NODE_ID}" -eq 64 ] || { echo '停止:课程节点容器 ID 不是完整 64 位' >&2; exit 1; }
printf 'cluster=%s\nnode=%s\nnode_id=%s\nlabel=%s\n' "$CLUSTER_NAME" "$NODE_NAME" "$COURSE_NODE_ID" "$CLUSTER_LABEL" | \
cmp -s - "$CLUSTER_MARKER" || { echo '停止:当前节点与集群归属账本不匹配' >&2; exit 1; }
NODE_FACTS="$(docker inspect --format '{{.Id}}|{{.Name}}|{{index .Config.Labels "io.x-k8s.kind.cluster"}}' "$COURSE_NODE_ID" 2>&1)" || { echo '停止:无法检查课程节点身份' >&2; exit 1; }
[ "$NODE_FACTS" = "$COURSE_NODE_ID|/$NODE_NAME|$CLUSTER_NAME" ] || { echo '停止:课程节点不可变身份校验失败' >&2; exit 1; }
cd "$COURSE_ROOT/taskboard" || { echo '停止:TaskBoard 项目目录不存在' >&2; exit 1; }
printf 'context=%s\nproject=%s\n' "$COURSE_CONTEXT" "$(pwd -P)"
printf 'cluster_identity=verified node_id_prefix=%.12s\n' "$COURSE_NODE_ID"context=kind-welearn-course
project=<用户目录>/welearn-kubernetes-course/taskboard
cluster_identity=verified node_id_prefix=<动态 12 位>用户目录和 ID 前缀会变化;context、路径后缀与 cluster_identity=verified 必须同时出现。任一检查失败都先回到对应章节恢复状态,不要在任意目录复制同名清单或改写归属 marker 继续。
容器镜像最有价值的属性之一是可重复:同一个镜像摘要应代表同一套应用文件和运行依赖。如果每换一个数据库地址、欢迎语或功能开关就重新构建镜像,我们很难判断环境差异来自代码还是配置,也无法用同一制品逐级验证后再推广。
Kubernetes 的 ConfigMap 正是为“非敏感配置与镜像解耦”提供的 API。官方文档把它描述为键值对象,Pod 可以把它消费为环境变量、命令行参数或卷中的配置文件。它不替应用理解配置语义,只负责让配置以声明式对象进入 Pod。
Secret 与 ConfigMap 的 API 形态相似,是因为二者都要把数据交给工作负载;它们的意图不同。Secret 表达“这份数据需要更严格的访问与处理”,因此可以独立授权、审计和接入静态加密或外部密钥系统。但 Kubernetes 官方明确说明:Secret 默认在 etcd 中并不加密,base64 也不提供保密性。把值换成 Secret 不会自动完成完整的凭据治理。
Downward API 解决的是第三类配置:值不来自人手维护的环境清单,而来自正在运行的 Pod 自身。Pod 名称、Namespace、标签和资源请求等信息只有创建实例时才最终确定。让应用通过 fieldRef 或 downwardAPI 卷读取,可以避免模板作者手工复制动态元数据。
所以本章不是把所有变量搬家,而是先分类:
这个边界让变更原因更清楚,却不会自动完成热更新。配置怎样送进进程——环境变量、卷文件还是应用自己的配置服务——会决定更新何时可见。后面的实验会把这个差异直接展示出来。
这些机制可以从 Kubernetes 官方的 ConfigMap 概念、ConfigMap 更新教程、Secret 概念 与 Secret 安全实践 中核对。
ConfigMap 适合保存不敏感、可以随应用一起讨论的配置,例如欢迎语、服务地址和功能开关。Secret 适合保存口令、令牌和证书等敏感数据。二者都能通过环境变量或卷提供给容器,但安全属性不同。
这次配置分工如下:
Secret 对象默认只是把值以 base64 形式放进 API 数据,并不等于加密。要保护存储在 etcd 中的数据,集群管理员还需要启用静态数据加密,并用 RBAC 限制读取权限。
ConfigMap 和 Secret 都可以通过环境变量或卷进入容器,看起来只是 YAML 写法不同,实际更新语义差别很大。选错方式,最常见的后果是“kubectl get 已经是新值,应用却还在用旧值”。
环境变量在容器进程启动时组成进程环境。Kubelet 创建容器前读取 ConfigMap 或 Secret,把值传给容器运行时;进程启动后,这份环境不会因为 API 对象变化而被外部改写。要看到新值,需要替换 Pod 或让容器重新启动。本章选择这种方式,是因为它简单,而且能清楚演示配置对象更新与进程生效是两个动作。
卷文件的行为不同。Kubelet 会把 ConfigMap 或 Secret 投影到 Pod 卷,并周期性同步更新;应用必须重新读取文件,才能使用新内容。同步也不是 API 更新后零延迟完成。使用 subPath 挂载单个文件还有额外限制:这类挂载不会接收 ConfigMap 或 Secret 的自动更新。生产系统要明确应用是否支持安全重载,不能看到文件变化就默认进程已生效。
命令行参数通常也是在容器启动时由环境变量展开或直接写入 command/args,更新语义与启动时配置相近。对于会出现在进程列表、日志或错误信息中的敏感值,命令行参数还可能扩大泄漏面。
此外,envFrom 会把对象中的多个键批量转成环境变量,清单简洁,但应用与配置键名耦合更强,也不容易为每个键单独标注来源。env.valueFrom 逐项声明更啰嗦,却能精确选择 Secret 键、重命名环境变量,并给必需键设置清晰失败边界。本章对普通配置使用 envFrom,对敏感口令使用 secretKeyRef,就是在展示这两种取舍。
当引用对象或键不存在时,Pod 可能停在 CreateContainerConfigError,而不是进入应用后再报错。这是 kubelet 在容器启动前发现配置无法组装。此时 kubectl logs 往往没有应用日志,因为进程根本没启动;应先 kubectl describe pod 查看 Events,并核对 ConfigMap、Secret 名称、Namespace 和 key。

把口令放进 Secret 有价值,但它解决的是“让敏感数据拥有独立 API 对象和访问策略”,不是让口令天然不可见。
首先,data 字段中的值通常只是 base64 编码。编码的用途是让二进制数据能进入 JSON/YAML,不是抵抗读取。拥有 get secret 权限的主体可以获取编码值并恢复明文;拥有创建 Pod 权限的主体还可能通过引用 Secret 的 Pod 间接读取它。因此,RBAC 设计不能只盯着 secrets 资源,还要考虑谁能创建工作负载、exec 进容器或读取节点数据。
其次,API Server 的底层存储默认可能保存未加密的 Secret 数据。生产集群需要配置 encryption at rest,密钥本身还要安全保存和轮换。高要求场景会接入 KMS 或外部 Secret 管理系统,并通过 CSI 驱动或控制器把凭据送入工作负载。KMS 是把加解密操作交给受控密钥服务的接口;CSI 是 Kubernetes 连接存储驱动的标准接口,Secret Store CSI 一类实现可以把外部凭据投影成 Pod 内文件。这些方案仍要考虑最终进入容器后的暴露面。
再次,Secret 不应出现在不必要的输出中。kubectl get secret -o yaml 会显示 base64 数据;命令行历史、CI 日志、Git 仓库和故障截图都可能长期保存它。本章只检查键名和字节数,是为了验证对象形状而不展示值。课程占位口令没有真实价值,但我们仍用正确习惯操作。
最后,凭据需要生命周期。创建一次 Secret 并长期不变,不等于安全。轮换时要回答旧新凭据能否并存、工作负载何时重新读取、依赖服务何时切换、失败怎样回滚。环境变量注入意味着轮换通常要配合 Pod 替换;卷或外部代理也要确认应用是否真的重新建立连接。
Secret 可以降低凭据与普通配置混放的风险,但不能把已经进入容器内存、进程环境或文件系统的值变成“任何人都读不到”。权限、存储加密、审计、轮换和应用使用方式共同构成边界。
我们先创建普通配置。此时 Redis 还没有部署,所以 REQUIRE_REDIS_READY 保持 false;否则 TaskBoard 会因为依赖不存在而无法进入 Ready 状态。
在项目工作目录创建 k8s/config.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: taskboard-config
namespace: taskboard
data:
WELCOME_MESSAGE: "你好,欢迎使用云原生任务板"
REDIS_HOST: "redis"
REDIS_PORT: "6379"
REQUIRE_REDIS_READY: "false"data 下所有值最终都是字符串,因此 REDIS_PORT 和 REQUIRE_REDIS_READY 即使看起来像数字与布尔值,也要按字符串理解。给 6379 和 false 加引号可以避免 YAML 解析器先把它们变成其他类型。REDIS_HOST 使用 Service 短名称 redis;第九章会在同一 Namespace 创建这个 Service。现在依赖尚不存在,所以 REQUIRE_REDIS_READY 必须为 false,否则 readiness 会把两个 TaskBoard Pod 都移出 Service 后端。
下面第一条命令把完整文件声明提交给 API Server;第二条以 YAML 形式读回服务端对象。普通配置可以直接显示,先确认四个键都已经进入集群。这里特意读回而不是只相信 created,因为字段拼写和 Namespace 才是后续 Pod 能否引用的依据:
kubectl apply -f k8s/config.yaml
kubectl get configmap taskboard-config -n taskboard -o yamlapiVersion: v1
data:
REDIS_HOST: redis
REDIS_PORT: "6379"
REQUIRE_REDIS_READY: "false"
WELCOME_MESSAGE: 你好,欢迎使用云原生任务板
kind: ConfigMap
metadata:
name: taskboard-config
namespace: taskboard服务端可能额外返回 resourceVersion、uid 和时间戳,这些都是动态元数据。这里要核对的是 data 下的四个配置键。
输出中 apiVersion、kind、metadata 证明读回的是 taskboard Namespace 中的 ConfigMap;data 的四个键与文件一致。键排序可能与清单不同,不影响语义。若看到 NotFound,先检查 -n taskboard 与 kubeconfig;若键名拼错,envFrom 会生成拼错的环境变量,应用可能回退默认值,因此要在工作负载发布前修正源文件并重新 apply。
课程需要一个可复现的占位口令,因此使用 course-only-password。它只有 20 个 ASCII 字节,只能用于隔离练习集群,不能复制到真实系统。命令使用 --dry-run=client -o yaml | kubectl apply -f -,这样重复执行时会创建或更新同一个对象。
命令中会出现课程占位口令,但后续检查只打印键名和解码后的字节数,不打印 base64 文本,也不打印口令本身。create secret generic 先在 kubectl 进程中生成对象;--dry-run=client 表示此步不直接写入 API;-o yaml 把对象交给标准输出;管道右侧 apply -f - 再从标准输入声明式创建或更新同名 Secret。这样命令可以重复执行,而不是第二次因 AlreadyExists 中断:
kubectl create secret generic taskboard-secret \
--namespace taskboard \
--from-literal=redis-password='course-only-password' \
--dry-run=client \
-o yaml | kubectl apply -f -
kubectl get secret taskboard-secret -n taskboard \
-o go-template='{{range $key, $value := .data}}{{printf "%s\t%d bytes\n" $key (len $value)}}{{end}}'secret/taskboard-secret created
redis-password 20 bytes第二行证明 Secret 中存在 redis-password,长度也符合预期,但没有泄露值。你再次执行创建命令时,第一行可能显示 configured,这是声明式更新的正常结果。
go-template 中 range 遍历 data 映射,value) 对 Secret API 返回的已解码字节切片计数,所以结果是 20 bytes。它不证明口令正确,也不验证 Redis 能接受它,只证明预期键存在且长度匹配。若出现 0 bytes 或没有任何行,应检查 from-literal 的键名与管道前半段是否成功;不要改用回显明文的命令做第一反应。
不要为了确认 Secret 而执行带有 .data.redis-password 的 jsonpath,也不要把解码命令写进教程日志。能读取 Secret 的人通常已经可以取得明文,base64 不是安全边界。
envFrom 会把 ConfigMap 中的每个键变成同名环境变量。env 则适合逐个声明来源,这里用它读取一个 Secret 键,并通过 Downward API 读取 Pod 名称和 Namespace。
在 k8s/deployment.yaml 中,用下面的完整片段替换容器原有的直接环境变量。它应位于 spec.template.spec.containers 中名为 api 的容器下,与 image、ports、resources 同级:
envFrom:
- configMapRef:
name: taskboard-config
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: taskboard-secret
key: redis-password
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
这段修改发生在 spec.template 内,因此它会改变 Deployment 的 Pod 模板哈希并创建新 ReplicaSet。旧 Pod 不会被“在线编辑”环境变量;Deployment 会按滚动策略建立读取新来源的 Pod,再逐步缩减旧 ReplicaSet。
envFrom.configMapRef.name 只写对象名,因为引用默认发生在 Pod 所在 Namespace,不能用它直接跨 Namespace 读取 ConfigMap。ConfigMap 中 WELCOME_MESSAGE、REDIS_HOST、REDIS_PORT、REQUIRE_REDIS_READY 会变成同名环境变量。
REDIS_PASSWORD 没有使用 envFrom 批量导入,而是通过 secretKeyRef 精确读取 taskboard-secret 的 redis-password 键。环境变量名与 Secret 键名不必相同,这让应用接口和存储键可以独立命名。
两个 fieldRef 读取 metadata.name 与 metadata.namespace。它们是 Downward API 的字段引用,不会向 API Server 发起运行期查询,也不要求容器持有 ServiceAccount token。每个新 Pod 在创建时都会得到自己的名称和相同 Namespace,因此 API 响应可以暴露实例身份用于诊断。
这三个来源的更新行为并不相同:
保存 Deployment 后重新应用清单。第一条命令修改 Deployment 期望模板;第二条等待新 ReplicaSet 的副本都通过 readiness;第三条仍从 toolbox-safe 访问稳定 Service,而不是挑一个新 Pod 直连。这样既验证配置注入,也验证滚动期间 Service 最终只保留就绪后端:
kubectl apply -f k8s/deployment.yaml
kubectl rollout status deployment/taskboard -n taskboard --timeout=90s
kubectl exec -n taskboard toolbox-safe -- \
wget -qO- http://taskboard/api/infodeployment.apps/taskboard configured
deployment "taskboard" successfully rolled out
{"service":"taskboard","version":"1.0.0","message":"你好,欢迎使用云原生任务板","pod":"<动态 Pod 名>","namespace":"taskboard","redisConfigured":true}Pod 名称和 ReplicaSet 哈希会变化。message 来自 ConfigMap,namespace 来自 Downward API,redisConfigured:true 只说明进程收到了非空口令,不会把口令内容放进响应。
第一行 configured 表示已有 Deployment 的模板发生变化;第二行成功说明新 revision 已达到期望可用副本数。JSON 中 version 仍为 1.0.0,证明本次没有更换应用镜像;message 与 ConfigMap 相等;pod 是动态实例名;namespace 来自 fieldRef;redisConfigured=true 是应用对“是否收到非空口令”的布尔证据。
如果 rollout 卡在 CreateContainerConfigError,应对异常 Pod 执行 kubectl describe pod,通常能看到缺失 ConfigMap、Secret 或 key。若 rollout 成功但 message 仍是旧的“你好,这是 Deployment”,先读取 Deployment 的实际 spec.template,确认 envFrom 已保存,再检查访问是否仍命中尚未终止的旧副本。rollout 完成后两个就绪副本都应来自新模板。
现在只改 ConfigMap,不改 Deployment。Kubernetes 会保存新配置,但两个已有容器中的环境变量仍是启动时的旧值。因此,修改后立即访问 Service,预计仍看到“你好,欢迎使用云原生任务板”。
先把 k8s/config.yaml 中的 WELCOME_MESSAGE 同步改成“你好,配置已经更新”,保证文件仍是可重放的声明源:
data:
WELCOME_MESSAGE: "你好,配置已经更新"
REDIS_HOST: "redis"
REDIS_PORT: "6379"
REQUIRE_REDIS_READY: "false"然后 patch 集群中的 ConfigMap,选取一个 TaskBoard Pod,只用 printenv WELCOME_MESSAGE 读取这个非敏感键,再通过 Service 请求应用。不要执行不带变量名的 printenv,否则会把 REDIS_PASSWORD 等敏感环境变量一起输出。这个顺序把“对象已经更新”和“进程尚未重启”分开观察:
kubectl patch configmap taskboard-config -n taskboard \
--type=merge \
-p '{"data":{"WELCOME_MESSAGE":"你好,配置已经更新"}}'
POD="$(kubectl get pod -n taskboard -l app=taskboard -o jsonpath='{.items[0].metadata.name}')"
test -n "$POD" || { echo '没有找到 TaskBoard Pod' >&2; exit 1; }
kubectl exec -n taskboard "
configmap/taskboard-config patched
你好,欢迎使用云原生任务板
{"service":"taskboard","version":"1.0.0","message":"你好,欢迎使用云原生任务板","pod":"<动态 Pod 名>","namespace":"taskboard","redisConfigured":true}ConfigMap 已经是新值,响应仍是旧值。这不是缓存故障,而是环境变量的工作方式。若应用需要自动读取变化,可以把 ConfigMap 挂载成文件并实现重载;当前 TaskBoard 选择显式重启,变化边界更清楚。
第一行 patched 只说明 API 对象 data 已更新。printenv WELCOME_MESSAGE 和随后的 JSON 都仍显示旧值,证明运行中进程的这个环境变量没有刷新;Service、EndpointSlice 和 HTTP 缓存都没有参与保存欢迎语。这里刻意只读取 WELCOME_MESSAGE,不枚举完整环境,避免把 Secret 注入的 REDIS_PASSWORD 带进终端或日志。
如果你的响应已经提前变成新值,先确认期间是否有人执行过 rollout restart、重新 apply 了 Pod 模板或删除了 Pod。任何导致 Pod 替换的动作都会让新进程读取 ConfigMap。不要据此得出“envFrom 支持热刷新”。
文件挂载也不是无条件实时。Kubelet 会在同步循环和缓存传播后更新投影文件,应用还必须重新打开或监听文件;把文件通过 subPath 挂载时不会收到这类更新。对无法安全热加载的应用,显式滚动重启通常比半自动刷新更可预测。对需要无中断证书或规则更新的应用,则应设计文件监听、校验、原子切换和失败回退。
rollout restart 会在 Pod 模板上写入重启注解,因此产生新模板哈希或新 revision,由 Deployment 按既有 RollingUpdate 策略替换 Pod。它不会修改 ConfigMap,也不会在旧进程内部执行 reload。下面先触发替换,再等待两个新 Pod Ready,最后仍通过 Service 验证应用值:
kubectl rollout restart deployment/taskboard -n taskboard
kubectl rollout status deployment/taskboard -n taskboard --timeout=90s
kubectl exec -n taskboard toolbox-safe -- \
wget -qO- http://taskboard/api/infodeployment.apps/taskboard restarted
deployment "taskboard" successfully rolled out
{"service":"taskboard","version":"1.0.0","message":"你好,配置已经更新","pod":"<动态 Pod 名>","namespace":"taskboard","redisConfigured":true}响应已经出现新欢迎语,Pod 名称也发生变化。配置对象的更新和工作负载的重启是两个独立动作,生产发布流程需要明确谁负责触发第二个动作。
三段输出形成完整因果链:restarted 表示模板被标记重启;successfully rolled out 表示替换收敛;message 出现新值表示新容器启动时读取了更新后的 ConfigMap。动态 Pod 名变化是替换的旁证,不能把具体名称写进自动化。
在生产流程里,常见做法包括由部署工具计算配置校验和并写入 Pod 模板注解,配置变化时自然触发 rollout;也可以由专门控制器监听配置对象后重启工作负载。无论选择哪种方式,都要让触发关系可审计,并避免多个工具同时修改模板导致意外发布。
TaskBoard 在上一节的 Deployment 中已经配置三类探针。它们都访问 HTTP 接口,但回答的问题不同:
在没有探针时,Kubernetes 能观察到的主要事实是容器进程是否还在运行。可“进程存在”和“业务可服务”并不是同一件事:线程可能死锁,依赖可能暂时不可用,应用可能仍在预热,HTTP 监听端口也可能已经打开但缓存尚未加载。用一个健康布尔值处理所有情况,会把“先别给我流量”和“请把我重启”混成同一动作。
liveness 最早解决的是进程还活着却无法继续工作的场景,例如死锁。失败达到阈值后,节点上的 kubelet重启容器。readiness 把流量资格从进程生死中分离:失败时 Pod 仍可以 Running,但不作为普通 Service 流量后端。startup 则为启动慢的应用提供独立窗口;只要 startup 尚未成功,liveness 与 readiness 的失败不会过早干预启动过程。
探针不是由 Deployment 控制器发 HTTP 请求。每个节点上的 kubelet 按 PodSpec 执行探测并更新 Pod condition;EndpointSlice 控制器再观察就绪状态,更新端点条件;Deployment 控制器又根据可用副本判断滚动发布能否推进。一个 /readyz 响应最终会影响多层控制循环,这就是探针配置错误为什么会同时表现为 Service 无后端和 rollout 卡住。
readiness 失败不等于容器死亡。一个应用可以进程正常、日志正常,却因为依赖暂不可用而不适合接收业务流量。此时应该让它留在 Pod 中继续恢复,而不是用 liveness 反复重启。
periodSeconds 决定探测间隔,failureThreshold 表示连续失败多少次才判定失败,timeoutSeconds 控制单次探测等待时间。粗略的故障摘流时间接近 periodSeconds 乘 failureThreshold,再叠加调度与 EndpointSlice 传播;它不是严格的实时保证。successThreshold 对 readiness 可用于要求连续成功后再恢复,liveness 和 startup 的成功阈值受 API 约束。生产值应依据启动分布、依赖抖动和可接受故障窗口确定,而不是复制一套固定数字。
Kubernetes 官方的 探针任务文档 明确区分:readiness 决定容器何时准备接收流量,不就绪 Pod 不接收普通 Service 流量;liveness 用于判断何时重启容器;startup 成功后另外两类探针才接手。
不要把所有依赖都放进 liveness probe。数据库短暂故障时,如果每个应用 Pod 都被同时重启,原本的依赖故障会被放大成整组应用抖动。

TaskBoard 的 /readyz 会检查 /tmp/not-ready 是否存在。由于 /tmp 挂载了 emptyDir,即使根文件系统只读,我们仍可以安全地创建这个临时标记。
我们只让一个 Pod 暂时不就绪。预期结果是:Pod 继续 Running,UID 不变,restartCount 不增加;对应 EndpointSlice 端点异步变成 ready=false,另一个 Pod 继续接收 Service 流量。删除标记后,同一 UID 的 Pod 应恢复 Ready,端点再异步回到 true。
下面整块必须在同一个 Shell 会话执行。先选取一个 Pod,并记录 UID、restartCount、phase 和 Ready condition;test -n 防止空基线进入比较。创建标记并等待 Pod Ready=false 后,再轮询 EndpointSlice 中这个 targetRef,直到它明确记录 ready=false。随后读取同一 Pod 的四个字段并与基线比较。删除标记后采用对称流程,等待 Pod 和 EndpointSlice 都恢复,再做最后一次身份与重启计数比较:
POD="$(kubectl get pod -n taskboard -l app=taskboard -o jsonpath='{.items[0].metadata.name}')"
test -n "$POD" || { echo '没有找到 TaskBoard Pod' >&2; exit 1; }
BASE_UID="$(kubectl get pod "$POD" -n taskboard -o jsonpath='{.metadata.uid}')"
BASE_RESTARTS="$(kubectl get pod "$POD"
before pod=<动态 Pod A> uid=<Pod UID A> restarts=0 phase=Running ready=True
pod/<动态 Pod A> condition met
endpoint_ready=false
not_ready pod=<动态 Pod A> uid=<Pod UID A> restarts=0 phase=Running ready=False
<动态 Pod A> ready=false
<动态 Pod B> ready=true
pod/<动态 Pod A> condition met
endpoint_ready=true
recovered pod=<动态 Pod A> uid=<Pod UID A> restarts=0 phase=Running ready=True
<动态 Pod A> ready=true
<动态 Pod B> ready=true三份状态记录中的 Pod 名与 UID 完全相同,restartCount 始终为 0,phase 始终为 Running;只有 Ready 从 True 变为 False,再恢复为 True。这组字段共同证明容器没有重启、Pod 没有被替换。EndpointSlice 的 false 与 true 都经过独立轮询,避免把 Pod condition 已变化但端点尚未同步的瞬间当成最终结果。
两个 condition met 分别来自 Pod 摘流和恢复。第一次 endpoint_ready=false 后,Service 不再把普通新连接交给目标 Pod;另一个 ready=true 的端点仍可服务。第二次 endpoint_ready=true 表示同一端点重新获得流量资格。readiness 因此是流量开关,不是进程重启开关。
如果等待 false 超时,先查看 /tmp/not-ready 是否存在以及 Readiness probe failed 事件;如果等待 true 超时,检查标记是否删除,并直接请求容器内 /readyz。TaskBoard 未来会在 REQUIRE_REDIS_READY=true 时检查 Redis;那时即便标记不存在,依赖失败仍可让 readiness 返回失败。下一章发布新版本时,Deployment 也会依据同一 Ready 信号决定何时缩减旧副本。
不要把 readiness 写成对所有下游依赖的无限级联检查。一个共享依赖短暂抖动时,所有应用 Pod 同时摘流可能让 Service 完全没有后端。应根据应用能否降级、请求是否安全失败和依赖故障模型决定检查范围。
第一个误区是“ConfigMap 适合所有非密码数据”。ConfigMap 不提供强模式校验,错误字符串可能直到应用启动才暴露。配置进入发布流程前仍应做 schema 校验、默认值检查和环境差异评审。体积很大的配置也不适合塞进单个对象;ConfigMap 单对象大小有 API 限制,二进制大文件更应放在制品或对象存储中。
第二个误区是“Secret 不会出现在环境里”。只要通过环境变量注入,拥有容器内进程查看、调试权限的主体就可能读取它,崩溃报告和调试工具也可能收集环境。卷文件可以配合文件权限和轮换,但同样会进入节点与容器可见范围。选择注入方式要以威胁模型和应用能力为依据。
第三个误区是“配置变化就应该自动重启所有 Pod”。自动化能减少人工步骤,也可能把一次错误 ConfigMap 变更立刻扩散到所有副本。成熟流程会先校验配置,按环境或分批策略发布,观察 readiness、错误率和业务指标,再继续扩大。配置也需要版本与回滚,不应被当作无风险文本。
第四个误区是“readiness 越严格越安全”。如果它依赖慢查询或很多下游服务,探测本身会制造负载,短暂网络抖动也可能大面积摘流。readiness 应便宜、快速,并准确回答“这个实例现在能否处理它被分配的请求”。liveness 更应保守,只处理重启确实可能修复的问题。
第五个误区是“Pod Ready 就说明发布完成”。Deployment 的 Available 还受滚动策略和可用条件影响,Service 后端更新又是异步的。发布系统应同时观察 rollout condition、Ready Pod、EndpointSlice 和真实请求,而不是把单一绿色字段当作全部健康。
下一章会发布 taskboard:2.0.0,并依赖 readiness 阻止未就绪版本接收流量。进入前请确认配置来源与流量状态都已恢复:
下面只读取非敏感字段与状态。Secret 仍只输出键名和长度;Deployment 的 jsonpath 读取 configMapRef 名称以及 REDIS_PASSWORD 对应的 secretKeyRef 名称和键,不读取 Secret 数据。最后从 toolbox-safe 请求 /api/info,把声明引用、工作负载状态、EndpointSlice 和应用实际响应串成闭环:
kubectl get configmap taskboard-config -n taskboard --output jsonpath='{.data.WELCOME_MESSAGE}{"\tREQUIRE_REDIS_READY="}{.data.REQUIRE_REDIS_READY}{"\n"}'
kubectl get secret taskboard-secret -n taskboard --output go-template='{{range $key, $value := .data}}{{printf "%s\t%d bytes\n" $key (len $value)}}{{end}}'
kubectl get deployment taskboard -n taskboard -o jsonpath='{"configMapRef="}{.spec.template.spec.containers[?(@.name=="api")].envFrom[0].configMapRef.name}{"\tsecretKeyRef="}{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="REDIS_PASSWORD")].valueFrom.secretKeyRef.name}{"/"}{.spec.template.spec.containers[?(@.name=="api")].env[?(@.name=="REDIS_PASSWORD")].valueFrom.secretKeyRef.key}{"\n"}'
kubectl get deployment taskboard -n taskboard
kubectl get endpointslice -n
你好,配置已经更新 REQUIRE_REDIS_READY=false
redis-password 20 bytes
configMapRef=taskboard-config secretKeyRef=taskboard-secret/redis-password
NAME READY UP-TO-DATE AVAILABLE AGE
taskboard 2/2 2 2 <动态时间>
<动态 Pod A> ready=true
<动态 Pod B> ready=true
{"service":"taskboard","version":"1.0.0","message":"你好,配置已经更新","pod":"<动态 Pod 名>","namespace":"taskboard","redisConfigured":true}Pod 名和 AGE 是动态值。第一行证明 ConfigMap 源已经更新,第二行只证明 Secret 形状,引用行证明 Deployment 模板确实消费这两个对象,Deployment 与 EndpointSlice 证明控制面收敛,最终 JSON 证明真实请求读到新欢迎语和非空口令。如果任意端点仍为 false,先清理该 Pod 的 /tmp/not-ready 并检查 readiness;否则第八章的滚动更新会被已有故障干扰。
现在项目具备了明确的配置来源、敏感值边界和可用性信号。下一章不会重新设计这些对象,而是利用它们回答两个运维问题:怎样在版本出错时保住旧副本,以及怎样把资源指标转换成副本变化。