- 背景
- 注意事项
- 思路
- 过程
- 2022/01/21 更新 toAdmissionReview方法
背景
公司内网的Kubernetes集群因为Istio sidecar的原因,经常会达到公司运维给我们组设置的资源上限。一个我们的业务服务通常由两部分容器组成,一个是我们真实对外服务的容器,另一个则是由Istio sidecar inject也就是Istio框架自动注入的Istio-sidecar。不提我们的业务容器,只针对Istio sidecar,他的resource/limit/cpu就被设置为了2,要知道我们组所属的namespace下,资源limit上限被设置为了100cpu,超过100之后服务就无法被部署成功。同时,作为一个内网的服务,他的上限也根本不用2,这一个较高的数字。
在Istio后续的1.4版本中,有一个专门为Pod.spec.resource.cpu.limit和memory设置的注解,但是在之前的版本中不提供这样的支持,所以只能通过我们自己来实现。最简单的方法自然是为每一个环境(内网或者外网)做一份不同的设置,考虑到我们服务过于多,一个一个添加不大科学,也不符合k8s不影响到业务集成过程的理念。综上所述,最终考虑到k8s原生支持的Mutating Admission Webhook来实现。
Mutating Admission Webhook对我而言,简而言之,就是Kube-api 在执行真正的部署命令之前,提前对你要部署的东西做一个修改。Istio的Istio sidecar injector也是通过这个来实现的。那对我来说,只要在Istio sidecar injector注入sider,也就是他对Pod做完修改之后,我在将他的istio proxy中的container.resource.cpu.limit修改为我要的值就好了。
在google和github上搜索一番,基本都是基于Go语言来实现Admission Webhook Server,没有发现有Java实现的,所以打算自己动手实现一个。
注意事项
- 第一次搞Mutating Webhook Admission Webhook,可以将在Pod中注入一个新的Label作为成功标准
- 接上条,可以把Webhook Admission Webhook Configuration中的failPolicy设置为Fail,即在Server端中有意外情况,算作失败,这样可以在Replicas看到部署失败的原因
思路
从官方文档上来说,比较关键的东西有三个
- Mutating Admission Webhook Configuration,一份Webhook的配置文件,他决定谁会被拦截,和将请求转发到那里去
- Mutating Admission Webhook Server,在本文中专指Springboot 实现的一个Server,他的作用是怎么修改即将要被部署的kubernetes 资源
- 请求,也就是第一点提供到谁会被拦截到的谁,一般来说是指某一个被打了特定label的namespace
比较琐碎的细节,包括但不限于Kube-api 里面是否开启了admission webhook的限制,configuration和server的证书配置,server端要求返回的格式
过程
几个关键节点大概分为这几个历程
- Object生成请求拦截(这里就是Pod生成之前会给我们的server发消息)
- RequestBody 反序列化 (可以看到我们的server接收到k8s发来的消息是什么样子的)
- 尝试修改pod参数 (尝试修改一下pod的label)
- 尝试修改 istio-proxy resource参数
在第一个关键节点上我们理论上就应该完成大部分的工作,后面的过程是在做细节上的处理。
我的Configuration文件是这个样子的
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: mutateme
labels:
app: mutateme
webhooks:
###name 要求是以域名的形式出现 所以设置为 12Dong-qwer是不行的
- name: mutateme.i18n-system.svc.cluster.local
clientConfig:
#这一张证书要求从k8s集群内部取 取法可以看这篇教程中 https://medium.com/ovni/writing-a-very-basic-kubernetes-mutating-admission-webhook-398dbbcb63ec 中的 kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}' 这个命令
caBundle: "12Dong*****"
service:
#请求转发到Server 也就是我们的Springboot Server
name: auto-fill-annotation
namespace: i18n-system
#对应的路径名是一个Post请求
path: "/mutate"
rules:
#什么样的请求会被拦截到 我这里设置的是当有Pod生成时,部署好之后 可以用kubectl apply 命令生成一个即可测验
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
mutateme: enabled
比较需要注意的,以这样的格式默认生成的是一个FailPolicy是Ignore的Webhook Configuration就是指,如果Server出了问题,他会选择无视当作无事发生处理。建议第一次使用时,手动修改为Fail。这样可以在相应的位置看到报错
比如说这样 在Replicaset中我看到报错的原因
把他apply到我们的集群中,开始处理下一步Server端的代码
Server端的代码配置流程有三步
第一步,开启服务内部的Https 这个可以参照SpringBoot的官方文档来做
springboot服务开启https配置
第二部,将证书的相关信息将k8s集群中注册,否则就会出现这样的报错。这里可以参照我之前的笔记来处理
第三步,根据Configuration的信息写一个Post请求的接口
@PostMapping(value = "/mutate")
public AdmissionReview autoFillAnnotation(@RequestBody AdmissionReview admissionReview) {
//check istio proxy exist
final AdmissionRequest request = admissionReview.getRequest();
log.info("{} has been intercepted, webhook server is invoked", request.getObject().getMetadata().getGenerateName());
try{
final String json = MAPPER.writeValueAsString(request.getObject());
final Pod pod = MAPPER.readValue(json, Pod.class);
if (!checkIstioProxyExist(pod)) {
return toAdmissionReview(admissionReview, null);
}
}catch(Exception e){
log.error("Fail to check istio proxy exist because {}", e);
return toAdmissionReview(admissionReview, null);
}
//make response
JsonPatch[] jsonPatches = new JsonPatch[1];
JsonPatch update = new JsonPatch();
update.setOp("replace");
update.setPath("/spec/containers/1/resources/limits/cpu");
update.setValue("500m");
jsonPatches[0] = update;
String patch = null;
try{
String json = MAPPER.writeValueAsString(jsonPatches);
patch = Base64.getEncoder().encodeToString(json.getBytes());
} catch(Exception e){
log.error("{} fail to make patch because {}", request.getObject().getMetadata().getGenerateName(), e);
}
return toAdmissionReview(admissionReview, patch);
}
处理之后,把AdmissionReview 打印出来,再将Springboot打包上线,进行测试。
第一个关键节点,就完成了
第二关键节点需要注意的就是在我所使用的包中,直接获取反序列的信息时,不能直接Get到他的Spec属性,这个Spec属性使用了Interface来实现,所以我将其特化为了对应的Pod类,之后就能获取Spec属性字段了。
第三、四个关键节点中,涉及到JsonPatch的使用,可以看官方文档,Json对象中有数组对象的话,我粗粗翻了一遍没有讲解怎么做,所以在查了一下,需要在数组对应的字段名后使用/num的方式指定对数组成员的第几个做修改,我这边因为Istio-sidercar固定是第二个所以我要修改istio-sidecar的resource.cpu.limit属性的话,就是 /spec/containers/1/resources/limits/cpu
到这里大致的流程已经比较详细的讲完了,因为是公司的项目所以不能吧代码直接放到github上给大家参考。
每个人碰上的问题可能大概不同,问题的思路也可能大不相同,希望大家能通过FailPolicy来查看为什么自己的代码不能成功的原因,从而推导出正确的做法。
2022/01/21 更新 toAdmissionReview方法
private AdmissionReview toAdmissionReview(final AdmissionReview requestBody, final String patch) {
final AdmissionReview admissionReview = new AdmissionReview();
admissionReview.setKind(requestBody.getKind());
admissionReview.setApiVersion(requestBody.getApiVersion());
final AdmissionResponse admissionResponse = new AdmissionResponse();
admissionResponse.setAllowed(true);
admissionResponse.setUid(requestBody.getRequest().getUid());
admissionReview.setResponse(admissionResponse);
if (patch == null) {
return admissionReview;
} else {
admissionResponse.setPatch(patch);
admissionResponse.setPatchType("JSONPatch");
return admissionReview;
}
}