导言:
最近在试用一个付费软件,主要是希望使用他们的Java Agent配合我们的Java应用采集一些数据,给应用做一些分析。试用前对方说的天花乱坠,什么只要一个命令,K8S集群上的对应应用就会自动带着他们的Java Agent跑起来,完全不用改任何应用代码,听的我也很有兴趣看看效果到底如何。当然,实际过程坎坷多了,毕竟,销售的嘴骗人的鬼...不过在不断地和对方研发一起debug的过程中,渐渐的也猜出了所谓的“自动”到底是怎么做到的,写一篇文章和大家分享,也许一样的思路,也能用到今后的工作中,解决一些问题。
首先展示一下所谓的一个命令的效果吧。如果“一切顺利”,确实也只需几行命令就能解决问题。
首先需要创建一个operator,里面定义了一个叫HellominAgent的自定义资源。然后创建一个HellominAgent,里面已经配置了一些例如应用的Statefulset名字等必要的信息。然后,两个kubectl解决问题:
代码语言:javascript复制$ kubectl create -f hellomin-operator.yaml
$ kubectl create -f hellomin-agent.yaml
此时再查看应用所在Pod的情况,会发现所有的Pod开始进入Terminating状态,等他们再次启动处于Running状态,我们登录进container,ps看看进程,会发现,咦,我们的Tomcat进程已经带着一些Jave Agent参数跑起来了:
代码语言:javascript复制# 进入tomcat Pod
$ kubectl exec -it tomcat bash
# 查看tomcat进程信息
$ ps -ef | grep tomcat
# 动态植入之前的tomcat启动命令,省略非必要信息
java org.apache.catalina.startup.Bootstrap start
# 动态植入之后的tomcat启动命令,省略非必要信息
java -Dhellomin=amazing -javaagent:/opt/hellomin/hellomin-agent.jar org.apache.catalina.startup.Bootstrap start
所以这里有几个问题:
- 这些参数是怎么传进去作为Tomcat启动参数的?
- 这里的Agent Jar包是从哪里来的?
让我们来一一解答。
从天而降的Jar包?InitContainer早已准备好了一切
首先,要给Tomcat的启动参数添加Java Agent,我们首先需要有Agent对应的Jar包吧?为啥重启了Pod之后,Jar包就凭空出现了呢?答案就在重启之后的Pod信息中。
通过查看被植入之后的Statefulset信息,我们可以看到类似这样的一些语句:
代码语言:javascript复制# 以yaml文件格式查看此时Pod信息
$ kubectl get sts hello-min -o yaml
# 此前的输出省略
initContainers:
- command:
- /bin/sh
- -c
- 'cp -r /opt/hellomin/. /opt/hellomin-java'
image: docker.io/hellomin/java-agent:latest
name: hellomin-agent
# 此后省略
可以看到,initContainer字段多了一个container的信息,从执行的命令基本可以看出,这个container做的事情,就是从一个特定的image里面把agent的Jar包拷贝到了另一个目录下面,那这个目录是哪里来的,拿来做什么呢?
首先,查看这个目录对应的volume相关的信息。还是一样的方式查看Statefulset,发现下面多声明了一个volume信息:
代码语言:javascript复制volumes:
# 省略大量其他volume声明
- emptyDir: {}
name: hellomin-agent-repo-java
这个Volume没有挂载出去,只是整个Pod共享的一块空间。
然后,我们在应用的container信息下面的volume信息里,也发现了这个volume,它被挂载到了我们应用的container里,即我们的应用是可以访问这个共享的Volume的存储空间的。注意,此处挂载的路径,正是之前initContainer把Jar包搬过去的地方哦。
代码语言:javascript复制volumeMounts:
# 省略其他volume信息
- mountPath: /opt/hellomin-java
name: hellomin-agent-repo-java
然后,之前的initiContainer也可以访问这个volume,因为它也同样挂到了这个initContainer下面:
代码语言:javascript复制# 以yaml文件格式查看此时Pod信息
$ kubectl get sts shello-min -o yaml
# 此前省略
initContainers:
- command:
# 中间忽略
volumeMounts:
- mountPath: /opt/hellomin-java
name: hellomin-agent-repo-java
# 此后省略
于是发生了什么基本就可以猜到了: Agent的Jar包被放在java-agent这个image的/opt/hellomin目录下,而这个initContainer要做的事情,就是把这个image里面的Jar包从这个目录拷贝到了一个我们的应用和Agent的image都能访问到的目录下。于是,我们的应用就可以访问这个Agent的Jar包了,从我们的角度来看,这个Jar包就“凭空出现了”~
能做到这一点,和initContainer的工作方式也有很大关系。在Statefulset中的Container运行之前,所有initContainer会依次运行并结束退出,所以,他们非常适合用来做一些应用运行之前的准备工作,这里的用法就是一个很好的例子。
有了Jar包,下面我们来想办法把它加到启动参数里面吧!
谁给Tomcat添加了参数?环境变量解决问题
Jar包的来源找到了,下一步就是把这个Jar包的启动信息添加到Tomcat的启动参数里面去了,这怎么能不修改任何代码就做到呢?
再次查看修改后的Statefulset信息,我们可以发现这样几行之前没有的信息:
代码语言:javascript复制spec:
containers:
- env:
- name: JAVA_OPTS
value: ' -Dhellomin=amazing -javaagent:/opt/hellomin-java/agent.jar'
Emmm,这不就是之前神秘出现的启动参数嘛?所以,所谓的不用修改代码,其实就是通过修改了Container的环境变量,把所需要添加的参数都通过环境变量的方式传递给Pod,这样在Container里,我们就可以读到这些参数了。
仅仅能读到还不够,是谁帮我们把这些参数加到启动参数里去的呢?这里其实Tomcat也是帮了一点小忙滴~
Tomcat的默认启动脚本Catalina.sh里面有这么一段:
代码语言:javascript复制eval $_NOHUP ""$_RUNJAVA"" "$JAVA_OPTS" "$CATALINA_OPTS"
# 省略不必要的参数
org.apache.catalina.startup.Bootstrap "$@" start
注意到中间的$JAVA_OPTS了吗,就是它啦!Tomcat启动的时候默认是带着这个环境变量的,所以你只要给这个环境变量里塞了东西,启动的时候就会被添加到启动参数里面了哟。
谁给它修改一切的权力?K8S中的权限管理
所以,谁给了这个HellominAgent这么大的权力,在我们的应用所在的container里面加了这么些东西?答案就在一开始运行的两行kubectl命令里。
说到这里,我们不得不聊聊K8S里面的权限管理问题。仔细查看hellomin-operator.yaml文件,也就是agent的operator文件,我们会发现这么几行配置:
首先,我们创建了一个叫做hellomin-cluster-agent的用户。
代码语言:javascript复制apiVersion: v1
kind: ServiceAccount
metadata:
name: hellomin-cluster-agent
namespace: hellomin
然后,我们创建了一个叫做hellomin-cluster-agent-instrumentation的ClusterRole,也就是集群中的权限角色,在这里,我们定义了这个ClusterRole可以在这个集群中做哪些操作,可以看到对Statefulset和pod,这个角色的用户都是拥有update权限的,可以进行更改的。
代码语言:javascript复制apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: hellomin-cluster-agent-instrumentation
rules:
- apiGroups:
- ""
resources:
- pods
- pods/exec
- secrets
- configmaps
verbs:
- create
- update
- delete
- apiGroups:
- apps
resources:
- daemonsets
- statefulsets
- deployments
- replicasets
verbs:
- update
最后,我们创建了一个叫做hellomin-cluster-agent-instrumentation的ClusterRoleBinding,把刚才定义的Account和Role建立了联结,即赋予了hellomin用户这个Role所拥有的权限。
代码语言:javascript复制kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: hellomin-cluster-agent-instrumentation
subjects:
- kind: ServiceAccount
name: hellomin-cluster-agent
namespace: hellomin
roleRef:
kind: ClusterRole
name: hellomin-cluster-agent-instrumentation
apiGroup: rbac.authorization.k8s.io
然后,在我们自己定义的HellominAgent资源的配置里,我们为它配置了这个已经赋好权限的账户:
代码语言:javascript复制kind: HellominAgent
metadata:
namespace: hellomin
spec:
serviceAccountName: hellomin-cluster-agent
# 省略其他配置
因此,通过我们自定义的这个HellominAgent创建出来的pod,就可以对集群中的Statefulset进行修改了。
最后,一切看上去都特别好,为啥我说坎坷呢?答案很简单。如果我们的应用就是直接使用Tomcat的脚本启动,确实挺好,但我们并没有...于是,我们根本就没有用到那个被赋值的环境变量,于是什么也没发生...
所以咯,遇到具体业务,还是要具体问题具体分析,不过这套思路还是很有趣的,一眼看去还是有种黑科技的感觉的,只是拆开来说白了,就发现其实也都是常见的打法吧?
下次再见!