使用 Python 编写 Kubernetes 验证 Webhook

语言: CN / TW / HK

在我对 Kubernetes 的不断学习中,我的兴趣有所偏离并转向了它提供的扩展。

对Operator、自定义资源、定义、webhooks 以及 Kubernetes 提供的其他东西产生了更多的好奇心。

在阅读和研究了一段时间后,我决定尝试 构建自己的验证 webhook

作为一个知名的项目,它非常出色,需要考虑很多事情,直到一切都组合在一起并开始工作。

现在一起了解下什么是 webhook 或准入控制器?

An admission controller is a piece of code that intercepts requests to the Kubernetes API server before persistence of the object, but after the request is authenticated and authorized.

准入控制器是一段代码,它在对象持久化之前,但在请求经过身份验证和授权之后,拦截对Kubernetes API服务器的请求。

在 Kubernetes 中,有两种类型的准入控制器,称为 ValidatingAdmissionWebhookMutatingAdmissionWebhook

正如他们的名字所暗示的那样;一个只验证请求,另一个在不符合规范的情况下修改请求。

现在我不会详细介绍它们。如果您想阅读更多内容,这里有一个指向官方文档的链接,其中包含更广泛的解释。

https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/

webhooks 的伟大之处在于,您 可以使用您喜欢的任何语言编写自己的语言以及自定义逻辑,无论是用于验证还是更改请求

我决定从使用 Python 和 Flask 框架的验证 webhook 开始。

只编写 webhook 逻辑是一回事,但设置它并使其工作是另一回事。

为了引导整个事情,在我的情况下它需要:

  • Python 和 Flask 框架

  • Docker

  • SSL证书

  • 还有一堆 Kubernetes 文件(服务、部署、密钥……)

一个简单的网络钩子就完成了这一切?!

虽然看起来有点乏味,但我保证这是一个非常有趣和有益的项目。

项目文件也可以在我的GitHub 存储库中找到。

https://github.com/k-mitevski/kubernetes-validating-webhook

让我们从头开始。

Python代码

有了这个基本功能,我用不到 50 行的 Python 代码编写了 webhook 代码。使用 Flask 使这变得非常容易。

互联网上有很多示例,您可以从中汲取灵感来编写它。

它最终会归结为两件事。

你需要:

  1. 分析请求并根据设定的规则验证或改变它

  2. 将 HTTP 响应发送回 Kubernetes 准入控制器

您添加到代码中的所有其他内容都是加分项。

此验证 webhook 将检查标签是否存在于部署创建中。

如果不是,则请求被阻止并显示错误消息。

我添加了从环境变量设置所需标签的选项,还包括日志记录。

最初,我想用 Django,但认为这对它来说太过分了,所以我选择用 Flask 来做。

它最终变得非常干净和简单:

from flask import Flask, request, jsonify
from os import environ
import logging


webhook = Flask(__name__)


webhook.config['LABEL'] = environ.get('LABEL')


webhook.logger.setLevel(logging.INFO)




if "LABEL" not in environ:
webhook.logger.error("Required environment variable for label isn't set. Exiting...")
exit(1)




@webhook.route('/validate', methods=['POST'])
def validating_webhook():
request_info = request.get_json()
uid = request_info["request"].get("uid")


if request_info["request"]["object"]["metadata"]["labels"].get(webhook.config['LABEL']):
webhook.logger.info(f'Object {request_info["request"]["object"]["kind"]}/{request_info["request"]["object"]["metadata"]["name"]} contains the required \"{webhook.config["LABEL"]}\" label. Allowing the request.')


return admission_response(True, uid, f"{webhook.config['LABEL']} label exists.")
else:
webhook.logger.error(f'Object {request_info["request"]["object"]["kind"]}/{request_info["request"]["object"]["metadata"]["name"]} doesn\'t have the required \"{webhook.config["LABEL"]}\" label. Request rejected!')


return admission_response(False, uid, f"The label \"{webhook.config['LABEL']}\" isn't set!")




def admission_response(allowed, uid, message):
return jsonify({"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response":
{"allowed": allowed,
"uid": uid,
"status": {"message": message}
}
})




if __name__ == '__main__':
webhook.run(host='0.0.0.0', port=5000)

第一件事是设置环境变量和日志记录。

初始if条件检查标签环境变量不为空,这是 webhook 工作所必需的。

从那里,在 POST 请求时,代码检查设置标签是否存在于 Deployment 的metadata字段中。如果标签不存在,准入控制器将拒绝部署。

我添加了记录器功能,因此它会根据请求的状态打印出一条日志消息。

第二个功能只是响应准入控制器。如果允许请求是true或 ,则使用 HTTP 响应false。

该Kubernetes文档,也有例子的反应应该是什么样子等。

https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response

如文档中所述,UID和allowed字段必须出现在AdmissionReview响应中。

现在有一件至关重要的事情……您必须在 来自 Webhook 和准入控制器的流量之间提供 SSL 加密

您选择如何提供证书和密钥取决于您。

您可以将它们打包到 docker 镜像中,也可以使用 Kubernetes 密钥 单独注入它们。我选择了后者。

在requirements.txt与库可以生成,或在 这里 存储库中找到。

https://github.com/k-mitevski/kubernetes-validating-webhook/blob/master/requirements.txt

Dockerfile 参考:

FROM ubuntu:20.10


RUN apt-get update -y && apt-get install -y python3-pip python-dev


WORKDIR /app


COPY requirements.txt /app/requirements.txt


RUN pip3 install -r /app/requirements.txt


COPY validate.py /app


COPY wsgi.py /app


CMD gunicorn --certfile=/certs/webhook.crt --keyfile=/certs/webhook.key --bind 0.0.0.0:443 wsgi:webhook

对于镜像,您可以选择所需的任何基本镜像。

其他事情是安装 Python、Flask、Gunicorn、库,并复制代码。

重要的是证书和密钥。

Gunicorn 将查看/certs路径,因此当它们安装在 pod 上时,它们必须在那里可用。

注意:如果您不熟悉,Gunicorn是应用服务器,它将通过WSGI协议将请求转发到您的 webhook 应用程序。

Deployment文件

部署 webhook 需要四个部分才能完成。

首先是 webhook 部署:

apiVersion: apps/v1
kind: Deployment
metadata:
name: validation-webhook
labels:
app: validate
spec:
replicas: 1
selector:
matchLabels:
app: validate
template:
metadata:
labels:
app: validate
spec:
containers:
- name: webhook
image: kmitevski/webhook:gunicorn
ports:
- containerPort: 443
env:
- name: LABEL
value: development
volumeMounts:
- name: certs-volume
readOnly: true
mountPath: "/certs"
imagePullPolicy: Always
volumes:
- name: certs-volume
secret:
secretName: admission-tls

webhook 可用的服务:

apiVersion: v1
kind: Service
metadata:
name: validate
spec:
selector:
app: validate
ports:
- port: 443

包含证书和密钥的Secret清单:

apiVersion: v1
kind: Secret
metadata:
name: admission-tls
type: Opaque
data:
webhook.crt: YOUR ENCODED BASE64 CERT
webhook.key: YOUR ENCODED BASE64 KEY

最后是验证 Webhook 配置文件:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook
webhooks:
- name: validate.default.svc
failurePolicy: Fail
sideEffects: None
admissionReviewVersions: ["v1","v1beta1"]
rules:
- apiGroups: ["apps", ""]
resources:
- "deployments"
apiVersions:
- "*"
operations:
- CREATE
clientConfig:
service:
name: validate
namespace: default
path: /validate/
caBundle: YOUR ENCODED BASE64 CERT

一起编译

为了将所有内容整合在一起并使 webhook 正常工作,您应该按照某种顺序部署事物。

我首先生成证书(您需要将其提供给 Gunicorn)和 webhook 配置。

我有很多尝试和错误要做,直到整个事情就位并开始工作。

生成证书

要生成证书,请使用该openssl工具:

openssl req -x509 -sha256 -newkey rsa:2048 -keyout webhook.key -out webhook.crt -days 1024 -nodes -addext "subjectAltName = DNS.1:validate.default.svc"

现在subjectAltName有一个问题,SAN –证书中必须包含 DNS 记录!

您必须将该 DNS 与您的服务名称和命名空间相匹配。记号是 service_name.namespace.svc

如果不这样做,您可能会感到苦涩,并希望 rm -rf * 整个事情。

我多次接近这个!

一切都会运行,但是当您决定测试 webhook 时,会弹出这个令人讨厌的消息:

open ssl x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0

要牢记这一点,您需要 在多个地方引用服务的名称

确保它保持不变,否则以后更改它将需要更改证书和 webhook 配置。

创建 Kubernetes 密钥

Kubernetes Secret 需要包含以 base64 格式编码的证书和密钥。

这在 Linux 上很容易做到:

cat webhook.key | base64 | tr -d '\n'
#LS0tLS1CRUdJTiBQUklWQVRFI....
cat webhook.crt | base64 | tr -d '\n'
#LS0tLS1CRUdJTiBDRVJUSUZJQ0FUR

复制webhook.crt输出并将其粘贴到密钥和 webhook 配置清单。

也复制webhook.key,尽管密钥只需要包含在 Secret   中。

复制时要小心,以免在提示中包含您的用户名:)。

Pod 规范中的挂载点设置在/certs.

您可以更改它,但请确保使用在 Gunicorn 证书和密钥路径中完成的更改创建一个新镜像。

创建镜像

文件夹和文件结构是这样的:

tree .
.
|-- Dockerfile
|-- kubernetes-manifests
| |-- label.yaml
| |-- webhook-config.yaml
| |-- webhook-deploy.yaml
| |-- webhook-secret.yaml
| `-- webhook-service.yaml
|-- requirements.txt
|-- validate.py
|-- webhook.crt
|-- webhook.key
`-- wsgi.py

我从根文件夹构建了 Docker 镜像。包括Dockerfile 所在的位置,以及 Python 文件和证书。

这wsgi.py只是 Gunicorn 运行 webhook 应用程序的帮助文件。

如果您打算使用自己的镜像,则需要使用以下命令作为示例来构建、标记并将其推送到存储库:

docker build -t webhook:gunicorn -f Dockerfile .
docker tag webhook:gunicorn kmitevski/webhook
docker push kmitevski/webhook:gunicorn

引导应用程序

  1. 创建证书

  2. 创建 Docker 镜像

  3. 将 Secret 清单应用到集群

  4. 应用部署、服务和 Webhook 配置清单

  5. 测试网络钩子

为了进行测试,您可以尝试使用 Nginx 镜像创建一个简单的部署。

这可以使用命令式方法轻松测试:

$ kubectl create deploy nginx --image=nginx
error: failed to create deployment: admission webhook "validate.default.svc" denied the request: The label "development" isn't set!

如果您检查 webhook pod 日志:

ERROR in validate: Object Deployment/nginx doesn't have the required "development" label. Request rejected!

现在尝试部署label.yaml清单。

$ kubectl apply -f label.yaml
deployment.apps/nginx created
INFO in validate: Object Deployment/nginx contains the required "development" label. Allowing the request.

终于成功了!!验证网络钩子有效!

本文作者:MITEVSKI

文章翻译:CloudNative.CC

文章来源:https://kmitevski.com/writing-a-kubernetes-validating-webhook-using-python/