欢迎光临
我们一直在努力

深入kube-apiserver准入控制机制:从AlwaysPullImages到Webhook的完整解析

前言

去年遇到过一次诡异的部署问题:我们部署的Pod总是自动多出一个Sidecar容器,检查YAML确认没有定义,最后发现是Istio的MutatingWebhook在"搞鬼"。虽然这是预期的行为,但让我深刻认识到**准入控制(Admission Control)**的强大——它可以在请求到达etcd之前,自动修改或验证资源。

今天就带大家深入源码,看看K8s的准入控制机制是如何实现的。

什么是准入控制?

准入控制是在认证和鉴权之后、对象持久化到etcd之前的一个拦截点。它可以:

  • 修改(Mutating):自动修改请求的资源(如注入Sidecar、设置默认值)
  • 验证(Validating):验证请求是否合法(如检查配额、安全策略)

准入控制在API请求生命周期中的位置:

客户端请求


┌─────────────────────────────────────────────────────────────┐
│ Authentication(认证) │
│ – 确认你是谁 │
└──────────────────┬──────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Authorization(鉴权) │
│ – 确认你能做什么 │
└──────────────────┬──────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ ADMISSION CONTROL(准入控制) ← 我们今天的主角 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 1: Mutating(变更阶段) │ │
│ │ – 可以修改对象 │ │
│ │ – 多个控制器按顺序执行 │ │
│ │ – 例如:设置默认值、注入Sidecar │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Phase 2: Validating(验证阶段) │ │
│ │ – 不能修改对象(只读) │ │
│ │ – 验证对象合法性 │ │
│ │ – 例如:配额检查、安全策略验证 │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────┬──────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ etcd(持久化存储) │
└─────────────────────────────────────────────────────────────┘

重要特性:

  • 任何控制器拒绝请求,整个请求都会失败
  • Mutating阶段可以修改对象
  • Validating阶段只能验证,不能修改

准入控制的分类

按功能分类

类型作用示例
Mutating 修改请求对象 AlwaysPullImages、ServiceAccount、NodeRestriction
Validating 验证请求合法性 ResourceQuota、PodSecurityPolicy、NodeRestriction

按实现方式分类

类型说明示例
静态准入控制器 编译在apiserver中 AlwaysPullImages、LimitRanger
动态Webhook 调用外部HTTP服务 MutatingAdmissionWebhook、ValidatingAdmissionWebhook

常用的准入控制器

1. AlwaysPullImages

作用:将所有Pod的镜像拉取策略设置为Always

场景:多租户集群,确保私有镜像只能被授权用户拉取

// plugin/pkg/admission/alwayspullimages/admission.go

func (a *AlwaysPullImages) Admit(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}

// 遍历所有容器,设置ImagePullPolicy为Always
pods.VisitContainersWithPath(&pod.Spec, field.NewPath("spec"), func(c *api.Container, _ *field.Path) bool {
c.ImagePullPolicy = api.PullAlways
return true
})

return nil
}

2. ServiceAccount

作用:为没有指定ServiceAccount的Pod自动设置default ServiceAccount

func (a *serviceAccountPlugin) Admit(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
pod := attributes.GetObject().(*api.Pod)

// 如果没有指定ServiceAccount,设置为"default"
if len(pod.Spec.ServiceAccountName) == 0 {
pod.Spec.ServiceAccountName = "default"
}

// 如果没有指定imagePullSecrets,从ServiceAccount继承
if len(pod.Spec.ImagePullSecrets) == 0 {
pod.Spec.ImagePullSecrets = serviceAccount.ImagePullSecrets
}

return nil
}

3. NodeRestriction

作用:限制kubelet只能修改自己的Node和绑定到本节点的Pod

与Node鉴权的区别:

  • Node鉴权:API层面的权限控制
  • NodeRestriction:准入控制层面的字段级限制

func (p *Plugin) Admit(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
switch attributes.GetResource().Resource {
case "nodes":
return p.admitNode(attributes)
case "pods":
return p.admitPod(attributes)
}
return nil
}

func (p *Plugin) admitNode(attributes admission.Attributes) error {
nodeName := attributes.GetName()

// 检查是否是kubelet自己的节点
if nodeName != p.nodeIdentifier.NodeIdentity(attributes.GetUser()) {
return admission.NewForbidden(attributes, fmt.Errorf("cannot modify other nodes"))
}

// 限制只能修改特定的字段
// 不能修改labels(除了kubernetes.io前缀的)
// 不能修改annotations

return nil
}

4. ResourceQuota

作用:限制namespace的总资源使用(CPU、内存、Pod数量等)

func (q *quotaAdmission) Admit(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
// 获取namespace的ResourceQuota
quotas, err := q.GetQuotas(attributes.GetNamespace())

// 计算请求的资源
usage := calculateUsage(attributes.GetObject())

// 检查是否超出配额
for _, quota := range quotas {
if exceedsQuota(usage, quota) {
return admission.NewForbidden(attributes, fmt.Errorf("exceeds quota"))
}
}

return nil
}

5. PodSecurityPolicy(已废弃)/ PodSecurity

作用:控制Pod的安全配置(是否以root运行、是否使用特权模式等)

func (p *Plugin) Validate(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) error {
pod := attributes.GetObject().(*api.Pod)

// 检查是否违反安全策略
if pod.Spec.SecurityContext.RunAsRoot != nil && *pod.Spec.SecurityContext.RunAsRoot {
return admission.NewForbidden(attributes, fmt.Errorf("cannot run as root"))
}

if pod.Spec.HostNetwork && !p.allowHostNetwork {
return admission.NewForbidden(attributes, fmt.Errorf("cannot use host network"))
}

return nil
}

准入控制的初始化

初始化入口

// cmd/kube-apiserver/app/server.go

// 1. 创建准入控制器初始化器
pluginInitializers, admissionPostStartHook, err := admissionConfig.New(
proxyTransport,
genericConfig.EgressSelector,
serviceResolver,
genericConfig.TracerProvider,
)

// 2. 应用准入控制配置
err = s.Admission.ApplyTo(
genericConfig,
versionedInformers,
kubeClientConfig,
utilfeature.DefaultFeatureGate,
pluginInitializers,
)

admissionConfig.New:创建初始化器

// pkg/kubeapiserver/admission/config.go

func (c *Config) New(
proxyTransport *http.Transport,
egressSelector *egressselector.EgressSelector,
serviceResolver webhook.ServiceResolver,
tp *trace.TracerProvider,
) ([]admission.PluginInitializer, genericapiserver.PostStartHookFunc, error) {

// 1. 创建Webhook认证解析器
webhookAuthResolverWrapper := webhook.NewDefaultAuthenticationInfoResolverWrapper(
proxyTransport, egressSelector, c.LoopbackClientConfig, tp,
)
webhookPluginInitializer := webhookinit.NewPluginInitializer(
webhookAuthResolverWrapper, serviceResolver,
)

// 2. 创建K8s专用初始化器
clientset, err := kubernetes.NewForConfig(c.LoopbackClientConfig)
discoveryClient := cacheddiscovery.NewMemCacheClient(clientset.Discovery())
discoveryRESTMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)

kubePluginInitializer := NewPluginInitializer(
cloudConfig,
discoveryRESTMapper,
quotainstall.NewQuotaConfigurationForAdmission(),
)

// 3. 创建PostStartHook(定期刷新discovery缓存)
admissionPostStartHook := func(context genericapiserver.PostStartHookContext) error {
discoveryRESTMapper.Reset()
go utilwait.Until(discoveryRESTMapper.Reset, 30*time.Second, context.StopCh)
return nil
}

return []admission.PluginInitializer{
webhookPluginInitializer,
kubePluginInitializer,
}, admissionPostStartHook, nil
}

PluginInitializer:插件的数据注入

// pkg/kubeapiserver/admission/initializer.go

type PluginInitializer struct {
cloudConfig []byte
restMapper meta.RESTMapper
quotaConfiguration quota.Configuration
}

func (i *PluginInitializer) Initialize(plugin admission.Interface) {
// 如果插件需要CloudConfig,注入
if wants, ok := plugin.(WantsCloudConfig); ok {
wants.SetCloudConfig(i.cloudConfig)
}

// 如果插件需要RESTMapper,注入
if wants, ok := plugin.(WantsRESTMapper); ok {
wants.SetRESTMapper(i.restMapper)
}

// 如果插件需要QuotaConfiguration,注入
if wants, ok := plugin.(initializer.WantsQuotaConfiguration); ok {
wants.SetQuotaConfiguration(i.quotaConfiguration)
}
}

准入控制器的注册和初始化

注册准入控制器

所有准入控制器在RegisterAllAdmissionPlugins函数中注册:

// pkg/kubeapiserver/options/plugins.go

func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
alwayspullimages.Register(plugins)
antiaffinity.Register(plugins)
defaulttolerationseconds.Register(plugins)
gc.Register(plugins)
limitranger.Register(plugins)
noderestriction.Register(plugins)
podnodeselector.Register(plugins)
podsecurity.Register(plugins)
resourcequota.Register(plugins)
serviceaccount.Register(plugins)
// … 更多插件
}

注册示例:AlwaysPullImages

// plugin/pkg/admission/alwayspullimages/admission.go

const PluginName = "AlwaysPullImages"

func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
return NewAlwaysPullImages(), nil
})
}

func NewAlwaysPullImages() *AlwaysPullImages {
return &AlwaysPullImages{
Handler: admission.NewHandler(admission.Create, admission.Update),
}
}

Plugins数据结构

// staging/src/k8s.io/apiserver/pkg/admission/plugins.go

type Factory func(config io.Reader) (Interface, error)

type Plugins struct {
lock sync.Mutex
registry map[string]Factory // 插件名 -> 工厂函数
}

func (ps *Plugins) Register(name string, factory Factory) {
ps.lock.Lock()
defer ps.lock.Unlock()

if _, found := ps.registry[name]; found {
panic(fmt.Sprintf("admission plugin %q was registered twice", name))
}
ps.registry[name] = factory
}

计算启用的插件列表

enabledPluginNames:确定哪些插件启用

// staging/src/k8s.io/apiserver/pkg/server/options/admission.go

func (a *AdmissionOptions) enabledPluginNames() []string {
// 默认关闭的插件
allOffPlugins := append(a.DefaultOffPlugins.List(), a.DisablePlugins)
disabledPlugins := sets.NewString(allOffPlugins)

// 明确启用的插件
enabledPlugins := sets.NewString(a.EnablePlugins)

// 从disabled中移除明确启用的
disabledPlugins = disabledPlugins.Difference(enabledPlugins)

// 按推荐顺序,保留未禁用的插件
orderedPlugins := []string{}
for _, plugin := range a.RecommendedPluginOrder {
if !disabledPlugins.Has(plugin) {
orderedPlugins = append(orderedPlugins, plugin)
}
}

return orderedPlugins
}

命令行参数

kube-apiserver \\
–enable-admission-plugins=NodeRestriction,AlwaysPullImages \\
–disable-admission-plugins=PodSecurityPolicy \\
–admission-control-config-file=/etc/kubernetes/admission-config.yaml

准入控制器的执行

两阶段执行流程

// staging/src/k8s.io/apiserver/pkg/admission/chain.go

func (admissionHandler) Admit(ctx context.Context, attributes Attributes, o ObjectInterfaces) error {
// Phase 1: Mutating
for _, plugin := range mutatingPlugins {
if err := plugin.Admit(ctx, attributes, o); err != nil {
return err // 任何插件拒绝,整个请求失败
}
}

// Phase 2: Validating
for _, plugin := range validatingPlugins {
if err := plugin.Validate(ctx, attributes, o); err != nil {
return err // 任何插件拒绝,整个请求失败
}
}

return nil
}

Admission Attributes

type Attributes interface {
GetName() string // 资源名称
GetNamespace() string // 命名空间
GetResource() schema.GroupVersionResource // 资源类型
GetOperation() Operation // 操作类型(Create/Update/Delete/Connect)
GetObject() runtime.Object // 请求对象(Mutating阶段可修改)
GetOldObject() runtime.Object // 旧对象(Update操作)
GetUserInfo() user.Info // 用户信息
}

Webhook准入控制

除了静态准入控制器,K8s还支持通过Webhook调用外部服务进行准入控制。

Webhook配置

# mutating-webhook.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: sidecarinjector
webhooks:
name: sidecarinjector.example.com
clientConfig:
service:
name: sidecarinjector
namespace: istiosystem
path: "/inject"
caBundle: <CA_BUNDLE>
rules:
operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
istio-injection: enabled
admissionReviewVersions: ["v1"]
sideEffects: None

Webhook执行流程

请求到达apiserver


MutatingAdmissionWebhook

├── 匹配Webhook配置

├── 序列化AdmissionReview

├── HTTP POST到Webhook服务

├── 接收AdmissionResponse

└── 如果允许,应用patches修改对象


继续其他准入控制器

Webhook响应示例

{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "c5d4e6f7-a8b9-4c0d-1e2f-3a4b5c6d7e8f",
"allowed": true,
"patchType": "JSONPatch",
"patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvY29udGFpbmVycy8tIiwgInZhbHVlIjogeyJuYW1lIjogInNpZGVjYXIiLCAiaW1hZ2UiOiAiaXN0aW8vcHJveHl2MjoxLjE0LjAifX1d"
}
}

// patch解码后是JSONPatch:
// [{"op": "add", "path": "/spec/containers/-", "value": {"name": "sidecar", "image": "istio/proxyv2:1.14.0"}}]

配置准入控制器

推荐配置

kube-apiserver \\
–enable-admission-plugins=\\
NodeRestriction,\\ # 限制kubelet权限
NamespaceLifecycle,\\ # 防止删除active namespace
LimitRanger,\\ # 限制资源范围
ServiceAccount,\\ # 自动设置ServiceAccount
DefaultStorageClass,\\ # 自动设置StorageClass
ResourceQuota,\\ # 资源配额
MutatingAdmissionWebhook,\\ # 支持Mutating Webhook
ValidatingAdmissionWebhook # 支持Validating Webhook

配置示例

# /etc/kubernetes/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
name: ResourceQuota
path: /etc/kubernetes/resourcequota.yaml
name: EventRateLimit
configuration:
apiVersion: eventratelimit.admission.k8s.io/v1alpha1
kind: Configuration
limits:
type: Namespace
qps: 100
burst: 200

踩坑实录:准入控制常见问题

坑1:准入控制器顺序问题

现象:ServiceAccount控制器在PodPreset之后执行,导致注入的volume配置丢失

解决方案:

# 注意准入控制器的执行顺序
# Mutating阶段按注册顺序执行
# Validating阶段按注册顺序执行

# K8s 1.10+支持使用reinvocationPolicy让Webhook被多次调用

坑2:Webhook超时

现象:Webhook响应慢,导致API请求超时

解决方案:

webhooks:
name: mywebhook.example.com
clientConfig:

timeoutSeconds: 10 # 设置超时时间(默认30s,最大30s)
failurePolicy: Ignore # 失败时忽略(Fail会拒绝请求)

坑3:Webhook证书问题

现象:Webhook调用失败,TLS握手错误

解决方案:

# 确保caBundle正确
# 如果是自签名证书,需要把CA证书放入caBundle

# 或者使用cert-manager自动管理

坑4:忽略namespace导致的问题

现象:系统组件(如kube-system中的Pod)被Webhook修改,导致集群异常

解决方案:

webhooks:
name: mywebhook.example.com
namespaceSelector:
matchExpressions:
key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "kube-public"]

总结

通过今天的分析,我们深入理解了kube-apiserver的准入控制机制:

  • 两个阶段:Mutating(变更)和Validating(验证)
  • 两种类型:静态准入控制器和动态Webhook
  • 注册机制:通过Plugins注册表管理所有控制器
  • 初始化流程:创建初始化器、计算启用列表、按顺序执行
  • 常见控制器:AlwaysPullImages、ServiceAccount、NodeRestriction、ResourceQuota等
  • 准入控制是K8s扩展性的重要体现,理解它可以帮助我们更好地扩展K8s功能。

    赞(0)
    未经允许不得转载:171主机测评 » 深入kube-apiserver准入控制机制:从AlwaysPullImages到Webhook的完整解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址