配置容器应用

官方文档:https://kubernetes.io/zh/docs/concepts/configuration/ ConfigMap和Secret是K8S系统上两种特殊类型的存储卷,ConfigMap对象用于为容器中的应用提供配置数据以制定程序的行为,不过敏感的配置信息,例如密钥,证书等通常由secret对象来进行配置。它们将相应的配置信息保存于对象中,而后在Pod资源上以存储卷的形式将其挂载并获取相关的配置,以事先配置与镜像文件的解耦

容器化应用配置方式

每个应用程序都是一个可执行程序文件,它包含操作码列表,CPU通过执行这些操作码来完成特定的操作。例如,cat明天是由/usr/bin/cat文件提供的,该文件含有机器指令的列表,在屏幕上显示指定文件的内容时需要使用这些及其指令。几乎每个程序的行为都可以通过其命令选项及参数或配置文件来按需定制。实践中,人们通常不会以默认的配置参数运行应用程序,而是需要根据特定的环境或具体的需求定制其运行特性,对于复杂的服务类应用程序更是如此,如nginx,tomcat和HBase等,而且通过配置文件定义其配置通常是首选甚至是唯一的途径。 那么,如何为容器中的应用提供配置信息呢?例如,为nginx配置一个特定server或指定worker进程的数量,为tomcat的JVM配置其堆内存大小。传统实践中,通常有这么几种途径:启动容器时直接向命令传递参数、将定义好的配置文件硬编码于(嵌入)镜像文件中、通过环境变量(Environment Variables)传递配置数据,以及基于Docker卷传送配置文件等

通过命令行参数进行配置

  • Docker容器可用来运行单个应用程序。在制作docker镜像时,Dockerfile中的ENTRYPOINT和CMD指令可用于指定容器启动时要运行的程序及其相关的参数。CMD指令以列表的形式指定要运行的程序及其相关的参数,但若同时存在ENTRYPOINT,则CMD指令中列表的所有元素均被视作是ENTRYPOINT指定的程序的命令行参数。换句话说就是,如果Dockerfile中同时存在ENTRYPOINT指令和CMD指令,那么CMD指令所定义的命令将会被当作参数传递给ENTRYPOINT指令。另外,在基于某镜像使用Docker命令创建容器时,可以在命令行向ENTRYPOINT中的程序传递额外的自定义参数,甚至还可以修改要运行的应用程序本身。例如,使用docker run命令创建并启动容器的格式为
  1. doker run [OPTIONS] IMAGE [COMMAND] [ARGS...]

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

在K8S系统上创建Pod资源时,也能够向容器化应用传递命令行参数,甚至指定运行其他应用程序,相关的字段分别为.spec.containers.command.spec.containers.args

将配置文件嵌入镜像文件

  • 所谓的将配置文件嵌入镜像文件,是指用户在Dockerfile中使用COPY指令把定义好的配置文件复制到镜像的文件系统上的目标位置,或者使用RUN指令调用sed或echo一类的命令修改配置文件从而达到容器化应用提供自定义配置文件的目的。使用时,若Docker Hub上的某镜像文件额外添加配置文件既能符合需要,则克隆其Dockerfile文件修改至符合需求之后再将其推送至GitHub,并由docker Hub自动构建出镜像文件即可
    这种方式的优势在于对于用户来说,简单易用,不用额外的设定就能启动符合需求的容器,用于K8S环境亦无须多余的配置。但配置文件相关的任何额外的修改需求都不得不通过重新构建镜像文件来实现,路径长,效率低。

通过环境变量向容器注入配置信息 通过环境变量为容器提供配置信息是docker hub上最常见的使用方式。例如,使用Mysql官方提供的镜像文件启动Mysql容器时使用的MYSQL_ROOT_PASSWORD环境变量,它用于为Mysql服务器的root用户设置登陆密码 在基于此类镜像启动容器时,用户为docker run命令通过-e选项向变量传值即可实现应用配置,命令的格式为docker run -e SETTING1=foo -e SETTING2=bar ... <image name>。启动时,容器的ENTRYPOINT启动脚本会抓取到这些环境变量,并在启动容器应用之前,通过sed或echo等一类的命令将变量值替换到配置文件中 一般来说,容器的ENTRYPOINT脚本应该为这些环境变量提供默认值,以便在用户未为环境变量传值时也能基于此类需要环境变量的镜像启动容器。使用环境变量这种配置方式的优势在于配置信息的动态化供给,不过,有些应用程序的配置可能会复杂到无法通过key/value格式的环境变量完成 另外,也可以让容器的ENTRYPOINT启动脚本通过网络中的K/V存储获取配置参数,常用的此类存储系统有consul或etcd等。这种方式较之简单的环境变量能够提供更复杂的配置信息,因为K/V存储系统支持多层级的嵌套数据结构,有一些应用广泛的数据抓取工具能够从K/V存储中加载相关的数据并替换于配置文件中,甚至于像confd这类的工具还能在K/V中的数据变化时自动将其重载至配置文件中,这一点实现了真正意义上的配置动态化。不过,这种方式为容器化应用引入了额外的依赖条件 K8S系统支持在为Pod资源配置容器时使用.spec.containers.env为容器的环境变量传值从而完成应用的配置,但是这种方式无法完成复杂的应用配置,因此提供的配置能力也很有限。

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

借助Docker config进行容器配置 Docker swarm service从1.13版本起支持使用secret于容器之外保存二进制数据,包括口令,ssh密钥,ssl证书以及其他不建议通过网络传输或不应该在dockerfile及程序源码中非加密保存的机密数据。用户可以使用secret集中化管理这类数据并将其安全关联至那些需要访问这些数据的容器中 另外,Docker从17.06版本起swarm service引入了允许用户于容器之外存储非敏感信息(如配置文件)的组件”service config”,从而支持用户创建通用目的镜像文件,并且不再需要通过挂载存储卷或使用环境变量为容器提供配置文件 docker swarm service secret和config为容器化应用的配置提供了极大的灵活性,不过,它们也只能适用于docker swarm service环境中,而不能应用于单独允许的容器之上 K8S系统也有类似的组件,它们被称为secret和ConfigMap,而且是K8S系统上一等类别的资源对象,它们要么被Pod资源以存储卷的形式加载,要么由容器通过envFrom字段以变量的形式加载

通过命令行参数配置容器应用

创建Pod资源时,可以在容器定义中自定义要运行的命令以及为其传递的选项和参数。在容器的配置上下文中,使用.spec.containers.command字段指定要运行的程序,而.spec.containers.args字段则用于指定传递给程序的选项及参数。在配置文件中定义的commandargs会覆盖镜像文件中相关的默认设定,这类程序会被直接运行,而不会由shell解释器解释运行,因此与shell相关的特性均不被支持,如命令行展开符合{}、重定向等操作 下面是定义在command-demo.yaml文件中的Pod资源示例,它在容器command-demo-container中将busybox镜像文件中默认运行的命令["/bin/sh","-c"]修改为["httpd"],并为其额外传递了["-f"]选项

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: command-demo
  5. spec:
  6. replicas: 1
  7. selector:
  8. matchLabels:
  9. app: command
  10. template:
  11. metadata:
  12. labels:
  13. app: command
  14. spec:
  15. containers:
  16. - name: command-demo-container
  17. image: docker.io/busybox:1.27
  18. ports:
  19. - name: http
  20. containerPort: 80
  21. command: ["httpd"]
  22. args: ["-f"]

事实上,用户也可以只在容器配置的上下文中提供.spec.containers.args字段,以事先向默认运行的程序提供额外的参数。如果默认的命令为shell解释器或entrypoint启动脚本,那么这些参数本身甚至还可以是要运行的命令及其参数。例如。下面的容器配置,表示要运行的程序为”/bin/sh -c httpd -f”,实现了以shell解释器解释运行指定的程序之目的

  1. spec:
  2. containers:
  3. - name: command-demo-container
  4. image: docker.io/busybox:1.27
  5. ports:
  6. - name: httpd
  7. containerPort: 80
  8. args: ["httpd","-f"]

由上述的应用示例可见,K8S配置文件中的.spec.containers.command字段对应于Dockerfile中的ENTRYPOINT,而配置文件中的.spec.containers.args字段则对应于Dockerfile中的CMD。在K8S中只给出.spec.containers.command字段时,它会覆盖Dockerfile中的ENTRYPOINT和CMD,只给出.spec.containers.args字段时,它仅覆盖CMD,而同时给出.spec.containers.command.spec.containers.args时,会对应覆盖ENTRYPOINT和CMD 通过命令行参数的方式向容器应用传递配置数据的操作比较简单,但是功能有限,难以为应用生成复杂配置,尤其是那些不支持通过命令行参数进行的配置将无法基于这种机制来实现。另外需要注意的是,在容器创建完成后,修改command和ars并不会直接生效,除非重建Pod对象

利用环境变量配置容器应用

容器运行时配置Docker中容器应用程序的第二种方式是在容器启动时向其传递环境变量。docker原生的应用程序应该使用很小的配置文件,并且每一项参数都可以由环境变量或命令行选项覆盖,从而能够在运行时完成任意的按需配置。然而,目前只有极少一部分应用程序为容器环境原生设计,毕竟为容器原生重构应用程序工程浩大,且旷日持久。好在有通过容器启动脚本为应用程序预设运行环境的方法可用,通行的做法是在制作docker镜像时,为ENTRYPOINT指令定义一个脚本,它能够在启动容器时将环境变量替换至应用程序的配置文件中,而后再由此启动相应的应用程序。基于这类镜像运行容器时,即可通过向环境变量传值的方式来配置应用程序。 再K8S中使用此类镜像启动容器时,也可以在Pod资源或Pod模板资源的定义中,为容器配置段使用env参数来定义所使用的环境变量列表。事实上,即便容器中的应用本身不处理环境变量,也一样可用向容器传递环境变量,只不过它不会被使用罢了 环境变量配置容器化应用时,需要在容器配置段中嵌套使用env字段,它的值是一个由环境变量构建的列表。环境变量通常由name和value(或valueFrom)字段构成

  • .spec.containers.env.name <string>:环境变量的名称,必选字段
  • .spec.containers.env.value <string>:环境变量的值,通过$(VAR_NAME)引用,逃逸格式为$$(VAR_NAME),默认值为空
  • .spec.containers.env.valueFrom <Object>:环境变量值的引用源,例如,当前Pod资源的名称,名称空间,标签等,不能与非空值的value字段同时使用,即环境变量的值要么源于value字段,要么源于valueFrom字段,二者不可同时提供数据
    valueFrom字段可引用的值有多种来源,包括当前Pod资源的属性值,容器相关的系统资源配置、ConfigMap对象中的Key以及Secret对象中的Key,它们应该分别使用不同的嵌套字段进行定义
  • .spec.containers.env.valueFrom.fieldRef <Object>:当前Pod资源的指定字段,目前支持使用的字段包括metadata.name、metadata.namespace、metadata.labels、metadata.annotations、spec.nodeName、spec.serviceAccountName、status.hostIP和status.podIP
  • .spec.containers.env.valueFrom.configMapKeyRef <Object>:ConfigMap对象中的特定key
  • .spec.containers.env.valueFrom.resourceFieldRef <Obejct>:当前容器的特定系统资源的最小值(配额)或最大值(限额),目前支持的引用包括limits.cpu、limits.memory、limits.ephemeral-storage、requests.cpu、requests.memory、和requests.ephemeral-storage
  • .spec.containers.env.valueFrom.secretKeyRef <Object>:Secret对象中的特定key

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

  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: env-demo
  5. spec:
  6. replicas: 1
  7. selector:
  8. matchLabels:
  9. app: env-demo
  10. template:
  11. metadata:
  12. labels:
  13. app: env-demo
  14. spec:
  15. containers:
  16. - name: env-demo
  17. image: docker.io/busybox:1.27
  18. ports:
  19. - name: httpd
  20. containerPort: 80
  21. command: ["httpd"]
  22. args: ["-f"]
  23. env:
  24. - name: HELLO_WORLD
  25. value: this is env-demo container
  26. - name: MY_POD_NAME
  27. valueFrom:
  28. fieldRef:
  29. fieldPath: metadata.name
  30. - name: MY_NODE_NAME
  31. valueFrom:
  32. fieldRef:
  33. fieldPath: spec.nodeName
  34. - name: MY_NODE_IP
  35. valueFrom:
  36. fieldRef:
  37. fieldPath: status.hostIP
  38. - name: MY_POD_NAMESPACE
  39. valueFrom:
  40. fieldRef:
  41. fieldPath: metadata.namespace
  42. - name: MY_POD_IP
  43. valueFrom:
  44. fieldRef:
  45. fieldPath: status.podIP

资源清单完成之后即可创建出Pod对象,而后打印它的环境变量列表,命令及其结果如下

  1. [root@k8s-master01 pv]# kubectl apply -f env-demo.yaml
  2. deployment.apps/env-demo created
  3. [root@k8s-master01 pv]#
  4. [root@k8s-master01 pv]# kubectl get pods -l "app=env-demo"
  5. NAME READY STATUS RESTARTS AGE
  6. env-demo-65f8955c68-qdp7b 1/1 Running 0 43s
  7. [root@k8s-master01 pv]#
  8. [root@k8s-master01 pv]# kubectl exec -ti env-demo-65f8955c68-qdp7b printenv |grep "^MY"
  9. MY_POD_NAME=env-demo-65f8955c68-qdp7b
  10. MY_NODE_NAME=172.18.15.113
  11. MY_NODE_IP=172.18.15.113
  12. MY_POD_NAMESPACE=default
  13. MY_POD_IP=10.244.59.7
  14. [root@k8s-master01 pv]#
  15. [root@k8s-master01 pv]# kubectl exec -ti env-demo-65f8955c68-qdp7b printenv |grep "HELLO_WORLD"
  16. HELLO_WORLD=this is env-demo container
  17. [root@k8s-master01 pv]#

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