云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret

菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret 十、ConfigMap和Secret 我们大部分应用程序的部署都离不开配置信息,就比如我们常见的SpringBoot应用,他就需要配置一些application.properties信息,而且通常情况下这些配置信息还不能跟应用程序耦合,一般是需要放在配置中心去管理的。但是我们发现,前面部署的应用程序都没提到过关于他们的配置信息该怎么处理。那么在K8s中到底怎么处理呢?
在上一章卷的类型中第一个就是配置信息类型,他们属于特殊的卷。关于配置信息类型的卷有两种:ConfigMap和Secret。下面一一介绍。
1、在没有ConfigMap的时代,是怎么处理配置文件的? 在了解ConfigMap之前,有必要再来回顾一下容器化应用通常是如何处理配置的。

在Docker中,有三种方式处理应用程序的配置信息:
  • 一种是通过命令行参数传入应用程序,然后应用程序在进行解析。
  • 由于配置项的逐渐增多,另一种方式是将配置文件化,然后嵌入应用程序本身,这种方式往往需要将配置文件打入容器镜像,亦或是挂载包含该文件的卷。
    • 将配置文件打入容器镜像的方式不可取,因为这种方式每次修改完配置都需要重新打镜像。而且,任何拥有镜像访问权限的人都可以看到配置文件中包含的敏感信息,比如证书和密钥。
    • 通过卷挂载的方式相比更好,但是这种方式需要在容器启动之前将配置文件已经写入相应的卷中。
  • 最后一种是借助环境变量,让应用程序主动查找某一特定环境变量的值,而不是读取配置文件或者解析命令行参数。
    • 比如MySQL官方镜像内部通过环境变量MYSQL_ROOT_PASSWORD设置超级用户root的密码。
在Kubernetes中,有一种专门用来存储配置数据的资源——ConfigMap,他可以很好的解决关于配置文件的问题。
下面我们先来看看在Docker中向容器中的应用程序传参的具体实现。
1.1 通过命令行参数
在Docker中 先来回顾一下Docker中Dockerfile的两个指令:ENTRYPOINT和CMD。
这里就不再赘述关于ENTRYPOINT和CMD的细节了,直接来说关于这两个指令用户的结论:(关于他们的详情可以戳这里>>>)
  • ENTRYPOINT用来定义容器启动时被调用的可执行程序。
  • CMD用来为ENTRYPOINT传递参数。
接下来使用一个案例来了解通过命令行为应用程序传参的过程。
我们准备的应用程序原先的功能是:它可以在固定时间内循环生成任意文本并写入index.html文件中。
我们现在要做的是:把这个固定时间变为可配置的,我们可以通过命令行传参的方式,让他变为在指定的时间修改index.html的内容。
(1)准备一个应用程序的镜像
######fortuneloop.sh文件,模拟一个简单的应用程序#######!/bin/bash trap "exit" SIGINT INTERVAL=$1### 他会读取启动命令中的第一个参数 echo Configured to generate new fortune every $INTERVAL seconds### 我们到时候可以通过这个应用程序的输出的这条语句看我们传入的参数是否生效 mkdir -p /var/htdocs while : do echo $(date) Writing fortune to /var/htdocs/index.html /usr/games/fortune > /var/htdocs/index.html # sleep 10### 原先固定的:原本是固定10秒刷新一次index.html的内容,现在改为通过参数可变的 sleep $INTERVAL### 现在可配的:睡眠时间就是我们指定的时间 done

(2)为这个应用程序制作镜像
FROM ubuntu:latest RUN apt-get update; apt-get -y install fortune### 安装这个自动生成文本的工具fortune ADD fortuneloop.sh /bin/fortuneloop.sh### 把我们的应用程序添加到容器内的指定位置 ENTRYPOINT ["/bin/fortuneloop.sh"]### 指定容器启动命令 CMD ["10"]### 默认时间设置为10,这个参数是可以在启动容器的时候被覆盖的

构建镜像:docker build -t fortune:args .
我在构建镜像的时候因为网络原因无法构建成功,如果大家也没构建成功可直接使用这个镜像:docker.io/luksa/fortune:args,因为我参考的就是这个镜像,功能都是一样的
(3)接下来通过docker命令行传参的方式指定间隔时间 docker run -it --name test-fortune-01 docker.io/luksa/fortune:args
不传参就是默认的10s
云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

docker run -it --name test-fortune-02 docker.io/luksa/fortune:args 20
传参就是传入的值:20s
云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

在K8s中
需要提前了解:在K8s中定义容器时,可以通过指定command和args属性来覆盖镜像中的ENTRYPOINT和CMD命令(但一般情况下只会覆盖CMD命令,而不会覆盖ENTRYPOINT命令,因为一般情况下ENTRYPOINT都是不需要改变的!)
kind: Pod spec: containers: - image: busybox command: ["/bin/command"] args: ["arg1", "arg2", "arg3"]

接下来通过Pod命令行传参的方式指定间隔时间
apiVersion: v1 kind: Pod metadata: name: fortune2s spec: containers: - image: luksa/fortune:args args: ["2"] name: html-generator

查看这个Pod的日志信息,发现确实是2s的间隔时间。
云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

1.2 通过环境变量
在K8s中 (1)修改应用程序 想要通过环境变量的方式获取动态参数我们需要先修改下我们应用程序的代码,让他从环境变量中去取参数
######fortuneloop.sh文件,模拟一个简单的应用程序#######!/bin/bash trap "exit" SIGINT # INTERVAL=$1### 注释掉这个就行了,下面通过$取参都会去环境变量中取 echo Configured to generate new fortune every $INTERVAL seconds### 我们到时候可以通过这个应用程序的输出的这条语句看我们传入的参数是否生效 mkdir -p /var/htdocs while : do echo $(date) Writing fortune to /var/htdocs/index.html /usr/games/fortune > /var/htdocs/index.html # sleep 10### 原先固定的:原本是固定10秒刷新一次index.html的内容,现在改为通过参数可变的 sleep $INTERVAL### 现在可配的:睡眠时间就是我们指定的时间 done

接下来还是制作镜像等流程,我就不再演示。
(2)接下来通过配置环境变量指定间隔时间
apiVersion: v1 kind: Pod metadata: name: fortune30s spec: containers: - name: html-generator image: luksa/fortune:env### 新的镜像,通过环境变量动态取值 env: - name: INTERVAL value: "30"

云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

他确实读取到了环境变量传入的指定时间
2、ConfigMap
如果对K8s的ConfigMap有一定的了解,可以直接跳到2.3小节。在2.3之前都是为初学者准备的前菜,只是为了循序渐进。
前面我们了解了在K8s中通过命令行和环境变量的方式为Pod中的容器动态传参的过程。但是这种方式不能够很好的不同环境参数的差异,那上面的例子来说,也就是如果我要这开发环境让这个应用程序的间隔时间是10s,而在生产环境要让他的间隔时间是20s,那我只是为了修改这一个参数值就要去分别创建两个Pod吗?难道就不能够仅仅把这个配置项从Pod的定义信息中解耦出来,然后复用其他的Pod定义信息吗?
可以!ConfigMap就能帮我们完成这件事!
前面举了那么多例子,主要就是为了说明:利用ConfigMap的关键就在于能够在多个环境中区分配置选项,将配置从应用程序源码中分离(我们这里把部署应用程序的Pod定义信息也认为是应用程序的源码),可以频繁变更配置值。
2.1 Pod引用ConfigMap条目的方式一——环境变量
先创建一个ConfigMap
apiVersion: v1 kind: ConfigMap metadata: name: fortune-config data: INTERVAL: "25"

这是一个简单的ConfigMap,他里面存了一个条目(后面,我们将ConfigMap中这样一个 k-v 对就称为一个条目)
apiVersion: v1 kind: Pod metadata: name: fortune-configmap spec: containers: - image: luksa/fortune:env name: html-generator envFrom: - configMapRef: name: fortune-config### 引用名为fortune-config的ConfigMap

这时,他就可以将fortune-config这个ConfigMap中的所有条目信息作为环境变量传入启动的容器中。(环境变量的名称与ConfigMap中的键名是相同的)
测试成功
云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

2.2 Pod引用ConfigMap条目的方式二——命令行参数
下面我们看一下如何将ConfigMap中的值作为参数值传递给运行在容器中的应用程序。
由于在pod.spec.containers.args属性中没有办法直接引用ConfigMap中的条目,但是我们可以曲线救国,利用ConfigMap的条目初始化某个环境变量,然后再在参数字段中引用该环节变量。
apiVersion: v1 kind: Pod metadata: name: fortune-args-from-configmap spec: containers: - image: luksa/fortune:args name: html-generator env: - name: INTERVAL### 指定环境变量中的key名 valueFrom:### 这种方式是从ConfigMap中引用单个指定条目的方式,而前面envFrom的方式是从ConfigMap中一次性引用所有条目的方式 configMapRef: name: fortune-config### 引用名为fortune-config的ConfigMap key: INTERVAL### 指定要引用的fortune-config中的条目的key名 args: ["$(INTERVAL)"]### 在参数设置中引用环境变量

2.3 Pod引用ConfigMap条目的方式三——使用configMap卷
从这里开始才是整个ConfigMap中的重头戏,前面的都是为初学者准备的前菜,为了循序渐进到这里。
环境变量或者命令行参数值作为配置值通常适用于变量值较短的场景。但是大多数情况下,配置文件的内容是有很多的,这时我们就可以借助在上一章提到的一种特殊的卷——configMap卷。
注意:我后面所说的configMap卷和ConfigMap不是同一个东西,大家需要加以区分。
configMap卷会将ConfigMap中的每个条目均暴露成一个文件。运行在容器中的应用程序可以读取文件内容来获取对应条目的值。
下面我们想要启动一个Nginx容器,然后将Nginx的配置文件由ConfigMap来管理,通过修改Nginx的配置来开启Nginx压缩传递给客户端响应的功能。
(1)创建ConfigMap
apiVersion: v1 kind: ConfigMap metadata: name: fortune-config data: ### 条目一:nginx的配置信息 my-ng-config.conf: | server { listen80; server_namewww.zhangaoqi.com; gzip on; ### 开启压缩功能 gzip_types text/plain application/xml; location / { root /usr/share/nginx/html; index index.html index.htm; } }### 条目二:这个条目大家可忽略,放在这里只是为了展示每个条目都会生成一个以条目key为文件名,以条目value为值的文件。 sleep-interval: | 20

(2)在卷内使用ConfigMap的条目
创建包含ConfigMap条目内容的卷只需要创建一个引用ConfigMap名称的卷并挂载到容器中就行了。
apiVersion: v1 kind: Pod metadata: name: fortune-configmap-volume spec: volumes:### 卷定义引用名为fortune-config的ConfigMap - name: config configMap: name: fortune-config containers: - name: mynginx image: nginx volumeMounts: - name: config mountPath: /etc/nginx/conf.d### 挂载configMap卷到/etc/nginx/conf.d这个位置 readOnly: true

云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

(3)检查被挂载的configMap卷的内容
看ConfigMap的两个条目均作为文件的形式被挂载到了/etc/nginx/conf.d下。云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

(4)注意
有一点必须要告诉大家,如果我们将卷挂载至某个文件夹,那么这个文件下原本存在的文件就会被覆盖(这就是Docker中空挂载的问题)。所以,挂载的时候一定要注意,自己新建一个文件夹,别把别人的文件覆盖了!
还是用上面的例子来说,如果我将ConfigMap的条目挂到/etc/文件夹下,那么/etc/这个文件夹下的其他文件都会被覆盖,导致我们的容器就会启动失败,如下:
云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

通过下面的这种方式,则可以不会覆盖原有文件下的其他文件。
但是它存在一种缺陷:就是如果挂载的是ConfigMap中的单条条目,则ConfigMap更新之后,他挂载在容器中的文件是不会被同步更新的!(如果像2.3(2)那样,挂载的是整个configMap,那么在ConfigMap资源被更新后,它对应的容器里面的文件也会同步更新!)
apiVersion: v1 kind: Pod metadata: name: fortune-configmap-volume spec: volumes:### 卷定义引用名为fortune-config的ConfigMap - name: config configMap: name: fortune-config containers: - name: mynginx image: nginx volumeMounts: - name: config mountPath: /etc/my-ng-config.conf### 挂载至某一文件,而不是文件夹 subPath: my-ng-config.conf### 仅仅挂载ConfigMap中的指定条目

2.4 ConfigMap配合SpringBoot项目实战案例
前面我们也提到过了利用ConfigMap的关键就在于能够在多个环境中区分配置选项,下面我们就通过一个SpringBoot项目的案例来看看他在实战中具体是怎么应用的。
我们以前用SpringBoot应用,为了区分不同环境会有不同的application.yaml文件,这些文件都是写在我们项目的resources路径下的,这样程序员就会看到生产环境的账密信息,为了不让程序员接触到生产环境的账密信息(防止开发人员知道生成环境的账密信息,删库跑路)。现在通过K8s来部署项目就可以通过ConfigMap来实现:
云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret
文章图片

SpringBoot项目生产环境的配置信息:
apiVersion: v1 kind: ConfigMap metadata: name: app-test-config data: application.yaml: | # xxxxxx生产环境配置信息xxxxxx

SpringBoot项目的Deployment部署信息:
apiVersion: apps/v1 kind: Deployment metadata: name: app-test namespace: app-test spec: replicas: 3 selector: matchLabels: app: app-test template: metadata: labels: app: app-test spec: volumes: - name: prod-conf configMap: name: app-test-config containers: - name: app-test image: app-test:latest### 这个镜像中的/app下存放的是SpringBoot项目的jar包app-test.jar imagePullPolicy: Always command: ["/bin/sh"] args: ["-c", "nohup java -jar /app/app-test.jar"] ports: - containerPort: 8888 volumeMounts: - name: prod-conf mountPath: /app

通过上云部署肯定需要给这个SpringBoot项目编写一个Deployment.yaml文件,我们可以在这个Deployment资源中为Pod模板添加一个关键的配置:volumeMounts,通过他来指定一个挂载,挂载到容器的/app路径下(这个路径需要是我们SpringBoot应用存放的jar的包位置)然后通过volumes来指定他挂载的configMap(只要我们项目不部署到生产环境这个ConfigMap就不会生效)。这时候只要我们项目一部署到生产环境,那么他会找对应的生产环境的ConfigMap,将这个ConfigMap的data的信息挂载到我们Pod容器中的 /app路径下。然后容器在启动的时候执行java -jar命令时,他会优先读取/app路径下的挂载的这个application.yaml,而不是我们SpringBoot项目jar包中的application.yaml(这是SpringBoot启动的默认行为,他会优先让外部的application.yaml文件生效)。这样开发人员就看不到生产环境的账密信息,只要项目一部署在生产环境,这个生产环境的配置信息就会生效。
看!是不是,我们可以利用ConfigMap成功的将我们SpringBoot应用的不同环境的配置信息很好的剥离了出来!
3、Secret 【云原生(K8s|菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret】前面通过ConfigMap给容器传递的信息都是一些非敏感数据,因为**ConfigMap保存的内容就是普通的明文文本,不会对内容进行编码。**但如果我们需要传一些证书或者密钥该怎么办呢?使用Secret就行了!Secret保存的内容是密文,他会对保存的内容通过Base64进行编码。
Secret的结构与ConfigMap类似,均是kv键值对的映射。Secret的使用方法也和ConfigMap相同:
  • 将Secret条目作为环境变量传递给容器
  • 将Secret条目暴露为卷的文件
3.1 创建Secret
通过命令行使用字符串创建Secret
### 使用基本字符串直接创建 kubectl create secret generic db-secret \### 注意这里的generic,他只是表示这个secret的类型 ### 还有其他类型:tls、docker-registry --from-literal=username=root \ --from-literal=password='123456'---## 上面命令生成的yaml文件 apiVersion: v1 kind: Secret metadata: name: db-secret data: password: cm9vdA==### 用base64编码了一下 username: MTIzNDU2### 用base64编码了一下

通过命令行使用文件创建Secret
### 准备两个文件 echo -n 'admin' > ./username.txt echo -n '123' > ./password.txt### 通过文件创建 kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt# 默认密钥名称是文件名。 你可以选择使用 --from-file=[key=]source 来设置密钥名称。如下 kubectl create secret generic db-user-pass-02 --from-file=un=./username.txt --from-file=pd=./password.txt

注意:如果我们想通过yaml文件去创建secret,那就需要在指定data的值的时候自己对内容采用base64编号码!
3.2 在Pod中使用Secret
在Pod中使用Secret的方式有两种:一种是通过环境变量的方式,一种是通过secret卷的方式。通过环境变量的方式不可取!因为这种方式不安全!推荐使用secret卷的方式!
apiVersion: v1 kind: Pod metadata: name: mypod spec: volumes:### 指定挂载详情 - name: foo### 该名字和volumeMounts.name对应 secret: secretName: mysecret### 指定secret的名字,如果只写到这里那么挂载的将是secret中的所有内容 items:### 指定挂载secret中的部分内容 - key: password.txt### 指定挂载mysecret中的password.txt的内容 path: pwd.txt### 挂载到容器中指定的新的文件名 containers: - name: mypod image: redis volumeMounts: - name: foo mountPath: "/etc/foo"### 容器内的挂载路径 readOnly: true

未完,待续>>>
参考:Kubernetes in Action

    推荐阅读