第四章:应用配置管理:ConfigMap与Secret

在构建容器化应用时,一个核心的最佳实践原则是将配置与应用镜像分离。如果将数据库地址、API密钥、功能开关等配置信息硬编码到容器镜像中,会带来一系列严重问题:

  • 可移植性差:同一个镜像无法不经修改就部署到开发、测试、生产等不同环境中,因为每个环境的配置(如数据库连接)都不同。
  • 更新流程繁琐:仅仅为了修改一个配置项,就需要重新构建、推送和部署整个镜像,效率低下。
  • 安全风险高:将敏感信息(如密码、Token)打包进镜像是极不安全的做法,任何能够访问镜像的人都能轻易获取这些信息。

为了解决这些问题,Kubernetes提供了两种专门的API资源来管理配置数据:ConfigMap用于存储非敏感的、通用的配置信息;Secret则用于存储和管理敏感数据。本章将深入探讨如何使用这两种资源,实现配置与代码的彻底解耦。

4.1 ConfigMap:将配置信息与应用镜像解耦

ConfigMap是一个API对象,用于将非机密的配置数据以键值对的形式存储在Kubernetes集群中。Pod可以像消费存储卷(Volume)或环境变量一样消费这些数据。

通过使用ConfigMap,你可以将环境相关的配置从容器镜像中抽离出来,使得你的应用程序镜像变得“环境无关”,更具可移植性。

创建ConfigMap

创建ConfigMap有多种灵活的方式,包括命令式和声明式。

1. 使用 kubectl create configmap 命令(命令式)

这是最快捷的创建方式,适合临时或简单的配置。

  • 从字面值创建 (--from-literal):

    # 创建一个名为 my-app-config 的ConfigMap,包含两个键值对
    kubectl create configmap my-app-config \
      --from-literal=app.properties.greeting=Hello \
      --from-literal=app.properties.farewell=Bye
    
  • 从单个文件创建 (--from-file): 文件名会成为key,文件内容会成为value

    # 1. 创建一个配置文件 game.properties
    echo "enemy.types=aliens,monsters" > game.properties
    echo "player.lives=3" >> game.properties
    
    # 2. 从文件创建ConfigMap
    kubectl create configmap game-config --from-file=game.properties
    

    创建后,这个ConfigMap会有一个名为game.propertieskey,其value是整个文件的内容。

  • 从多个文件创建 (--from-file): 可以指定一个目录,目录下的每个文件名都会成为一个key

    # 1. 创建一个配置目录
    mkdir -p config-dir
    echo "ui.color=blue" > config-dir/ui.properties
    echo "log_level=info" > config-dir/app.conf
    
    # 2. 从目录创建ConfigMap
    kubectl create configmap app-config-dir --from-file=config-dir
    

    这将创建一个ConfigMap,包含ui.propertiesapp.conf两个key

2. 使用YAML文件(声明式)- 推荐方式

在生产环境中,强烈建议使用YAML文件来定义ConfigMap,这样可以将其纳入版本控制系统(如Git),实现基础设施即代码(IaC)。

# my-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config-yaml
data:
  # key-value 格式,value是纯字符串
  database.url: "jdbc:mysql://db.example.com:3306/mydb"
  api.endpoint: "https://api.example.com/v1"

  # 也可以嵌入整个文件内容作为value,使用 | 保留换行符
  nginx.conf: |
    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
    }

应用该文件:kubectl apply -f my-configmap.yaml

你可以使用 kubectl get configmap 查看已创建的ConfigMap,使用 kubectl describe configmap <name> 查看其详细内容。

如何通过环境变量和Volume挂载使用ConfigMap

一旦ConfigMap被创建,Pod中的容器就可以通过两种主要方式来使用它。

方式一:作为环境变量注入容器

这是最简单直接的使用方式,适合将少量的、简单的配置项注入应用。

  • 注入所有键值对 (envFrom): 将ConfigMap中的所有key都作为环境变量注入容器。

    # pod-with-env-from-cm.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: test-pod-envfrom
    spec:
      containers:
        - name: test-container
          image: busybox
          command: [ "/bin/sh", "-c", "env && sleep 3600" ]
          envFrom:
            - configMapRef:
                name: my-app-config-yaml # 引用ConfigMap的名称
    

    创建这个Pod后,进入容器内部执行env命令,你会看到 database.urlapi.endpoint 等环境变量。注意nginx.conf这个key不符合环境变量的命名规范(不能包含.),所以它不会被注入。

  • 注入特定的键 (valueFrom): 只选择ConfigMap中的某一个key,并将其值赋给一个指定的环境变量。

    # pod-with-env-valuefrom-cm.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: test-pod-valuefrom
    spec:
      containers:
        - name: test-container
          image: busybox
          command: [ "/bin/sh", "-c", "env && sleep 3600" ]
          env:
            - name: DB_HOST_URL    # 定义我们自己的环境变量名
              valueFrom:
                configMapKeyRef:
                  name: my-app-config-yaml  # 引用ConfigMap名称
                  key: database.url        # 引用具体的key
    

    这个例子中,容器内会有一个名为DB_HOST_URL的环境变量,其值为jdbc:mysql://db.example.com:3306/mydb

方式二:作为文件挂载到容器中 (volumes & volumeMounts)

当配置项是完整的配置文件(如nginx.conf, settings.xml)或者配置项很多时,将它们作为文件挂天在到容器中是更好的选择。

# pod-with-volume-cm.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pod-volume
spec:
  containers:
    - name: test-container
      image: busybox
      command: [ "/bin/sh", "-c", "ls -l /etc/config && sleep 3600" ]
      volumeMounts:
      - name: config-volume   # 挂载点的名称,需与下面的volumes.name对应
        mountPath: /etc/config # 挂载到容器内的这个目录
  volumes:
    - name: config-volume     # 定义一个Volume
      configMap:
        name: my-app-config-yaml # Volume的数据来源是这个ConfigMap
        # 可选: 使用items字段精确控制挂载哪些key以及文件名
        items:
        - key: database.url
          path: db_url.txt  # database.url的值会写入db_url.txt文件
        - key: nginx.conf
          path: my_nginx.conf # nginx.conf的值会写入my_nginx.conf文件

创建这个Pod后,进入容器,你会在/etc/config目录下看到db_url.txtmy_nginx.conf这两个文件,它们的内容分别对应ConfigMap中database.urlnginx.conf的值。如果省略items字段,ConfigMap中的每个key都会成为一个文件名,其value成为文件内容。

4.2 Secret:管理敏感数据

Secret的设计理念和使用方式与ConfigMap几乎完全相同,但其目的是为了存储和管理敏感信息,如:

  • 数据库密码
  • API密钥或Token
  • TLS/SSL证书和私钥
  • Docker仓库的认证信息

虽然用法相似,但Kubernetes对Secret提供了额外的保护措施:

  1. Secret只会被分发到需要它的Pod所在的Node节点上。
  2. Kubelet会将Secret存储在内存(tmpfs)中,而不是写入磁盘。
  3. 可以配置RBAC策略,严格控制哪些用户或ServiceAccount有权限读取某个Secret。
  4. 在更高安全性的集群中,可以启用Etcd的静态加密功能,使得存储在etcd中的Secret数据本身也是加密的。
【重难点】 Secret的编码方式(Base64)并非加密

这是一个极其重要的、初学者最容易误解的概念。当你查看一个Secret的YAML描述时,会发现它的data字段下的值都是经过Base64编码的字符串。

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
type: Opaque # Opaque是默认的通用Secret类型
data:
  # 'admin' 的 Base64 编码是 'YWRtaW4='
  username: YWRtaW4=
  # 'S!B3rP@ssw0rd' 的 Base64 编码是 'UyFCM3JQYXNzdzByZA=='
  password: UyFCM3JQYXNzdzByZA==

必须明确:Base64是一种编码(Encoding),而不是加密(Encryption)!

  • 编码 (Encoding): 是为了方便数据在特定媒介上传输或存储而改变其表现形式,这个过程是可逆的,并且不需要密钥。任何人拿到YWRtaW4=这个字符串,都可以通过echo 'YWRtaW4=' | base64 --decode命令轻松地解码出原始值admin
  • 加密 (Encryption): 是为了保护数据不被未授权访问而将其转换成密文,这个过程需要密钥,没有正确的密钥,就无法解密出原文。

那为什么Kubernetes要使用Base64?
主要原因是为了处理二进制数据。Secret不仅可以存字符串,还可以存储证书、密钥文件等二进制内容。YAML和JSON这类文本格式无法直接嵌入原始的二进制数据,而Base64可以将任意二进制数据转换成一个只包含ASCII字符的文本字符串,从而可以安全地嵌入到YAML或JSON文件中。

结论:不要把Base64编码当作一种安全措施。 Secret的安全性依赖于Kubernetes的访问控制(RBAC)、静态加密等机制,而不是这种可轻易逆转的编码。

Secret的创建和使用

创建和使用Secret的方式与ConfigMap惊人地相似。

创建Secret:

  • 命令式:

    # 创建一个包含用户名和密码的通用Secret
    kubectl create secret generic db-user-pass \
      --from-literal=username=myuser \
      --from-literal=password='My$tRoNgP@ssw0rd'
    

    kubectl会自动对value进行Base64编码。

  • 声明式 (YAML):
    你需要手动对数据进行Base64编码。

    # 手动编码
    echo -n 'myuser' | base64  # 输出: bXl1c2Vy
    echo -n 'My$tRoNgP@ssw0rd' | base64 # 输出: TXkkdFJvTmdQQHNzdzByZA==
    
    # db-secret.yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: db-user-pass-yaml
    type: Opaque
    data:
      username: bXl1c2Vy
      password: TXkkdFJvTmdQQHNzdzByZA==
    

    应用该文件:kubectl apply -f db-secret.yaml

在Pod中使用Secret:
使用方法与ConfigMap完全相同,只是引用的资源类型从configMapKeyRefconfigMapRef变成了secretKeyRefsecretRef

  • 作为环境变量:

    # pod-with-secret-env.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: test-pod-secret-env
    spec:
      containers:
      - name: test-container
        image: busybox
        command: [ "/bin/sh", "-c", "env && sleep 3600" ]
        env:
          - name: DB_USERNAME
            valueFrom:
              secretKeyRef:
                name: db-user-pass # 引用Secret的名称
                key: username
          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: db-user-pass
                key: password
    
  • 作为文件挂载:

    # pod-with-secret-volume.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: test-pod-secret-volume
    spec:
      containers:
      - name: test-container
        image: busybox
        command: ["/bin/sh", "-c", "ls -l /etc/db-credentials && cat /etc/db-credentials/password && sleep 3600"]
        volumeMounts:
        - name: db-secret-volume
          mountPath: "/etc/db-credentials"
          readOnly: true # 挂载Secret时,建议设置为只读
      volumes:
      - name: db-secret-volume
        secret:
          secretName: db-user-pass
    

    当Secret作为Volume挂载时,Kubernetes会自动进行Base64解码,所以在容器内的文件中看到的是原始数据

4.3 热更新: 在不重启应用的情况下更新配置

这是一个非常实用的高级功能。想象一下,你只想调整一下应用的日志级别,或者修改一个功能开关,难道非要触发一次完整的滚动更新、重启所有Pod吗?这不仅效率低,还可能导致服务瞬间抖动。

ConfigMap和Secret都支持配置的热更新。 其工作机制如下:

  1. 当你使用kubectl edit configmap <name>kubectl apply -f更新了一个ConfigMap或Secret后,其在etcd中的数据会立即改变。
  2. kubelet组件会周期性地(默认大约1分钟)检查其所在节点上Pod所挂载的ConfigMap和Secret是否发生了变化。
  3. 如果kubelet检测到变化,它会自动更新挂载在容器内的文件内容

关键点来了: Kubernetes只负责更新文件,它不会自动重启或通知你的应用程序。你的应用程序需要自己有能力检测到配置文件的变化,并重新加载配置。

如何让应用感知配置变化?

  • 应用原生支持: 很多现代框架和库(如Spring Cloud Config, Nginx, Prometheus)都支持在不重启进程的情况下重新加载配置文件。例如,Nginx可以通过发送nginx -s reload信号来重新加载其nginx.conf
  • 文件监控:你的应用可以内置一个文件监控的逻辑(watch),当发现配置文件被修改时,自动触发内部的重载逻辑。
  • Sidecar模式: 对于不具备原生重载能力的应用,可以部署一个轻量的“边车”容器。这个Sidecar容器的唯一任务就是监控配置文件(例如使用inotify工具),一旦文件变化,它就通过进程信号(如SIGHUP)或调用应用的API端点来通知主应用容器重新加载配置。
实战示例:Nginx配置热更新

让我们来实践一下。我们将使用ConfigMap来管理Nginx的配置文件,并演示如何在不重启Nginx Pod的情况下修改其配置。

步骤1:创建包含nginx.conf的ConfigMap

# nginx-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data:
  default.conf: |
    server {
        listen 80;
        server_name localhost;
        location / {
            root /usr/share/nginx/html;
            index index.html;
            # 返回一个自定义的头部信息
            add_header X-App-Version "v1.0";
        }
    }

kubectl apply -f nginx-config.yaml

步骤2:创建Nginx Deployment,并挂载ConfigMap

# nginx-hot-reload-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-hot-reload
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-hot
  template:
    metadata:
      labels:
        app: nginx-hot
    spec:
      containers:
      - name: nginx
        image: nginx:1.21
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-conf-volume
          # 挂载到Nginx默认读取配置的目录
          mountPath: /etc/nginx/conf.d
      volumes:
      - name: nginx-conf-volume
        configMap:
          name: nginx-conf

kubectl apply -f nginx-hot-reload-deployment.yaml

步骤3:暴露服务并验证
为了方便访问,我们创建一个NodePort服务。
kubectl expose deployment nginx-hot-reload --type=NodePort --port=80

获取服务的NodePort和Node IP,然后使用curl进行访问:

# -I 选项只看头部信息
curl -I http://<Node-IP>:<Node-Port>

你应该能在返回的头部信息中看到 X-App-Version: v1.0

步骤4:更新ConfigMap
现在,我们来修改ConfigMap,将版本号改为v2.0
kubectl edit configmap nginx-conf
在打开的编辑器中,将add_header X-App-Version "v1.0";改为add_header X-App-Version "v2.0";,然后保存退出。

步骤5:等待并触发重载
等待大约一分钟,让kubelet将更新后的ConfigMap同步到Pod内的挂载点。
然后,进入Nginx Pod内部,手动触发配置重载。

# 获取Pod名称
POD_NAME=$(kubectl get pods -l app=nginx-hot -o jsonpath='{.items[0].metadata.name}')

# 进入Pod
kubectl exec -it $POD_NAME -- /bin/bash

# 在Pod内部,检查配置文件是否已更新
cat /etc/nginx/conf.d/default.conf # 你会看到 v2.0

# 发送重载信号
nginx -s reload

# 退出Pod
exit

步骤6:再次验证
再次执行curl命令:

curl -I http://<Node-IP>:<Node-Port>

这一次,你应该能看到返回的头部信息变成了 X-App-Version: v2.0。我们成功地在没有重启Pod的情况下,更新了应用的配置!在实际生产中,触发nginx -s reload的步骤可以通过Sidecar等方式自动化。

【重难点总结】

  • 配置与代码分离的重要性: 这是云原生应用设计的基石,遵循了“十二要素应用”中的“配置”原则。它带来了无与伦比的灵活性(同一镜像部署多环境)、敏捷性(快速修改配置无需重建镜像)和安全性(敏感信息不进入版本库和镜像仓库)。将配置作为独立的K8s资源进行管理,使得运维和开发职责分离,配置管理更加清晰和规范。

  • Secret的Base64编码并非加密: 必须牢记这一点以避免产生错误的安全感。Base64仅仅是为了数据传输的兼容性。真正的安全来自于最小权限原则(通过RBAC限制对Secret的访问)、静态加密(加密etcd中的数据)、网络策略(限制Pod间的网络访问)以及运行时安全(保护Node和容器环境)等一系列纵深防御措施。

通过熟练掌握ConfigMap和Secret,你就能构建出更加健壮、安全和易于管理的Kubernetes应用,这是从入门到专业的必经之路。

K8s 详细学习笔记 目录

以下是整个系列的10章目录,点击章节标题即可跳转阅读:

更多推荐