文档章节

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

WaltonWang
 WaltonWang
发布于 2017/12/17 20:11
字数 2651
阅读 1623
收藏 3

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进行工作机制和代码分析。

© 著作权归作者所有

WaltonWang
粉丝 226
博文 106
码字总数 226882
作品 0
深圳
程序员
私信 提问
Kubernetes 新概念 “Initializers”解析(上):能让你为集群编写插件的新模型

Kubernetes 如今能大展拳脚的原因有二:一是,因为他社区的无限优势;二是,源于 Kubernetes API 的灵活性,以及能轻而易举地在其上编写自定义扩展或者插件。而在本文中,我将深入剖析一个新...

店家小二
2018/12/14
0
0
Kubernetes 新概念 “Initializers”解析(中):能让你为集群编写插件的新模型

initialization 剖析 1.配置需要 initialization 的资源类型 : InitializerConfiguration(https://kubernetes.io/docs/admin/extensible-admission-controllers/#configure-initializers-o......

店家小二
2018/12/14
0
0
Kubernetes 的安全机制 APIServer 认证、授权、准入控制

本文讲解 kubernetes 的安全机制。主要会按照这几个部分来讲解:APIServer 认证、授权、准入控制等。 我们都知道 kubenetes 默认在两个端口提供服务:一个是基于 https 安全端口 6443,另一个...

店家小二
2018/12/29
0
0
Kubernetes上的服务网格 Istio - 分布式追踪篇

微服务架构将复杂系统切分若干小服务,每个服务可以被独立地开发、部署和伸缩;微服务架构和容器(Docker/Kubernetes)是天作之合,可以进一步简化微服务交付,加强整体系统的弹性和健壮性。...

易立
2017/12/12
0
0
KubeVirt:通过CRD扩展Kubernetes实现虚拟机管理

KubeVirt是什么? KubeVirt[1]是个Kubernetes的一个插件,使其在原本调度容器之余能够并行调度传统虚拟机。它通过运用自定义资源定义(以下简称CRD)及其他Kubernetes相关功能来无缝扩展现有...

Docker
2018/09/22
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Kafka实战(五) - 核心API及适用场景全面解析

1 四个核心API ● Producer API 允许一个应用程序发布一串流式的数据到一个或者多个Kafka topic。 ● Consumer API 允许一个应用程序订阅一个或多个topic ,并且对发布给他们的流式数据进行处...

JavaEdge
今天
11
0
实现线程的第三种方式——Callable & Future

Callable Runnable 封装一个异步运行的任务, 可以把它想象成为一个没有参数和返回值的异步方 法。Callable 与 Runnable 类似, 但是有返回值。Callable 接口是一个参数化的类型, 只有一 个...

ytuan996
今天
11
0
OSChina 周六乱弹 —— 不要摁F了!

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @巴拉迪维 : 朴树写的词曲都给人一种莫名的失落感,不过这首歌他自己却没有唱,换成赵传这种高音阶嘶喊的确很好,低沉但却有力,老男人的呐喊...

小小编辑
今天
12
0
Android Binder机制 - interface_cast和asBinder讲解

研究Android底层代码时,尤其是Binder跨进程通信时,经常会发现interface_cast和asBinder,很容易被这两个函数绕晕,下面来讲解一下: interface_cast 下面根据下述ICameraClient例子进行分析...

天王盖地虎626
昨天
13
0
计算机实现原理专题--存储器的实现(二)

计算机实现原理专题--存储器的实现(一)中描述了一种可以记住输入端变化的装置。现需要对其功能进行扩充,我们将上面的开关定义为置位,下面的开关定义为复位,然后需要增加一个保持位,当保...

FAT_mt
昨天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部