前言
在上一篇文章中,我们规划了要开发一个自动注入Nginx Sidecar的Webhook。但在真正开始写代码之前,必须先做好充分的准备工作。
我曾经踩过一个坑:代码写完了,部署到集群却发现apiserver根本没有启用MutatingAdmissionWebhook插件,导致Webhook完全不生效。回过来检查配置、重新编译apiserver,浪费了大量时间。
今天就详细讲讲开发K8s准入控制器前的准备工作,包括集群环境检查、项目初始化、配置设计等。
第一步:检查集群准入配置
1.1 检查Admission Registration API
首先确认集群是否启用了准入控制的API:
kubectl api-versions | grep admission
期望的输出:
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
说明:
v1是稳定版本(Kubernetes 1.16+)v1beta1是旧版本,建议用v1- 如果没有输出,说明集群版本太旧(<1.9),不支持Webhook
1.2 检查准入控制插件
确认apiserver启用了MutatingAdmissionWebhook和ValidatingAdmissionWebhook:
# 方法1:查看apiserver进程参数
ps aux | grep kube-apiserver | grep enable-admission-plugins
# 方法2:如果是静态Pod
kubectl get pod -n kube-system -l component=kube-apiserver -o yaml | grep enable-admission-plugins
# 方法3:查看kube-apiserver帮助(如果在本地有apiserver二进制文件)
kube-apiserver -h | grep enable-admission-plugins
Kubernetes 1.20+默认启用的插件:
NamespaceLifecycle, LimitRanger, ServiceAccount, TaintNodesByCondition,
PodSecurity, Priority, DefaultTolerationSeconds, DefaultStorageClass,
StorageObjectInUseProtection, PersistentVolumeClaimResize, RuntimeClass,
CertificateApproval, CertificateSigning, CertificateSubjectRestriction,
DefaultIngressClass, MutatingAdmissionWebhook, ValidatingAdmissionWebhook,
ResourceQuota
关键插件:
- ✅
MutatingAdmissionWebhook:必须启用,用于Mutating Webhook - ✅
ValidatingAdmissionWebhook:必须启用,用于Validating Webhook
如果发现没有启用:
# 修改apiserver启动参数
kube-apiserver \
--enable-admission-plugins=...,MutatingAdmissionWebhook,ValidatingAdmissionWebhook
# 如果是kubeadm部署的集群,修改/etc/kubernetes/manifests/kube-apiserver.yaml
1.3 测试Webhook连通性
可以先创建一个简单的测试Webhook验证环境:
# 创建一个简单的nginx服务模拟Webhook
kubectl create deployment webhook-test --image=nginx
kubectl expose deployment webhook-test --port=443 --target-port=443
# 检查是否可以访问
kubectl get svc webhook-test
第二步:创建Go项目
2.1 初始化项目
# 创建项目目录
mkdir -p ~/projects/kube-mutating-webhook-inject-pod
cd ~/projects/kube-mutating-webhook-inject-pod
# 初始化Go模块
go mod init kube-mutating-webhook-inject-pod
# 创建项目结构
mkdir -p pkg deploy certs
2.2 添加依赖
# 添加K8s API依赖
go get k8s.io/api@latest
go get k8s.io/apimachinery@latest
# 添加YAML解析依赖
go get gopkg.in/yaml.v2
# 添加日志依赖(可选,也可以用标准库)
go get github.com/golang/glog
# 整理依赖
go mod tidy
生成的go.mod文件:
module kube-mutating-webhook-inject-pod
go 1.21
require (
github.com/golang/glog v1.2.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.28.0
k8s.io/apimachinery v0.28.0
)
第三步:设计配置结构
3.1 配置文件设计
我们需要一个配置文件定义要注入的Sidecar容器。复用K8s原生的Container和Volume结构:
# config.yaml
containers:
- name: sidecar-nginx
image: nginx:1.25-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
protocol: TCP
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
volumes:
- name: nginx-conf
configMap:
name: nginx-sidecar-config
设计说明:
containers:要注入的sidecar容器列表(可以注入多个)volumes:sidecar需要的volume- 使用K8s原生结构,便于验证和使用
3.2 Go结构体定义
// pkg/config.go
package main
import (
corev1 "k8s.io/api/core/v1"
)
// Config 存储注入配置
type Config struct {
Containers []corev1.Container `yaml:"containers"` // 要注入的容器
Volumes []corev1.Volume `yaml:"volumes"` // 要注入的volume
}
3.3 配置加载函数
// pkg/config.go
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"github.com/golang/glog"
"gopkg.in/yaml.v2"
)
// loadConfig 从文件加载配置
func loadConfig(configFile string) (*Config, error) {
// 读取配置文件
data, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// 计算配置文件的hash,便于调试
glog.Infof("New configuration: sha256sum %x", sha256.Sum256(data))
// 解析YAML
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &cfg, nil
}
第四步:搭建HTTPS服务器
Webhook必须使用HTTPS(Kubernetes要求),所以我们需要:
- TLS证书(后续会生成)
- HTTPS服务器
4.1 Webhook配置结构
// pkg/webhook.go
package main
import (
"net/http"
)
// webhookServer Webhook服务器
type webhookServer struct {
sidecarConfig *Config // 注入配置
server *http.Server // HTTP服务器
}
4.2 命令行参数
// main.go
package main
import (
"flag"
"fmt"
"github.com/golang/glog"
)
// webHookSvrOptions Webhook服务器选项
type webHookSvrOptions struct {
port int // HTTPS监听端口
certFile string // TLS证书文件路径
keyFile string // TLS私钥文件路径
sidecarCfgFile string // Sidecar配置文件路径
}
func main() {
var runOption webHookSvrOptions
// 解析命令行参数
flag.IntVar(&runOption.port, "port", 8443, "Webhook server port.")
flag.StringVar(&runOption.certFile, "tlsCertFile", "/etc/webhook/certs/cert.pem",
"File containing the x509 Certificate for HTTPS.")
flag.StringVar(&runOption.keyFile, "tlsKeyFile", "/etc/webhook/certs/key.pem",
"File containing the x509 private key to --tlsCertFile.")
flag.StringVar(&runOption.sidecarCfgFile, "sidecarCfgFile", "config.yaml",
"File containing the mutation configuration.")
flag.Parse()
// 加载配置
sidecarConfig, err := loadConfig(runOption.sidecarCfgFile)
if err != nil {
glog.Errorf("Failed to load configuration: %v", err)
return
}
glog.Infof("[sidecarConfig:%v]", sidecarConfig)
}
4.3 加载TLS证书
// main.go
import (
"crypto/tls"
)
func main() {
// ... 加载配置 ...
// 加载TLS证书对
pair, err := tls.LoadX509KeyPair(runOption.certFile, runOption.keyFile)
if err != nil {
glog.Errorf("Failed to load key pair: %v", err)
return
}
glog.Infof("Loaded TLS certificate successfully")
}
4.4 创建HTTPS服务器
// main.go
import (
"fmt"
"net/http"
)
func main() {
// ... 加载配置和证书 ...
// 创建Webhook服务器实例
webhooksvr := &webhookServer{
sidecarConfig: sidecarConfig,
server: &http.Server{
Addr: fmt.Sprintf(":%v", runOption.port),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{pair},
},
},
}
// 创建路由
mux := http.NewServeMux()
mux.HandleFunc("/mutate", webhooksvr.serveMutate)
mux.HandleFunc("/health", webhooksvr.serveHealth)
webhooksvr.server.Handler = mux
glog.Infof("Starting webhook server on port %d", runOption.port)
// 在goroutine中启动服务器
go func() {
// 注意:ListenAndServeTLS的第一个参数为空字符串,使用server.Addr
if err := webhooksvr.server.ListenAndServeTLS("", ""); err != nil {
glog.Errorf("Failed to listen and serve webhook server: %v", err)
}
}()
}
4.5 添加Handler
// pkg/webhook.go
import (
"io"
"net/http"
"github.com/golang/glog"
)
// serveMutate 处理/mutate请求
func (ws *webhookServer) serveMutate(w http.ResponseWriter, r *http.Request) {
// 读取请求体
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("could not read request body: %v", err), http.StatusBadRequest)
return
}
glog.Infof("Received mutation request: %s", string(body))
// TODO: 解析AdmissionReview,构造响应
// 这部分在下一篇文章中详细讲解
}
// serveHealth 健康检查
func (ws *webhookServer) serveHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
4.6 优雅关闭
// main.go
import (
"context"
"os"
"os/signal"
"syscall"
)
func main() {
// ... 启动服务器 ...
// 监听系统信号,实现优雅关闭
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// 等待信号
<-signalChan
glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...")
// 优雅关闭服务器
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := webhooksvr.server.Shutdown(ctx); err != nil {
glog.Errorf("Server shutdown error: %v", err)
}
}
第五步:本地测试准备
5.1 创建测试证书(临时)
在正式部署前,可以用自签名证书本地测试:
# 生成私钥
openssl genrsa -out certs/server.key 2048
# 生成证书签名请求
openssl req -new -key certs/server.key -out certs/server.csr \
-subj "/CN=localhost/O=Test"
# 生成自签名证书
openssl x509 -req -days 365 -in certs/server.csr \
-signkey certs/server.key -out certs/server.crt
5.2 创建测试配置
# test-config.yaml
containers:
- name: sidecar-nginx
image: nginx:alpine
ports:
- containerPort: 80
volumes: []
5.3 本地运行测试
# 编译
go build -o webhook-server .
# 运行(使用测试证书)
./webhook-server \
--port=8443 \
--tlsCertFile=certs/server.crt \
--tlsKeyFile=certs/server.key \
--sidecarCfgFile=test-config.yaml
# 测试健康检查
curl -k https://localhost:8443/health
# 输出: ok
第六步:容器化准备
6.1 Dockerfile
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o webhook-server .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /
COPY --from=builder /app/webhook-server /webhook-server
# 创建证书和配置目录
RUN mkdir -p /etc/webhook/certs /etc/webhook/config
EXPOSE 8443
ENTRYPOINT ["/webhook-server"]
6.2 构建镜像
# 构建
docker build -t nginx-sidecar-injector:v1.0 .
# 测试运行
docker run -d \
-p 8443:8443 \
-v $(pwd)/certs:/etc/webhook/certs \
-v $(pwd)/test-config.yaml:/etc/webhook/config/config.yaml \
nginx-sidecar-injector:v1.0 \
--sidecarCfgFile=/etc/webhook/config/config.yaml
准备工作清单
在开始写核心业务逻辑前,确保完成了以下检查:
- 集群启用了
admissionregistration.k8s.io/v1API - 集群启用了
MutatingAdmissionWebhook和ValidatingAdmissionWebhook插件 - 创建了Go项目并初始化了模块
- 设计了配置文件结构和加载逻辑
- 搭建了基本的HTTPS服务器框架
- 实现了配置加载功能
- 实现了健康检查接口
- 创建了Dockerfile用于容器化
- 本地测试可以正常运行
常见问题排查
问题1:kubectl api-versions没有admissionregistration
原因:集群版本太旧(<1.9)
解决:升级Kubernetes版本
问题2:加载证书失败
Failed to load key pair: open /etc/webhook/certs/cert.pem: no such file or directory
原因:证书路径错误或证书不存在
解决:
# 检查证书文件是否存在
ls -la certs/
# 确保路径正确
./webhook-server --tlsCertFile=./certs/server.crt --tlsKeyFile=./certs/server.key
问题3:端口被占用
Failed to listen and serve webhook server: listen :8443: bind: address already in use
解决:
# 查找占用端口的进程
lsof -i :8443
# 杀掉进程或更换端口
./webhook-server --port=8444
问题4:配置文件解析失败
Failed to load configuration: failed to parse config: yaml: unmarshal errors
原因:YAML格式错误
解决:使用YAML验证工具检查配置文件
总结
完成准备工作后,项目结构应该是:
kube-mutating-webhook-inject-pod/
├── go.mod
├── go.sum
├── main.go # 主程序入口
├── config.yaml # 配置文件
├── pkg/
│ └── webhook.go # Webhook逻辑(待实现)
├── certs/ # 证书目录
│ ├── server.crt
│ └── server.key
├── deploy/ # 部署文件(待创建)
└── Dockerfile
现在基础框架已经搭建好了,下一篇文章我们将实现核心的Mutation逻辑——解析AdmissionReview、构造JSON Patch、返回响应。
下一步
准备好环境后,就可以开始实现核心的Webhook逻辑了:
- 解析AdmissionReview请求
- 构造Pod Patch
- 返回AdmissionResponse
- 部署到K8s集群
你准备好了吗?
- 你的集群启用了MutatingAdmissionWebhook吗?
- 你是如何管理项目依赖的?Go Modules还是其他工具?
- 你在搭建HTTPS服务器时遇到过什么问题?
求助与交流
如果你在准备工作中遇到了问题,欢迎在评论区交流。


556

被折叠的 条评论
为什么被折叠?



