简介
容器镜像应当能够在不同环境中复用。Kubernetes 将非敏感配置存储在 ConfigMap 中,将凭据存储在 Secret 中,然后以环境变量或文件的形式注入这两类对象。
你将使用这两种机制配置同一个应用,安全地检查配置结果,更新配置,并观察环境变量与挂载卷为何具有不同的刷新行为。
本实验的目标不仅是创建两个 API 对象。你还将完整跟踪每个值如何从 Kubernetes 的存储进入运行中的容器,然后修改这些值,观察不同传递机制对刷新行为的影响。
创建应用配置
在本步骤中,你将创建一个命名空间,并根据字面值创建 ConfigMap。ConfigMap 会在 data 下存储字符串键值,但不会对这些内容进行加密。
先进入实验工作目录。该命名空间可以将本示例中的所有对象集中管理,同时避免误选其他实验中名称相似的配置。
cd /home/labex/project/application-config
kubectl create namespace practice-config
kubectl create configmap app-settings -n practice-config --from-literal=APP_MODE=production --from-literal=LOG_LEVEL=info
kubectl get configmap app-settings -n practice-config -o yaml
每个 --from-literal=KEY=VALUE 参数都会添加一条配置。YAML 输出会直接显示已存储的键,适合用于检查非敏感配置。
在 data: 下查找 APP_MODE 和 LOG_LEVEL。Kubernetes 已经保存了期望值,但目前还没有对象使用它们;仅创建 ConfigMap 不会改变任何 Pod。
输出中的相关部分就是普通文本:
data:
APP_MODE: production
LOG_LEVEL: info
kind: ConfigMap
uid、resourceVersion 和 creationTimestamp 等字段由 Kubernetes 自动生成,因此每次可能不同。在本步骤中,只需关注 data,因为这里存放的是你提供的配置。
安全创建 Secret
在本步骤中,你将创建一个 Secret,而不会把凭据值写入 YAML 文件。Secret 数据在 API 中以 Base64 编码保存,并不会因为对象类型是 Secret 就自动加密;RBAC 和静态数据加密仍然十分重要。
generic 表示这是普通的键值 Secret,而不是专用的 TLS Secret 或镜像拉取 Secret。密码周围的引号可以避免 shell 在密码包含特殊字符时误解释其中的标点。
kubectl create secret generic app-credentials -n practice-config --from-literal=username=appuser --from-literal=password='practice-pass'
kubectl get secret app-credentials -n practice-config
kubectl get secret app-credentials -n practice-config -o jsonpath='{.data.username}' | base64 --decode; echo
最后一条管道命令仅为演示目的解码用户名。日常运维输出中应避免打印密码。
管道符会将经过编码的 JSONPath 结果传给 base64 --decode。分号开始执行最后的 echo,为解码后的值补充换行,使下一个 shell 提示符保持可读。看到 appuser 说明了编码的工作方式,也表明绝不能将 Base64 当作安全边界。
中间那条命令只显示元数据,不会暴露任何值:
NAME TYPE DATA AGE
app-credentials Opaque 2 ...
appuser
DATA=2 表示该 Secret 包含两个键,而不是表示字节数。Opaque 是普通键值 Secret 使用的 API 类型。
将配置注入 Pod
在本步骤中,你将把 ConfigMap 中的值作为环境变量使用,并将 Secret 以只读文件的形式挂载到容器中。
创建 Pod 之前,先在清单中区分这两种传递方式:
envFrom会将 ConfigMap 中的每个键转换为容器环境变量。volumes将 Secret 连接到 Pod,而volumeMounts决定该卷在当前容器中的挂载位置。
Pod 中的循环不会执行任何应用逻辑,只是让容器持续运行,以便你检查这两种传递路径。
cd /home/labex/project/application-config
cat <<'EOF' > configured-app.yaml
apiVersion: v1
kind: Pod
metadata:
name: configured-app
namespace: practice-config
spec:
containers:
- name: app
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "while true; do sleep 30; done"]
envFrom:
- configMapRef:
name: app-settings
volumeMounts:
- name: credentials
mountPath: /etc/app-credentials
readOnly: true
volumes:
- name: credentials
secret:
secretName: app-credentials
EOF
应用清单,并等待 Pod 就绪:
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
现在,从容器内部检查两种传递路径。printenv 用于读取进程环境变量,而 ls 用于显示由 Secret 卷投射出的文件:
kubectl exec configured-app -n practice-config -- printenv APP_MODE LOG_LEVEL
kubectl exec configured-app -n practice-config -- ls -l /etc/app-credentials
envFrom 会导入 ConfigMap 中的所有键。Secret 卷会为每个键创建一个文件,并且在容器内以只读方式挂载。
预期的环境变量输出依次为 production 和 info。目录中应包含名为 username 和 password 的文件;文件名与 Secret 的键名对应。
在实际容器中,列表输出类似于:
production
info
total 0
lrwxrwxrwx ... password -> ..data/password
lrwxrwxrwx ... username -> ..data/username
这些条目是由 Kubernetes 管理的符号链接。这种间接结构使 kubelet 能够在应用读取文件时,将挂载的数据切换到新版本,而无需直接重写文件。你的应用仍然可以通过熟悉的路径 /etc/app-credentials/username 和 /etc/app-credentials/password 打开这些文件。
更新挂载的 Secret 数据
在本步骤中,你将更新 Secret 并观察挂载文件的变化。kubectl create ... --dry-run=client -o yaml | kubectl apply -f - 会在本地生成目标 YAML,并通过管道直接交给 apply;其中的 - 表示从标准输入读取。
这种方式适合更新通过命令式方式生成的对象,而无需删除对象。左侧命令会输出完整的 Secret 清单,右侧命令则将该清单发送给 API 服务器。
kubectl create secret generic app-credentials -n practice-config --from-literal=username=serviceuser --from-literal=password='rotated-pass' --dry-run=client -o yaml | kubectl apply -f -
由于该 Secret 最初是使用 kubectl create 创建的,第一次更新时,成功提示之前可能会出现以下警告:
Warning: resource secrets/app-credentials is missing the kubectl.kubernetes.io/last-applied-configuration annotation ...
secret/app-credentials configured
该警告表示 apply 没有可供比较的上一次声明式配置快照。Kubernetes 会在本次更新过程中自动添加该注解,因此 configured 表示更新成功;你不需要删除并重新创建 Secret。
kubelet 会异步更新投射的 Secret 卷内容。轮询检查,直到出现新的用户名:
current_username=""
for attempt in $(seq 1 24); do
current_username=$(kubectl exec configured-app -n practice-config -- cat /etc/app-credentials/username)
if [ "$current_username" = "serviceuser" ]; then
break
fi
sleep 5
done
echo "$current_username"
test "$current_username" = "serviceuser"
该循环最多重试两分钟,因为卷投射采用最终一致性。在测试环境中,API 对象会立即发生变化,但挂载文件可能需要超过一分钟才能完成刷新。
$(...) 是命令替换语法:shell 会运行 seq 1 24,并将生成的数字作为循环变量。每次循环都会把当前文件内容保存到 current_username;break 会在首次匹配时退出循环,sleep 5 则避免持续高频轮询 API。最后的 test 会在值始终未变化时返回失败,防止命令在使用过期数据的情况下静默继续。成功输出应为 serviceuser。
刷新环境变量配置
在本步骤中,你将更新 LOG_LEVEL,并将其与运行中的 Pod 进行比较。容器启动时会固定环境变量,因此修改 ConfigMap 不会改写当前进程的环境变量。
合并补丁只会修改 data 下的一项。立即读取运行中进程的环境变量,可以清楚展示它与挂载卷之间的重要区别:
kubectl patch configmap app-settings -n practice-config --type=merge -p '{"data":{"LOG_LEVEL":"debug"}}'
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL
Pod 仍然会输出 info。从已保存的清单重新创建 Pod,使其读取新值:
configmap/app-settings patched
info
这是符合预期的结果,并不表示补丁失败:ConfigMap 中已经包含 debug,但正在运行的进程仍然使用启动时创建的环境变量。
cd /home/labex/project/application-config
kubectl delete pod configured-app -n practice-config --wait=true
kubectl apply -f configured-app.yaml
kubectl wait --for=condition=Ready pod/configured-app -n practice-config --timeout=60s
kubectl exec configured-app -n practice-config -- printenv LOG_LEVEL
替换后的 Pod 会输出 debug。这正是生产工作负载在基于环境变量的配置发生变化后通常需要触发滚动更新的原因。
pod "configured-app" deleted
pod/configured-app created
pod/configured-app condition met
debug
清单本身仍然只通过名称引用 ConfigMap;没有任何值被复制到 configured-app.yaml 中。重新创建 Pod 后,kubelet 会再次解析该引用,并注入最新数据。
总结
你已将应用配置和凭据从镜像中分离出来,将 ConfigMap 数据作为环境变量使用,将 Secret 数据以文件形式挂载,轮换了投射的 Secret,并通过重新创建 Pod 刷新了基于环境变量的配置。


