CodexDave头像
关注

Kubernetes 上手实战(4):配置管理与密钥

上一篇让 Deployment 能滚动升级和回滚。本篇把环境差异从镜像中移出:普通配置进入 ConfigMap,敏感值通过 Secret 注入,并明确更新传播、触发重启和生产密钥管理的边界。

一、痛点:同一镜像如何进入不同环境

把 API 地址、功能开关或数据库密码写进镜像,会迫使每个环境重新构建,也会让秘密进入镜像层和构建日志。Kubernetes 提供 ConfigMap 保存非敏感键值,Secret 保存敏感数据。二者可注入环境变量或挂载成文件,应用无需调用 Kubernetes API。

Secret 并非天然保险箱。清单中的 data 只是 Base64 编码,不是加密;拥有读取 Secret 权限的人仍可还原。生产要启用 etcd 静态加密、严格 RBAC、审计和外部密钥系统,避免把明文 Secret 提交 Git。External Secrets Operator、Secrets Store CSI Driver 或云厂商方案可让 Git 只保存引用,但选型应结合威胁模型与运维能力。

环境变量简单,却只在容器启动时读取,更新对象不会改变已运行进程。卷挂载文件通常会被 kubelet最终同步更新,但应用还要监听或定期重读;通过 subPath 挂载单文件时不会获得自动更新。是否热加载必须由应用契约明确,不能假定 Kubernetes 会替应用刷新内存。

二、原理:数据投影与发布语义

ConfigMap 和 Secret 都受对象大小限制,不适合大文件或二进制制品。键可以通过 envFrom 批量导入,也可逐项映射以控制变量名;文件投影会把每个键变成文件。逐项声明较啰嗦,却能在评审中看见应用真正依赖哪些值,并避免未来新增键意外进入进程环境。

下面保存为 config.yaml。示例 Secret 通过命令生成,不把明文写进清单;先创建 namespace 中的对象,再应用 Deployment。学习值不是生产凭据,运行后仍应删除。应用使用 Nginx,通过 ConfigMap 挂载完整配置,并把示例令牌作为环境变量展示注入方法,但不会打印令牌。

apiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
data:
  default.conf: |
    server {
      listen 8080;
      location /healthz {
        access_log off;
        return 200 "ok\n";
      }
      location / {
        default_type text/plain;
        return 200 "configured web\n";
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: web
  template:
    metadata:
      labels:
        app.kubernetes.io/name: web
    spec:
      containers:
        - name: web
          image: nginx:1.27.4-alpine
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: APP_TOKEN
              valueFrom:
                secretKeyRef:
                  name: web-secret
                  key: token
          volumeMounts:
            - name: config
              mountPath: /etc/nginx/conf.d
              readOnly: true
          readinessProbe:
            httpGet:
              path: /healthz
              port: http
      volumes:
        - name: config
          configMap:
            name: web-config

三、实现:创建、验证和轮换

脚本从环境变量读取学习令牌,使用客户端 dry-run 生成对象再 apply,避免命令行采用裸字面值。注意进程环境和 shell 历史也可能泄密,这种方式仅用于本地实验;CI 应从受保护的密钥存储注入,并禁止调试回显。

#!/usr/bin/env bash
set -euo pipefail

: "${LAB_APP_TOKEN:?export LAB_APP_TOKEN with a disposable value}"
test "$(kubectl config current-context)" = "kind-k8s-lab"

kubectl create secret generic web-secret \
  --from-literal="token=${LAB_APP_TOKEN}" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f config.yaml
kubectl rollout status deployment/web --timeout=120s

pod_name="$(kubectl get pod -l app.kubernetes.io/name=web -o jsonpath='{.items[0].metadata.name}')"
kubectl exec "${pod_name}" -- nginx -t
kubectl exec "${pod_name}" -- wget -qO- http://127.0.0.1:8080/healthz

kubectl patch configmap web-config --type=merge -p \
  '{"data":{"default.conf":"server { listen 8080; location /healthz { return 200 \"ok\\n\"; } location / { return 200 \"configured-v2\\n\"; } }"}}'
kubectl rollout restart deployment/web
kubectl rollout status deployment/web --timeout=120s

kubectl create secret generic web-secret \
  --from-literal="token=rotated-disposable-value" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/web
kubectl rollout status deployment/web --timeout=120s

脚本会让 nginx -t 成功并让健康端点返回 ok。Secret 轮换后必须重启,因为环境变量不会动态刷新。若使用卷并且应用支持热加载,可选择不重启,但仍应监控加载是否成功,并保留旧凭据的短暂重叠窗口,避免连接长时间存活时瞬间失效。

GitOps 中常给 Pod template annotation 写入配置内容的哈希;配置变化会改变 template,从而自然触发 Deployment rollout。原生 Kubernetes 不会自动为 ConfigMap 更新重启所有引用它的 Pod。Helm 篇会实现 checksum annotation;纯清单也可用 Kustomize 的 configMapGenerator 生成带哈希名称,实现不可变配置引用。

四、踩坑:泄露通常发生在边界处

不要在文章、工单、日志和截图里打印 Secret,也不要用 kubectl get secret -o yaml 作为普通排障动作。Base64 字符串可轻易解码。RBAC 应让工作负载 ServiceAccount 只读取必要对象,最好通过卷注入而不是授予应用列出整个 namespace Secret 的 API 权限。

把 ConfigMap 挂在目录会遮蔽镜像中该目录原有文件,因此应确认应用所需文件全部存在。文件更新采用符号链接切换,长期打开文件描述符的程序可能仍读旧内容。配置语法错误也可能让新 Pod 启动失败;应在流水线先运行 nginx -t 等专用校验,再依赖 rollout 的 readiness 保护旧副本。

Secret immutable: true 可防止误改并降低 API 服务器监听负担,但轮换时需创建新名字并更新引用。对于证书、双密钥或数据库密码,应设计新旧并存、消费者切换、验证、撤销旧值的阶段流程,而不是覆盖后立即删除旧值。

五、验证:证明配置和密钥都按预期生效

验收应覆盖配置内容可读、探针通过、秘密未出现在 PodSpec 明文字段和常规日志、更新后运行实例取得新值、旧凭据最终被撤销。可以用 kubectl auth can-i get secret/web-secret --as=system:serviceaccount:lab:default 检查权限,但这只回答授权,不代表应用必须拥有该权限。

可靠配置管理的核心是把“值是什么”和“何时进入运行实例”分开记录。对象版本、Pod template 修订、应用加载结果都应可追踪。测试结束删除 web-secret,并从 shell 环境清除实验变量;若误用真实凭据,应立即在源系统撤销而不是只删 Kubernetes 对象。

下一篇将处理另一种跨重建状态:用 emptyDir 理解 Pod 生命周期,再用 PersistentVolumeClaim 让数据独立于 Pod,验证删除与重建后的持久化。

为配置建立数据字典也很重要:记录键名、类型、默认值、合法范围、是否敏感、拥有者、变更是否需要重启以及兼容窗口。应用启动时应校验必填值并输出不含秘密的配置摘要;发现非法配置立即失败,比带着半有效状态接流量更安全。删除配置键要经过“消费者不再读取、所有实例已升级、观察期结束”三个阶段,不能仅凭仓库搜索不到旧代码就立刻清理。

灾难演练可故意提交语法错误的配置,确认新 Pod 不就绪、旧 Pod 继续服务、流水线超时并留下诊断;随后恢复正确配置并验证滚动过程。另一次演练轮换一次性令牌,确认新旧凭据重叠时间符合设计,旧值最终在源系统失效。这里验收的是完整传播路径,而不仅是接口中的资源版本已经变化。

配置是否应触发发布,可以用内容摘要确定,而不依赖人工记版本号。下面程序对规范化后的非敏感配置计算摘要;键顺序变化不会误触发,值变化一定产生新摘要。真实项目把摘要写入 Pod template annotation,即可让 Deployment 自动滚动。

import hashlib
import json

def config_digest(config: dict[str, str]) -> str:
    payload = json.dumps(
        config,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()[:12]

first = {"LOG_LEVEL": "info", "REGION": "cn-north"}
same = {"REGION": "cn-north", "LOG_LEVEL": "info"}
changed = {"LOG_LEVEL": "warning", "REGION": "cn-north"}

first_hash = config_digest(first)
same_hash = config_digest(same)
changed_hash = config_digest(changed)
print(f"stable_order={first_hash == same_hash}")
print(f"change_detected={first_hash != changed_hash}")
print(f"rollout={'required' if first_hash != changed_hash else 'skipped'}")

运行输出:

stable_order=True
change_detected=True
rollout=required

Secret 的 base64 只是编码。第二个程序不打印秘密,只检查必填键、值是否为空以及轮换版本是否增加;这种“只输出结论、不输出内容”的模式也适用于 CI 日志。

from dataclasses import dataclass

@dataclass(frozen=True)
class SecretMetadata:
    name: str
    keys: tuple[str, ...]
    nonempty: tuple[str, ...]
    version: int

def validate(secret: SecretMetadata, previous_version: int) -> list[str]:
    errors: list[str] = []
    required = {"username", "password"}
    if not required.issubset(secret.keys):
        errors.append("missing required key")
    if not required.issubset(secret.nonempty):
        errors.append("empty secret value")
    if secret.version <= previous_version:
        errors.append("rotation version did not increase")
    return errors

candidate = SecretMetadata(
    name="web-credentials",
    keys=("username", "password"),
    nonempty=("username", "password"),
    version=4,
)
errors = validate(candidate, previous_version=3)
print(f"secret={candidate.name}")
print(f"checked_keys={len(candidate.keys)}")
print("validation=" + ("PASS" if not errors else "FAIL"))

运行输出:

secret=web-credentials
checked_keys=2
validation=PASS

参考来源


👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

🚀 本文属于 《Kubernetes 上手实战》 系列,持续更新,关注不迷路。

📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 [email protected],我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_67153745/article/details/163966039

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--