深入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: sidecar-injector
webhooks:
  - name: sidecar-injector.example.com
    clientConfig:
      service:
        name: sidecar-injector
        namespace: istio-system
        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/resource-quota.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: my-webhook.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: my-webhook.example.com
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system", "kube-public"]

总结

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

  1. 两个阶段:Mutating(变更)和Validating(验证)
  2. 两种类型:静态准入控制器和动态Webhook
  3. 注册机制:通过Plugins注册表管理所有控制器
  4. 初始化流程:创建初始化器、计算启用列表、按顺序执行
  5. 常见控制器:AlwaysPullImages、ServiceAccount、NodeRestriction、ResourceQuota等

准入控制是K8s扩展性的重要体现,理解它可以帮助我们更好地扩展K8s功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

加倍巴巴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值