前言
去年遇到过一次诡异的部署问题:我们部署的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的准入控制机制:
- 两个阶段:Mutating(变更)和Validating(验证)
- 两种类型:静态准入控制器和动态Webhook
- 注册机制:通过Plugins注册表管理所有控制器
- 初始化流程:创建初始化器、计算启用列表、按顺序执行
- 常见控制器:AlwaysPullImages、ServiceAccount、NodeRestriction、ResourceQuota等
准入控制是K8s扩展性的重要体现,理解它可以帮助我们更好地扩展K8s功能。


229

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



