最近,公司内的k8s集群逐渐完善,恰逢有一个不是非常紧急的新项目开始推进,与SA讨论后决定尝试在k8s集群中进行应用的部署,加强对服务的全面管理以及提升服务的灵活性。前段时间摸索完一些市面上常用的应用配置管理方案后,刚配合公司内的配置管理服务实现了一套JAVA SDK,自然而然的在讨论转型方案开始时第一个想到的问题就是,在k8s集群内我们是不是还需要用到这些嵌入在应用内的配置SDK呢?
其实,应用迁移到k8s集群中并不是非要做什么特殊改造,即便是最传统的进程部署的方式也只需要进行把应用容器化即可,关于配置上的改造其实可以原样保留。只不过,k8s提供了一种用于配置分离管理的模式,利用额外定义的configmap来实现应用的配置管理,应用的配置文件无需打包在应用代码中,也无需额外的部署例如nacos之类的配置管理服务。关于configmap的概念和使用,可以参考一下官方文档,文档还是描述的比较详细的,这里就不继续展开了。接下来主要围绕着两种不同的使用configmap的方式,来讨论Spring Cloud应用关于配置管理方面的改造
Spring Cloud Kubernetes provides implementations of well known Spring Cloud interfaces allowing developers to build and run Spring Cloud applications on Kubernetes. While this project may be useful to you when building a cloud native application, it is also not a requirement in order to deploy a Spring Boot app on Kubernetes.
首先正如官方文档上面提到的,实际上对于Spring Boot应用而言,这并不是必须的。Spring Cloud K8s为Spring Cloud应用提供了一系列的k8s API Server的接口封装,那么我们可以利用API Server来做些什么呢?我相信大家应该都猜到了,获取分离部署的应用配置Configmap。除了Get,他还能做什么呢,那就是Watch(监听),Spring Cloud K8s实现了监听configmap的变更,使得应用程序在需要的时候可以更加方便快捷,而不需要费劲力气的去实现配置的获取并监听配置的变更。简单到开发者只需要引入如下的Maven配置,
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-fabric8-config</artifactId>
</dependency>
然后在应用的bootstrap配置文件中添加如下的配置,即可完成configmap配置动态获取。
spring:
cloud:
kubernetes:
client:
namespace: default
config:
name: k8s-demo
namespace: default
sources:
- namespace: default
name: k8s-demo
reload:
enabled: true
monitoring-config-maps: true
monitoring-secrets: false
strategy: refresh
mode: event
period: 10s
另外顺便提一下在高版本的Spring Cloud中,bootstrap并不是默认就包含的,如果发现不生效,记得引入spring-cloud-starter-bootstrap的依赖。
我们先来说说上面的几个配置,config.name、config.namespace是用来定义情况下的configmap与namespace的取值,sources则定义了实际要取的configmap,可以同时加载多个配置。reload则定义了是否需要对configmap进行动态刷新,默认是关闭的。可以选择单独监听configmap或是secret。有event与polling两种方式。event则是利用watch接口监听API Server发出的configmap配置变更事件。而polling就显得简单直接一点,通过轮询的方式来获取配置。主要关注一下strategy,支持了refresh、restart_context与shutdown两种方式。如果是refresh的话,要注意只对加上了@RefreshScope注解以及@ConfigurationProperties生效,而restart实际上是相对暴力的,虽然是gracefully restart ApplicationContext,shutdown就相对还要再暴力一点,直接重启容器。默认情况下的refresh,还是优先选择的~
再来说说Spring Cloud K8s还能做些什么。首先对于configmap,他可以是几个属性对,也可以是一整个配置文件的内容。其次,支持根据active profile来选择对应的configmap,例如dev环境,除了查找配置文件中指定的名字叫k8s-demo的configmap之外,还会查找k8s-demo-dev的configmap。最后当多个配置文件同时加载时,如果出现了覆盖,可以利用prefix相关属性,通过增加前缀的方式获取指定的属性(可能并不是非常常用)。
关于实际的使用细节,官方文档给出了很多具体的案例,有兴趣的同学可以移步官方文档,接下来我们主要聊聊在使用过程中可能会出现的哪些问题。
Spring Cloud K8s的功能是依赖于API Server的,在k8s集群内,应用是不能随意的对API Server进行访问的。首先对于应用而言,我们需要为其定义一个Service Account,用于标识应用的身份。其实,需要利用k8s的RBAC来为这个账户赋予API操作的权限,例如下面的配置,我们赋予了get、list、watch的权限。
kind: ServiceAccount
apiVersion: v1
metadata:
labels:
app: k8s-demo
name: k8s-demo
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: k8s-demo-role
rules:
- apiGroups: [ "" ]
resources: [ "configmaps" ]
verbs: [ "get", "list", "watch" ]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: k8s-demo-role-binding
namespace: default
subjects:
- kind: ServiceAccount
name: k8s-demo
apiGroup: ""
roleRef:
kind: Role
name: k8s-demo-role
apiGroup: ""
然后在应用部署的Deployment中挂上这个Service Account的即可完成对应用的访问API Server的授权。
在上面的配置中,应用除了可以获取到自己指定的配置文件,实际上还可以获取到这个namespace下其他的配置文件。权限粒度太粗了,是有一定的管控风险的。单从API Server角度而言,实际上可以通过在Role资源文件中利用resourceNames的字段来控制Role能够访问的具体资源,但是为什么这样不可行呢。首先我们可以从官方文档中得知,list接口实际上只能限定到某个具体的namespace而暂时不能支持某个资源,而在Spring Cloud K8s的实现中,作者认为应用启动时是可能会获取多个配置文件的,更具体的场景就是应用激活了多个profile,所以他并不是通过get请求去逐个获取配置文件的,而是通过list后过滤的方式来获取的,作者认为这样可以不必在启动时造成过多的请求。详细的代码在Fabric8ConfigMapPropertySource中,就不具体展开了。
Spring Cloud K8s提供了fabric8与k8s client两个不同的实现,区别是在于其底层使用的k8s client,后者来源于官方仓库。要用哪个好呢?其实最开始我也无脑选择了官方的spring-cloud-starter-kubernetes-client-config,但是事与愿违,除了polling方式没有什么大的问题,event模式简直是问题百出,证书、连接断开等等。用上了fabric8之后,一切都消失了。跟踪过代码的执行,也确确实实的能够找到本地的kubeconfig,无奈提了issue,等待回复中。尝鲜的同学可以考虑fabric8,虽然官方主页宣布不维护,但是奇怪的是还是活跃的?
另外大家可能会担心本地开发,fabric8在启动时正如前面说的,即便不在应用容器中也会去找kubelet使用的kubeconfig,所以要连远程集群在kubeconfig中配置好集群即可。
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: k8s.gcr.io/busybox
command: [ "/bin/sh", "-c", "ls /etc/config/" ]
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
# Provide the name of the ConfigMap containing the files you want
# to add to the container
name: special-config
restartPolicy: Never
一般来说,在云原生的使用方式上,configmap通常不是采用API获取的方式,而是如上述配置代码示例通过挂载到应用容器中的方式来实现的。(别忘了除了挂载,启动要记得指定spring.config.location或是spring.config.additional-location之类)这样一来,解决了两个问题,第一是应用无需植入额外的代码,第二也解决了上述的权限控制问题。运维在部署时可以通过指定configmap的方式来防止应用获取到不属于自己的配置文件,无需给应用分配一个Service Account。
但是也引入另一个新的问题,如果configmap更新了,应用该如何感知到他的变更呢?configmap的更新并不会主动的通知被挂载容器去处理这个变更,这里就需要引入一个额外的config watcher,用于统一的管理事件变更的事件,当然这个watcher还是需要Service Account的,但是集中管理减少了捣乱的风险。也就是为什么在Spring Cloud K8s文档中通过k8s api来完成动态更新的方式被标记为deprecated,而是推荐了spring cloud k8s config watcher.
对于这个watcher,想必大家也能一下子反应过来他需要做什么,首先需要监听配置,其次需要通知到应用容器。监听配置可以继续使用watch API,而通知到应用容器则需要利用actuator的refresh端点,也就是说应用同时也需要开放出来这个端点。config watcher在收到了监听事件以后,会根据configmap的名字来找到对应service后的所有endpoint,逐个调用refresh端点。
这里有几件事需要提一下:
1.查找service用的是configmap中metadata.name,所以没有对profile进行处理
2.会有2min的延迟,因为configmap变更后在集群内至少需要2min来刷新到所有的pod中,2min后才会发出refresh请求,如果期间watcher下线,变更是会丢失的
3.除了http刷新,还可以使用spring cloud bus来远程通知,可能项目中用的会比较少
4.并不是所有的configmap都会处理,需要有特定的annotation(spring.cloud.kubernetes.config)才可以
尽管作者认为config watcher is production ready,但是其实大家细想一些边界条件的问题还是存在的,但是开源,所以可以尽情发挥,代码在spring cloud kubernetes controller中~
对比两种方式,我会更倾向于configmap挂载的方式,代码入侵少,更加的偏向云原生应用一些,只不过config watcher如果需要面面俱到,需要自己付出一些努力,虽然应用变更配置大部分时候是比较低频的。我们再回过头来看是不是需要抛弃掉nacos之类的配置管理,相比于nacos,configmap对于配置变更这个操作没有提供很丰富的能力,比如变更历史、继承等等,对于生产来说这些都是很重要的,毕竟追溯到谁来背锅= =。只不过,我司的配置管理服务,可以在提供丰富的配置管理功能的同时,可以同步到对应集群的configmap中,所以也就少了这些顾虑。所以大家在选择时,还是需要斟酌一下。