TOC
ConfigMap
Kubernetes是一个容器编排平台,虽然它又额外加了一层外层Pod,并以容器的方式来运行应用程序的特定,在Docker时代所面临的配置容器化应用的问题,在Kubernetes时代依然存在,如果想配置应用程序,我们想用到的途径和方式无非是两种;
第一如果程序的镜像是云原生的,或者借助于entrypoint脚本能够通过传递环境变量接受参数,完成配置这是一种比较轻量化的配置方式,在启动容器时传递几个环境变量,分别代表着几个重要的配置参数,从而完成应用程序配置;
第二以Nginx为例,启动Nginx需要加载我们自定义的配置文件模块,我们还可以使用存储卷的方式进行配置文件的传递,将外部的配置文件加载到容器内部
这两种配置方式有缺陷,如果使用环境变量配置的话,环境变量是在容器启动时被装入的,然后进行替换,一旦替换就没法更改了,如果想修改某一个参数的值,只能重新启动容器,如果使用存储卷的方式去做,存储卷中的内容也是在进程启动时被装入的,后面就算修改了存储卷的配置,也还是需要手动触发进程reload加载新的配置;
如果说我们在一个Pod控制器下,控制有五个Pod,这个五个Pod配置信息应该都是一样的,意味着这五个Pod他们会共享同一个存储卷,如果修改了存储卷的当中的内容,很显然,这个五个Pod都能加载到新的内容,但是这五个Pod都得进程重载,这就很麻烦了,尤其在以后使用Hpa控制器的时候,它会根据用户的访问量来自定伸缩Pod副本,那这个时候就完全不知道该重载哪些Pod,所以在这种情况之下,一样会面临复杂的配置问题,这个时候最好能够提供一种机制,让我们可以改完配置信息以后能自动被相关的Pod重载,而且能够一对多,只需要改一遍,所有加载这个配置文件的Pod都能够被自动进行重载;
在kubernetes之上用于实现配置自动重载的功能的组件,他们分别是ConfigMap、Secret,他们都是用于把配置文件配置为Kubernetes资源,其中ConfigMap用于提供非敏感配置,而Secret用户配置敏感配置信息(密码、密钥等)进行编码存放,他们内部存储的都是键值对,而这种键值,键可以是我们的配置参数,值可以做得很复杂,比如我们可以把一个配置文件的文件名当作键,整个文件的配置当作值;
这样子也就意味着我们常见的配置文件,都可以直接被实例化为ConfigMap中的键值对象,所以一个ConfigMap就是一个键值存储系统,我们一旦定义好ConfigMap需要在我们的Kubernetes中的Pod调用有两种方式;
第一,我们可以在Kubernetes容器中,把每一个Key的Value映射为这个容器内部环境变量的值,比如我们在一个Pod中有一个环境变量名是MYSQL_USER它的值可以来自于一个ConfigMap的键的值,我们向MYSQL_USER值不是直接赋值而是给它赋值一个ConfigMap中的键,从而他能够实现键的替换,然后也就能够被我们的应用程序引用到;
Kubernetes的ConfigMap和Pod都是标准的Kubernetes资源,因此在这种情况下有一个名称空间,我们做了有一个ConfigMap,在这个ConfigMap种我们定义了两个数据项第一叫redis_host值为172.16.1.1,第二叫redis_port值为6379,随后我们配置一个Pod,这个Pod是一个filebeat,这个Pod能够通过一个环境变量来引用redis主机和端口的配置,对应环境变量名是的host和prot,那么我们就可以这样定义Pod,“host:redis-cfg.redis_host,port:redis_port”,以后我们想要修改配置只需要配置ConfigMap即可,但是这种引用有一个缺陷,就是只要你把Pod启动起来就不会再重新获取值了,也就意味着后面我们修改了ConfigMap里面的数据也不会被Pod加载到,这个时候 还是需要重建来进行加载新配置,这是环境变量的缺陷;
还有一种方式,ConfigMap资源可以直接被Pod当作容器使用,比如配置一个Nginx,Nginx是可以在/etc/nginx/conf.d目录中去装载我们Nginx配置文件的,因此我们仍然定义一个ConfigMap资源,这个ConfigMap叫做nginx-cfg,它有一个键叫做server1.conf他里面还有一个键值比如server_name值为172.16.1.1,随后我们在定义Pod时候先定义一个存储卷,这个存储卷的类型是ConfigMap,它是ConfigMap中的存储卷,而这个卷也不是某个存储设备的存储空间,而就是引用了ConfigMap中的资源,我们可以通过存储卷的方式引用一个ConfigMap资源,而后在Nginx容器当中就可以把这个存储卷挂在到/etc/nginx/conf.d目录下,一旦我们挂在完成之后,它能够自动把这个server1.conf键映射成文件名,并且把server1.conf键的值映射为文件的内容,所以直接变成容器内部的/etc/nginx/conf.d下面的server.conf文件,更方便的是,这种引用方式,如果我们修改了ConfigMap里面的配置,过一个随机时长,可能几秒内,会自动同步到所有引用这个ConfigMap存储卷的Pod,而且一旦只要容器应用支持还重载,那么 同步完成之后它会自动重载容器应用,使得新配置生效,但是如果通过重载应用无法生效, 那就得需要重建Pod使配置生效,Secret也是一样,只不过数据做了一层base64加密;
环境变量方式配置
使用ConfigMap的方式配置,我们后面可以通过entrypoint脚本来进行配置替换,但是这种方式有一种缺陷,就是热修改配置的时候,Pod里面的应用不会重新加载配置;
# 创建一个名称空间
[root@node1 ~]# kubectl create ns config
# 创建一个configmap
[root@node1 ~]# kubectl create configmap -n config filebeat-cfg --from-literal=redis_host="redis.default.svc.cluster.local" --from-literal=redis_port="6379"
# 查看创建的configmap
[root@node1 ~]# kubectl get configmaps -n config filebeat-cfg -o yaml apiVersion: v1
data:
redis_host: redis.default.svc.cluster.local # 配置都存储在data对象下面
redis_port: "6379"
kind: ConfigMap
metadata:
creationTimestamp: "2019-11-29T07:20:47Z"
name: filebeat-cfg
namespace: config
resourceVersion: "34819"
selfLink: /api/v1/namespaces/config/configmaps/filebeat-cfg
uid: 592a60fc-9188-4782-b9af-dceac9766418
# 编写Pod配置
[root@node1 ~]# cat test.yaml
apiVersion: v1
kind: Pod
metadata:
name: config-test
namespace: config
spec:
restartPolicy: Always
containers:
- name: vol-pod
image: busybox:latest
command:
- /bin/httpd
- -f
env:
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: filebeat-cfg
key: redis_host
- name: REDIS_PORT
valueFrom:
configMapKeyRef:
name: filebeat-cfg
key: redis_port
# 查看环境变量是否加载成功
[root@node1 config]# kubectl exec -it -n config config-test printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=config-test
TERM=xterm
REDIS_HOST=redis.default.svc.cluster.local
REDIS_PORT=6379 # 可以看到环境变量已经加载进来了
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
HOME=/root
# 测试修改configmap里面的值
[root@node1 config]# kubectl edit configmaps -n config filebeat-cfg
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
redis_host: redis.default.svc.cluster.local
redis_port: "6380" # 修改为6380
kind: ConfigMap
metadata:
creationTimestamp: "2019-11-29T07:59:00Z"
name: filebeat-cfg
namespace: config
resourceVersion: "38149"
selfLink: /api/v1/namespaces/config/configmaps/filebeat-cfg
uid: aa81b1c9-a29a-4c73-b480-8146273d3c17
# 重新查看容器会发现环境变量没有重新加载
[root@node1 config]# kubectl exec -it -n config config-test printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=config-test
TERM=xterm
REDIS_HOST=redis.default.svc.cluster.local
REDIS_PORT=6379 # 没有变动
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
HOME=/root
# 手动删除Pod并重建查看是否是新的环境变量
[root@node1 config]# kubectl delete -f test.yaml
pod "config-test" deleted
[root@node1 config]# kubectl apply -f test.yaml
pod/config-test created
[root@node1 config]# kubectl exec -it -n config config-test printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=config-test
TERM=xterm
REDIS_HOST=redis.default.svc.cluster.local
REDIS_PORT=6380 # 配置已经变动
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
HOME=/root
volume方式配置
volume方式配置就是直接通过挂载configmap的方式将配置写入到Pod容器指定的路径下面,这种方式会自动加载新配置,但是有的程序是不支持reload的,以Nginx为例;
# 创建Nginx配置
[root@node1 nginx]# cat server1.conf server2.conf
server {
server_name 0.0.0.0;
listen 80;
location / {
root /data/wwwroot/server1;
}
}
server {
server_name 0.0.0.0;
listen 8080;
location / {
root /data/wwwroot/server2;
}
}
# 将nginx配置创建为ConfigMap资源
[root@node1 nginx]# kubectl create ns config-volume
[root@node1 nginx]# kubectl create configmap nginx-conf --from-file=server1=server1.conf --from-file=server2=server2.conf -n config-volume
configmap/nginx-conf created
[root@node1 nginx]# kubectl get configmaps -n config-volume -o yaml
apiVersion: v1
items:
- apiVersion: v1
data:
server1: "server {\n\tserver_name 0.0.0.0;\n\tlisten\t\t80;\n\tlocation / {\n\t\troot
/data/wwwroot/server1;\n\t}\n}\n"
server2: "server {\n\tserver_name 0.0.0.0;\n\tlisten\t\t8080;\n\tlocation / {\n\t\troot
/data/wwwroot/server2;\n\t}\n}\n"
kind: ConfigMap
metadata:
creationTimestamp: "2019-11-29T13:52:41Z"
name: nginx-conf
namespace: config-volume
resourceVersion: "6558"
selfLink: /api/v1/namespaces/config-volume/configmaps/nginx-conf
uid: 9e0ff284-70ae-4911-a975-7990b91a7373
kind: List
metadata:
resourceVersion: ""
selfLink: ""
# 配置Pod
[root@node1 nginx]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: config-volume-pod
namespace: config-volume
spec:
containers:
- name: nginx
image: nginx:1.14-alpine
volumeMounts:
- name: conf # 要挂载volumes的名字
mountPath: /etc/nginx/conf.d # 挂载的目的目录
volumes:
- name: conf # 该volumes的名字
configMap:
name: nginx-conf # 要使用configMap的名字
items:
- key: server1 # 要使用指定configMap里面的键名
path: s1.conf # 挂载之后的文件名
- key: server2 # 要使用指定configMap里面的键名
path: s2.conf # 挂载之后的文件名
# 查看配置是否生效
[root@node1 nginx]# kubectl exec -it -n config-volume config-volume-pod /bin/sh
/ # ls /etc/nginx/conf.d/ # 两个配置文件已经加入进来
s1.conf s2.conf
/ # cat /etc/nginx/conf.d/* # 配置和我们定义的一致
server {
server_name 0.0.0.0;
listen 80;
location / {
root /data/wwwroot/server1;
}
}
server {
server_name 0.0.0.0;
listen 8080;
location / {
root /data/wwwroot/server2;
}
}
/ # netstat -ntlp # 端口也启动起来了
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1/nginx: master pro
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1/nginx: master pro
/ #
# 测试修改配置
[root@node2 ~]# kubectl edit -n config-volume configmaps nginx-conf
configmap/nginx-conf edited
# 80变更为81,8080变更为8081
[root@node2 ~]# kubectl edit -n config-volume configmaps nginx-conf
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
data:
server1: "server {\n\tserver_name 0.0.0.0;\n\tlisten\t\t81;\n\tlocation / {\n\t\troot
/data/wwwroot/server1;\n\t}\n}\n"
server2: "server {\n\tserver_name 0.0.0.0;\n\tlisten\t\t8081;\n\tlocation / {\n\t\troot
/data/wwwroot/server2;\n\t}\n}\n"
kind: ConfigMap
metadata:
creationTimestamp: "2019-11-29T13:52:41Z"
name: nginx-conf
namespace: config-volume
resourceVersion: "8765"
selfLink: /api/v1/namespaces/config-volume/configmaps/nginx-conf
uid: 9e0ff284-70ae-4911-a975-7990b91a7373
# 数秒后会发现Nginx配置文件已变更,但是nginx未重载,可以借助其他插件实现
[root@node1 nginx]# kubectl exec -it -n config-volume config-volume-pod cat /etc/nginx/conf.d/s1.conf
server {
server_name 0.0.0.0;
listen 81;
location / {
root /data/wwwroot/server1;
}
}
配置中心概念
Kubernetes用来配置分布式或者负载均衡集群化的配置中心,比如如果在nginx后面维护有20个tomcat,需要改修tomcat配置文件的一些配置信息,比如在修改tomcat连接redis的账号密码,那如何让20个tomcat都应用到新配置,此前的做法就是使用ansible,然后分批推,推的时候还需要以灰度的方式进行,如果一下全部推送那在某一时刻服务都无法访问了,所以即使借助了ansible来编排,也得一个一个来,但这种才20个,但是如果有30个或者更多这就比较麻烦了,所以很多规模非常大的网站都不会通过这种方式来配置应用程序,而是让应用程序所有配置都不通过本地配置文件加载,而是通过联系一个配置中心的服务去加载配置,所以称之为配置中心;
启动多个应用程序时,抛弃了传统从本地文件加载配置的方法,而是通过一个中心服务器来加载配置,这个服务叫做配置中心,随后我们在这个配置中心修改了配置以后,会通知给每一个进程,让进程自动进行重载,就能完成所有的进程进行重载,还可以以灰度的方式通知,那么他们就可以以灰度的方式去更新自己的配置信息;
国内有两个项目,为非容器化应用程序提供配置中心,协程的Apollo、百度的Distconf,所以在非容器化模式中,也可以使用配置中心,要使用配置中心还需要我们的程序支持从配置中心加载配置才行;
1