Kubernetes 配置管理实战:ConfigMap 与 Secret 完全指南

应用跑起来之后,配置怎么管理?把配置写死在镜像里,改一次配置就要重新打包一次镜像,显然不可取。Kubernetes 提供了两个专门管理配置的 API 对象——ConfigMap(配置)Secret(机密),让配置与应用代码彻底分离。本文从创建、引用到综合实战,一次讲透。

一、环境准备

先创建一个独立的命名空间,避免实验污染集群环境:

kubectl create ns configure
kubectl config set-context --current --namespace configure

后续所有操作都默认在 configure 命名空间下进行,实验结束后直接删除整个命名空间即可清理干净。

二、ConfigMap 介绍

ConfigMap 是一个存储其他对象所需配置的 API 对象,使用 databinaryData 字段保存键值对数据:

  • data:保存 UTF-8 字符串,例如配置文件内容、环境变量值;
  • binaryData:保存二进制数据(base64 编码后的字符串)。

几个必须知道的规则:

  • ConfigMap 的名称必须是合法的 DNS 子域名;
  • data / binaryData 下每个键的名称只能由字母、数字、-_. 组成;
  • databinaryData 中的键名不能重叠;
  • 从 Kubernetes v1.19 开始,可以给 ConfigMap 加 immutable: true,创建后不可修改,适合真正不会变动的配置。

使用建议:

  • ConfigMap 只用来存配置数据,不要存数据文件,上限 1 MiB
  • 需要保存大量数据时,请使用存储卷、数据库或独立的文件服务,ConfigMap 在设计上就不是为大数据准备的。

三、ConfigMap 创建

创建命令的通用格式:

kubectl create configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run=...]

1. 键值对类型

kubectl create configmap mysql --from-literal=password=redhat

# 查看结果
kubectl get configmaps mysql -o yaml | grep ^data -A1
data:
  password: redhat

2. 文件类型

echo Hello World > index.html
kubectl create configmap web1 --from-file=./index.html

kubectl get configmaps web1 -o yaml | grep ^data -A2
data:
  index.html: |
    Hello World

此时文件名成为键名,文件内容成为值。

3. 目录类型

echo error > error.html
mkdir web2
mv index.html error.html web2
kubectl create configmap web2 --from-file=./web2

kubectl get configmaps web2 -o yaml | grep ^data -A4
data:
  error.html: |
    error
  index.html: |
    Hello World

目录下的每个文件都会变成一个键值对,非常适合把一组静态页面或配置文件整体放入 ConfigMap。

四、ConfigMap 引用

1. 环境变量方式

注意:环境变量属于容器级别,只在指定的容器内生效。

准备 Pod 清单 pod-with-env-from-configmap.yaml

apiVersion: v1
kind: Pod
metadata:
  name: mysql
  labels:
    name: mysql
spec:
  containers:
  - image: mysql:latest
    imagePullPolicy: IfNotPresent
    name: mysql
    ports:
    - containerPort: 3306
      name: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      # 不再写死 value,而是从 ConfigMap 取值
      valueFrom:
        configMapKeyRef:
          name: mysql
          key: password

应用并验证:

kubectl apply -f pod-with-env-from-configmap.yaml
kubectl get pods -o wide

# 进入容器查看环境变量
kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat

MySQL 的 root 密码完全来自 ConfigMap,Pod 清单里不出现任何明文密码。

2. Volume 方式(整体挂载)

volume 属于 Pod 级别,通过 volumeMounts 挂载到容器。这种方式会把 ConfigMap 中所有键值对挂载为目录下的文件。

准备 pod-with-volume-from-configmap.yaml

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: web
  name: web
spec:
  containers:
  - image: nginx:latest
    imagePullPolicy: IfNotPresent
    name: web
    volumeMounts:
    - name: webcontent
      # mountPath 是挂载点,ConfigMap 中所有键值对都会出现在这里
      mountPath: "/usr/share/nginx/html"
  volumes:
  - name: webcontent
    configMap:
      name: web2

验证:

kubectl apply -f pod-with-volume-from-configmap.yaml
kubectl exec web -- ls /usr/share/nginx/html
error.html
index.html

curl http://<POD_IP>
Hello World
curl http://<POD_IP>/error.html
error

Nginx 的网页目录被 ConfigMap 里的两个文件“填满”了。

3. Volume 方式(引用特定 key)

如果只想要部分键,用 items 指定:

volumes:
- name: webcontent
  configMap:
    name: web2
    items:
    - key: index.html
      path: index.html

也可以使用 subPath,效果类似(注意此时 mountPath 是文件路径):

volumeMounts:
- name: webcontent
  mountPath: "/usr/share/nginx/html/index.html"
  subPath: index.html
volumes:
- name: webcontent
  configMap:
    name: web2

一个重要的特性: 通过 Volume 挂载的 ConfigMap 内容会被自动更新。kubelet 每次周期性同步时会检查挂载的 ConfigMap 是否最新,更新的键最终会被投射到文件中(环境变量方式不会自动更新)。

五、集群中的 ConfigMap:以 kube-proxy 为例

Kubernetes 系统组件本身就大量使用 ConfigMap。比如 kube-system 命名空间中常见的 ConfigMap:

kubectl get configmaps -n kube-system
NAME                                                  DATA   AGE
calico-config                                         4      37h
coredns                                               1      37h
kube-proxy                                            2      37h
kubeadm-config                                        1      37h
kubelet-config                                        1      37h
...

kube-proxy 的 ConfigMap 里保存了 config.confkubeconfig.conf 两个配置文件,其中 config.conf 就是 kube-proxy 的完整配置。kube-proxy 有两种工作模式:ipvsiptables。默认 mode 为空,从日志可以看出实际使用的是 iptables:

kubectl logs -n kube-system kube-proxy-8kp8w
I1019 02:26:00.975333       1 server_others.go:69] "Using iptables proxy"

想切换成 ipvs,直接编辑 ConfigMap:

kubectl edit cm -n kube-system kube-proxy
# 把 mode 改为 "ipvs"
mode: "ipvs"

然后删除 kube-proxy Pod,等待 DaemonSet 控制器重新创建:

kubectl delete pod -n kube-system kube-proxy-{8kp8w,ghgxw,p6qnk}

再次查看日志,确认已经使用 ipvs:

kubectl logs -n kube-system kube-proxy-8swmx
... "Using ipvs Proxier"

这就是“改配置不动镜像”的典型场景:修改 ConfigMap → 触发 Pod 重建 → 新配置生效。

六、综合案例:haproxy + web 负载均衡

需求:

  1. 创建两个 Nginx Pod(webapp-1webapp-2),各自的 index.htmlerror.html 由 ConfigMap 提供;
  2. 创建 HAProxy Pod,配置文件 haproxy.cfg 保存在 ConfigMap 中,通过 Volume 挂载到容器,把流量转发到两个 webapp。

第一步:创建 webapp-1 及其 ConfigMap

kubectl create cm webapp-1 \
  --from-literal=index.html="hello webapp-1" \
  --from-literal=error.html="sorry, error."

pod-webapp-1.yaml

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: webapp-1
  name: webapp-1
spec:
  containers:
  - name: nginx
    image: nginx:latest
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: config
      mountPath: "/usr/share/nginx/html"
      readOnly: true
  volumes:
  - name: config
    configMap:
      name: webapp-1
kubectl apply -f pod-webapp-1.yaml

第二步:创建 webapp-2 及其 ConfigMap

操作完全相同,只是内容不同:

kubectl create cm webapp-2 \
  --from-literal=index.html="hello webapp-2" \
  --from-literal=error.html="sorry, error."

pod-webapp-2.yaml 与上面几乎一样(名称和 ConfigMap 名改为 webapp-2),然后 kubectl apply -f pod-webapp-2.yaml

第三步:创建 HAProxy

查看两个 Pod 的 IP:

kubectl get pods -o wide
NAME       READY   STATUS    RESTARTS   AGE   IP
webapp-1   1/1     Running   0          63m   10.224.84.80
webapp-2   1/1     Running   0          63m   10.224.149.25

编写 haproxy.cfg,把流量转发到两个后端:

global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client 50000ms
    timeout server 50000ms

frontend http-in
    bind *:8080
    default_backend servers

backend servers
    server  app1 10.224.84.80:80 check
    server  app2 10.224.149.25:80 check

把配置文件放入 ConfigMap,并编写 HAProxy Pod:

kubectl create cm haproxy.cfg --from-file=haproxy.cfg=./haproxy.cfg

haproxy.yaml

apiVersion: v1
kind: Pod
metadata:
  name: haproxy
spec:
  containers:
  - name: haproxy
    image: haproxy
    imagePullPolicy: IfNotPresent
    securityContext:
      allowPrivilegeEscalation: true
    volumeMounts:
    - name: config
      mountPath: "/usr/local/etc/haproxy"
      readOnly: true
  volumes:
  - name: config
    configMap:
      name: haproxy.cfg

验证负载均衡效果:

kubectl apply -f haproxy.yaml
kubectl get pods haproxy -o wide

curl http://<HAPROXY_IP>:8080
hello webapp-1
curl http://<HAPROXY_IP>:8080
hello webapp-2

两次请求分别打到了两个后端,HAProxy 的轮询生效了。整个过程中,HAProxy 的配置完全由 ConfigMap 提供,没有重新构建镜像。

七、Secret 介绍

Secret 与 ConfigMap 类似,但专门用来保存敏感信息:密码、令牌、密钥等。使用 Secret 意味着机密数据不需要出现在 Pod 清单或镜像中,也可以独立于使用它们的 Pod 创建,降低敏感数据暴露的风险。

两者最核心的区别一句话就能说清:

Secret 对数据编码(base64),ConfigMap 不对数据编码。

注意:base64 只是编码不是加密,Secret 不适合存放真正需要加密保护的数据,生产环境建议配合 KMS 等方案加密。

Secret 的三种常见类型

类型说明Kubernetes 类型
generic键值对形式保存任意敏感数据Opaque
docker-registry访问镜像仓库的凭据kubernetes.io/dockerconfigjson
tlsTLS 公钥/私钥对kubernetes.io/tls

八、Secret 创建

1. generic:基于键值对

一般用于传递变量值:

kubectl create secret generic mysecret1 \
  --from-literal=user=tom \
  --from-literal=password1=redhat \
  --from-literal=password2=redhat

kubectl get secrets mysecret1 -o yaml
apiVersion: v1
data:
  password1: cmVkaGF0
  password2: cmVkaGF0
  user: dG9t
kind: Secret
type: Opaque

可以看到数据已经被 base64 编码:

echo -n tom | base64
dG9t
echo -n redhat | base64
cmVkaGF0

2. generic:基于普通文件

一般用于传递配置文件。文件名作为 key,文件内容作为 value,也可以自定义 key:

echo -n tom > user
echo -n redhat > password1
echo -n redhat > password2

# 文件名即 key
kubectl create secret generic mysecret2 --from-file=./user --from-file=./password1 --from-file=./password2

# 自定义 key:username
kubectl create secret generic mysecret3 --from-file=username=./user --from-file=./password1 --from-file=./password2

3. generic:基于键值对内容的文件

如果文件里本身就是 key=value 格式,用 --from-env-file 一步导入:

cat > env.txt <<EOF
user=tom
password1=redhat
password2=redhat
EOF

kubectl create secret generic mysecret4 --from-env-file=./env.txt

4. generic:基于目录

mkdir config
mv user password1 password2 config
kubectl create secret generic mysecret5 --from-file=./config

5. generic:基于 YAML 文件

YAML 中必须使用 base64 转换后的值:

apiVersion: v1
kind: Secret
metadata:
  name: mysecret6
type: Opaque
data:
  user: dG9t
  password1: cmVkaGF0
  password2: cmVkaGF0

6. docker-registry 类型

回顾 Docker 登录:

docker login DOCKER_REGISTRY_SERVER -u=DOCKER_USER -p=DOCKER_PASSWORD

登录凭据默认保存在 ~/.dockercfg 中,后续 pull/push 都靠它。Kubernetes 中用 docker-registry 类型的 Secret 保存同样的信息:

kubectl create secret docker-registry registry \
  --docker-username=ningcode \
  --docker-password=redhat \
  --docker-email=admin@example.com \
  --docker-server=registry.example.com

kubectl get secrets registry -o yaml
...
type: kubernetes.io/dockerconfigjson

创建后,Pod 可以通过 imagePullSecrets 引用它,从而从私有仓库拉取镜像。

7. TLS 类型

保存公钥/私钥对,公钥证书必须是 PEM 编码:

# 1. 生成私钥
mkdir certs && cd certs
openssl genrsa -out www.key 2048

# 2. 生成证书请求文件(CN 必须是网站域名)
openssl req -new -key www.key -out www.csr \
  -subj "/C=CN/ST=JS/L=NJ/O=LM/OU=DEVOPS/CN=www.example.com/emailAddress=admin@example.com"

# 3. 使用私钥自签名生成证书
openssl x509 -req -days 3650 -in www.csr -signkey www.key -out www.crt

# 4. 创建 TLS Secret
kubectl create secret tls www-tls --cert=./www.crt --key=./www.key

九、Secret 引用

Secret 同样支持环境变量和 Volume 两种方式。

1. 环境变量方式

env:
- name: MYSQL_ROOT_PASSWORD
  valueFrom:
    secretKeyRef:
      name: mysecret1
      key: password1

验证:

kubectl exec -it mysql -- bash -c 'echo $MYSQL_ROOT_PASSWORD'
redhat

2. Volume 方式(整体挂载)

volumes:
- name: webcontent
  secret:
    secretName: web

3. Volume 方式(引用特定 key)

volumes:
- name: webcontent
  secret:
    secretName: web
    items:
    - key: index.html
      path: index.html

也可以配合 subPath 只挂载单个文件。与 ConfigMap 一样,以卷方式引用的 Secret 支持动态更新:修改 Secret 后等待 kubelet 周期同步(约 30 秒左右),挂载的文件内容就会更新。

4. 文件权限控制

映射过去的文件默认权限是 644(八进制),对应十进制 420。JSON 格式不支持八进制,所以权限值要用十进制写:

volumes:
- name: foo
  secret:
    secretName: mysecret1
    # 256(十进制) = 400(八进制):所有文件只读
    defaultMode: 256

只设置单个文件权限:

volumes:
- name: foo
  secret:
    secretName: mysecret1
    items:
    - key: user
      path: user
      mode: 511   # 511(十进制) = 777(八进制)

十、综合实验(练习题)

使用 mysql 和 wordpress 镜像部署一个 blog 应用:

  1. MySQL 密码和 WordPress 站点证书用 Secret 存储;
  2. 为 blog 站点配置 vhost,配置文件由 ConfigMap 提供。

这个练习把本文的知识点全部串了起来:Secret 管敏感信息、ConfigMap 管配置文件、Volume 负责挂载。建议独立完成后再对照官方文档查漏补缺。

十一、环境清理

kubectl delete ns configure

删除命名空间会连同其中所有 Pod、ConfigMap、Secret 一起清理,非常方便。

总结

  • ConfigMap 存配置、Secret 存机密,两者都可以通过环境变量或 Volume 方式注入 Pod;
  • 配置文件类数据推荐 Volume 挂载,还能享受自动更新特性;
  • Secret 的 base64 只是编码,不是安全加密;
  • ConfigMap / Secret 上限 1 MiB,大数据请用存储卷或数据库;
  • 修改 kube-proxy 等系统组件的配置,本质就是编辑它的 ConfigMap 并触发 Pod 重建。

配置与代码分离,是 Kubernetes 应用管理的基本功。掌握 ConfigMap 与 Secret,你就已经拿到了“改配置不重建镜像”的钥匙。

更多推荐