Kubernetes集群中Java应用的Java Agent自动注入方式分享

2022-06-27 11:40:06 浏览数 (1)

导言:

最近在试用一个付费软件,主要是希望使用他们的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的脚本启动,确实挺好,但我们并没有...于是,我们根本就没有用到那个被赋值的环境变量,于是什么也没发生...

所以咯,遇到具体业务,还是要具体问题具体分析,不过这套思路还是很有趣的,一眼看去还是有种黑科技的感觉的,只是拆开来说白了,就发现其实也都是常见的打法吧?

下次再见!

0 人点赞