k8s Webhook 使用java springboot实现webhook 学习总结
大纲
- 基础概念
- 准入控制器(Admission Controllers)
- ValidatingWebhookConfiguration 与 MutatingWebhookConfiguration
- 准入检查(AdmissionReview)
- 使用Springboot实现k8s-Webhook
- 证书创建
- 创建MutatingWebhookConfiguration 资源
- java代码编写
- 测试部署Pod
- 将Webhook部署在k8s集群内部
基础概念
Webhook就是一种HTTP回调,很多软件都支持webhook,例如 以前的文章中提到的graylog 也可以使用webhook进行通知。kubernetes也支持Webhook,例如istio就是通过 Mutating webhooks 来自动将Envoy这个 sidecar 容器注入到 Pod 中去的。
kubernetes利用webhook可以实现类似Java Web Filter的功能,拦截对资源的操作请求。 将操作增强或者拦截验证
例如
创建一个Pod,可以对这个操作添加额外的配置,比如添加标签,添加容器,添加挂载等,或者验证Pod操作是不是满足某些要求,不满足则不执行等
kubernetes实现Webhook主要是利用的 动态准入控制 实现
准入控制器
kubernetes 支持多种准入控制器例如:
- LimitRanger
- NamespaceExists
- NodeRestriction
- …略 详细见准入控制器
- MutatingAdmissionWebhook
- ValidatingAdmissionWebhook
准入控制过程分为两个阶段
- 第一阶段,运行变更准入控制器。
- 第二阶段,运行验证准入控制器。
注意:某些控制器既是变更准入控制器又是验证准入控制器。
利用 –enable-admission-plugins 可以添加准入控制器,多个控制器使用,号分开
--disable-admission-plugins 配置可以关闭控制器
MutatingAdmissionWebhook & ValidatingAdmissionWebhook 准入控制器
要实现kubernetes的webhook主要是 MutatingAdmissionWebhook 与 ValidatingAdmissionWebhook 这两个准入控制器
-
ValidatingAdmissionWebhook准入控制器 主要作用就是对操作做验证性质的准入
-
MutatingAdmissionWebhook准入控制器 主要作用就是对操作做修改性质的准入
例如:
想在操作资源之前进行修改(Mutating Webhook),比如增加Container或者修改部署的一些属性
想在操作资源之前进行校验(Validating Webhook),不满足条件的资源直接拒绝并给出相应信息
整体的执行流程如下图
本次测试的k8s 集群版本1.17默认是开启了MutatingAdmissionWebhook 与 ValidatingAdmissionWebhook 准入控制器,如果未开启需要添加这两个控制
在master节点的 /etc/kubernetes/manifests文件夹下修改 kube-apiserver.yaml后 api-server会自动重启
ValidatingWebhookConfiguration 与 MutatingWebhookConfiguration
前面提到的MutatingAdmissionWebhook & ValidatingAdmissionWebhook 准入控制器作用是开启准入的webhook调用。但如何调用,哪些资源允许调用则是由ValidatingWebhookConfiguration 与 MutatingWebhookConfiguration来配置
以MutatingWebhookConfiguration为例,可以配置如下条件
- 请求webhook服务的地址路径
- 匹配的k8s资源类型
- …略
以下是一个简单的MutatingWebhookConfiguration配置文件
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration #类型
metadata:
name: my-test-webhook1 #创建MutatingWebhookConfiguration 的名称
webhooks:
- admissionReviewVersions: #指定可接受的 AdmissionReview 对象版本 这里支持v1beta1 v1
- v1beta1
- v1
clientConfig:
# CA根证书内容 需要base64编码
caBundle: JBRUN3YR..
# 只支持https请求
# 配置k8s webhook准入器调用的 webhook服务地址(部署在k8s集群外部)
url: "https://webhooktest.liuyijiang.com/mutate/v1" #webhook web服务访问的地址
# 注意 url和service不能同时存在
# webhook服务部署在k8s集群内部可以使用service的方式访问
# service请求域名为 <servicename>.<namespace>.svc
service:
namespace: my-namespace
name: my-webhook
path: /mutate/v1 #指定请求地址
port: 443 #端口默认是443可以不配置
failurePolicy: Fail
matchPolicy: Exact #精确匹配
name: webhooktest.liuyijiang.com #名称随意但是必须是域名格式
namespaceSelector:
matchLabels:
my-web-inject: enabled #必须匹配标签为my-web-inject=enabled的命名空间内的资源才会被拦截
rules:
- apiGroups:
- ""
apiVersions: #匹配的版本
- v1
operations: #拦截CREATE操作
- CREATE
resources: #拦截执行类型是pod
- pods
scope: '*' #所有命名空间
sideEffects: None #配置是否有副作用,None表示调用 Webhook 没有副作用
timeoutSeconds: 30 #请求超时时间
更多详细配置见Webhook 配置
注意:使用k8s的webhook功能需要提前配置ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration
准入检查(AdmissionReview)
当配置好ValidatingWebhookConfiguration 或 MutatingWebhookConfiguration后,满足条件的kubernetes资源操作就会触发webhook动作。
k8s会对配置中的地址(url中配置的地址或者service配置的地址)发送POST请求。请求的Content-Type为application/json,内容则是一个AdmissionReview json字符串。
我们的程序需求对AdmissionReview json字符串中的内容做分析修改操作完成自己的业务
具体的AdmissionReview参数可以参考官方文档Webhook 请求与响应
一个整体的k8s webhook调用流程如下
使用springboot实现k8s-Webhook
Webhook只是一个k8s执行准入阶段后会调用的一个http地址而已,所以只要按照k8s-Webhook标准的输入和输出实现的一个web项目都可以作为一个Webhook
这里使用java语言来开发测试
web项目可以部署在k8s集群外部也可以部署在集群内部,但k8s访问Webhook必须是https的请求!
在配置动态准入的时候可以有如下两种方式
-
在k8s集群内部
以下配置使用service访问集群内部的webhook
clientConfig:
service:
name: istiod
namespace: istio-system
path: /inject
port: 443 -
在k8s集群外部
以下配置使用url地址访问集群外部的webhook
clientConfig:
url: “https://webhooktest.medcrab.com/mutate”
开发前准备:
- 1 确保k8s集群启用 MutatingAdmissionWebhook控制器,通常是已经启用的
- 2 确保启用了 admissionregistration.k8s.io/v1 API
本例子的k8s集群使用 1.17版本,默认已开启支持
此例子实现一个动态的修改Pod的标签的Webhook,并且webhook部署在集群外部,主要功能如下
当部署pod的时候自动的给pod添加一个新的标签,可以基于此例子扩展,例如动态的添加新的容器用于监控(类似istio的边车模式)
step1 证书创建
k8s访问Webhook必须是https的请求,所以我们需要先准备对应的证书让我们的springboot项目支持https访问
如果有权威机构(花钱买的)签署证书则可以直接使用此证书来部署程序,否则就需要自己制作自签名证书
先定义一个将使用的域名例如 webhooktest.liuyijiang.com 后续请求都使用这个域名访问,我们需要使用根证书对此域名签名
这里将使用cfssl工具来创建证书,关于证书的制作可以参考此文章 使用cfssl为程序添加https证书
准备根证书
首先使用cfssl 创建根证书,这个根证书后续会用在1 创建域名签名证书 2配置webhook的caBundle
ca-csr.json 内容如下
{
"CN": "LYJCA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Chengdu",
"ST": "Sichuan",
"O": "liuyjCA",
"OU": "System"
}
]
}
使用命令 cfssl gencert -initca ca-csr.json | cfssljson -bare ca 创建CA根证书
得到ca.pem(根证书) ca-key.pem(根证书私钥)
用根证书给域名签发
使用生成的ca.pem ca-key.pem 对webhooktest.liuyijiang.com域名签名
ca-config.json 与 webhooktest.liuyijiang.com-csr.json 内容如下
ca-config.json
{
"signing":{
"default":{
"expiry":"8760h" //指定了证书的有效期
},
"profiles":{//配置策略
"mytest":{ //配置一个名称为mytest的策略
"expiry":"8760h", //指定了证书的有效期
"usages":[
"signing", //表示该证书可用于签名其它证书
"key encipherment",
"server auth", // client端(客户端) 可以用该 CA 对 server 提供的证书进行验证
"client auth" // server端(服务端) 可以用该 CA 对 client 提供的证书进行验证
]
}
}
}
}
webhooktest.liuyijiang.com-csr.json
{
"CN":"liuyijiang.com",
"hosts":[
"webhooktest.liuyijiang.com"
],
"key":{
"algo":"rsa",
"size":2048
},
"names":[
{
"C": "CN",
"L": "Chengdu",
"ST": "Sichuan",
"O": "liuyijiang.com",
"OU": "System"
}
]
}
使用命令 cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=mytest webhooktest.liuyijiang.com-csr.json | cfssljson -bare webhooktest.liuyijiang.com
得到webhooktest.liuyijiang.com.pem(证书) webhooktest.liuyijiang.com-key.pem(证书私钥)
springboot项目开启https
springboot目前是不支持.pem证书的,需要把刚才创建的.pem证书转换为p12
使用 openssl命令进行证书转换
命令格式
openssl pkcs12 -export -in "待转换的证书文件" -inkey "待转换的私钥文件" -out "指定生成的p12证书文件"
使用命令 openssl pkcs12 -export -in webhooktest.liuyijiang.com.pem -inkey webhooktest.liuyijiang.com-key.pem -out key.p12 注意输入自定义密码(例如123456)
得到key.p12 证书
将key.p12证书放入到项目的src/main/resources文件夹下,这样只需要修改项目的application.properties文件加入配置即可开启https
server.port=443
server.ssl.protocol=TLS
server.ssl.key-store=classpath:key.p12
server.ssl.key-store-password=123456
server.ssl.key-store-type=PKCS12
验证证书生效
启动springboot项目,修改host文件将 webhooktest.liuyijiang.com 域名映射到127.0.0.1
参考 使用cfssl为程序添加https证书 将刚才的CA根证书放到浏览器的受信根证书里重启浏览器后访问
证书验证成功
到此证书的配置完成
step2 创建MutatingWebhookConfiguration 资源
因为需要在准入阶段动态的修改 Pod配置,所以需要使用MutatingWebhookConfiguration
MutatingWebhookConfiguration配置文件webhook1.yaml内容如下
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: my-test-webhook1 #创建MutatingWebhookConfiguration 的名称
webhooks:
- admissionReviewVersions: #指定可接受的 AdmissionReview 对象版本 这里支持v1beta1 v1
- v1beta1
- v1
clientConfig:
# CA根证书内容
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tL省略
# 只支持https请求
url: "https://webhooktest.liuyijiang.com/mutate/v1" #webhook web服务访问的地址
failurePolicy: Fail
matchPolicy: Exact #精确匹配
name: webhooktest.liuyijiang.com #名称随意但是必须是域名格式
namespaceSelector:
matchLabels:
my-web-inject: enabled #必须匹配标签为my-web-inject=enabled的命名空间内的资源才会被拦截
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations: #拦截CREATE操作
- CREATE
resources: #拦截执行类型是pod
- pods
scope: '*' #所有命名空间
sideEffects: None #配置是否有副作用,None表示调用 Webhook 没有副作用
timeoutSeconds: 30 #请求超时时间
注意:caBundle 内容是一个用 PEM 编码的 CA 证书,用于校验 Webhook 的服务器证书
这里就是使用开始创建的CA证书,可以把k8s-apiserver理解成一个浏览器,当访问webhooktest.liuyijiang.com时需要戴上一个根证书来确保webhook服务的证书正确
注意:如果webhook服务的证书是自签名证书则需要配置caBundle指定为CA根证书,如果webhook服务的证书是权威机构(花钱买的)签署证书则可以不用配置caBundle
使用cat ca.pem | base64 -w 0 将根证书base64编码
注意:webhooktest.liuyijiang.com是一个自建的一个域名,需要在k8s master节点上配置域名映射
192.168.0.204为我本机ip地址
域名能够ping通
curl --cacert ca.pem https://webhooktest.liuyijiang.com/echo 带证书访问正常
部署MutatingWebhookConfiguration资源
kubectl apply -f webhook1.yaml
kubectl get MutatingWebhookConfiguration
到此MutatingWebhookConfiguration的配置完成
step3 java代码编写
当K8S请求webhook时会发送 POST请求时,请求的Content-Type为application/json ,内容是一个AdmissionReview 对象的JSON序列化字符串
AdmissionReview的内容可以参考 https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response
可以根据AdmissionReview request的kind实现自己的业务逻辑
代码编写很简单需要注意以下关键点
- 1 controller中的mapping必须支持POST请求
- 2 注意JSONPatch对字段的操作,需要base64 JSONPatch的操作方式
- 3 响应也必须是一个Content-Type为application/json并且一个包含 AdmissionReview 对象的 JSON 序列化格式字符串
- 4 响应中必须要有response字段并且已被有效填充。uid与allowed字符段必须有值
这里贴出一个极简的代码
/**
* 实现修改性质的准入 Webhook (Mutating Admission Webhook)
*/
@RequestMapping("/mutate/v1")
public String mutateV1(HttpServletRequest req) throws Exception {
InputStream in = req.getInputStream();
/**
* 将输入流转成一个字符串 得到一个AdmissionReview(准入审查json字符串)
*/
BufferedInputStream bis = new BufferedInputStream(in);
byte[] b = new byte[1024];
int len = 0;
StringBuilder sb = new StringBuilder();
while ((len = bis.read(b)) != -1) {
sb.append(new String(b, 0, len));
}
System.out.println(sb);
/**
* 使用fastjson进行字符串转jsonobject
* 转换为一个AdmissionReview jsonobject
*/
JSONObject admissionReview = JSON.parseObject(sb.toString());
/**
*
* 给pod加一个标签 这是一个jsonpatch 操作
* op 代表操作类型
* path 代表操作的json路径
* value 代表修改的值
* 需要把字符串转换为base64编码
*/
String patchStr = "[{ \"op\": \"add\", \"path\": \"/metadata/labels\", \"value\": {\"new-label\":\"webhooktest\"} }]";
Base64 base64= new Base64();
String patch = new String(base64.encode(patchStr.getBytes()));
/**
* uid,从发送到 Webhook 的 request.uid 中复制而来
*/
String uid = admissionReview.getJSONObject("request").getString("uid");
/**
* 创建返回值
*/
Map<String,Object> response = new HashMap<>();
response.put("uid", uid); //创建一个UUID
response.put("allowed", true); //准入
response.put("patchType", "JSONPatch"); //类型是JSONPatch
response.put("patch", patch); //base64后的patch
admissionReview.put("response", response);
String data = admissionReview.toJSONString();
System.out.println(data);
return data;
}
也可以使用java k8s client库 io.fabric8,该库对k8s各种资源操作做了很好的封装
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>6.4.1</version>
</dependency>
以下代码使用 client库实现
step4 测试部署Pod
部署Pod之前需要准备一个两个部署文件
- 1 创建命名空间
- 2 创建Pod
注意:命名空间需要带有MutatingWebhookConfiguration 中 namespaceSelector匹配的标签
namespace.yaml配置文件如下
apiVersion: v1
kind: Namespace #类型 指定为Namespace
metadata:
name: webhook-ns #namespace的名称 注意只能是英文小写和数字
labels:
my-web-inject: enabled #注意: 命名空间需要带有namespaceSelector中匹配的标签
Pod部署文件指定命名空间是刚才创建的命名空间(webhook-ns),注意没有配置Pod的标签
pod.yaml配置文件如下
apiVersion: v1
kind: Pod
metadata:
name: user-service-webhook-test
namespace: webhook-ns
spec:
# 容器配置
containers:
- image: registry.cn-hangzhou.aliyuncs.com/jimliu/user-service:v3
imagePullPolicy: IfNotPresent #Always
name: user-service
ports:
- containerPort: 5555
name: http
protocol: TCP
测试部署
kubectl apply -f namespace.yaml
kubectl apply -f pod.yaml
此时本机的eclipse中已经打印出k8s的请求日志
查看刚才部署的pod,发现已经动态的添加了标签了
kubectl -n webhook-ns get pods -o wide --show-labels
将Webhook部署在k8s集群内部
刚才的例子是将webhook部署在集群外部的方式,下面将测试把将Webhook部署在k8s集群内部,k8s访问内部Webhook使用的是service域名
step1 配置域名
k8s中service的域名默认格式是
<servicename>.<namespace>.svc.<clusterdomain>
使用my-service-webhook作为service的名称,此service后面将部署在my-in-webhook命名空间中!所以service的域名应该是
完整域名 my-service-webhook.my-in-webhook.svc.cluster.local
简写域名 my-service-webhook.my-in-webhook.svc
k8s在请求webhook是会使用my-service-webhook.my-in-webhook.svc这个域名
step2 给Webhook服务配置证书
我们继续使用上面已经存在的CA根证书 创建Webhook服务的证书
service-csr.json内容如下
{
"CN":"liuyijiang.com",
"hosts":[
"my-service-webhook.my-in-webhook.svc"
],
"key":{
"algo":"rsa",
"size":2048
},
"names":[
{
"C": "CN",
"L": "Chengdu",
"ST": "Sichuan",
"O": "liuyijiang.com",
"OU": "System"
}
]
}
注意hosts中的域名为my-service-webhook.my-in-webhook.svc
使用命令 cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=mytest service-csr.json | cfssljson -bare service 签名域名证书
得到service.pem(证书) service-key.pem(证书私钥)
使用命令 openssl pkcs12 -export -in service.pem -inkey service-key.pem -out key2.p12
得到key2.p12
修改配置application.properties
server.port=443
server.ssl.protocol=TLS
server.ssl.key-store=classpath:key2.p12
server.ssl.key-store-password=123456
server.ssl.key-store-type=PKCS12
到此证书配置完成
step3 创建镜像并推送到私库
将项目打包成jar文件制作镜像
docker build -t webhook .
docker tag webhook registry.cn-hangzhou.aliyuncs.com/jimliu/webhook:v1
docker push registry.cn-hangzhou.aliyuncs.com/jimliu/webhook:v1
到此webhook镜像创建完成
step4 部署webhook到k8s集群
部署配置文件如下deploy.yaml
apiVersion: v1
kind: Namespace #类型 指定为Namespace
metadata:
name: my-in-webhook #namespace的名称 注意只能是英文小写和数字
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webhook-pod-deploy
namespace: my-in-webhook
spec:
replicas: 1
selector:
matchLabels:
app: webhook-pod
template:
metadata:
labels:
app: webhook-pod
spec:
imagePullSecrets:
- name: myaliyunsecret
containers:
- name: webhook-run-container
image: registry.cn-hangzhou.aliyuncs.com/jimliu/webhook:v1
imagePullPolicy: IfNotPresent #Always
ports:
- containerPort: 443
protocol: TCP
name: http
---
apiVersion: v1
kind: Service
metadata:
name: my-service-webhook
namespace: my-in-webhook
spec:
ports:
- protocol: TCP
port: 443
targetPort: 443
name: http
selector:
app: webhook-pod
type: ClusterIP
部署webhook
kubectl apply -f deploy.yaml
pod service 成功部署运行
到此webhook相关Pod Service部署完成
step5 创建MutatingWebhookConfiguration 资源
与外部webhook不同,内部部署的webhook不使用url而是配置service 其他的都一致
webhook2.yaml内容如下
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: my-test-webhook2 #创建MutatingWebhookConfiguration 的名称
webhooks:
- admissionReviewVersions: #指定可接受的 AdmissionReview 对象版本 这里支持v1beta1 v1
- v1beta1
- v1
clientConfig:
# CA根证书内容
caBundle: LS0tLS1CRUdJTiBDRVJUS略..
# 只支持https请求
service:
name: my-service-webhook # service的名称
namespace: my-in-webhook #service所在的命名空间
path: /mutate/v1 #指定请求地址
port: 443 #端口默认是443可以不配置
failurePolicy: Fail
matchPolicy: Exact #精确匹配
name: my-service-webhook.my-in-webhook.svc #名称随意但是必须是域名格式
namespaceSelector:
matchLabels:
my-web-inject: enabled #必须匹配标签为my-web-inject=enabled的命名空间内的资源才会被拦截
rules:
- apiGroups:
- ""
apiVersions: #匹配的版本
- v1
operations: #拦截CREATE操作
- CREATE
resources: #拦截执行类型是pod
- pods
scope: '*' #所有命名空间
sideEffects: None #配置是否有副作用,None表示调用 Webhook 没有副作用
timeoutSeconds: 30 #请求超时时间
注意:caBundle 还是使用先前创建的CA根证书
部署MutatingWebhookConfiguration
到此MutatingWebhookConfiguration部署完成
step6 测试效果
kubectl apply -f namespace.yaml
kubectl apply -f pod.yaml
使用kubectl -n my-in-webhook logs -f webhook-pod-deploy-6bb498fbf6-4jkm9 查看日志
查看刚才部署的pod,发现已经动态的添加了标签了
kubectl -n webhook-ns get pods -o wide --show-labels