3-10-1-应用配置

熊 0 阅读 容器

ConfigMap和Secret是Kubernetes系统上两种特殊类型的存储卷 , 前者用于为容器中的应用提供配置数据以定制程序的行为 , 而敏感的配置信息 , 例如密钥、证书等则通常由后者来配置。ConfigMap和 Secret将相应的配置信息保存于资源对象中 , 而后在Pod对象上以存储卷的形式将其挂载并加载相关的配置 , 降低了配置与镜像文件的耦合 关系。本章主要讲解ConfigMap与Secret存储卷的用法。

1. 容器化应用配置

应用程序是可执行程序文件 , 它含有指令列表 , CPU通过执行这些指令完成代码运行。例如 , Linux工程师最常用的命令之一cat对应 于/usr/bin/cat程序文件 , 该文件含有按特定目的组织的机器指令列表 , 用于在屏幕上显示指定文件的内容。大多数应用程序的行为都可以支持命令行选项及参数、环境变量或配置文件这一类的“配置工件”来按需定制 , 以灵活满足不同的使用需求。实践中 , 人们通常都 不会以默认的配置参数运行应用程序 , 而是需要根据特定的环境或具 体目标定制其运行特性 , 复杂的服务类应用程序尤其如此 , 例如 Nginx、Tomcat或Envoy等 , 而且通过配置文件定义其配置通常是服务类应用首选甚至是唯一的途径。

1.1 容器化应用配置的常见方式

容器镜像一般由多个只读层叠加组成 , 构建完成后无法进行修改 , 另一方面 , “黑盒化”运行的容器使用隔离的专用文件系统 , 那么 , 如何为容器化应用提供配置信息呢?传统实践中 , 通常有这么几种途径。

  • 启动容器时直接向应用程序传递参数。

  • 将定义好的配置文件硬编码(嵌入)于镜像文件中。

  • 通过环境变量传递配置数据。

  • 基于存储卷传送配置文件。

1.1.1 命令行参数

Dockerfile中的ENTRYPOINT和CMD指令用于指定容器启动时要运行 的程序及其相关的参数。其中 , CMD指令以列表形式指定要运行的程序及其相关的参数 , 若同时存在ENTRYPOINT指令 , 则CMD指令中的列表所有元素均被视作由ENTRYPOINT指定程序的命令行参数。另外 , 在基于某镜像创建容器时 , 可以通过向ENTRYPOINT中的程序传递额外的自定义参数 , 甚至还可以修改要运行的应用程序本向。例如 , 使用docker run命令创建并启动容器的格式为:

docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

其中的[COMMAND]即为自定义运行的程序 , [ARG]则是传递给程序的参数。若定义相关的镜像文件时使用了ENTRYPOINT指令 , 则 [COMMAND]和[ARG]都会被当作命令行参数传递给ENTRYPOINT指令中指 定的程序 , 除非为docker run命令额外使用--entrypoint选项覆盖 ENTRYPOINT指令而指定运行其他程序。使用详情请参考Docker的相关教程。

在Kubernetes系统上创建Pod资源时 , 也能够向容器化应用传递命令行参数 , 甚至指定运行其他应用程序 , 相关的字段分别为 pods.spec.containers[].command和pods.spec.containers[].args , 该话题在Pod资源的相关话题中已有过介绍。

1.1.2 将配置文件嵌入镜像文件

所谓将配置文件嵌入镜像文件 , 是指用户在Dockerfile中使用 COPY指令把定义好的配置文件复制到镜像文件系统上的目标位置 , 或者使用RUN指令调用sed或echo一类的命令修改配置文件 , 从而达到为 容器化应用提供自定义配置文件之目的。

这种方式的优势在于简单易用 , 用户无须任何额外的设定就能启 动符合需求的容器。但配置文件相关的任何额外的修改需求都不得不通过重新构建镜像文件来实现 , 路径长且效率低。

1.1.3 通过环境变量向容器注入配置信息

通过环境变量为镜像提供配置信息是最常见的容器应用配置方式 之一 , 例如使用MySQL官方提供的镜像文件启动MySQL容器时使用的 MYSQL_ROOT_PASSWORD环境变量 , 它用于为MySQL服务器的root用户设置登录密码。

在基于此类镜像启动容器时 , 通过docker run命令的-e选项向环 境变量传值即能实现应用配置 , 命令的使用格式为docker run -e SETTING1=foo -e SETTING2=bar ... <image name>。非云原生的应用 程序容器化时通常会借助entrypoint启动脚本以在启动时获取到这些环境变量 , 并在启动容器应用之前 , 通过sed或echo等一类命令将变量值替换到配置文件中。

一般说来 , 容器的entrypoint启动脚本应该为这些环境变量提供默认值 , 以便在用户未为环境变量传值时也能基于此类必需环境变量的镜像启动容器。使用环境变量这种配置方式的优势在于配置信息的动态化供给 , 不过有些应用程序的配置也可能会复杂到难以通过键值格式的环境变量完成。

我们也可以让容器的entrypoint启动脚本通过网络中的键值存储系统获取配置参数 , 常用的该类存储系统有Consul或etcd等 , 它们能够支持多级嵌套的数据结构 , 因而能够提供较之环境变量更为复杂的 配置信息。不过 , 这种方式为容器化应用引入了额外的依赖条件。

Kubernetes系统支持在为Pod资源配置容器时使用 spec.containers.env为容器的环境变量传值从而完成应用的配置

1.1.4 通过存储卷向容器注入配置信息

Docker存储卷能够将宿主机之上的任何文件或目录映射进容器文件系统上 , 因此 , 可以事先将配置文件放置于宿主机之上的某特定路 径中 , 而后在启动容器时进行加载。这种方式灵活易用 , 但也依赖于用户事先将配置数据提供在宿主机上的特定路径。而且在多主机模型 中 , 若容器存在被调度至任一主机运行的可能性时 , 用户还需要将配 置共享在任一宿主机以确保容器能正确获取到它们。

Kubernetes系统把配置信息保存于标准的API资源ConfigMap和 Secret中 , Pod资源可通过抽象化的同名存储卷插件将相关的资源对象 关联为存储卷 , 而后引用该存储卷上的数据赋值给环境变量 , 或者由 容器直接挂载作为配置文件使用。ConfigMap和Secret资源是 Kubernetes系统上的“一等公民” , 也是配置Pod中容器应用最常用的 方式。

1.2 容器环境变量

在运行时配置Docker容器中应用程序的第二种方式是在容器启动 时向其传递环境变量。Docker原生的应用程序应该使用很小的配置文 件 , 并且每一项参数都可由环境变量或命令行选项覆盖 , 从而能够在运行时完成任意的按需配置。然而 , 目前只有极少一部分应用程序是 为容器环境原生设计 , 毕竟为容器原生重构应用程序工程浩大 , 且旷日持久。好在有利用容器启动脚本为应用程序预设运行环境的方法可 用 , 通行的做法是在制作Docker镜像时 , 为ENTRYPOINT指令定义一个脚本 , 它能够在启动容器时将环境变量替换至应用程序的配置文件 中 , 而后由此脚本启动相应的应用程序。基于这类镜像运行容器时 , 即可通过向环境变量传值的方式来配置应用程序。

在Kubernetes中使用此类镜像启动容器时 , 也可以在Pod资源或 pod模板资源的中定义 , 通过为容器配置段使用env参数来定义使用的 环境变量列表。事实上 , 即便容器中的应用本身不处理环境变量 , 也 一样可以向容器传递环境变量 , 只不过它不被使用罢了。

通过环境变量配置容器化应用时 , 需要在容器配置段中嵌套使用 env字段 , 它的值是一个由环境变量构建的列表。每个环境变量通常由 name和value(或valueFrom)字段构成。

  • name : 环境变量的名称 , 必选字段。

  • value : 环境变量的值 , 通过$(VAR_NAME)引用 , 逃逸 格式为$$(VAR_NAME)默认值为空。

  • valueFrom : 环境变量值的引用源 , 例如当前Pod资源 的名称、名称空间、标签等 , 不能与非空值的value字段同时使用 , 即 环境变量的值要么源于value字段 , 要么源于valueFrom字段 , 二者不可同时提供数据。

valueFrom字段可引用的值有多种来源 , 包括当前Pod资源的属性值 , 容器相关的系统资源配置、ConfigMap对象中的Key以及Secret对象中的Key , 它们分别要使用不同的嵌套字段进行定义。

  • fieldRef <Object> : 当前Pod资源的指定字段 , 目前支持使用的字段包括metadata.name、metadata.namespace、 metadata.labels、metadata.annotations、spec.nodeName、 spec.serviceAccountName、status.hostIP和status.podIP等。

  • configMapKeyRef : ConfigMap对象中的特定Key。

  • secretKeyRef : Secret对象中的特定Key。

  • resourceFieldRef : 当前容器的特定系统资源的最小 值( 配额) 或最大值( 限额) , 目前支持的引用包括limits.cpu、 limits.memory、limits.ephemeral-storage、requests.cpu、 requests.memory和requests.ephemeral-storage。

下面是定义在资源清单文件env-demo.yaml中的Pod资源 , 它配置容器通过环境变量引用当前Pod资源及其所在的节点的相关属性值。 fieldRef字段的值是一个对象 , 它一般由apiVersion( 创建当前Pod资源的API版本) 或fieldPath嵌套字段所定义。事实上 , 这正是downwardAPI的一种应用示例。

apiVersion: v1 
kind: Pod 
metadata: 
  name: env-demo 
  labels: 
    purpose: demonstrate-environment-variables 
spec: 
  containers: 
  - name: env-demo-container 
    image: busybox 
    command: ["httpd"] 
    args: ["-f"] 
    env: 
    - name: HELLO_WORLD 
      value: just a demo 
    - name: MY_NODE_NAME 
      valueFrom: 
        fieldRef: 
          fieldPath: spec.nodeName 
    - name: MY_NODE_IP 
      valueFrom: 
        fieldRef: 
          fieldPath: status.hostIP 
    - name: MY_POD_NAMESPACE 
      valueFrom: 
        fieldRef: 
          fieldPath: metadata.namespace 
  restartPolicy: OnFailure

创建上面资源清单中定义的Pod对象env-demo , 而后打印它的环境变量列表 , 命令及其结果如下。

~]$ kubectl exec env-demo printenv 
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 
HOSTNAME=env-demo 
MY_NODE_NAME=k8s-node02.ilinux.io 
MY_NODE_IP=172.16.0.67 
MY_POD_NAMESPACE=default
HELLO_WORLD=just a demo 
……

容器的启动脚本或应用程序调用或处理这些环境变量 , 即可实现容器化应用的配置。相较于命令行参数的方式来说 , 使用环境变量的配置方式清晰、易懂 , 尤其对首次使用相关容器的用户来说能快速了解容器的配置方式。不过 , 这两种配置方式有着一个共同的缺陷: 无法在容器应用运行过程中更新环境变量从而达到更新应用的目的。这通常意味着用户不得不为production、development和stage等不同的 环境分别配置Pod资源。好在 , 用户还有ConfigMap资源可用。

2. 应用程序配置管理与ConfigMap资源

ConfigMap资源用于在运行时将配置文件、命令行参数、环境变量、端口号以及其他配置工件绑定至Pod的容器和系统组件。 Kubernetes借助于ConfigMap对象实现了将配置文件从容器镜像中解耦 , 从而增强了工作负载的可移植性 , 使配置更易于更改和管理 , 并防止将配置数据硬编码到Pod配置清单中。但ConfigMap资源用于存储和共享非敏感、未加密的配置信息 , 若要在集群中使用敏感信息 , 则必须使用Secret资源。

简单来说 , 一个ConfigMap对象就是一系列配置数据的集合 , 这些数据可注入到Pod的容器当中为容器应用所使用 , 注入的途径有直接挂载存储卷和传递为环境变量两种。ConfigMap支持存储诸如单个属性一类的细粒度的信息 , 也可用于存储粗粒度的信息 , 例如将整个配置文件保存在ConfigMap对象之中。

2.1 创建ConfigMap对象

ConfigMap是Kubernetes标准的API资源类型 , 它隶属名称空间级别 , 支持命令式命令、命令式对象配置及声明式对象配置3种管理接 口。命令式命令的创建操作可通过kubectl create configmap进行 , 它支持基于目录、文件或字面量(literal)值获取配置数据完成 ConfigMap对象的创建。该命令的语法格式如下所示。

kubectl create configmap <map-name> <data-source>

命令中的<data-source>就是可以通过直接给定的键值、文件或目录(内部的一到多个文件)来获取的配置数据来源 , 但无论是哪一种 数据供给方式 , 配置数据都要转换为键值类型 , 其中的键由用户在命令行给出或是文件类型数据源的文件名 , 且仅能由字母、数字、连接号和点号组成 , 而值则是字面量值或文件数据源的内容。

2.1.1. 字面量值数据源

为kubectl create configmap命令使用--from-literal选项可在命令行直接给出键值对来创建ConfigMap对象 , 重复使用此选项则可以 一次传递多个键值对。命令格式如下:

kubectl create configmap configmap_name --from-literal=key1=value-1 …

例如 , 下面的命令创建demoapp-config时传递了两个键值对 , 一 个是demoapp.host= 0.0.0.0 , 一个是demoapp.port=8080。

$ kubectl create configmap demoapp-config --from-literal=demoapp.host='0.0.0.0' \ 
    --from-literal=demoapp.port='8080' --namespace='default'

ConfigMap对象仅是Kubernetes API存储中的数据 , 并没有与之相关联的其他组件存在 , 因而无须status字段来区分期望的状态 (desired state)和当前状态(current state)。我们从下面的get configmap命令中输出的demoapp-config对象YAML格式信息可以看出 , ConfigMap资源没有spec和status字段 , 而是直接使用data字段嵌套键值数据。

~$ kubectl get configmaps demoapp-config -o yaml 
apiVersion: v1 
data: 
  demoapp.host: 0.0.0.0 
  demoapp.port: "8080" 
kind: ConfigMap 
metadata: 
  creationTimestamp: "2020-08-13T06:18:30Z" 
  managedFields: 
  …… 
  name: demoapp-config 
  namespace: default 
  resourceVersion: "2660869" 
  selfLink: /api/v1/namespaces/default/configmaps/demoapp-config 
  uid: e86036cc-e677-4529-87ce-64f58e72ecc7

显然 , 若要基于配置清单创建ConfigMap资源时 , 仅需要指定 apiVersion、kind、metadata和data这4个字段 , 以类似上面的格式定义出相应的资源即可。

2.1.2 文件数据源

ConfigMap资源也可用于为应用程序提供大段配置 , 这些大段配置通常保存于一到多个文本编码的文件中 , 可由kubectl create configmap命令通过--from-file选项一次加载一个配置文件的内容为指定键的值 , 多个文件的加载可重复使用--from-file选项完成。命令格式如下 , 省略键名时 , 将默认使用指定的目标文件的基名。

kubectl create configmap <configmap_name> \ 
   --from-file[=<key-name>]=<path-to-file>

例如 , 下面的命令可以把事先准备好的Nginx配置文件模板保存于 ConfigMap对象nginx-confs中 , 其中一个直接使用myserver.conf文件名作为键名 , 而另一个myserver-status.cfg对应的键名则自定义为 status.cfg。

~$ kubectl create configmap nginx-confs --from-file=./nginx-conf.d/myserver.conf \ 
    --from-file=status.cfg=./nginx-conf.d/myserver-status.cfg --namespace='default'

我们可以从nginx-confs对象的配置清单来了解各键名及其相应的键值。

~ $ kubectl get configmap nginx-confs -o yaml 
apiVersion: v1 
data: 
status.cfg: |                # “|”是键名及多行键值的分割符 , 多行键值要进行固定缩进 
    location /nginx-status {   # 该缩进范围内的文本块即为多行键值 
        stub_status on; 
        access_log off; 
    } 
myserver.conf: | 
    server { 
        listen 8080; 
        server_name www.ik8s.io; 
        include /etc/nginx/conf.d/myserver-*.cfg; 
        location / { 
            root /usr/share/nginx/html; 
        } 
    } 
kind: ConfigMap 
……

通过这种方式创建的ConfigMap资源可以直接以键值形式收纳应用 程序的完整配置信息 , 各个文件的内容以键值的形式保存于专用的键 名称之下。当需要配置清单保留ConfigMap资源的定义 , 而键数据又较为复杂时 , 也需要以类似上面命令输出结果中的格式 , 将配置文件内 容直接定义在配置清单当中。

2.1.3 目录数据源

对于配置文件较多且又无须自定义键名称的场景 , 可以直接在 kubectl create configmap命令的--from-file选项上附加一个目录路 径就能将该目录下的所有文件创建于同一ConfigMap资源中 , 各文件名为即为键名。命令格式如下。

kubectl create configmap <configmap_name> --from-file=<path-to-directory>

下面的命令把nginx-conf.d目录下的所有文件都保存于nginx-config-files对象中 , 从命令格式也不难揣测出 , 我们无法再为各文件内容自定义其键名称。

~$ kubectl create configmap nginx-config-files --from-file=./nginx-conf.d/

此目录中包含myserver.conf、status.cfg和gzip.cfg这3个配置文件 , 它们会被分别存储为3个键值数据 , 如下面的命令及其结果所 示。

~$ kubectl describe configmap nginx-config-files 
Name:         nginx-config-files 
Namespace:    default 
Labels:       <none> 
Annotations:  <none> 
Data 
==== 
myserver-gzip.cfg:      # 键值数据1 , describe命令的输出中键和值使用“----”分割符 ---- 
gzip on; 
…… 
myserver.conf:          # 键值数据2 ---- 
server { 
    …… 
} 
myserver-status.cfg:    # 键值数据3 ---- 
location /nginx-status { 
    stub_status on; 
    access_log off; 
} 
Events:  <none>

注意 , describe命令和get -o yaml命令都可显示由文件创建而成 的键与值 , 但二者使用的键和值之间的分隔符不同。另外需要说明的 是 , 基于字面量值和基于文件创建的方式也可以混合使用。例如下面 的命令创建demoapp-confs对象时 , 使用--from-file选项加载 demoapp-conf.d目录下的所有文件( 共有envoy.yaml和eds.conf两 个) , 又同时使用了两次--from-literal选项分别以字面量值的方式定义了两个键值数据。

~$ kubectl create configmap demoapp-confs --from-file=./demoapp-conf.d/ \ 
      --from-literal=demoapp.host='0.0.0.0' --from-literal=demoapp.port='8080'

该对象共有4个数据条目 , 它们分别是来自于demoapp-conf.d目录下的envoy.yaml和eds.conf , 以及命令行直接给出的demoapp.host和 demoapp.port , 这可以从下面命令的结果中得以验证。

~$ kubectl get configmaps/demoapp-confs 
NAME           DATA    AGE 
demoapp-confs   4      12s

2.1.4 ConfigMap资源配置清单

基于配置文件创建ConfigMap资源时 , 它所使用的字段包括通常的 apiVersion、kind和metadata字段 , 以及用于存储数据的关键字段 data。例如下面的示例所示。

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: configmap-demo 
  namespace: default 
data: 
  host: 0.0.0.0 
  port: "10080" 
  app.config: | 
    threads = 4 
    connections = 1024

若键值来自文件内容 , 使用配置文件创建ConfigMap资源的便捷性远不如直接通过命令行进行创建 , 因此我们可先使用命令行加载文件或目录的方式进行创建 , 在创建完成后使用get -o yaml命令获取到相关信息后进行编辑留存。

2.2 通过环境变量引用ConfigMap键值

Pod资源配置清单中 , 除了使用value字段直接给定变量值之外 , 容器环境变量的赋值还支持通过在valueFrom字段中嵌套 configMapKeyRef来引用ConfigMap对象的键值 , 它的具体使用格式如 下。

env: 
- name <string>          # 要赋值的环境变量名 
  valueFrom:             # 定义变量值引用 
    configMapKeyRef:     # 变量值来自ConfigMap对象的某个指定键的值 
      key <string>       # 键名称 
      name <string>      # ConfigMap对象的名称 
      optional <boolean> # 指定的ConfigMap对象或者指定的键名称是否为可选

这种方式赋值的环境变量的使用方式与直接赋值的环境变量并无区别 , 它们都可用于容器的启动脚本或直接传递给容器应用等。

下面是保存于配置文件configmaps-env-demo.yaml的资源定义示例 , 它包含了两个资源 , 彼此间使用“---”相分隔。第一个资源是名为demoapp-config的ConfigMap对象 , 它包含了两个键值数据 ; 第二个资源是名为configmaps-env-demo的Pod对象 , 它在环境变量PORT和 HOST中分别引用了demoapp-config对象中的demoapp.port和 demoapp.host的键的值。

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: demoapp-config 
  namespace: default 
data: 
  demoapp.port: "8080" 
  demoapp.host: 0.0.0.0 
--- 
apiVersion: v1 
kind: Pod 
metadata: 
  name: configmaps-env-demo 
  namespace: default 
spec: 
  containers: 
  - image: ikubernetes/demoapp:v1.0 
    name: demoapp 
    env: 
    - name: PORT 
      valueFrom: 
        configMapKeyRef: 
          name: demoapp-config 
          key: demoapp.port 
          optional: false 
    - name: HOST 
      valueFrom: 
        configMapKeyRef: 
          name: demoapp-config 
          key: demoapp.host 
          optional: true

demoapp支持通过环境变量HOST和PORT为其指定监听的地址与端口。将上面配置文件中的资源创建完成后 , 我们便可以来验证Pod资源 监听的端口等配置信息是否为demoapp-config对象中定义的内容 , 如 下面的命令及结果所示。

~$ kubectl apply -f configmaps-env-demo.yaml 
configmap/demoapp-config created 
pod/configmaps-env-demo created 
~$ kubectl exec configmaps-env-demo -- netstat -tnl  
Active Internet connections (only servers) 
Proto Recv-Q Send-Q Local Address      Foreign Address         State        
tcp        0      0 0.0.0.0:8080        0.0.0.0:*             LISTEN

需要注意的是 , 被引用的ConfigMap资源必须事先存在 , 否则将无法在Pod对象中启动引用了ConfigMap对象的容器 , 但未引用或不存在 ConfigMap资源的容器将不受影响。另外 , ConfigMap是名称空间级别的资源 , 它必须与引用它的Pod资源在同一空间内。

提示 : 在容器清单中的command或args字段中引用环境变量要使用 $(VAR_NAME)的格式。

若ConfigMap资源中存在较多的键值数据 , 而且其大部分甚至是全部键值数据都需要由容器进行引用时 , 为容器逐一配置相应的环境变量将是一件颇为劳心费神之事 , 而且极易出错。对此 , Pod资源支持在容器中使用envFrom字段直接将ConfigMap资源中的所有键值一次性地导入。

envFrom:  
- prefix <string>        # 为引用的ConfigMap对象中的所有变量添加一个前缀名 
  configMapRef:          # 定义引用的ConfigMap对象 
    name <string>        # ConfigMap对象的名称 
    optional <boolean>   # 该ConfigMap对象是否为可选

envFrom字段值是对象列表 , 用于同时从多个ConfigMap对象导入 键值数据。为了避免从多个ConfigMap引用键值数据时产生键名冲突 , 可以为每个引用中将被导入的键使用prefix字段指定一个特定的前缀 , 例如HTCFG_一类的字符串 , 于是ConfigMap对象中的PORT键名将成为容器中名为HTCFG_PORT的变量。

注意 : 如果键名中使用了连字符“-” , 转换为变量名的过程会自动将 其替换为下划线“_”。

例如 , 把上面示例中的配置清单转为如下形式的定义 (configmap-envfrom-demo.yaml配置文件)后 , 引用ConfigMap进行配置的效果并无不同。

apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: demoapp-config-for-envfrom 
  namespace: default 
data: 
  PORT: "8090" 
  HOST: 0.0.0.0 
--- 
apiVersion: v1 
kind: Pod 
metadata: 
  name: configmaps-envfrom-demo 
  namespace: default 
spec: 
  containers: 
  - image: ikubernetes/demoapp:v1.0 
    name: demoapp 
    envFrom: 
    - configMapRef: 
        name: demoapp-config-for-envfrom 
        optional: false

由envFrom从ConfigMap对象一次性引入环境变量时无法自定义每 个环境变量的名称 , 因此 , ConfigMap对象中的键名称必须要与容器中 的应用程序引用的变量名保持一致。待Pod资源创建完成后 , 可通过查看其环境变量验证其导入的结果。

~$ kubectl apply -f configmaps-envfrom-demo.yaml       
configmap/demoapp-config-for-envfrom created 
pod/configmaps-envfrom-demo created 
~$ kubectl exec configmaps-envfrom-demo -- printenv | grep -E '^(PORT|HOST)\b' 
HOST=0.0.0.0 
PORT=8090

值得提醒的是 , 从ConfigMap对象导入环境变量时若省略了可选的 prefix字段 , 各变量名将直接引用ConfigMap资源中的键名。若不存在键名冲突的可能性 , 例如从单个ConfigMap对象导入变量或在 ConfigMap对象中定义键名时已添加了特定前缀时 , 省略前缀的定义既 不会导致键名冲突 , 又能保持变量的简洁。

2.3 ConfigMap存储卷

使用环境变量导入ConfigMap对象中来源于较长的内容文件的键值 会导致占据过多的内存空间 , 而考虑此类数据通常用于为容器应用提供配置文件 , 将其内容直接以文件格式进行引用是为了更好地选择。 Pod资源的configMap存储卷插件专用于以存储卷形式引用ConfigMap对象 , 其键值数据是容器中的ConfigMap存储卷挂载点路径或直接指向的配置文件。

2.3.1 挂载整个存储卷

基于ConfigMap存储卷插件关联至Pod资源上的ConfigMap对象可由内部的容器挂载为一个目录 , 该ConfigMap对象的每个键名将转为容器挂载点路径下的一个文件名 , 键值则映射为相应文件的内容。显然 , 挂载点路径应该以容器加载配置文件的目录为其名称 , 每个键名也应 该有意设计为对应容器应用加载的配置文件名称。

在Pod资源上以存储卷方式引用ConfigMap对象的方法非常简单 , 仅需要指明存储卷名称及要引用的ConfigMap对象名称即可。下面是在配置文件configmaps-volume-demo.yaml中定义的Pod资源 , 它引用了前面创建的ConfigMap对象nginx-config-files , 并由nginx-server容器挂载至Nginx加载配置文件模块的目录/etc/nginx/conf.d之下。

apiVersion: v1 
kind: Pod 
metadata: 
  name: configmaps-volume-demo 
  namespace: default 
spec: 
  containers: 
  - image: nginx:alpine 
    name: nginx-server 
    volumeMounts: 
    - name: ngxconfs 
      mountPath: /etc/nginx/conf.d/ 
      readOnly: true 
  volumes: 
  - name: ngxconfs 
    configMap: 
      name: nginx-config-files 
      optional: false

此Pod资源引用的nginx-config-files中包含3个配置文件 , 其中 myserver.conf定义了一个虚拟主机www.ik8s.io , 并通过include指令 包含/etc/nginx/conf.d/目录下以myserver-为前缀、以.cfg为后缀的 所有配置文件 , 例如在nginx-config-files中包含的myserver-status.cfg和myserver-gzip.cfg , 如图所示。

1716162-20211026221033187-1331268723

创建此Pod资源后 , 在Kubernetes集群中的某节点直接向Pod IP的 8080端口发起访问请求 , 即可验证由nginx-config-files资源提供的 配置信息是否生效 , 例如通过/nginx-status访问其内置的stub status。

~$ POD_IP=$(kubectl get pods configmaps-volume-demo -o go
template={{.status.podIP}}) 
~$ curl http://${POD_IP}:8080/nginx-status 
Active connections: 1 
server accepts handled requests 
 1 1 1 
Reading: 0 Writing: 1 Waiting: 0

当然 , 我们也可以直接于Pod资源configmaps-volume-demo之上的 相应容器中执行命令来确认文件是否存在于挂载点目录中:

~$ kubectl exec configmaps-volume-demo -- ls /etc/nginx/conf.d/ 
myserver-gzip.cfg 
myserver-status.cfg 
myserver.conf

我们还可以在容器中运行其他命令来进一步测试由ConfigMap对象 提供的配置信息是否已生效 , 以示例中的Nginx为例 , 我们可运行如下 的配置测试与打印命令进行配置信息的生效确认。

~$ kubectl exec  configmaps-volume-demo -- nginx -T 
…… 
# configuration file /etc/nginx/conf.d/myserver.conf: 
server { 
    listen 8080; 
    server_name www.ik8s.io; 
    …… 
} 
# configuration file /etc/nginx/conf.d/myserver-gzip.cfg: 
…… 
# configuration file /etc/nginx/conf.d/myserver-status.cfg: 
……

由上面两个命令的结果可见 , nginx-config-files中的3个文件都被添加到了容器中 , 并且实现了由容器应用Nginx加载并生效。

2.3.2 挂载存储卷中的部分键值

有些应用场景中 , 用户很可能期望仅向容器中的挂载点暴露Pod资源关联的ConfigMap对象上的部署键值 , 这在通过一个ConfigMap对象为单个Pod资源中的多个容器分别提供配置时尤其常见。例如前面曾创建了一个名为demoapp-confs的ConfigMap对象 , 它包含有4个键值 , 其中的envoy.yaml和eds.conf可为envoy代理提供配置文件 , 而 demoapp.port能够为demoapp( 通过环境变量) 定义监听的端口。下面 配置清单示例定义的Pod资源中定义了两个容器 , envoy和demoapp , demoapp-confs为envoy容器提供两个配置文件 , 为demoapp容器提供了一个配置参数。

apiVersion: v1 
kind: Pod 
metadata: 
  name: configmaps-volume-demo2 
  namespace: default 
spec: 
  containers: 
  - name: proxy 
    image: envoyproxy/envoy-alpine:v1.14.1 
    volumeMounts: 
    - name: appconfs 
      mountPath: /etc/envoy 
      readOnly: true 
  - name: demo 
    image: ikubernetes/demoapp:v1.0 
    imagePullPolicy: IfNotPresent 
    env: 
    - name: PORT 
      valueFrom: 
        configMapKeyRef: 
          name: demoapp-confs 
          key: demoapp.port 
          optional: false 
  volumes: 
  - name: appconfs 
    configMap:    # 存储卷插件类型 
      name: demoapp-confs 
      items:      # 要暴露的键值数据 
      - key: envoy.yaml 
        path: envoy.yaml 
        mode: 0644 
      - key: lds.conf 
        path: lds.conf 
        mode: 0644 
      optional: false

configMap卷插件中的items字段的值是一个对象列表 , 可嵌套使用3个字段来组合指定要引用的特定键。

  • key : 要引用的键名称 , 必选字段。

  • path : 对应的键在挂载点目录中映射的文件名称 , 它 可不同于键名称 , 必选字段。

  • mode : 文件的权限模型 , 可用范围为0~0777。

上面的配置示例( configmap-volume-demo2.yaml) 中 , 把 envoy.yaml和eds.conf两个键名分别映射为/etc/envoy目录下的两个 与键同名的文件 , 且均使用0644的权限。

2.3.3 独立挂载存储卷中的单个键值

前面的两种方式中 , 无论是装载ConfigMap对象中的所有还是部分文件 , 挂载点目录下原有的文件都会被隐藏。对于期望将ConfigMap对象提供的配置文件补充在挂载点目录下的需求来说 , 这种方式显然难以如愿。以Nginx应用为例 , 基于nginx:alpine启动的容器 的/etc/nginx/conf.d目录中原本就存在一些文件( 例如default.conf 等) , 有时候我们需要把nginx-config-files这个ConfigMap对象中的全部或部分文件装载进此目录中而不影响其原有的文件。

事实上 , 此种需求可以通过在容器上的volumeMounts字段中使用 subPath字段来解决 , 该字段用于支持从存储卷挂载单个文件或单个目 录而非整个存储卷。例如 , 下面的示例就单独挂载了两个文件在/etc/nginx/conf.d目录中 , 但保留了目录下原有的文件。

apiVersion: v1 
kind: Pod 
metadata: 
  name: configmaps-volume-demo3 
  namespace: default 
spec: 
  containers: 
  - image: nginx:alpine 
    name: nginx-server 
    volumeMounts: 
    - name: ngxconfs 
      mountPath: /etc/nginx/conf.d/myserver.conf 
      subPath: myserver.conf 
      readOnly: true 
    - name: ngxconfs 
      mountPath: /etc/nginx/conf.d/myserver-gzip.cfg 
      subPath: myserver-gzip.cfg 
      readOnly: true 
  volumes: 
  - name: ngxconfs 
    configMap: 
      name: nginx-config-files

基于上述配置创建了Pod资源后 , 即可通过命令验 证/etc/nginx/conf.d目录中原有文件确实能够得以保留 , 如下面的命令及其结果所示。

~$ kubectl exec configmaps-volume-demo3 -- ls /etc/nginx/conf.d/ 
default.conf 
myserver-gzip.cfg 
myserver.conf

接下来也可将该Pod资源创建于集群上 , 验证myserver主机的配 置 , 正常情况下 , 它应该启动了页面压缩功能 , 但因未装载myserver-status.cfg配置而不支持内置的status页面 , 感兴趣的读者可自行完 成测试。

2.4 容器应用重载新配置

相较于环境变量来说 , 使用ConfigMap资源为容器应用提供配置的优势之一在于支持容器应用动态更新其配置: 用户直接更新ConfigMap 对象 , 而后由相关Pod对象的容器应用重载其配置文件即可。

细心的读者或许已经发现 , 挂载有ConfigMap存储卷的容器上 , 挂载点目录中的文件都是符号链接 , 它们指向了挂载点目录中名为..data隐藏属性的子目录 , 而..data自身也是一个符号链接 , 它指向了名字形如..2020_05_15_03_34_10.435155001这样的以挂载操作时 的时间戳命名的临时隐藏目录 , 该目录才是存储卷的真正挂载点。例 如 , 查看Pod对象configmaps-volume-demo的容器中的挂载点目录下的 文件列表 , 它将显示出类似如下结果。

~ $ kubectl exec -it configmaps-volume-demo -- ls -lA 
/etc/nginx/conf.d 
total 0 
drwxr-xr-x   2 root   root   79 Apr 14 03:34 
..2020_05_15_03_34_10.435155001 
lrwxrwxrwx   1 root   root   31 Apr 14 03:34 ..data -> ..2020_05_15_03_34_10.435155001 
lrwxrwxrwx   1 root   root   24 Apr 14 03:34 myserver-gzip.cfg -> ..data/myserver-gzip.cfg 
lrwxrwxrwx   1 root   root   26 Apr 14 03:34 myserver-status.cfg -> ..data/myserver-status.cfg 
lrwxrwxrwx   1 root   root   20 Apr 14 03:34 myserver.conf -> ..data/myserver.conf

这种两级符号链接设定的好处在于 , 当引用的ConfigMap对象中的 数据发生改变时 , 它将被重新挂载至一个以当前时间戳命名的新的临 时目录下 , 而后将..data指向这个新的挂载点便达到了同时更新存储卷上所有文件数据的目的。例如 , 使用kubectl edit cm命令直接在 ConfigMap对象nginx-config-files中的myserver-status.cfg配置段 增加“allow 127.0.0.0/8;”和“deny all;”两行 , 稍等片刻之后再次查看configmap-volume-demo中容器挂载点目录中的文件列表 , 结果 是其挂载点已经指向新的位置 , 例如下面的命令及其结果所示。

~$ kubectl edit configmaps/nginx-config-files -n default 
…… 
data: 
  …… 
  myserver-status.cfg: | 
    location /nginx-status { 
        stub_status on; 
        access_log off; 
allow 127.0.0.0/8; 
deny all; 
    } 
…… 
~ $ kubectl exec -it configmaps-volume-demo -- ls -lA 
/etc/nginx/conf.d 
total 0 
drwxr-xr-x   2 root   root   79 Apr 14 03:45 ..2020_05_15_03_45_25.239510550 
lrwxrwxrwx   1 root   root   31 Apr 14 03:45 ..data -> ..2020_05_15_03_45_25.239510550 
lrwxrwxrwx   1 root   root   24 Apr 14 03:34 myserver-gzip.cfg -> ..data/myserver-gzip.cfg 
lrwxrwxrwx   1 root   root   26 Apr 14 03:34 myserver-status.cfg -> ..data/myserver-status.cfg 
lrwxrwxrwx   1 root   root   20 Apr 14 03:34 myserver.conf -> ..data/myserver.conf

ConfigMap对象中的数据更新同步至应用容器后并不能直接触发生效新配置 , 还需要在容器上执行应用重载操作。例如Nginx可通过其 nginx -s reload命令完成配置文件重载 , 如下面的命令所示。

~$ kubectl exec configmaps-volume-demo -- nginx -s reload 
2020/05/15 03:52:50 [notice] 32#32: signal process started

新增的两行配置信息对/nginx-status这个URL施加了访问控制机制 , 它仅允许来自本地回环接口上的访问请求 , 因而此容器之外访 问/nginx-status页面的请求将会被拒绝。

对于不支持配置文件重载操作的容器应用来说 , 仅那些在 ConfigMap对象更新后创建的Pod资源中的容器会应用到新配置 , 因此手动重启旧有的容器之前会存在配置不一致的问题。即使对于支持重载操作的应用来说 , 由于新的配置信息并非同步推送进所有容器中 , 而且在各容器中进行的手动重载操作也未必能同时进行 , 因此在更新时 , 短时间内仍然会存在配置不一致的现象。还有 , 以单个文件形式独立挂载ConfigMap存储卷中的容器并未采用两级链接的方式进行文件映射 , 因此存储卷无法确保所有挂载的文件可以被同时更新至容器中。为了确保配置信息的一致性 , 目前这种类型的挂载不支持文件更新操作。

有些云原生应用支持配置更新时的自动重载功能 , 例如Envoy支持基于XDS协议订阅文件系统上的配置文件 , 并在该类配置文件更新时间 戳发生变动时自动重载配置。然而 , 采用联合挂载多层叠加且进行写 时复制的容器隔离文件系统来说 , 这种时间戳的更新未必能够触发内 核中的通知机制 , 也就难以触发应用程序的自动重载功能。总结起 来 , 在Pod资源中调用ConfigMap对象时要注意以下几个问题。

  • 以存储卷方式引用的ConfigMap对象必须先于Pod对象存在 , 除非在Pod对象中把它们统统标记为optional , 否则将会导致Pod无法正常启动 ; 同样 , 即使ConfigMap对象存在 , 但引用的键名不存在时 , 也会 导致同样的错误。

  • 以环境变量方式引用的ConfigMap对象的键不存在时会被忽略 , Pod对象可以正常启动 , 但错误引用的信息会以InvalidVariableNames 事件记录于日志中。

  • ConfigMap对象是名称空间级的资源 , 能够引用它的Pod对象必须位于同一名称空间。

  • Kubelet仅支持那些由API Server管理的Pod资源来引用 ConfigMap对象 , 因而那些由kubelet在节点上通过--manifest-url或--config选项加载配置清单创建的静态Pod , 以及由用户直接通过 kubelet的RESTful API创建的Pod对象。

ConfigMap无法替代配置文件 , 它仅在Kubernetes系统上代表对应用程序配置文件的引用 , 我们可以将它类比为在Linux主机上表示/etc 目录及内部文件的一种方法。

3 . Secret资源: 向容器注入配置信息

出于增强可移植性的需求 , 我们应该从容器镜像中解耦的不仅有配置数据 , 还有默认口令( 例如 Redis或MySQL服务的访问口令) 、用于SSL通信时的数字证书和私钥、用于认证的令牌和ssh key等 , 但这些敏感数据不宜存储于ConfigMap资源中 , 而是要使用另一种称为Secret的资源类型。将敏感数据存储在 Secret中比明文存储在ConfigMap或Pod配置清单中更加安全。借助Secret , 我们可以控制敏感数据的使用方式 , 并降低将数据暴露给未经授权用户的风险。

Secret对象存储数据的机制及使用方式都类似于ConfigMap对象 , 它们以键值方式存储数据 , 在Pod资源中通过环境变量或存储卷进行数据访问。不同的地方在于 , Secret对象仅会被分发至调用了该对象的 Pod资源所在的工作节点 , 且仅支持由节点将其临时存储于内存中。另外 , Secret对象的数据存储及打印格式为Base64编码的字符串而非明文字符 , 用户在创建Secret对象时需要事先手动完成数据的格式转换。 但在容器中以环境变量或存储卷的方式访问时 , 它们会被自动解码为明文数据。

注意 : Base64编码并非加密机制 , 其编码的数据可使用base64 --decode一类的命令进行解码。

Secret对象以非加密格式存储于etcd中 , 管理员必须精心管控对etcd服务的访问以确保敏感数据的机 密性 , 包括借助于TLS协议确保etcd集群节点间以及API Server间的加密通信和双向身份认证等。此外还 要精心组织Kubernetes API Server服务的访问认证和授权 , 因为拥有创建Pod资源的用户都可以使用 Secret资源并能够通过Pod对象中的容器访问其数据。

目前 , Secret资源主要有两种用途: 一是作为存储卷注入Pod对象上 , 供容器应用程序使用 ; 二是用于kubelet为Pod里的容器拉取镜像时向私有仓库提供认证信息。不过 , 后面使用ServiceAccount资源自建的Secret对象是一种更安全的方式。

3.1 创建Secret资源

类似于Config Map资源 , 创建Secret对象时也支持使用诸如字面量值、文件或目录等数据源 , 而根据 其存储格式及用途的不同 , Secret对象还会划分为如下3种类别。

  • generic: 基于本地文件、目录或字面量值创建的Secret , 一般用来存储密码、密钥、信息、证书 等数据。

  • docker-registry: 用于认证到Docker Registry的Secret , 以使用私有容器镜像。

  • tls: 基于指定的公钥/私钥对创建TLS Secret , 专用于TLS通信中 ; 指定公钥和私钥必须事先存在 , 公钥证书必须采用PEM编码 , 且应该与指定的私钥相匹配。

这些类别也体现在kubectl create secret generic|docker-registry|tls命令之中 , 每个类别代表 一个子命令 , 并分别有着各自专用的命令行选项。

3.1.1通用Secret

通用类型的Secret资源用于保存除用于TLS通信之外的证书和私钥 , 以及专用于认证到Docker注册表服务之外的敏感信息 , 包括访问服务的用户名和口令、SSH密钥、OAuth令牌、CephX协议的认证密钥等。

使用Secret为容器中运行的服务提供用于认证的用户名和口令是一种较为常见的应用场景 , 以MySQL 或PostgreSQL代表的开源关系型数据库系统的镜像就支持通过环境变量来设置管理员用户的默认密码。此类Secret对象可以直接使用kubectl create secret generic --from-literal=key=value 命令 , 以给定的字面量值直接进行创建 , 通常用户名要使用username为键名 , 而密码则要使用password为 键名。例如下面的命令 , 以root/iLinux分别为用户名和密码创建了一个名为mysql-root-authn的Secret 对象:

~$ kubectl create secret generic mysql-root-authn --from-literal=username=root \ 
    --from-literal=password=iLinux

由下面获取Secret对象资源规范的命令及其输出结果可以看出: 未指定类型时 , 以generic子命令创建的Secret对象是Opaque类型 , 其键值数据会以Base64编码格式保存和打印。

~$ kubectl get secrets/mysql-root-authn -o yaml 
apiVersion: v1 
data: 
  password: aUxpbnV4 
  username: cm9vdA== 
kind: Secret 
metadata: 
  name: mysql-root-authn 
  namespace: default 
  …… 
type: Opaque

但Kubernetes系统Secret对象的Base64编码数据并非加密格式 , 许多相关的工具程序可轻松完成解码 , 例如将上面命令结果中的password字段的值可交由下面所示的Base64命令进行解码。

~$ echo aUxpbnV4 | base64 -d 
iLinux

将用户名和密码用于Basic认证时 , 需要在创建命令中额外使用--type选项明确定义Secret对象的类型 , 该选项值固定为"kubernetes.io/basic-auth" , 并要求用户名和密码各自的键名必须为username和 password , 如下面Secret对象的创建和显示命令结果所示。

~$ kubectl create secret generic web-basic-authn --from-literal=username=ops \ 
    --from-literal=password=iK8S --type="kubernetes.io/basic-auth" 
~$ kubectl get secrets/mysql-ops-authn 
NAME                         TYPE            DATA   AGE 
web-basic-authn   kubernetes.io/basic-auth   2      1m

有些应用场景仅需要在Secret中保存密钥信息即可 , 用户名能够以明文的形式由客户端直接提供而无须保存于Secret对象中。例如 , 在Pod或PV资源上使用的RBD存储卷插件以CephX协议认证到Ceph存储集群 时 , 使用内嵌的user字段指定用户名 , 以secretRef字段引用保存有密钥的Secret对象 , 且创建该类型的 Secret对象需要明确指定类型为kubernetes.io/rbd , 如下面的命令所示。

$ kubectl create secret generic ceph-kube-secret --type="kubernetes.io/rbd" \ 
  --from-literal=key='AQB9+4teoywxFxAAr2d63xPmV3Yl/E2ohfgOxA=='

对于文件中的敏感数据 , 可以在命令上使用--from-file选项以直接将该文件作为数据源 , 例如创建用于SSH认证的Secret对象时就可以直接从认证的私钥文件加载认证信息 , 其键名需要使用ssh privatekey , 而类型标识为kubernetes.io/ssh-auth。下面的命令先创建出一对用于测试的认证密钥 , 而 后将其私钥创建为Secret对象。

~$ ssh-keygen -t rsa -P "" -f  ${HOME}/.ssh/id_rsa 
~$ kubectl create secret generic ssh-key-secret  
    --from-file=ssh-privatekey=${HOME}/.ssh/id_rsa \ 
    --type="kubernetes.io/ssh-auth"

Kubernetes系统上还有一种专用于保存ServiceAccount认证令牌的Secret对象 , 它存储有 Kubernetes集群的私有CA的证书( ca.crt) 以及当前Service账号的名称空间和认证令牌。该类资源以 kubernetes.io/service-account-token为类型标识 , 并附加专用资源注解kubernetes.io/service-account.name和kubernetes.io/service-account.uid来指定所属的ServiceAccount账号名称及ID信息。 kube-system名称空间中默认存在许多该类型的Secret对象 , 下面的第一个命令先获取到以node-controller开头的Secret资源( ServiceAccount/node-controller资源的专用Secret) 的名称 , 而后第二 个命令以YAML格式打印该资源的详细规范。下面命令用于打印kube-system名称空间下的Secret/node-controller资源对象的信息。

~$ secret_name=$(kubectl get secrets -n kube-system | awk '/^node-controller/{print $1}') 
~ $ kubectl get secrets $secret_name -o yaml -n kube-system 
apiVersion: v1 
data: 
  ca.crt: …… 
  namespace: a3ViZS1zeXN0ZW0= 
  token: …… 
kind: Secret 
metadata: 
  annotations: 
    kubernetes.io/service-account.name: node-controller 
    kubernetes.io/service-account.uid: 54dedc06-09db-4024-b756-e4e64ed1a1cf 
  name: node-controller-token-6wlvm 
  namespace: kube-system 
  …… 
type: kubernetes.io/service-account-token

还有一种专用于Kubernetes集群自动引导( bootstrap) 过程的Secret类型 , 最早由kubeam引入 , 类型标识为bootstrap.kubernetes.io/token , 它需要由auth-extra-groups、description、token-id和 token-secret等专用键名来指定所需的数据。由kubeadm部署的集群上 , 会在kube-system名称空间中默认生成一个以bootstrap-token为前缀的该类Secret对象。

~$ bs_name=$(kubectl get secrets -n kube-system | awk '/^bootstrap-token/{print $1}') 
~$ kubectl get secret $bs_name -o yaml -n kube-system                                               
apiVersion: v1 
data: 
  auth-extra-groups: …… 
  description: …… 
  token-id: ZG5hY3Y3 
  token-secret: YjE1MjAzcm55ODV2ZW5kdw== 
  usage-bootstrap-authentication: dHJ1ZQ== 
  usage-bootstrap-signing: dHJ1ZQ== 
kind: Secret 
metadata: 
  name: bootstrap-token-dnacv7 
  namespace: kube-system 
  …… 
type: bootstrap.kubernetes.io/token

以配置清单创建以上各种类型的通用Secret对象 , 除Opaque外 , 都需要使用type字段明确指定类型 , 并在data字段中嵌套使用符合要求的字段指定所需要数据。

3.1.2 TLS Secret

为TLS通信场景提供专用数字证书和私钥信息的Secret对象有其专用的TLS子命令 , 以及专用的选项--cert和--key。例如 , 为运行于Pod中的Nginx应用创建SSL虚拟主机之时 , 需要事先通过Secret对象向相应

容器注入服务证书和配对的私钥信息 , 以供nginx进程加载使用。出于测试的目的 , 我们先使用类似如下 命令生成私钥和自签证书。

~$ openssl rand -writerand $HOME/.rnd 
~$ (umask 077; openssl genrsa -out nginx.key 2048) 
~$ openssl req -new -x509 -key nginx.key -out nginx.crt \ 
    -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=www.ilinux.io

而后即可使用如下命令将这两个文件创建为secret对象。需要注意的是 , 无论用户提供的证书和私钥文件使用什么名称 , 它们一律会分别转换为以tls.key( 私钥) 和tls.crt( 证书) 为其键名。

~$ kubectl create secret tls nginx-ssl-secret --key=./nginx.key --cert=./nginx.crt 
secret "nginx-ssl-secret" created

该类型的Secret对象的类型标识符为kubernetes.io/tls , 例如下面命令结果所示。

~$ kubectl get secret nginx-ssl -o yaml 
apiVersion: v1 
data: 
  tls.crt: …… 
  tls.key: …… 
kind: Secret 
metadata: 
  name: nginx-ssl-secret 
  namespace: default 
  …… 
type: kubernetes.io/tls

3.1.3 Docker Registry Secret

当Pod配置清单中定义容器时指定要使用的镜像来自私有仓库时 , 需要先认证到目标Registry以下载 指定的镜像 , pod.spec.imagePullSecrets字段指定认证Registry时使用的、保存有相关认证信息的 Secret对象 , 以辅助kubelet从需要认证的私有镜像仓库获取镜像。该字段的值是一个列表对象 , 它支持指定多个不同的Secret对象以认证到不同的Resgistry , 这在多容器Pod中尤为有用。

创建这种专用于认证到镜像Registry的Secret对象有其专用的docker-registry子命令。通常 , 认证到Registry的过程需要向kubelet提供Registry服务器地址、用户名和密码 , 以及用户的E-mail信息 , 因此docker-registry子命令需要同时使用以下4个选项。

  • --docker-server: Docker Registry服务器的地址 , 默认为https://index.docker.io/v1/。

  • --docker-user: 请求Registry服务时使用的用户名。

  • --docker-password : 请求访问Registry服务的用户密码。

  • --docker-email : 请求访问Registry服务的用户E-mail。

这4个选项指定的内容分别对应使用docker login命令进行交互式认证时所使用的认证信息 , 下面的命令创建了名为local-registry的docker-registry Secret对象。

~$ kubectl create secret docker-registry local-registry --docker-username=Ops \ 
    --docker-password=Opspass --docker-email=ops@ilinux.io

该类secret对象打印的类型信息为kubernetes.io/dockerconfigjson , 如下面的命令结果所示。

~$ kubectl get secrets local-registry 
NAME             TYPE               DATA    AGE 
local-registry   kubernetes.io/dockerconfigjson   1         7s

另外 , 创建docker-registry Secret对象时依赖的认证信息也可使用--from-file选项从dockercfg配置文件( 例如~/.dockercfg) 或JSON格式的Docker配置文件( 例如~/.docker/config.json) 中加载 , 但 前者的类型标识为kubernetes.io/dockercfg , 后者的类型则与前面使用字面量值的创建方式相同。

在Pod资源上使用docker-registry Secret对象的方法有两种。一种方法是使用 spec.imagePullSecrets字段直接引用 ; 另一种是将docker-registry Secret对象添加到某特定的 ServiceAccount对象之上 , 而后配置Pod资源通过spec. serviceAccountName来引用该服务账号。第二种 方法的实现我们放在ServiceAccount资源的相关话题中进行介绍 , 这里先以下面的示例说明第一种方法的用法。

apiVersion: v1 
kind: Pod 
metadata: 
  name: secret-imagepull-demo 
  namespace: default 
spec: 
  imagePullSecrets: 
  - name: local-registry 
  containers: 
  - image: registry.ilinux.io/dev/myimage 
    name: demoapp

上面的配置清单仅是一个示例 , 付诸运行时 , 需要由读者将引用的Secret对象中的内容及清单资源的 镜像等修改为实际可用的信息。

当运行的多数容器镜像均来自私有仓库时 , 为每个Pod资源在imagePullSecrets显式定义一或多个引 用的Secret对象实在不是一个好主意 , 我们应该将docker-registry Secret对象的引用定义在一个特定的ServiceAccount之上 , 而后由各相关的Pod资源进行引用才是更好的选择。

3.1.4 Secret资源清单

Secret资源是标准的Kubernetes API资源类型之一 , 但它仅是存储于API Server上的数据定义 , 无须 区别期望状态与现实状态 , 无须使用spec和status字段。除了apiVersion、kind和metadata字段 , 它可用的其他字段如下。

  • data : key:value格式的数据 , 通常是敏感信息 , 数据格式需是以Base64格式编码的字符串 , 因此需要用户事先完成编码。另外 , 不同类型的Secret资源要求使用的嵌套字段(键名) 也不尽相同 , 甚至ServiceAccount专用类型的Secret对象还要求使用专用的注解信息。

  • stringData : 以明文格式( 非Base64编码) 定义的键值数据。无须用户事先对数据进行Base64编码 , 而是在创建为Secret对象时自动进行编码并保存于data字段中。stringData字段中的明文不会被API Server输出 , 但使用kubectl apply命令进行创建的Secret对象 , 其注解信息可能会直接输出这些信息。

  • type : 仅为了便于编程处理Secret数据而提供的类型标识。

下面是保存于配置文件secrets-demo.yaml中的Secret资源定义示例 , 它使用stringData提供了明文格式的键–值数据 , 从而免去了事先手动编码的麻烦。

apiVersion: v1 
kind: Secret 
metadata: 
  name: secrets-demo 
stringData: 
  username: redis 
  password: redisp@ss 
type: Opaque

为保存于配置清单文件中的敏感信息创建Secret对象时 , 用户需要先将敏感信息读出并转换为Base64编码格式 , 再将其创建为清单文件 , 过程烦琐 , 反而不如命令式创建来得便捷。不过 , 如果存在多次创建 或者重构之需 , 将其保存为配置清单也是情势所需。

3.2 使用Secret资源

类似于Pod资源使用ConfigMap对象的方式 , Secret对象可以注入为容器环境变量 , 也能够通过Secret 卷插件定义为存储卷并由容器挂载使用。但是 , 容器应用通常会在发生错误时将所有环境变量保存于日志信息中 , 甚至有些应用在启动时会将运行环境打印到日志中。另外 , 容器应用调用第三方程序为子进程时 , 这些子进程能够继承并使用父进程的所有环境变量。这都有可能导致敏感信息泄露 , 因而通常仅在必要的情况下才使用环境变量引用Secret对象中的数据。

3.2.1 环境变量

Pod资源以环境变量方式消费Secret对象也存在两种途径 :

  1. 一对一地将指定键的值传递给指定的环境变量 ;

  2. 将Secret对象上的全部键名和键值一次性全部映射为容器的环境变量。

前者在容器上使用 env.valueFrom字段进行定义 , 而后者则直接使用envFrom字段 , 如下面给出的详细配置格式所示。

containers: 
- name: … 
  image: … 
  env: 
  - name: <string>           # 变量名 , 其值来自某Secret对象上的指定键的值 
    valueFrom:               # 键值引用 
      secretKeyRef:        
        name: <string>       # 引用的Secret对象的名称 , 需要与该Pod位于同一名称空间 
        key: <string>        # 引用的Secret对象上的键 , 其值将传递给环境变量 
        optional: <boolean>  # 是否为可选引用 
  envFrom:                   # 整体引用指定的Secret对象的全部键名和键值 
  - prefix: <string>         # 将所有键名引用为环境变量时统一添加的前缀 
    secretRef:         
      name: <string>         # 引用的Secret对象名称 
      optional: <boolean>    # 是否为可选引用

下面Pod资源配置清单( secrets-env-demo.yaml) 示例中 , 容器mariadb运行时初始化root用户的密 码 , 引用自此前创建的Secret对象mysql-root-authn中的password键的值。

apiVersion: v1 
kind: Pod 
metadata: 
  name: secrets-env-demo 
  namespace: default 
spec: 
  containers: 
  - name: mariadb 
    image: mariadb 
    imagePullPolicy: IfNotPresent 
    env: 
    - name: MYSQL_ROOT_PASSWORD 
      valueFrom: 
        secretKeyRef: 
          name: mysql-root-authn 
          key: password

mariadb的镜像并不支持从某个文件中加载管理员root用户的初始密码 , 这里也就只能使用环境变量赋值的方式来引用Secret对象中的敏感数据。下面完成测试步骤 , 首先将清单中的Pod对象创建在集群上:

~$ kubectl apply -f secrets-env-demo.yaml  
pod/secrets-env-demo created

而后使用保存在mysql-root-authn对象中的password字段的值iLinux作为密码进行数据库访问 , 如下面命令所示。

~$ kubectl exec -it secrets-env-demo -- mysql -uroot -piLinux 
Welcome to the MariaDB monitor.  Commands end with ; or \g. 
Your MariaDB connection id is 8 
Server version: 10.4.12-MariaDB-1:10.4.12+maria~bionic mariadb.org binary distribution 
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 
MariaDB [(none)]>

命令结果表明使用MySQL客户端工具以root用户和iLinux密码认证到容器mariadb的操作成功完成 , 经由环境变量向容器传递Secret对象中保存的敏感信息得以顺利实现。

3.2.2 Secret存储卷

Pod资源上的Secret存储卷插件的使用方式同ConfigMap存储卷插件非常相似 , 除了其类型及引用标识要替换为secret及secretName之外 , 几乎完全类似于ConfigMap存储卷 , 包括支持使用挂载整个存储卷、 只挂载存储卷中指定键值以及独立挂载存储卷中的键等使用方式。

下面是定义在配置清单文件secrets-volume-demo.yaml中的Secret资源使用示例 , 它将nginx-ssl-secret对象关联为Pod对象上名为nginxcert的存储卷 , 而后由容器ngxservrer挂载至/etc/nginx/certs目录下。

apiVersion: v1 
kind: Pod 
metadata: 
  name: secrets-volume-demo 
  namespace: default 
spec: 
  containers: 
  - image: nginx:alpine 
    name: ngxserver 
    volumeMounts: 
    - name: nginxcerts 
      mountPath: /etc/nginx/certs/ 
      readOnly: true 
    - name: nginxconfs 
      mountPath: /etc/nginx/conf.d/ 
      readOnly: true 
  volumes: 
  - name: nginxcerts 
    secret: 
      secretName: nginx-ssl-secret 
  - name: nginxconfs 
    configMap: 
      name: nginx-sslvhosts-confs 
      optional: false

ConfigMap对象nginx-sslvhosts-confs中存储有证书文件tls.cert和私钥文件tls.key , 这些文件是可调用容器通过挂载nginx-ssl-secret在/etc/nginx/certs/目录下生成的 , 并根据证书与私钥文件定义 了一个SSL类型的虚拟主机。并且 , 所有发往80端口的流量都会被重定向至SSL虚拟主机。其中的关键配置 部分如下所示。

server { 
    listen 443 ssl; 
    server_name www.ik8s.io; 
 
    ssl_certificate /etc/nginx/certs/tls.crt;  
    ssl_certificate_key /etc/nginx/certs/tls.key; 
    ssl_session_timeout 5m; 
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;  
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;  
    ssl_prefer_server_ciphers on; 
 
    location / { 
        root /usr/share/nginx/html; 
    } 
} 
 
server { 
    listen 80; 
    server_name www.ilinux.io;  
    return 301 https://$host$request_uri;  
}

我们知道 , 由Pod资源引用的所有ConfigMap和Secret对象必须事先存在 , 除非它们被显式标记为 optional: true。因此 , 在创建该Pod对象之前 , 我们需要事先生成其引用的ConfigMap对象nginx-sslvhosts-confs , 相关的所有配置文件保存在nginx-ssl-conf.d/目录下 , 因而直接运行如下命令即可完成创建。

~$ kubectl create configmap nginx-sslvhosts-confs --from-file=./nginx-ssl-conf.d/

而后 , 将上面资源清单文件中定义的Pod资源创建于集群之上 , 待其正常启动后可查看容器挂载点目录中的文件 , 以确认其挂载是否成功完成 , 或直接向Pod中的Nginx服务发起访问请求进行验证。

~$ kubectl apply -f secrets-volume-demo.yaml  
pod/secrets-volume-demo created

而后 , 使用openssl s_cleint命令向该Pod对象的IP地址发起TLS访问请求 , 确认其证书是否为前面自签生成的测试证书。

~$ podIP=$(kubectl get pods secrets-volume-demo -o jsonpath={.status.podIP}) 
~$ openssl s_client -connect $podIP:443 -state

不过 , 这里的测试请求使用了IP地址而非证书中的主体名称www.ilinux.io , 因而证书的验证会失 败 , 但我们只需关注证书内容即可 , 尤其是证书链中显示的信息。若能成功证明响应中的证书来自nginx-ssl-secret对象中保存的自签证书 , 也就意味着通过存储卷方式向容器提供敏感信息的操作成功了。

4. 应用Downward API存储卷配置信息

除了通过ConfigMap和Secret对象向容器注入配置信息之外 , 应用程序有时候还需要基于所运行的外在系统环境信息设定自身的运行特 性。例如nginx进程可根据节点的CPU核心数量自动设定要启动的 worker进程数 , JVM虚拟机可根据节点内存资源自动设定其堆内存大小 等。这种功能有点类似于编程中的反射机制 , 它旨在让对象加载与自身相关的重要环境信息并据此做出运行决策。

Kubernetes的Downward API支持通过环境变量与文件 (downwardAPI卷插件) 将Pod及节点环境相关的部分元数据和状态数据注入容器中 , 它们的使用方式同ConfigMaps和Secrets类似 , 用于完成将外部信息传递给Pod中容器的应用程序。然而 , Downward API并不 会将所有可用的元数据统统注入容器中 , 而是由用户在配置Pod对象自行选择需要注入容器中的元数据。可选择注入的信息包括Pod对象的 IP、主机名、标签、注解、UID、请求的CPU与内存资源量及其限额 , 甚至是Pod所在的节点名称和节点IP等。Downward API的数据注入方式如图所示。

image-20220608104601165

但是与ConfigMap和Secret这两个标准的API资源类型不同的是 , Downward API自身便是一种附属于API Server之上API , 在Pod资源的定义中可直接进行引用而无须事先进行任何资源定义。

4.1 环境变量式元数据注入

类似于ConfigMap或Secret资源 , 容器能够在环境变量valueFrom 字段中嵌套fieldRef或resourceFieldRef字段来引用其所属Pod对象的元数据信息。不过 , 通常只有常量类型的属性才能够通过环境变量注入容器中 , 毕竟进程启动完成后无法再向其告知变量值的变动 , 于是环境变量也就不支持中途的更新操作。在容器规范中 , 可在环境变量中配置valueFrom字段内嵌fieldRef字段引用的信息包括如下这些。

  • metadata.name : Pod对象的名称。

  • metadata.namespace : Pod对象隶属的名称空间。

  • metadata.uid : Pod对象的UID。

  • metadata.labels[''] : Pod对象标签中的指定键的值 , 例如metadata.labels['mylabel'] , 仅Kubernetes 1.9及之后的版本才 支持。

  • metadata.annotations['']: Pod对象注解信息中的指定键 的值 , 仅Kubernetes 1.9及之后的版本才支持。

容器上的计算资源需求和资源限制相关的信息 , 以及临时存储资 源需求和资源限制相关的信息可通过容器规范中的resourceFieldRef 字段引用 , 相关字段包括requests.cpu、limits.cpu、 requests.memory和limits.memory等。另外 , 可通过环境变量引用的信息有如下几个。

  • status.podIP :Pod对象的IP地址。

  • spec.serviceAccountName : Pod对象使用的ServiceAccount资源名称。

  • spec.nodeName : 节点名称。

  • status.hostIP : 节点IP地址。

另外 , 还可以通过resourceFieldRef字段引用当前容器的资源请 求及资源限额的定义 , 因此它们包括requests.cpu、 requests.memory、requests.ephemeral-storage、limits.cpu、 limits.memory和limits.ephemeral-storage这6项。

下面的资源配置清单示例( downwardAPI-env.yaml) 中定义的Pod 对象通过环境变量向容器demoapp中注入了Pod对象的名称、隶属的名称空间、标签app的值以及容器自身的CPU资源限额和内存资源请求等信息。

apiVersion: v1 
kind: Pod 
metadata: 
  name: downwardapi-env-demo 
  labels: 
    app: demoapp 
spec: 
  containers: 
    - name: demoapp 
      image: ikubernetes/demoapp:v1.0 
      command: [ "/bin/sh", "-c", "env" ] 
      resources: 
        requests: 
          memory: "32Mi" 
          cpu: "250m" 
        limits: 
          memory: "64Mi" 
          cpu: "500m" 
      env: 
        - name: THIS_POD_NAME 
          valueFrom: 
            fieldRef: 
              fieldPath: metadata.name 
        - name: THIS_POD_NAMESPACE 
          valueFrom: 
            fieldRef: 
              fieldPath: metadata.namespace 
        - name: THIS_APP_LABEL 
          valueFrom: 
            fieldRef: 
              fieldPath: metadata.labels['app'] 
        - name: THIS_CPU_LIMIT 
          valueFrom: 
            resourceFieldRef: 
              resource: limits.cpu 
        - name: THIS_MEM_REQUEST 
          valueFrom: 
            resourceFieldRef: 
              resource: requests.memory 
              divisor: 1Mi 
  restartPolicy: Never

该Pod对象创建并启动后向控制台打印所有的环境变量即终止运行 , 它仅用于测试通过环境变量注入信息到容器的使用效果。我们先根据下面的命令创建出配置清单中定义的Pod资源Pod/downwardapi-env-demo。

~$ kubectl apply -f downwardapi-env-demo.yaml 
pod/downwardapi-env-demo created

等该Pod对象的状态转为Completed之后即可通过控制台日志获取 注入的环境变量 , 如下面的命令及结果所示

~$ kubectl logs downwardapi-env-demo | grep "^THIS_" 
THIS_CPU_LIMIT=1 
THIS_APP_LABEL=demoapp 
THIS_MEM_REQUEST=32 
THIS_POD_NAME=downwardapi-env-demo 
THIS_POD_NAMESPACE=default

示例最后一个环境变量的定义中还额外指定了一个divisor字段 , 它用于为引用的值指定一个除数 , 以对引用的数据进行单位换算。CPU 资源的divisor字段默认值为1 , 它表示为1个核心 , 相除的结果不足1 个单位时则向上圆整( 例如0.25向上圆整的结果为1) , 它的另一个可 用单位为1m , 即表示1个微核心。内存资源的divisor字段默认值也是 1 , 不过它意指1个字节 , 此时32MiB的内存资源则要换算为33554432予以输出。其他可用的单位还有1KiB、1MiB、1GiB等 , 于是在将divisor 字段的值设置为1MiB时 , 32MiB的内存资源换算的结果即为32。

注意 未给容器定义资源请求及资源限额时 , 通过downwardAPI引用的值则默认为节点的可分配CPU及内存资源量。

4.2 存储卷式元数据注入

downwardAPI存储卷能够以文件方式向容器中注入元数据 , 将配置 的字段数据映射为文件并可通过容器中的挂载点访问。事实上 , 4.1 节中通过环境变量方式注入的元数据信息也都可以使用存储卷方式进 行信息暴露 , 但除此之外 , 我们还能够在downwardAPI存储卷中使用 fieldRef引用下面两个数据源。

  • metadata.labels: Pod对象的所有标签信息 , 每行一个 , 格式为 label-key="escaped-label-value"。

  • metadata.annotations: Pod对象的所有注解信息 , 每行一个 , 格式为annotation-key="escaped-annotation-value"。

下面的资源配置清单示例( downwardapi-volumes-demo.yaml) 中定义的Pod资源通过downwardAPI存储卷向容器demoapp中注入了Pod对象隶属的名称空间、标签、注解以及容器自身的CPU资源限额和内存资 源请求等信息。存储卷在容器中的挂载点为/etc/podinfo目录 , 因而 注入的每一项信息均会映射为此路径下的一个文件。

kind: Pod 
apiVersion: v1 
name: downwardapi-volume-demo 
metadata: 
  labels: 
    zone: zone1 
    rack: rack100 
    app: demoapp 
  annotations: 
    region: ease-cn 
spec: 
  containers: 
    - name: demoapp 
      image: ikubernetes/demoapp:v1.0 
      resources: 
        requests: 
          memory: "32Mi" 
          cpu: "250m" 
        limits: 
          memory: "64Mi" 
          cpu: "500m" 
      volumeMounts: 
      - name: podinfo 
        mountPath: /etc/podinfo 
        readOnly: false 
  volumes: 
  - name: podinfo 
    downwardAPI: 
      defaultMode: 420 
      items: 
      - fieldRef: 
          fieldPath: metadata.namespace 
        path: pod_namespace 
      - fieldRef: 
          fieldPath: metadata.labels 
        path: pod_labels 
      - fieldRef: 
          fieldPath: metadata.annotations 
        path: pod_annotations 
      - resourceFieldRef: 
          containerName: demoapp 
          resource: limits.cpu 
        path: "cpu_limit" 
      - resourceFieldRef: 
          containerName: demoapp 
          resource: requests.memory 
          divisor: "1Mi" 
        path: "mem_request"

创建资源配置清单中定义的Pod对象后即可测试访问由 downwardAPI存储卷映射的文件pod_namespace、pod_labels、 pod_annotations、limits_cpu和mem_request等。

~$ kubectl apply -f downwardapi-volume-demo.yaml  
pod/downwardapi-volume-demo created

待Pod对象正常运行后即可测试访问上述的映射文件 , 例如访问/etc/podinfo/pod_labels文件以查看Pod对象的标签列表:

~$ kubectl exec downwardapi-volume-demo -- cat /etc/podinfo/pod_labels              
app="demoapp" 
rack="rack100" 
zone="zone1"

如命令结果所示 , Pod对象的标签信息每行一个地映射于自定义的 路径/etc/podinfo/pod_labels文件中 , 类似地 , 注解信息也以这种方式进行处理。如前面的章节所述 , 标签和注解支持运行时修改 , 其改 动的结果也会实时映射进downwardAPI生成的文件中。例如 , 为 downwardapi-volume-demo对象添加新的标签:

~$ kubectl label pods/downwardapi-volume-demo release="Canary" 
pod/downwardapi-volume-demo labeled

而后再次查看容器内的pod_labels文件的内容 , 由如下的命令结 果可知新的标签已经能够通过相关的文件获取到。

~$ kubectl exec downwardapi-volume-demo -- cat /etc/podinfo/pod_labels 
app="demoapp" 
rack="rack100" 
release="Canary" 
zone="zone1"

downwardAPI存储卷为Kubernetes上运行容器化应用提供了获取外 部环境信息的有效途径 , 这对那些非云原生应用在不进行代码重构的 前提下获取环境信息 , 以进行自身配置等操作时尤为有用。