
一、背景介绍
在 Kubernetes 集群中,默认情况下 Pod 可以被调度到任何节点上。但在实际生产环境中,我们经常有这样的需求:
- GPU 节点限制:只有特定的应用才能使用 GPU 节点
- 高可用隔离:关键应用只运行在特定节点
- 资源隔离:不同团队使用不同的节点池
- 安全合规:某些节点只允许特定权限的 Pod 运行
虽然 Kubernetes 提供了 nodeSelector 和 nodeAffinity 可以实现节点选择,但这些配置是由用户主动设置的,无法强制约束。如果用户不指定或指定了错误的节点,Pod 仍然可以被调度到不该去的节点。
解决方案:通过 Admission Webhook 强制限制 Pod 只能调度到白名单节点。
二、什么是 Admission Webhook?
Admission Webhook 是 Kubernetes 提供的准入控制机制,可以在 Pod 创建、更新、删除时进行拦截和校验。
两种 Webhook 类型:
| 类型 | 作用 | 说明 |
| ValidatingWebhook | 验证请求 | 检查 Pod 配置是否合规,不合规则拒绝 |
| MutatingWebhook | 修改请求 | 在 Pod 创建时自动注入配置 |
工作流程:
![图片[1]-实战:用 Kubernetes Webhook 实现 Pod 节点调度白名单-UCBK · 栈域](https://cos.ucbk.cn/wp-content/uploads/2026/08/202608111425474166a7ac0.png?imageMogr2/format/webp/interlace/0|watermark/2/text/VUNCSyDCtyDmoIjln59AeGlhb2hlaQ%3D%3D/font/5bm85ZyGLlRURg/fontsize/12/fill/I2I1YjViNQ%3D%3D/dissolve/30/shadow/30/batch/1/degree/320)
三、功能特性
本项目实现了一套完整的 Webhook 方案,具备以下功能:
1.节点白名单控制
只允许 Pod 在指定的节点上运行,白名单节点通过环境变量配置:
ALLOWED_NODES: "node-01,node-02,node-03"
2.自动注入节点选择器
用户创建 Pod 时可以不指定节点,Webhook 会自动:
- 选择白名单中的一个节点
- 注入 nodeSelector: {“allowed-node”: “true”}
3.强制指定节点
用户也可以显式指定 nodeName,Webhook 会校验是否在白名单中。
4.命名空间级别控制
通过 namespaceSelector 控制 Webhook 只在特定命名空间生效。
5.完整的 RBAC 权限控制
通过 ServiceAccount + RBAC 精细化控制谁能创建 Pod。
四、架构设计
![图片[2]-实战:用 Kubernetes Webhook 实现 Pod 节点调度白名单-UCBK · 栈域](https://cos.ucbk.cn/wp-content/uploads/2026/08/202608111428244336a7ac1.png?imageMogr2/format/webp/interlace/0|watermark/2/text/VUNCSyDCtyDmoIjln59AeGlhb2hlaQ%3D%3D/font/5bm85ZyGLlRURg/fontsize/12/fill/I2I1YjViNQ%3D%3D/dissolve/30/shadow/30/batch/1/degree/320)
五、快速部署
1.下载项目
git clone https://github.com/MoeXiaoHei/webhook-node-validator.git
cd webhook-node-validator
2. 修改配置
编辑 5-webhook-deployment.yaml,设置白名单节点:
env:
- name: ALLOWED_NODES
value: "node01,node01,node01" # 改为你的节点
3.一键部署
# 给脚本添加执行权限
chmod +x deploy.sh cleanup.sh generate-certs.sh
# 执行部署
./deploy.sh
4.验证部署
# 查看 Webhook Pod 状态
kubectl get pods -n my-namespace -l app=node-validation-webhook
# 输出示例:
# NAME READY STATUS RESTARTS AGE
# node-validation-webhook-cb47f8996-q54jp 1/1 Running 0 5m
# node-validation-webhook-cb47f8996-x6rdl 1/1 Running 0 5m
# 查看日志
kubectl logs -n my-namespace -l app=node-validation-webhook
六、使用方式
1.获取 Token
# 获取 ServiceAccount Token
SA_SECRET=$(kubectl get serviceaccount multi-node-operator-sa -n my-namespace -o jsonpath='{.secrets[0].name}')
TOKEN=$(kubectl get secret ${SA_SECRET} -n my-namespace -o jsonpath='{.data.token}' | base64 -d)
echo ${TOKEN}
2.创建 Pod(指定节点)
# 在白名单节点创建 Pod
kubectl run my-pod \
--image=nginx:latest \
--restart=Never \
-n my-namespace \
--overrides='{"spec":{"nodeName":"node01"}}' \
--token=${TOKEN}
3.创建 Pod(不指定节点)
4.使用 Java 代码调用
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class K8sClient {
private static final String API_URL = "https://kubernetes.default.svc:6443";
private static final String TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6...";
private final HttpClient httpClient = HttpClient.newHttpClient();
public String createPod(String namespace, String podName, String nodeName) throws Exception {
String url = API_URL + "/api/v1/namespaces/" + namespace + "/pods";
String podJson = String.format(
"{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"name\":\"%s\"},\"spec\":{\"nodeName\":\"%s\",\"containers\":[{\"name\":\"nginx\",\"image\":\"nginx:latest\"}]}}",
podName, nodeName
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(podJson))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
}
5.使用 Fabric8 Kubernetes Client
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
public class K8sClient {
private final KubernetesClient client;
public K8sClient(String token) {
Config config = new ConfigBuilder()
.withMasterUrl("https://kubernetes.default.svc:6443")
.withOauthToken(token)
.withTrustCerts(true)
.build();
this.client = new KubernetesClientBuilder().withConfig(config).build();
}
public Pod createPod(String namespace, String name, String nodeName) {
Pod pod = new PodBuilder()
.withNewMetadata().withName(name).endMetadata()
.withNewSpec()
.withNodeName(nodeName)
.addNewContainer().withName("nginx").withImage("nginx:latest").endContainer()
.endSpec()
.build();
return client.pods().inNamespace(namespace).create(pod);
}
}
6.使用 curl 调用
# 创建 Pod
curl -k -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
https://kubernetes.default.svc:6443/api/v1/namespaces/my-namespace/pods \
-d '{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": "my-pod"},
"spec": {
"nodeName": "node01",
"containers": [{"name": "nginx", "image": "nginx:latest"}]
}
}'
七、测试验证
测试 1:在白名单节点创建 Pod(成功)
kubectl run test-allowed \
--image=nginx:latest \
--restart=Never \
-n my-namespace \
--overrides='{"spec":{"nodeName":"node01"}}' \
--token=${TOKEN}
# 查看 Pod
kubectl get pod test-allowed -n my-namespace -o wide
# NAME READY STATUS RESTARTS AGE IP NODE
# test-allowed 1/1 Running 0 10s 10.244.1.2 node01
测试 2:在非白名单节点创建 Pod(失败)
kubectl run test-denied \
--image=nginx:latest \
--restart=Never \
-n my-namespace \
--overrides='{"spec":{"nodeName":"node-04"}}' \
--token=${TOKEN}
# 预期输出:
# Error from server: admission webhook "node-validation.my-namespace.svc" denied the request: Node 'node-03' is not allowed. Allowed nodes: node01, node02, node03
测试 3:不指定节点(自动调度)
kubectl run test-auto \
--image=nginx:latest \
--restart=Never \
-n my-namespace \
--token=${TOKEN}
# 查看 Pod 自动调度到哪个节点
kubectl get pod test-auto -n my-namespace -o wide
八、命名空间级别控制
通过 namespaceSelector 控制 Webhook 只在特定命名空间生效:
7-validating-webhook.yaml
# 7-validating-webhook.yaml
namespaceSelector:
matchLabels:
webhook-enabled: "true" # 只有打了这个标签的命名空间才生效
启用命名空间
kubectl label namespace my-namespace webhook-enabled=true
禁用命名空间
kubectl label namespace my-namespace webhook-enabled-
查看命名空间标签
kubectl get namespaces --show-labels
九、清理
# 一键清理所有资源
./cleanup.sh
十、项目文件说明
| 文件 | 作用 |
|---|---|
1-serviceaccount.yaml | ServiceAccount,用于生成 Token |
2-rbac.yaml | RBAC 权限配置 |
3-webhook-code-configmap.yaml | Webhook Python 代码 |
5-webhook-deployment.yaml | Webhook Deployment |
6-webhook-service.yaml | Webhook Service |
7-validating-webhook.yaml | ValidatingWebhookConfiguration |
9-mutating-webhook.yaml | MutatingWebhookConfiguration |
deploy.sh | 一键部署脚本 |
cleanup.sh | 一键清理脚本 |
十一、总结
通过 Admission Webhook 方案,我们可以:
- 强制限制 Pod 只能调度到白名单节点
- 自动注入 nodeSelector,用户无需手动指定
- 灵活控制 命名空间级别的生效范围
- 最小改动 用户代码无需修改,只需使用特定 Token
适用场景:
- 多租户集群资源隔离
- GPU 节点专机专用
- 高可用应用节点限制
- 安全合规要求
🎉 项目地址: https://github.com/MoeXiaoHei/webhook-node-validator.git
点点赞赏,手留余香
共 0 人








请登录后查看评论内容