Kubernetes ConfigMap 与 Secret:分离配置和镜像

ConfigMap 与 Secret 的注入、挂载、变更联动,以及「Secret 不是加密」这条底线。

技术文章约 18 分钟222 次阅读

把配置从镜像中拆出来,是 12-Factor 应用的基本要求:同一个镜像,靠注入的配置跑不同环境(开发、预发、生产),而不是为每个环境重新构建镜像。Kubernetes 为此提供了两个原生对象——ConfigMap(普通配置)与 Secret(敏感配置),这篇文章要解决的是:如何正确创建、注入、更新这两类配置,并理解它们"为什么是这样工作的"。背后最基础的事实只有三条:ConfigMap 与 Secret 本质上都是 etcd 里按命名空间存放的键值对象;Secret 的"掩藏"只是 base64 编码,不是加密;注入方式(环境变量还是 volume 挂载)决定了更新语义完全不同。把这三条拆开讲透,绝大多数"改了不生效""以为加密了"的事故,都可以在动手之前被避免。

#底层机制:ConfigMap 与 Secret 在 etcd 里到底是什么

#从 12-Factor 到集群内配置

12-Factor 第三条(Config,配置)要求"配置与代码严格分离":代码仓库里不放环境差异,环境差异由运行时注入。镜像一旦构建,就应该是不可变的交付物;dev 与 prod 的差异(日志级别、数据库地址、功能开关)全部来自外部配置。ConfigMap 与 Secret 就是 Kubernetes 提供的"外部配置"载体:ConfigMap 放非敏感的键值配置,Secret 放敏感配置(口令、证书、Token)。两者在 API、存储、注入方式上高度同构,唯一的本质区别是语义与展示方式——这也正是很多人踩坑的起点。

#etcd 中的对象形态:一对键值对象

要理解 ConfigMap/Secret,先理解 Kubernetes 的数据底座。整个集群的控制面状态都存在 etcd 里,kube-apiserver 是唯一的数据写入口:你执行的 kubectl apply 会经 apiserver 完成校验、鉴权、准入(Admission),最终序列化为 JSON 写入 etcd,存储键形如 /registry/configmaps/<namespace>/<name>、/registry/secrets/<namespace>/<name>。所以 ConfigMap 与 Secret 首先是两个普通 API 对象:有 metadata(名字、命名空间、标签、注解),有独立的 data 负载,被限定在某个命名空间内,受 RBAC 控制。它们"都是键值对象"这句话,直接对应到数据结构上:

  • ConfigMap 的 data 是 map[string]string,明文键值对;
  • Secret 的 data 是 map[string][]byte,键是名字、值是一段任意字节。

除此之外,Secret 还有一个写时辅助字段 stringData(map[string]string),它只在创建/更新请求里存在:你写成明文,apiserver 在校验后把它合并进 data 并按字节序列化,对象读回时只有 data,看不到 stringData。这就是原稿强调"用 stringData 写清单更不容易错"的原因——data 要求你自己先 base64,stringData 让人类以明文书写,编码交给集群。

#为什么 Secret 的 data 是 base64:为了承载任意字节,不是为了保密

这是整个主题里最容易被误解的一点。Secret 的 data 之所以表现为 base64,是因为 API 对象要经过 JSON 序列化,而 JSON 字符串无法直接承载任意二进制(密钥、证书、Docker 凭证里的未知字节):如果把原始字节塞进 JSON,可能会出现非法字符、控制字符或编码错误。于是 Kubernetes 把所有 []byte 在序列化时统一编码为 base64 字符串——base64 只负责把字节变成可打印字符,与安全性没有任何关系。任何人只要拿到对象,就能用一行命令还原:

kubectl get secret demo-api-secret -o jsonpath='{.data.DATABASE_URL}' | base64 -d

Linux 下 base64 -d 即可;macOS 旧版需要 base64 -D。因此 Secret 的"编码"最多只能防"日志里意外出现明文",防不了任何有读取权限的人。原稿的结论必须作为第一原则记住:编码 ≠ 加密。

#键值对象的共同边界:命名空间、类型与大小

三个与存储方式无关、但影响使用的客观边界:

  • 命名空间隔离:ConfigMap/Secret 都属于某个命名空间,Pod 只能引用同命名空间下的对象(StatefulSet 等通过 namespace 字段也无法跨命名空间引用)。这既是一种隔离,也意味着"所有能读取该命名空间资源的账号都能读这些对象"。
  • Secret 的 type 字段:Secret 在 data 之外还有一个 type,它不改变存储方式(存进 etcd 的都是 base64 数据),但改变 apiserver 对内容的解读与校验。常用类型:Opaque(默认,任意键值)、kubernetes.io/tls(固定要求 tls.crt、tls.key 两个键)、kubernetes.io/dockerconfigjson(存放 .dockerconfigjson,供 imagePullSecrets 使用)、kubernetes.io/service-account-token(由控制器管理的 SA Token)。普通配置用 Opaque,TLS 证书建议用 kubernetes.io/tls 以获得校验与语义。
  • 大小上限:官方限制单个 ConfigMap 或 Secret 不超过 1 MiB。它不是设计来装大文件的,超过 1 MiB 的配置应拆分对象,或改用 PVC/其他存储。

#两种注入路径:为什么更新行为完全不同

容器里消费 ConfigMap/Secret 有两条主流路径:注入成环境变量、挂载成文件。它们的底层实现不同,导致更新语义完全不同——这是配置管理里最核心的工程认知。

#环境变量注入:Pod 启动那一刻就定型

无论是单键注入还是整包注入,最终都落在容器的环境变量上。Pod 被创建时,kubelet 会把 spec 里的 env 展开成容器运行时的环境变量,写入容器创建请求;环境变量是容器进程启动参数的组成部分,容器运行期间永远不会被外部改写。由此推出两个必然结论:

  1. ConfigMap/Secret 更新后,已运行 Pod 的环境变量不会热更新,必须重启 Pod 才会重新取到新值;
  2. 哪怕用 kubectl set env 之类的命令改 Deployment,其本质也是修改 Pod 模板、触发滚动更新重建 Pod,而不是"在线修改"环境变量。

所以任何"应用只读环境变量、不读文件"的场景,改配置后都绕不开一次 Pod 重建。

#volume 挂载:kubelet 定期同步

另一条路径是把 ConfigMap/Secret 挂载成文件。这里的写盘动作由 kubelet 完成:kubelet 通过 API 拉取对象内容,把每个键写成一个文件放进 volume 对应目录(Secret 卷内的文件默认以只读呈现,权限可用 defaultMode 显式控制,例如 defaultMode: 0400)。关键在于 kubelet 不是"挂上去就不管",而是周期性检查对象是否有更新:

  • kubelet 对挂载的 ConfigMap/Secret 使用 TTL 缓存(configMapAndSecretChangeDetectionStrategy 默认 TTL),缓存有效期默认 1 分钟;
  • 在 TTL 策略下,kubelet 按轮询间隔(默认约 1 分钟)重新拉取对象,发现版本变化后重写 volume 目录下的文件,容器进程可以直接读到新内容,不需要重启 Pod;
  • 官方文档对总延迟的估计是:kubelet 同步周期(默认 1 分钟)+ 缓存 TTL(默认 1 分钟),实际常见在 1 分钟上下。

这就是"挂载成文件 > 注入成环境变量"这句话的机制来源:文件挂载有 kubelet 兜底的同步链路,env 没有。注意这里还嵌套了一个重要事实:更新体积大与否无关紧要,同步的是对象整体——kubelet 拉取的是整个对象,重写的是卷内所有文件。

#子路径挂载:绕开了自动更新

volume 挂载还有一个变体:subPath。它把卷里的**单个文件(或子目录)**直接绑定挂载到容器内的指定路径,而不是把整个目录接进去。这也带来一个明确的坑:通过 subPath 挂载的 ConfigMap/Secret 不会收到自动更新。原因在于 subPath 挂载的是宿主机上某个文件的绑定挂载,kubelet 在更新卷内容时通过写新文件的方式替换目录条目,绑定挂载仍然指向旧的文件位置,容器内看到的还是旧内容。官方文档对此有明确表述:ConfigMap/Secret 的 subPath 挂载不会自动更新。所以:需要热更新的配置文件不要用 subPath;必须用 subPath(例如只想注入单个文件且路径不可变)时,应把更新动作与 Pod 重建绑定。

#完整可复现操作

#前置条件与执行环境

以下操作以 Kubernetes v1.30 为例(概念与命令在 v1.20+ 均适用,个别字段名有版本差异会单独注明),需要一个可用的集群与配置好的 kubectl。先确认环境并建立独立命名空间:

kubectl version --client
kubectl cluster-info
kubectl create namespace demo-config
kubectl config set-context --current --namespace demo-config

set-context 会改动当前上下文默认命名空间;如果不想改,也可以给每条命令加 -n demo-config。示例镜像使用 busybox:1.36,仅作演示容器,可替换为你环境里可拉取的任意镜像。

#创建对象:字面量与文件两种来源

方式一:字面量(--from-literal),适合少量键值。kubectl create 会直接向 apiserver 提交对象,不需要手写 YAML:

kubectl create configmap demo-api-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=API_MODE=production

kubectl create secret generic demo-api-secret \
  --from-literal='DATABASE_URL=postgresql://app:change-me@example-db:5432/app'

方式二:文件(--from-file),适合内容较长或本身就有配置文件的情况。键名默认取文件名,也可以显式指定 键名=文件:

cat > app.conf <<'EOF'
retries=3
timeout=5s
EOF

kubectl create configmap demo-api-config --from-file=CONFIG_FILE=app.conf

创建后对象就已存在于 etcd,但还没有任何 Pod 使用它。

#创建对象:清单方式与 stringData

清单方式的好处是可提交到 Git、可评审、可复现。下面是一个完整清单,包含普通配置 demo-api-config 与 Opaque 敏感配置 demo-api-secret:

apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-api-config
  namespace: demo-config
data:
  LOG_LEVEL: info
  API_MODE: production
  CONFIG_FILE: |
    retries=3
    timeout=5s
---
apiVersion: v1
kind: Secret
metadata:
  name: demo-api-secret
  namespace: demo-config
type: Opaque
stringData:
  DATABASE_URL: postgresql://app:change-me@example-db:5432/app

真实证书一般由证书管理流程产生,开发环境可以用 openssl 自签并直接生成 Secret:

openssl req -x509 -nodes -newkey rsa:2048 -days 30 \
  -keyout tls.key -out tls.crt -subj "/CN=demo-api"
kubectl create secret tls demo-api-tls --cert=tls.crt --key=tls.key
kubectl apply -f demo-config.yaml

#注入方式一:环境变量(envFrom 与单键 env)

整包注入用 envFrom,把对象的所有键一次性变成环境变量;单键注入用 env 与 configMapKeyRef/secretKeyRef,可以只挑需要的键并改名。下面的 Deployment 同时演示两种方式,并加上 volume 挂载(下文展开):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-api
  namespace: demo-config
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-api
  template:
    metadata:
      labels:
        app: demo-api
    spec:
      containers:
        - name: demo-api
          image: busybox:1.36
          command: ["/bin/sh", "-c", "sleep 3600"]
          envFrom:
            - configMapRef:
                name: demo-api-config
            - secretRef:
                name: demo-api-secret
          env:
            - name: LOG_LEVEL
              valueFrom:
                configMapKeyRef:
                  name: demo-api-config
                  key: LOG_LEVEL
            - name: DATABASE_URL_ALIAS
              valueFrom:
                secretKeyRef:
                  name: demo-api-secret
                  key: DATABASE_URL
          volumeMounts:
            - name: config-file
              mountPath: /etc/app/config
              readOnly: true
            - name: tls
              mountPath: /etc/app/tls
              readOnly: true
      volumes:
        - name: config-file
          configMap:
            name: demo-api-config
        - name: tls
          secret:
            secretName: demo-api-tls

环境变量部分的行为:envFrom 注入 demo-api-config 与 demo-api-secret 的全部键;env 显式声明了与前者重名的 LOG_LEVEL 和改名后的 DATABASE_URL_ALIAS。关于冲突的精确规则,见"边界与坑"一节。

#注入方式二:volume 挂载与子路径挂载

上面的 Deployment 已经把 ConfigMap 挂到 /etc/app/config、Secret 挂到 /etc/app/tls:目录下每个键是一个文件(LOG_LEVEL、API_MODE、CONFIG_FILE、tls.crt、tls.key),挂载以只读方式呈现。readOnly: true 是推荐写法,防止容器误写配置。

如果只想注入单个文件且接受"不热更新"的代价,用 subPath 单独演示:

apiVersion: v1
kind: Pod
metadata:
  name: subpath-demo
  namespace: demo-config
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["/bin/sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: config-file
          mountPath: /etc/app/CONFIG_FILE
          subPath: CONFIG_FILE
          readOnly: true
  volumes:
    - name: config-file
      configMap:
        name: demo-api-config

应用清单并确认 Deployment 就绪:

kubectl apply -f demo-config.yaml
kubectl apply -f demo-api-deployment.yaml
kubectl apply -f subpath-demo.yaml
kubectl rollout status deployment/demo-api --timeout=120s

#验证:怎么确认我做对了

#验证对象本身:内容、编码与可见性

kubectl get configmap demo-api-config -o yaml
kubectl get secret demo-api-secret -o yaml
kubectl get secret demo-api-secret -o jsonpath='{.data.DATABASE_URL}' | base64 -d; echo

预期结果:ConfigMap 的 data 是明文键值;Secret 的 YAML 中看不到 stringData 字段,data.DATABASE_URL 是一串 base64 字符,解码后等于 postgresql://app:change-me@example-db:5432/app。这直接验证了上文两点:stringData 已被合并编码;base64 编码对任何读权限者都可逆。

#验证 env 注入

kubectl exec deploy/demo-api -- env | grep -E 'LOG_LEVEL|API_MODE|DATABASE_URL'

预期看到 LOG_LEVEL=info、API_MODE=production、DATABASE_URL=postgresql://app:change-me@example-db:5432/app,以及显式注入的 LOG_LEVEL(env 覆盖 envFrom)和 DATABASE_URL_ALIAS。若变量缺失,优先检查对象是否存在、命名空间是否一致、键名是否拼对。

#验证文件挂载

kubectl exec deploy/demo-api -- ls -l /etc/app/config
kubectl exec deploy/demo-api -- cat /etc/app/config/CONFIG_FILE
kubectl exec deploy/demo-api -- ls -l /etc/app/tls
kubectl exec deploy/demo-api -- cat /etc/app/tls/tls.crt

预期:/etc/app/config 下有三个文件,CONFIG_FILE 内容为 retries=3、timeout=5s;/etc/app/tls 下有 tls.crt、tls.key 两个文件。文件权限随 kubelet 版本与 defaultMode 设置而不同,以实际 ls -l 输出为准。

#验证两种注入的更新差异(核心实验)

修改 ConfigMap,观察 volume 文件热更新、env 不更新:

kubectl patch configmap demo-api-config --type merge \
  -p '{"data":{"LOG_LEVEL":"debug","API_MODE":"staging"}}'
sleep 90
kubectl exec deploy/demo-api -- cat /etc/app/config/LOG_LEVEL
kubectl exec deploy/demo-api -- env | grep LOG_LEVEL

预期:第一次 exec 读到 debug(volume 文件已被 kubelet 同步,等待时间覆盖同步周期 + TTL 缓存,官方最坏约 2 分钟);第二次 exec 仍是 info(环境变量不会热更新)。这个实验同时验证了 kubelet 同步机制确实存在、以及 env 注入的"一次性"本质。

#验证 subPath 不更新

kubectl exec subpath-demo -- cat /etc/app/CONFIG_FILE
kubectl patch configmap demo-api-config --type merge \
  -p '{"data":{"CONFIG_FILE":"mutated=1\n"}}'
sleep 90
kubectl exec subpath-demo -- cat /etc/app/CONFIG_FILE

预期:文件内容仍是旧的 retries=3、timeout=5s。修改会写进 etcd、也会反映到主 Deployment 的卷目录,但 subPath 绑定挂载的文件不会被替换。要让它更新,唯一可靠办法是重建 Pod。

#边界与坑

#编码、加密、访问控制是三件事

很多事故源于把三者混为一谈。分开看:

  • 编码:Secret 的 data 是 base64,可逆,防不了读取者。
  • 静态加密(Encryption at Rest):默认情况下,写入 etcd 的就是上面这份 base64 数据,等于是明文落盘(能读 etcd 备份的人直接看到内容)。生产环境必须给 apiserver 配置 --encryption-provider-config,对 secrets 资源做加密,常用 provider 为 aescbc(本地密钥)或 kms(对接云厂商 KMS,如 AWS KMS、GCP KMS、Azure Key Vault、阿里云 KMS)。EncryptionConfiguration 示例:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - kms:
          apiVersion: v2
          name: myKmsProvider
          endpoint: unix:///var/run/kms-plugin.sock
      - aescbc:
          keys:
            - name: key1
              secret: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
      - identity: {}

aescbc 的密钥可用 head -c 32 /dev/urandom | base64 生成;上面 YAML 中的 MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= 仅为演示值,生产必须换成随机生成的 32 字节密钥;kms 需要部署 KMS 插件并通过 unix socket 与 apiserver 通信,具体插件按云厂商或开源方案(如 KMS 插件 SDK)选择。配置后需重启 apiserver 生效,老数据会在读取时被重新加密。

  • 访问控制:Secret 的读取权限由 RBAC 决定。默认情况下"能读该命名空间资源"的账号就能 kubectl get secret;Pod 内的进程如果能访问 API(ServiceAccount Token),其权限同样受 RBAC 约束。建议用最小权限 Role 而非宽泛的 cluster-admin 或 * 通配:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-secret-reader
  namespace: demo-config
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list", "watch"]

并且只在确实需要的 ServiceAccount 上绑定这个 Role。此外要意识到:一旦 Secret 被挂载进 Pod,任何能 kubectl exec 进该 Pod 或能访问其挂载点的人都能读文件——挂载本身扩大了访问面。

#真实事故:Secret 明文进入日志

原稿记录过一个真实事故:CI 脚本执行了 kubectl get secret -o jsonpath='{.data.DATABASE_URL}' | base64 -d,把明文数据库口令打印到了标准输出,随后 CI 日志被同步到第三方平台,口令直接外泄。这印证了上面的判断:编码可逆 + 日志无差别收集 = 泄露。工程上要立两条规矩:凡是含 Secret 的命令一律不允许把明文输出到 stdout;CI 模板里对这类命令统一走"写入临时文件、用完即删"或直接禁止。日志采集侧也可以对 DATABASE_URL、password 等字段做脱敏,但第一道防线是"不放明文到 stdout"。

#envFrom 键冲突:覆盖的精确规则

原稿提到的排查事故"配置怎么没生效",多数根因是键冲突。Kubernetes 对 env/envFrom 合并的规则是确定性的:

  1. 多个 envFrom 来源之间:后列出的来源覆盖前列的来源(后写的赢);
  2. env 里显式声明的变量:优先于任何 envFrom 来源(显式赢);
  3. 注入的值会覆盖镜像本身通过 Dockerfile ENV 定义的同名变量。

于是最常见的坑是:ConfigMap 里恰好有个键叫 PATH、HOME 或应用自己约定的变量名,经 envFrom 整包注入后悄悄改掉了容器环境。排查时不要只盯 Pod 清单,要把"最终环境变量"视为 镜像 ENV + env + envFrom 三层合并的结果,用 kubectl exec -- env 直接看最终值。

#改了配置不重启不生效:checksum 注释是正确姿势

多数应用只读环境变量,因此改 ConfigMap/Secret 后必须触发一次 Pod 重建。规范做法是给 Deployment 的 Pod 模板加一个内容校验注释:把配置内容的 hash 填进 annotation,hash 变化 → Pod 模板变化 → Deployment 自动滚动更新。不要写"每次发布都重启"的脚本,那既粗暴又不可追溯。

先取 hash:

kubectl get configmap demo-api-config -o jsonpath='{.data}' | sha256sum

把输出第一列(hash)填入模板:

template:
  metadata:
    annotations:
      checksum/config: 8b1a9953c4611296a827abf8c47804d7
    labels:
      app: demo-api

流程化之后,每次更新 ConfigMap 就重复"重算 hash → 更新 annotation → apply",Deployment 自动滚动,无需人为重启。Helm 用户可以直接用模板函数在任何 ConfigMap 变更时自动更新 hash:

annotations:
  checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

#更新延迟、删除对象与不可变对象

三个容易误判的边界:

  • 延迟不是"立即":volume 文件更新受 kubelet 同步周期与 TTL 缓存影响,最坏约 1~2 分钟。依赖"改配置立刻生效"的脚本要预留等待窗口,不要假设秒级一致。
  • 删除对象不影响已运行 Pod:环境变量早已固化,运行中的 Pod 不会因对象被删而崩溃;但重启 Pod 时,若引用的 ConfigMap/Secret 不存在,Pod 会停留在 CreateContainerConfigError。这解释了"为什么改配置要重启前先确认对象存在"。
  • immutable 优化:ConfigMap/Secret 支持 immutable: true。一旦设置,对象内容不可修改,只能删除重建。代价是失去热更新,收益是 kubelet 不再需要监听/轮询该对象,降低 apiserver 与 kubelet 负担。高频读取、极少变更的配置(如 CA 证书)适合 immutable。

#什么时候不该把敏感配置放进 Secret

即使做了静态加密与 RBAC,Secret 依然有先天局限:它存在集群里,任何集群管理员都能读;它由 etcd 统一存放,备份与审计面大;它无法解决"配置从哪来、如何轮换"的问题。所以生产环境的真实凭据应尽量不进 Git、不进镜像、不进日志,交给外部密钥管理。三条主流路线:

1. 云厂商 KMS / 静态加密:加密 etcd 里的 Secret,解决"落盘明文",不解决"谁能读 API"。

2. Sealed Secrets(Bitnami):把明文 Secret 在集群外用 kubeseal 加密成 SealedSecret CRD,只有集群内 controller 的私钥能解开,可以安全提交 Git:

kubectl create secret generic demo-api-secret \
  --from-literal='DATABASE_URL=change-me' --dry-run=client -o yaml > secret.yaml
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
kubectl apply -f sealed-secret.yaml

controller 解密后生成普通 Secret 供 Pod 使用。适合"密钥要进 Git 但不想明文"的团队。

3. External Secrets Operator(ESO)/ Vault:让 Secret 的内容由外部系统按需注入或同步,集群里不落地静态明文。ESO 的典型结构:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: demo-api-secret
  namespace: demo-config
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: demo-api-secret
  data:
    - secretKey: DATABASE_URL
      remoteRef:
        key: demo-api
        property: DATABASE_URL

其中 SecretStore 需按你的后端(Vault、AWS Secrets Manager 等)单独配置;ESO 会按 refreshInterval 轮换目标 Secret,天然支持密钥轮换。这是原稿"生产强推后者"的落地方式。

#收尾

ConfigMap/Secret 的问题,九成出在两个误解:"改了不生效"和"以为是加密的"。记住三条原则可以兜底:改配置要触发滚动更新(checksum 注释);Secret 默认只是 base64,静态加密与 RBAC 是两件必须主动做的事;真实凭据永远走外部密钥管理,不进 Git、不进镜像、不进日志。依此实施,配置管理的稳定性就从"靠经验"变成"靠机制"。下一步建议按成本从低到高迭代:先把 checksum 注释接入发布流程,再开启 etcd 静态加密,最后在团队最关键的业务里引入 External Secrets 或 Vault——到那时,ConfigMap/Secret 在你的集群里就只是"配置的载体",而不再是"泄露的入口"。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

Kubernetes ConfigMap 与 Secret:分离配置和镜像 · LXH·BLOG