深度剖析Kubernetes动态准入控制之Initializers

Author: xidianwangtao@gmail.com

Admission Controll的最佳配置

配置过kube-apiserver的同学一定记得这个配置--admission-control或者--admission-control-config-file,你可以在这里顺序的配置你想要的准入控制器,默认是AlwaysAdmit

  • 在Kubernetes 1.9中,所有允许的控制器列表如已经支持多达32个:
    • AlwaysAdmit,
    • AlwaysDeny,
    • AlwaysPullImages,
    • DefaultStorageClass,
    • DefaultTolerationSeconds,
    • DenyEscalatingExec,
    • DenyExecOnPrivileged,
    • EventRateLimit,
    • ExtendedResourceToleration,
    • ImagePolicyWebhook,
    • InitialResources,
    • Initializers,
    • LimitPodHardAntiAffinityTopology,
    • LimitRanger,
    • MutatingAdmissionWebhook,
    • NamespaceAutoProvision,
    • NamespaceExists,
    • NamespaceLifecycle,
    • NodeRestriction,
    • OwnerReferencesPermissionEnforcement,
    • PVCProtection,
    • PersistentVolumeClaimResize,
    • PersistentVolumeLabel,
    • PodNodeSelector,
    • PodPreset,
    • PodSecurityPolicy,
    • PodTolerationRestriction,
    • Priority,
    • ResourceQuota,
    • SecurityContextDeny,
    • ServiceAccount,
    • ValidatingAdmissionWebhook

注意,在我写这博客的时候Dynamic Admission Controll官方文档还没来得及更新到1.9对应内容,官方文档中还是写的GenericAdmissionWebhook,实际上Webhook类已经分为MutatingAdmissionWebhook和ValidatingAdmissionWebhook了,而没有GenericAdmissionWebhook这一项,其实它就是ValidatingAdmissionWebhook在Kubernetes 1.9后作的rename而已。

这么多的准入控制器,如果你并不想去了解那么多(虽然我不推荐你这么做,每一项的具体含义请参考admission-controllers官方文档),没关系,Kubernetes也有推荐项给你。

  • 如果你使用Kubernetes 1.6 ~ 1.8,官方推荐配置如下: --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds
  • 如果你使用Kubernetes 1.9,官方推荐配置如下: --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ValidatingAdmissionWebhook,ResourceQuota,DefaultTolerationSeconds,MutatingAdmissionWebhook

再次强调一点,--admission-control配置的控制器列表是有顺序的,越靠前的越先执行,一旦某个控制器返回的结果是reject的,那么整个准入控制阶段立刻结束,所以这里的配置顺序也是有讲究的,配置顺序不好,会导致性能会差些。

built-in准入控制的缺陷

即便Kubernetes提供了这么多的准入控制器,也不可能满足所有企业的需求,因此Kubernetes提供了三个Dynamic Admission Controller:

  • Initializers(Alpha, Default disable in 1.9)
  • MutatingAdmissionWebhook(Belta, Default enable in 1.9)
  • ValidatingAdmissionWebhook(Alpha in 1.8, Belta in 1.9, Default enable in 1.9)

这三个Dynamic Admission Controller都是为了解决其他内置插件化准入控制器的两个缺陷:

  • 在kube-apiserver编译时打包进去的,如果有定制化修改,需要重新编译kube-apiserver。
  • 如果需要修改--admission-controll中的控制器列表(包括顺序),都需要重启kube-apiserver。
    • 如果你没做Kubernetes Master HA,会导致Kubernetes Master中断服务;
    • 如果你做了Kubernetes Master HA,就完全没问题了吗?当然也不完全是,服务不会中断,但是存在一段时间会存在不同的kube-apiserver有不同的--admission-controll配置,导致同样的请求如果分发到不一样配置的kube-apiserver,就不能做到幂等性了。当然,这好像影响也并不大。

Initializers工作机制

Initializers有什么用

我们什么时候需要用Initializers呢?当集群管理员需要强制对某些请求或者所有请求都进行校验或者修改的时候,就可以考虑使用Initializers。

  • 通过Initializers,你可以给每个即将创建的Pod都插入一个SideCar容器。
  • 通过Initializers,给所有Pod都插入一个带有测试数据的volume用于业务测试。
  • 通过Initializers,检查Secret的长度是否满足要求,以此来保证密码的复杂度,如果不满足就拒绝create pod请求。

另外我之前思考的关于Harbor镜像安全的问题:在多租户环境中,某个用户在某个Node上pull了一个带有敏感数据的镜像并且启动为Pod了。此时,另外一个用户只要知道这个image name,并且设置imagePullPolicy为IfNotPresent,那么这个用户的Pod就可能会被调度到这个节点(如果scheduler配置了ImageLocalityPriority priority policy,非默认配置,但在经常会配置,以提高pod启动速度),然后就把别人的敏感镜像跑起来了,这在公有云中是不可被接受的。

我们如何解决这个问题呢?在私有云中,会通过DevOps平台做好权限的控制,用户只能选择自己的app进行部署,并不能指定别人的镜像名称。在Kubernetes层面,有办法解决这个问题吗?嗯,利用Initializers就能很好解决(幸运的是,Kubernetes已经提供了AlwaysPullImages这个Admission Controller),所有用户创建的Pod请求,都经过你的Initializers进行检查和修改,强制修改Pod ImagePullPolicy为Always即可。

如何启用Initializers

  • 前面提到,需要在每个kube-apiserver实例(考虑到Kubernetes Master HA)中--admission-controll中添加Initializers
  • 另外,还需要在每个kube-apiserver实例的--runtime-config中添加admissionregistration.k8s.io/v1alpha1

Initializers的工作原理

  • 首先部署你自己写的Initializers controller。这个controller通过watch你想要的resource type,捕获后对这些resource的POST请求做修改。我们以envoy-initializer为例:
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  initializers:
    pending: []
  labels:
    app: envoy-initializer
  name: envoy-initializer
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: envoy-initializer
      name: envoy-initializer
    spec:
      containers:
        - name: envoy-initializer
          image: gcr.io/hightowerlabs/envoy-initializer:0.0.1
          imagePullPolicy: Always
          args:
            - "-annotation=initializer.kubernetes.io/envoy"
            - "-require-annotation=true"

部署envoy-initializer时,千万要注意设置metadata.initializers.pending为空,防止envoy-initializer的部署被自己stuck了。

  • 然后你要创建你的initializerConfigurationAPI Object, 比如你想通过Initializers给每个之后创建的Deployment注入一个envoy proxy sidecar容器:
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
  name: envoy
initializers:
  - name: envoy.initializer.kubernetes.io
    rules:
      - apiGroups:
          - "*"
        apiVersions:
          - "*"
        resources:
          - deployments
  • initializerConfiguration创建后,你需要等待几秒,然后再通过Deployment部署你的应用,这个时候对应的Initializers就会自动append到Deployment的metadata.initializers.pending数组中,以上面的example为例,就是附加metadata.initializers.pending[0]=envoy.initializer.kubernetes.io
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  annotations:
    "initializer.kubernetes.io/envoy": "true"
  labels:
    app: helloworld
    envoy: "true"
  name: helloworld-with-annotation
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: helloworld
        envoy: "true"
      name: helloworld-with-annotation
    spec:
      containers:
        - name: helloworld
          image: gcr.io/hightowerlabs/helloworld:0.0.1
          imagePullPolicy: Always
          args:
            - "-http=127.0.0.1:8080"

注意:metadata.initializers.pending不为null的时候,默认是无法通过api获取到该deployment object的,因此Initializers controller list&wath 对象的时候需要在request url中添加参数?includeUninitialized=true

  • 然后这一创建Deployment对象的event被你自定义的Initializers controller捕获到了,Initializers controller就按照你的逻辑对该Deployment进行修改,比如注入sidecar container和volume等,并且会从对象的metadata.initializers.pending中删除掉自己对应的Initializers controller。
  • 如果有多个Initializers映射到这个对象, 那么就会串行的按照上面的逻辑处理。因此如果是不需要对Object做修改操作的Admission Controller,建议通过webhook的方式处理(并行的),那样性能会更高。initializers的串行方式注定性能会低,所以最好不要创建多的initializers。
  • 当该Object的metadata.initializers.pending为null的时候,就认为已经完成初始化流程,接下来scheduler和controller-managers管理的controllers就能看到这些Object,继续后面的调度和自动驾驶逻辑。

注意:当你通过kubectl或者rest api提交创建对象请求的时候,如果这个对象有相应的Initializers,那么这个对象会保持uninitialized状态,需要要等待Initializers Controllers执行完对应的逻辑后才会返回,并且有个超时时间为30s。

Initializers注意事项

基于上面对Initializers工作机制的理解,我们发现它也有缺陷或者注意事项:

  • 如果你部署的Initializers Controllers不能正常工作了或者性能很低,在高并发场景下会导致大量的相关对象停留在uninitialized状态,无法进行后续的调度。这可能会影响你的业务,比如你使用了HPA对相关Deployment对象进行弹性扩容,当负债上来的时候,你的Initializers Controllers不能正常工作了,会导致你的应用不能弹性伸缩,后果可想而知!所以写一个高性能的稳定的Initializers Controllers是你必须的技能。
  • 目前Initializers准入控制仍属于Alpha,你懂得。
  • 你部署的Initializers Controllers是如此重要,所以建议你给它部署在kube-system或者单独的一个namespace中,给他分配足够的ResourceQuota和LimitRanger,以保障它的稳定性。
  • 如果你有多个Initializers Controllers关联到某类resource,那么每次创建resource的时候,生成的metadata.initializers.pending数组元素顺序可能是不一样的,所以建议这些Initializers Controllers不应该有相互依赖。
  • 再次强调一下,部署你的Initializers Controllers时,千万要注意设置metadata.initializers.pending为空,防止Initializers Controllers的部署被自己stuck了。

如何开发一个自定义的Initializers

...
type config struct {
	Containers []corev1.Container
	Volumes    []corev1.Volume
}

func main() {
	...
	// Watch uninitialized Deployments in all namespaces.
	restClient := clientset.AppsV1beta1().RESTClient()
	watchlist := cache.NewListWatchFromClient(restClient, "deployments", corev1.NamespaceAll, fields.Everything())

	// Wrap the returned watchlist to workaround the inability to include
	// the `IncludeUninitialized` list option when setting up watch clients.
	includeUninitializedWatchlist := &cache.ListWatch{
		ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
			options.IncludeUninitialized = true
			return watchlist.List(options)
		},
		WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
			options.IncludeUninitialized = true
			return watchlist.Watch(options)
		},
	}

	resyncPeriod := 30 * time.Second

	_, controller := cache.NewInformer(includeUninitializedWatchlist, &v1beta1.Deployment{}, resyncPeriod,
		cache.ResourceEventHandlerFuncs{
			AddFunc: func(obj interface{}) {
				err := initializeDeployment(obj.(*v1beta1.Deployment), c, clientset)
				if err != nil {
					log.Println(err)
				}
			},
		},
	)

	stop := make(chan struct{})
	go controller.Run(stop)

	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
	<-signalChan

	log.Println("Shutdown signal received, exiting...")
	close(stop)
}

func initializeDeployment(deployment *v1beta1.Deployment, c *config, clientset *kubernetes.Clientset) error {
	if deployment.ObjectMeta.GetInitializers() != nil {
		pendingInitializers := deployment.ObjectMeta.GetInitializers().Pending

		if initializerName == pendingInitializers[0].Name {
			log.Printf("Initializing deployment: %s", deployment.Name)

			o, err := runtime.NewScheme().DeepCopy(deployment)
			if err != nil {
				return err
			}
			initializedDeployment := o.(*v1beta1.Deployment)

			// Remove self from the list of pending Initializers while preserving ordering.
			if len(pendingInitializers) == 1 {
				initializedDeployment.ObjectMeta.Initializers = nil
			} else {
				initializedDeployment.ObjectMeta.Initializers.Pending = append(pendingInitializers[:0], pendingInitializers[1:]...)
			}

			if requireAnnotation {
				a := deployment.ObjectMeta.GetAnnotations()
				_, ok := a[annotation]
				if !ok {
					log.Printf("Required '%s' annotation missing; skipping envoy container injection", annotation)
					_, err = clientset.AppsV1beta1().Deployments(deployment.Namespace).Update(initializedDeployment)
					if err != nil {
						return err
					}
					return nil
				}
			}

			// Modify the Deployment's Pod template to include the Envoy container
			// and configuration volume. Then patch the original deployment.
			initializedDeployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, c.Containers...)
			initializedDeployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, c.Volumes...)

			oldData, err := json.Marshal(deployment)
			if err != nil {
				return err
			}

			newData, err := json.Marshal(initializedDeployment)
			if err != nil {
				return err
			}

			patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, v1beta1.Deployment{})
			if err != nil {
				return err
			}

			_, err = clientset.AppsV1beta1().Deployments(deployment.Namespace).Patch(deployment.Name, types.StrategicMergePatchType, patchBytes)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func configmapToConfig(configmap *corev1.ConfigMap) (*config, error) {
	var c config
	err := yaml.Unmarshal([]byte(configmap.Data["config"]), &c)
	if err != nil {
		return nil, err
	}
	return &c, nil
}

Kubernetes 1.9对Initializers的增强

  • kubectl annotate, apply, edit-last-applied, delete, describe, edit, get, label, set命令可以增加--include-uninitialized来对uninitialized进行操作;
  • Initializers的启用不需要手动配置feature gate,admission controll中配置后会自动添加到feature gate中;
  • Initializer名称至少包含两个.,分隔成至少3段;
  • Fixes an initializer bug where update requests which had an empty pending initializers list were erroneously rejected.

总结

相信你已经对Kubernetes Initializers的工作机制和使用注意事项已经有所了解了,后续我会对Kubernetes Initializers的代码进行走读分析,然后再对MutatingAdmissionWebhook和ValidatingAdmissionWebhook进行工作机制和代码分析。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏iOS122-移动混合开发研究院

写给iOS小白的MVVM教程(一): 从MVC到MVVM之一个典型的MVC应用场景

前言 本着实践为主的原则,此系列文章不做过多的概念性的阐述和讨论;更多的代码和篇幅用来展示MVC和MVVC下的基础代码结构与具体实现,来展示各自优劣.这篇文章,...

3617
来自专栏FreeBuf

远程RPC溢出EXP编写实战之MS06-040

0x01 前言 MS06-040算是个比较老的洞了,在当年影响十分之广,基本上Microsoft大部分操作系统都受到了影响,威力不亚于17年爆出的”永恒之蓝”漏...

30610
来自专栏草根专栏

从头编写 asp.net core 2.0 web api 基础框架 (3)

Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-...

4987
来自专栏Kubernetes

Kubernetes ReplicationController源码分析

虽然在Kubernetes v1.2中,Kubernetes推出了Deployments特性,Deployment通过创建ReplicaSet来管理Pod,R...

7398
来自专栏bboysoul

把树莓派的系统装到u盘里面

因为买不起高速卡,所以只能使用普通的内存卡,但是又怕内存卡坏掉,而且内存卡只有8g容量太小,正好我有一个usb3.0接口的128g u盘,虽然树莓派不支持usb...

1592
来自专栏Kubernetes

解析Kubernetes 1.8中的

Author: xidianwangtao@gmail.com Kubernetes 1.8中对scheduler的更新 【Alpha】支持定义Prior...

3088
来自专栏Kubernetes

原 荐 Kubernetes Resourc

更多关于kubernetes的深入文章,请看我csdn或者oschina的博客主页。 ResoureQuota介绍 关于ResoureQuota和Resourc...

5309
来自专栏GopherCoder

『No18: Go 实现世界杯后台管理系统』

趁着周末更新一期,上一期讲到 如何快速熟悉一个项目, 文章的最后讲到,最好的方法是借用相同的技术栈重新实现一个项目。

1881
来自专栏blackpiglet

在 Kubernetes中,fluentd 以 sidecar 模式收集日志,并发送至 ElasticSearch

ElasticSearch 在日志收集和分析领域非常流行,而 fluentd 是一种万用型的日志收集器,当然也支持 ES(ElasticSearch)。不过在 ...

1932
来自专栏黑泽君的专栏

day54_BOS项目_06

第一步:根据提供的 业务受理.pdm 文件生成建表文件 bos_qp.sql 第二步:由于业务受理.pdm 文件中有伪表,所以我们需要修改生成的建表文件,修改如...

992

扫码关注云+社区

领取腾讯云代金券