1.配置容器化应用程序
回顾如何传递配置数据给运行在Kubernetes中的应用程序之前,首先来看一下容器化应用通常是如何被配置的。
开发一款新应用程序的初期,除了将配置嵌入应用本身,通常会以命令行参数的形式配置应用。随着配置选项数量的逐渐增多,将配置文件化。
另一种通用的传递配置选项给容器化应用程序的方法是借助环境变量。应用程序主动查找某一特定环境变量的值,而非读取配置文件或者解析命令行参数。例如,MySQL官方镜像内部通过环境变量MYSQL_ROOT_PASSWORD设置超级用户root 的密码。
为何环境变量的方案会在容器环境下如此常见?通常直接在Docker容器中采用配置文件的方式是有些许困难的,往往需要将配置文件打入容器镜像,抑或是挂载包含该文件的卷。显然,前者类似于在应用程序源代码中硬编码配置,每次修改完配置之后需要重新构建镜像。除此之外,任何拥有镜像访问权限的人可以看到配置文件中包含的敏感信息,如证书和密钥。相比之下,挂载卷的方式更好,然而在容器启动之前需确保配置文件己写入响应的卷中。
如果采用gitRepo卷作为配置源。这并不是一个坏主意,通过它可以保持配置的版本化,并且能比较容易地按需回滚配置。然而有一种更加简便的方法能将配置数据置于Kubernetes的顶级资源对象中, 并可与其他资源定义存入同一Git仓库或者基于文件的存储系统中。用以存储配置数据的Kubernetes资源称为ConfigMap。
无论是否在使用ConfigMap存储配置数据,以下方法均可被用作配置你的应用程序:
- 向容器传递命令行参数
- 为每个容器设置自定义环境变量
- 通过特殊类型的卷将配置文件挂载到容器中
2.向容器传递命令行参数
2.1 在Docker中定义命令与参数
首先需要阐明的是,容器中运行的完整指令由两部分组成:命令与参数。
了解ENTRYPOINT与CMD
Dockerfile中的两种指令分别定义命令与参数这两个部分:
- ENTRYPOINT定义容器启动时被调用的可执行程序。
- CMD指定传递给ENTRYPOINT的参数。
尽管可以直接使用CMD指令指定镜像运行时想要执行的命令,正确的做法依旧是借助ENTRYPOINT指令,仅仅用CMD指定所需的默认参数。这样,镜像可以直接运行,无须添加任何参数:
或者是添加一些参数,覆盖Dockerile中任何由CMD指定的默认数值:
了解shell与exec形式的区别
上述两条指令均支持以下两种形式:
- shell形式---如ENTRYPOINT node app.js。
- exec形式---如ENTRYPOINT ["node","app.js"]。
两者的区别在于指定的命令是否是在shell中被调用。
对于测试的kubia镜像,如果使用exec形式的ENTRYPOINT指令:
ENTRYPOINT ["node", "app.js"]
可以从容器中的运行进程列表看出:这里是直接运行node进程,而并非在shell中执行。
如果采用shell形式(ENTRYPOINT node app.js),容器进程如下所示:
可以看出,主进程(PID 1)是shell进程而非node进程,node进程(PID 7)于shell中启动。shell进程往往是多余的,因此通常可以直接采用exec形式的ENTRYPOINT指令。
可配置化fortune镜像中的间隔参数
通过修改fortune脚本与镜像Dockefile使循环的延迟间隔可配置。如下面这段代码所示,在fortune脚本中添加VARIABLE变量并用第一个命令行参数对其初始化。
现在修改Dockerfile,采用exec形式 的ENTRYPOINT指令,以及利用CMD设置间隔的默认值为10,如下面的代码清单所示。
现在可以重新构建镜像并推送至Docker Hub。这里将镜像的tag由latest修改为args:
可以用Docker在本地启动该镜像并进行测试:
也可以传递一个间隔参数覆盖默认睡眠间隔值:
现在可以确保镜像能够正确应用传递给它的参数。现在看一下在pod中如何使用它。
2.2 在kubernetes中覆盖命令和参数
在Kubernetes中定义容器时,镜像的ENTRYPOINT和CMD均可以被覆盖,仅需在容器定义中设置属性command和args的值,如下面的代码清单所示。
绝大多数情况下,只需要设置自定义参数。命令一般很少被覆盖,除非针对一些未定义ENTRYPOINT的通用镜像,例如busybox。
注意: command和args字段在pod创建后无法被修改。
表7.1在Docker与Kubernetes中指定可执行程序及其参数 | ||
Docker | Kubernetes | 描述 |
ENTRYPOINT | command | 容器中运行的可执行文件 |
CMD | args | 传给可执行文件的参数 |
用自定义间隔值运行fortune pod
为了能够用自定义的延迟间隔值运行fortune pod,首先复制文件fortune-pod.yaml并重命名为fortune-pod-args.yaml,然后修改它,如下面的代码清单所示。
现在已经在容器定义中添加了args数组参数,可以尝试创建该pod。数组值会在pod运行时作为命令行参数传递给容器。
少量参数值的设置可以使用上述的数组表示。多参数值情况下可以采用如下标记:
提示:符串值无须用引号标记数值需要。
下面介绍环境变量完成配置
3.为容器设置环境变量
容器化应用通常会使用环境变量作为配置源。Kubernetes为pod中的每一个容器都指定自定义的环境变量集合。尽管从pod层面定义环境变量同样有效,然而当前并未提供该选项。
注意:与容器的命令和参数设置相同,环境变量列表无法在pod创建后被修改。
通过环境变量配置化fortune镜像中的间隔值
再来看一下如何通过环境变量使fortuneloop.sh脚本中的睡眠间隔值可配置化,具体如下面的代码清单所示。
3.1 在容器定义中指定环境变量
构建完成新镜像后并推送至Docker Hub之后,可以通过一个新的pod来运行。如下面代码,在容器定义中写入环境变量传递脚本。
注意:不要忘记在每个容器中,Kubernetes会自动暴露相同命令空间下每个service对应的环境变量。这些环境变量基本上可以被看成自动注入配置。
3.2 在环境变量值中引用其他环境变量
在示例中,环境变量的值是固定的。可以采用$(VAR)语法在环境变量值中引用其他的环境变量。假设定义了两个环境变量,第二个变量定义中可包含第一个环境变量的值,如下面的代码清单所示。
3.3 了解硬编码环境变量的不足之处
pod定义硬编码意味着需要有效区分生产环境与开发过程中的pod定义。为了能在多个环境下复用pod的定义,需要将配置从pod定义描述中解耦出来。幸运的是,可以通过一种叫作ConfigMap的资源对象完成解耦,用valueFrom字段替代value字段使ConfigMap成为环境变量值的来源。
4.利用ConfigMap解耦配置
应用配置的关键在于能够在多个环境中区分配置选项,将配置从应用程序源码中分离,可频繁变更配置值。如果将pod定义描述看作是应用程序源代码,显然需要将配置移出pod定义。微服务架构下正是如此,该架构定义了如何将多个个体组件组合成功能系统。
4.1 ConfigMap介绍
Kubernetes允许将配置选项分离到单独的资源对象ConfigMap中,本质上就是一个键/值对映射,值可以是短字面量,也可以是完整的配置文件。
应用无须直接读取ConfigMap,甚至根本不需要知道其是否存在。映射的内容通过环境变量或者卷文件(如图7.2所示)的形式传递给容器,而并非直接传递给容器。命令行参数的定义中可以通过$(ENV_VAR)语法引用环境变量,因而可以达到将ConfigMap的条目当作命令行参数传递给进程的效果。
当然,应用程序同样可以通过Kubernetes Rest API按需直接读取ConfigMap的内容。不过除非是需求如此,应尽可能使你的应用保持对Kubernetes的无感知。
不管应用具体是如何使用ConfigMap的,将配置存放在独立的资源对象中有助于在不同环境(开发、测试、质量保障和生产等)下拥有多份同名配置清单。pod是通过名称引用ConfigMap的,因此可以在多环境下使用相同的pod定义描述,同时保持不同的配置值以适应不同环境(如图7.3所示)。
4.2 创建ConfigMap
了解一下如何在pod中使用ConfigMap。首先从最简单的例子开始,先创建一个仅包含单一键的映射,并用它填充之前示例中的环境变量INTERVAL。这里将使用指令kubectl create configmap创建ConfigMap,而非通用指令kubectl create -f。
使用指令kubectl创建ConfigMap
利用kubectl创建ConfigMap的映射条目时可以指定字面量或者存储在磁盘上的文件。先创建一个简单的字面量条目:
通过这条命令创建了一个叫作fortune-config的ConfigMap,仅包含单映射条目sleep-interval=25。
ConfigMap—般包含多个映射条目。通过添加多个--from-literal参数可创建包含多条目的ConfigMap。
观察一下通过kubectl创建的ConfigMap的YAML格式的定义描述:
编写这个YAML文件很容易,除了metadata中的名称无须指定其他字段,然后通过Kubernetes create 或Kubernetes apply来创建。
从文件夹创建 ConfigMap
除单独引入每个文件外,甚至可以引入某一文件夹中的所有文件:
这种情况下,kubectl会为文件夹中的每个文件单独创建条目,仅限于那些文件名可作为合法ConfigMap键名的文件。
合并不同选项
创建ConfigMap时可以混合使用这里提到的所有选项
这里的ConfigMap创建自多种选项:完整文件夹、单独文件、自定义键名的条目下的文件(替代文件名作键名)以及字面量。图7.5显示了所有源选项以及最终的ConfigMap。
4.3 给容器传递ConfigMap条目作为环境变量
如何将映射中的值传递给pod的容器?有三种方法。首先尝试最为简单的一种-设置环境变量,将会使用到valueFrom字段。pod的定义描述如下代码:
这里定义了一个环境变量INTERVAL,并将其值设置为fortune-config ConfigMap中键名为sleep-interval对应的值。运行在html-generator容器中的进程读取到环境变量INTERVAL的值为25 (如图7.6所示)。
在pod中引用不存在的ConfigMap
你可能会好奇如果创建pod时引用的ConfigMap不存在会发生什么?Kubernetes会正常调度pod并尝试运行所有的容器。然而引用不存在的ConfigMap的容器会启动失败,其余容器能正常启动。如果之后创建了这个缺失的ConfigMap,失败容器会自动启动,无须重新创建pod。
注意:可以标记对ConfigMap的引用是可选的(设置configmapkeyref.optional:true)。这样,即便ConfigMap不存在,容器也能正常启动。
这个例子展示了如何将配置从pod定义中分离。这样能使所有的配置项较为集中(甚至多个pod也是如此),而不是分散在各处(或者冗余复制于多个pod定义清单)。
4.4 —次性传递ConfigMap的所有条目作为环境变量
如果ConfigMap包含不少条目,为每个条目单独设置环境变量的过程是单调乏味且容易出错的。幸运的是,1.6版本的Kubernetes提供了暴露ConfigMap的所有条目作为环境变量的手段。
假设一个ConfigMap包含FOO、BAR和FOO-BAR三个键。可以通过envFrom属性字段将所有条目暴露作为环境变量,而非使用前面例子中的env字段。示例代码如下所示。
如你所见,可以为所有的环境变量设置前缀,如本例中的CONFIG_,容器中两个环境变量的名称为:CONFIG_FOO与CONFIG_BAR。
注意:前缀设置是可选的,若不设置前缀值,环境变量的名称与ConfigMap中的键名相同。
是否注意到前面说的是两个环境变量,然而ConfigMap拥有三个条目(FOO、BAR和FOO-BAR)? 为何没有对应FOO-BAR条目的环境变量呢?
原因在于CONFIG_FOO-BAR包含破折号,这并不是一个合法的环境变量名称。Kubernetes不会主动转换键名(例如不会将破折号转换为下画线)。如果ConfigMap的某键名格式不正确,创建环境变量时会忽略对应的条目(忽略时不会发出事件通知)。
4.5 传递ConfigMap条目作为命令行参数
现在来看一下如何将ConfigMap中的值作为参数值传递给运行在容器中的主进程。在字段pod.spec.containers.args中无法直接引用ConfigMap的条目,但是可以利用ConfigMap条目初始化某个环境变量,然后再在参数字段中引用该环境变量,具体如图7.7所示。
代码7.11展示了如何在YAML文件中做到这一点。
环境变量的定义与之前相同,需通过$(env_variable_name)将环境变量的值注入参数值。
4.6 使用configMap卷将条目暴露为文件
环境变量或者命令行参数值作为配置值通常适用于变量值较短的场景。由于ConfigMap中可以包含完整的配置文件内容,当你想要将其暴露给容器时,可以借助前面章节提到过的一种称为configMap卷的特殊卷格式。
configMap卷会将ConfigMap中的每个条目均暴露成一个文件。运行在容器中的进程可通过读取文件内容获得对应的条目值。
尽管这种方法主要适用于传递较大的配置文件给容器,同样可以用于传递较短的变量值。
创建ConfigMap
这里不再修改脚本fortuneloop.sh,将尝试另一个不同的示例,使用配置文件配置运行在fortune pod的Web服务器容器中的Nginx web服务器。如果想要让Nginx服务器压缩传递给客户端的响应,Nginx的配置文件需开启压缩配置,如下面的代码清单所示。
代码7.12 开启gzip压缩的Nginx配置文件:my-nginx-config.conf
现在首先通过kubectl delete configmap fortune-config删除现有的ConfigMap fortune-config,然后用存储在本地磁盘上的Nginx配置文件创建一个新的ConfigMap。
创建一个新文件夹confimap-files并将上面的配置文件存储于configmap-files/my-nginx-config.conf中。另外在该文件夹中添加一个名为sleep-interval的文本文件, 写入值为25,使ConfigMap同样包含条目sleep-interval,如图7.8所示。
从文件夹创建ConfigMap:
下面展示了ConfigMap的YAML格式内容
注意:所有条目第一行最后的管道符号表示后续的条目值是多行字面量。
ConfigMap包含两个条目,条目的键名与文件名相同。接下来将在pod的容器中使用该ConfigMap。
在卷内使用ConfigMap的条目
创建包含ConfigMap条目内容的卷只需要创建一个引用ConfigMap名称的卷并挂载到容器中。己经学会了如何创建及挂载卷,接下来要学习的仅是如何用ConfigMap的条目初始化卷。
Nginx需读取配置文件/etc/nginx/nginx.conf,而Nginx镜像内的这个文件包含默认配置,并不想完全覆盖这个配置文件。幸运的是,默认配置文件会自动嵌入子文件夹/etc/nginx/conf.d/下的所有conf文件,因此只需要将你的配置文件置于该子文件夹中即可。图7.9展示了如何做到这一点。
pod的定义描述如代码清单7.14所示(省略无关部分)。
检查Nginx是否使用被挂载的配置文件
现在的web服务器应该己经被配置为会压缩响应,可以将localhost:8080转发到pod的80端口,利用curl检查服务器响应来验证配置是否生效,如下面的代码所示。
检查被挂载的configMap卷的内容
服务器响应说明配置成功生效。现在来看一下文件夹/etc/nginx/conf.d下的内容:
ConfigMap的两个条目均作为文件置于这一文件夹下。条目sleep-interval对应的文件也被包含在内,然而它只会被fortuneloop容器所使用。可以创建两个不同的ConfigMap,一个用以配置容器fortuneloop,另一个用来配置Webserver, 然而采用多个ConfigMap去分别配置同一pod中的不同容器的做法是不好的。毕竟同一pod中的容器是紧密联系的,需要被当作整体单元来配置。
卷内暴露指定的ConfigMap条目
幸运的是,可以创建仅包含ConfigMap中部分条目的configMap卷---本示例中的条目my-mginx-config.conf。这样容器fortuneloop不会受到影响,条目sleep-interval会作为环境变量传递给容器而不是以卷的方式。
通过卷的items属性能够指定哪些条目会被暴露作为configMap卷中的文件,如下面的代码清单所示。
指定单个条目时需同时设置条目的键名称以及对应的文件名。如果采用上面的配置文件创建pod,/etc/nginx/conf.d文件夹是比较干净的,仅包含所需的gzip.conf文件。
挂载某一文件夹会隐藏该文件夹中已存在的文件
这里有一件重要的事情需要讨论。在当前与此前的示例中,将卷挂载至某个文件夹,意味着容器镜像中/etc/nginx/conf.d文件夹下原本存在的任何文件都会被稳藏。
Linux系统挂载文件系统至非空文件夹时通常表现如此。文件夹中只会包含被挂载文件系统中的文件,即便文件夹中原本的文件是不可访问的也是同样如此。
本示例中,这种现象并不会带来比较糟糕的副作用。不过假设挂载文件夹是/etc,该文件夹通常包含不少重要文件。由于/etc下的所有文件不存在,容器极大可能会损坏。如果你希望添加文件至某个文件夹如/etc,绝不能采用这种方法。
ConfigMap独立条目作为文件被挂载且不隐藏文件夹中的其他文件
顺理成章,你会好奇如何能挂载ConfigMap对应文件至现有文件夹的同时不会隐藏现有文件。volumeMount额外的subPath字段可以被用作挂载卷中的某个独立文件或者是文件夹,无须挂载完整卷。图7.10的形象化解释可能更加容易理解。
假设拥有一个包含文件myconfig.conf的configMap卷,希望能将其添加为/etc文件夹下的文件someconfig.conf。通过属性subPath可以将该文件挂载的同时又不影响文件夹中的其他文件。pod定义中的相关部分如下面的代码清单所示。
挂载任意一种卷时均可以使用subPath属性。可以选择挂载部分卷而不是挂载完整的卷。不过这种独立文件的挂载方式会带来文件更新上的缺陷,接下来的小节中学习到更多的相关知识,在这里还是先要说一些文件权限问题对configMap卷的讨论进行收尾。
为configMap卷中的文件设置权限
configMap卷中所有文件的权限默认被设置为644(-rw-r-r--)。可以通过卷规格定义中的defaultMode属性改变默认权限,如下面的代码清单所示。
ConfigMap通常被用作存储非敏感数据,不过依旧可能希望仅限于文件拥有者的用户和组可读写,正如上面的例子所示。
4.7 更新应用配置且不重启应用程序
在此之前提到过,使用环境变量或者命令行参数作为配置源的弊端在于无法在进程运行时更新配置。将ConfigMap暴露为卷可以达到配置热更新的效果,无须重新创建pod或者重启容器。
ConfigMap被更新之后,卷中引用它的所有文件也会相应更新,进程发现文件被改变之后进行重载。Kubernetes同样支持文件更新之后手动通知容器。
警告:更新ConfigMap之后对应文件的更新耗时会出人意料地长(往往需要数分钟)。
修改ConfigMap
现在来瞧一瞧如何修改ConfigMap,同时运行在pod中的进程会重载ConfigMap卷中对应的文件。你需要修改前面示例中的Nginx配置文件,使得Nginx能够在不重启pod的前提下应用新配置。尝试用kubectl edit命令修改ConfigMap fortune-config来关闭gzip压缩:
编辑器打开,行gzip on改为gzip off,保存文件后关闭编辑器。ConfigMap被更新不久之后会自动更新卷中的对应文件。用kubectl exec命令 打印出该文件内容进行确认:
若尚未看到文件内容被更新,可稍等一会儿后重试。文件更新过程需要一段时间。最终会看到配置文件的变化,然而发现这对Nginx并没有什么影响,这是因为Nginx不会去监听文件的变化并自动重载。
通知Nginx重载配置
Nginx会持续压缩响应直到你通过以下命令主动通知它:
现在再次用curl命令访问服务器后会发现响应不再被压缩(响应头中未包含 Content-Encoding:gzip)。在无须重启容器或者重建pod的同时有效修改了应用配置。
了解文件被自动更新的过程
你可能会疑惑在Kubernetes更新完configMap卷中的所有文件之前,应用是否会监听到文件变化并主动进行重载。幸运的是,这不会发生,所有的文件会被自动一次性更新。Kubernetes通过符号链接做到这一点。如果尝试列出configMap卷挂载位置的所有文件,会看到如下内容。
可以看到,被挂载的configMap卷中的文件是..data文件夹中文件的符号链接,而.data文件夹同样是__4984_09_04_something的符号链接。每当ConfigMap被更新后,Kubernetes会创建一个这样的文件夹,写入所有文件并重新将符号..data链接至新文件夹,通过这种方式可以一次性修改所有文件。
挂载至已存在文件夹的文件不会被更新
涉及到更新configMap卷需要提出一个警告:如果挂载的是容器中的单个文件而不是完整的卷,ConfigMap更新之后对应的文件不会被更新!至少在写1.12之前的版本的时候表现如此。
如果现在你需要挂载单个文件并且在修改源ConfigMap的同时会自动修改这个文件,一种方案是挂载完整卷至不同的文件夹并创建指向所需文件的符号链接。符号链接可以原生创建在容器镜像中,也可以在容器启动时创建。
了解更新ConfigMap的影响
容器的一个比较重要的特性是其不变性,从同一镜像启动的多个容器之间不存在任何差异。那么通过修改被运行容器所使用的ConfigMap来打破这种不变性的行 为是否是错误的?
关键点在于应用是否支持重载配置。ConfigMap更新之后创建的pod会使用新配置,而之前的pod依旧使用旧配置,这会导致运行中的不同实例的配置不同。这也不仅限于新pod,如果pod中的容器因为某种原因重启了,新进程同样会使用新配置。因此,如果应用不支持主动重载配置,那么修改某些运行pod所使用的ConfigMap并不是一个好主意。
如果应用支持主动重载配置,那么修改ConfigMap的行为就算不了什么。不过 有一点仍需注意,由于configMap卷中文件的更新行为对于所有运行中示例而言不是同步的,因此不同pod中的文件可能会在长达一分钟的时间内出现不一致的情况。
作者:小家电维修
转世燕还故榻,为你衔来二月的花。