Kubernetes 开发【1】——webhook 实现 API Server 请求拦截和修改

目录

技术背景

什么是admission controller?

应用场景

具体实现

代码结构

签发证书

创建对象 

测试api


技术背景

什么是admission controller?

admission controller是一段代码,它会在请求通过认证和授权之后、对象被持久化之前拦截到达 API 服务器的请求。控制器编译进 kube-apiserver 可执行文件,并且只能由集群管理员配置。

这有点类似于插件的概念,官方提供了一系列的插件来帮助我们实现一些api层面的简单处理:

使用准入控制器 | Kubernetes

我们也可以通过自己编写一段代码二次开发来实现更为高级更为复杂的需求,

官方有具体的实例,该实例的用途是

only allow pods to pull images from specific registry.

即仅允许pod从指定的镜像仓库拉取image,否则apiserver将会拒绝本次请求。

应用场景

从运维角度,我们也可以借此约束非集群管理员的某些行为来实现安全运维的目的,或者通过admission controller的另外一个用途:修改请求,通过自动为k8s声明式的api自动注入配置规则,为非集群管理员的一些非约性的apply行为解绑。

举几个简单场景:

1.开发人员可能只有对应namespace下deployment的create,upduate,delete权限,,我们通过自动注入私仓的dockerconfig.json的secret对象作为imagePullSecret,来让开发人员尽可能少关注与应用本身描述无关的,诸如此类的配置选项。

2.传统微服务架构依赖注册中心,因此在pod终止时如何从注册中心下线来实现优雅停机成为了问题,为了实现低代码侵入,将这部分问题的解决下沉到运维层面,我们可以使用k8s提供prestop机制来实现pod在收到sigterm信号之前就处理掉这个问题。

具体实现

接下来我们来看具体实现

代码结构

.
├── go.mod
├── go.sum
├── lib
│   ├── config.go //用于配置tls证书和密钥
│   ├── convert.go //将具体的error转换成webhook回调返回的对象
│   ├── pods.go //输入请求对象,return一个返回对象,需要我们在其中实现判断逻辑
│   └── schema.go //存放了用于将byte转换成请求对象的反序列化器
├── main.go //http server并配置tls,反序列化后调用pods.go获得返回对象

其中的大部分代码都可以在kubernetes/main.go at release-1.21 · kubernetes/kubernetes · GitHub 

中获取到。

main.go中httpserver的handler:

http.HandleFunc("/pods", func(writer http.ResponseWriter, request *http.Request) {
	var body []byte
	if request.Body != nil {
		if data, err := ioutil.ReadAll(request.Body); err == nil {
			body = data
		}
	}
	//第二步
	reqAdmissionReview := v1.AdmissionReview{} //请求
	resAdmissionReview := v1.AdmissionReview{  //响应 ---完整的对象在 https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/extensible-admission-controllers/#response
		TypeMeta: metav1.TypeMeta{
			Kind:       "AdmissionReview",
			APIVersion: "admission.k8s.io/v1",
		},
	}
	//第三步,把body decode成对象
	deserializer := lib.Codecs.UniversalDeserializer()
	if _, _, err := deserializer.Decode(body, nil, &reqAdmissionReview); err != nil {
		resAdmissionReview.Response = lib.ToV1AdmissionResponse(err)
	} else {
		resAdmissionReview.Response = lib.AdmitPods(reqAdmissionReview)
	}

	resAdmissionReview.Response.UID = reqAdmissionReview.Request.UID
	marshal, _ := json.Marshal(resAdmissionReview)
	writer.Write(marshal)
})

签发证书

完成编码后,我们需要生成假证书,可以通过cfssl工具生成,参考下面这篇文章:

手把手-安装-cfssl - 光速狼 - 博客园

 这里说下大概的步骤——

1.创建ca配置文件 (ca-config.json)

"ca-config.json":可以定义多个 profiles,分别指定不同的过期时间、使用场景等参数;后续在签名证书时使用某个 profile;

"signing":表示该证书可用于签名其它证书;生成的 ca.pem 证书中 CA=TRUE

{
  "signing": {
    "default": {
      "expiry": "87600h"
    },
    "profiles": {
      "server": {
        "usages": ["signing"],
        "expiry": "87600h"
      }
    }
  }
}

2.创建ca证书签名(ca-csr.json)

{
  "CN": "Kubernetes", //SelfSignedCA
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "zh",
      "L": "bj",
      "O": "bj",
      "OU": "CA"
   }
  ]
}

3.生成ca证书和私钥

cfssl gencert -initca ca-csr.json | cfssljson -bare ca

——生成ca私钥 ca-key.pem 和 ca公钥ca.pem

4.创建服务端证书签名(server-csr.json)

        ——这个服务端可以是etcd,docker,apiserver等

{
  "CN": "admission", //etcd
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "zh",
      "L": "bj",
      "O": "bj",
      "OU": "bj"
    }
  ]
}

5.生成服务端证书server.pem和私钥server-key.pem,也就是签发证书

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=myhook.ops.svc \
  -profile=server \
  server-csr.json | cfssljson -bare server

注意hostname与service的fqdn域名相对应。

创建对象 

1.cat ca.pem | base64

获取ca公钥填入admission webhook资源yaml的cabundle

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: myhook
webhooks:
  - clientConfig:
      caBundle: |
        LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURoakNDQW02Z0F3SUJBZ0lVZU41ZU5td29t
        RXZMREFvSUpnYjhTVUZUandFd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNl
        bWd4Q3pBSkJnTlZCQWNUQW1KcU1Rc3dDUVlEVlFRS0V3SmlhakVMTUFrRwpBMVVFQ3hNQ1EwRXhF
        ekFSQmdOVkJBTVRDa3QxWW1WeWJtVjBaWE13SGhjTk1qSXdPREF5TURJeU56QXdXaGNOCk1qY3dP
        REF4TURJeU56QXdXakJKTVFzd0NRWURWUVFHRXdKNmFERUxNQWtHQTFVRUJ4TUNZbW94Q3pBSkJn
        TlYKQkFvVEFtSnFNUXN3Q1FZRFZRUUxFd0pEUVRFVE1CRUdBMVVFQXhNS1MzVmlaWEp1WlhSbGN6
        Q0NBU0l3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMVk5rY1pUcWpkdHJD
        Yzh3YkhqMXlVejhucHl2QkVECkJxNmFyZElOU2NBRldyT0wzNVRJVmNOZnFQRWlhbkxhbjJsbkFG
        dWp1UE9WaFhYOS9CMzhoSkU1QjJBeXhTYXgKYzdxM1lUWnpYS0xsQ2c1UVc0Wlk2SVFVdTdGbHZ2
        T0RIRjEyNTl6OEd4dGFsdjQ1Z0pYSDV5Nnp4WlNBMEJxZgpURnR3bHFRb1krMVN3QkhtZ2lBRWpX
        ekxZV0cydHVRRndkZW9YR2tWd0Y2dkwzM1NoM09yb1ZHQTQ2aVRiMUdKCkgxenBKNWpVYlpZbFAx
        SFVta0R4dnF1NDJJaGJnN1lPNWIvUktMaWpvVVJza0p2d1dzb3dvVU03dU1GSllrdUsKdFc3MlJ3
        S1UwNnoxN20xUmxZZlpzWkt2NU9ONjd3RlR4M0plQUlTcno1OHBKN2cwaUwySTh0MENBd0VBQWFO
        bQpNR1F3RGdZRFZSMFBBUUgvQkFRREFnRUdNQklHQTFVZEV3RUIvd1FJTUFZQkFmOENBUUl3SFFZ
        RFZSME9CQllFCkZPWWRwMzJkL09VanM0VkpMMDZyRTJtZlAwZkZNQjhHQTFVZEl3UVlNQmFBRk9Z
        ZHAzMmQvT1VqczRWSkwwNnIKRTJtZlAwZkZNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFrYXVN
        T01OQU9DTUN2QmFqR0JhYUhXRFpBekdvTAo5cHJhWWRDWEwvQkw4Z25qczJsZmRjcWZZclpUM3pY
        Q3IzNXlmUEJSZitNUFRtKzdlQkF5bHlCeWlNK0xPcDRMCkM5MEVWT2NUM0hxK2EvUlBURjJEbmxB
        emwva1JkMGN3RFM2WTdLUGovQWxlc3FzVUNQVXVLbVlnb3hadmNqa04KU1NYVEs4VWk2Vncyekd2
        MzU5bFR0QjA3Y3paZjhYR09xeEZpQi9tUUVERldOOGxxYkF3b2k1NHVZbHlsaXowcApyWFp0cHhy
        N0tza0dHb08rcTVEdTRwVnZUNlFUakd6NzNlYktacnRieURsbzBnbDdCZmxPTHBEM1d0WWw0b3N4
        CmlYZ3NYaVNxdmNUcEkrbGJLNUQ3dlMrS1pybXpmaFozWWRWdW9mR083UCtsMFUwKzZaUytjMi9v
        Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
      service:
        name: myhook
        namespace: ops
        path: /pods
    failurePolicy: Fail
    sideEffects: NoneOnDryRun
    name: myhook.xxx.com
    admissionReviewVersions: ["v1", "v1beta1"]
    namespaceSelector: 
      matchExpressions:
        - key: env
          operator: In
          values: ["prod", "pre", "dev", "test"]
    rules:
      - apiGroups:   [""]
        apiVersions: ["v1"]
        operations:  ["CREATE"]
        resources:   ["pods"]

 ——service对应webhook的service,namespaceSelector通过label限定作用域命名空间,rules匹配GVK。

2.把上一节生成服务端私钥和证书创建成tls类型的secret

kubectl create secret tls myhook --cert=server.pem --key=server-key.pem  -n ops

3.在我们的webhook代码里添加tls认证

tlsConfig := lib.Config{
	CertFile: "/etc/webhook/certs/tls.crt",
	KeyFile:  "/etc/webhook/certs/tls.key",
}

server := http.Server{
	Addr:      ":443",
	TLSConfig: lib.ConfigTLS(tlsConfig),
}

err := server.ListenAndServeTLS("", "")
if err != nil {
	panic(err)
}

通过交叉编译我们得到myhook可执行文件。

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build

4. 创建webhook deployment(仅测试用,需要指定节点)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myhook
  namespace: ops
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myhook
  template:
    metadata:
      labels:
        app: myhook
    spec:
      nodeName: k8s1
      containers:
        - name: myhook
          image: alpine:3.12
          imagePullPolicy: IfNotPresent
          command: ["/app/myhook"]
          volumeMounts:
            - name: hooktls
              mountPath: /etc/webhook/certs
              readOnly: true
            - name: app
              mountPath: /app
          ports:
            - containerPort: 443
      volumes:
        - name: app
          hostPath:
            path: /root/app
        - name: hooktls
          secret:
            secretName: myhook
---
apiVersion: v1
kind: Service
metadata:
  name: myhook
  namespace: ops
  labels:
    app: myhook
spec:
  type: ClusterIP
  ports:
    - port: 443
      targetPort: 443
  selector:
    app: myhook

——把服务端证书和私钥通过secret的方式挂在到容器里,并通过443端口对外提供服务。

测试api

最后可以通过apply一个新的pod来测试是否通过webhook的验证规则,如果未通过,则不会继续持久化对象。如果通过,则会触发对apiserver的回调。

% kubectl apply -f newpod.yaml 
Error from server: error when creating "newpod.yaml": admission webhook "myhook.xxx.com" denied the request: container's image must be from private hub.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

常鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值