​ 基于云原生的周边工具

Kubectl

kubectl 是 Kubernetes 的命令行工具(CLI),是 Kubernetes 用户和管理员必备的管理

工具。该kubectl工具控制Kubernetes集群管理器。它可以让您检查集群资源,创建、删除和更新组

件以及更多功能。kubectl 提供了大量的子命令,方便管理 Kubernetes 集群中的各种功能。

  • kubectl -h 查看子命令列表
  • kubectl options 查看全局选项
  • kubectl --help 查看子命令的帮助
  • kubectl command -o= 设置输出格式(如 json、yaml、jsonpath 等)
  • kubectl explain RESOURCE 查看资源的定义

krew

krew 是一个用来管理 kubectl 插件的工具,类似于 apt 或 yum,支持搜索、安装和管理kubectl 插件。

安装

  set -x; cd "$(mktemp -d)" &&
  curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/download/v0.3.2/krew.{tar.gz,yaml}" &&
  tar zxvf krew.tar.gz &&
  ./krew-"$(uname | tr '[:upper:]' '[:lower:]')_amd64" install \
    --manifest=krew.yaml --archive=krew.tar.gz

添加环境变量

export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
source ~/.bashrc

使用

kubectl krew update: 插件索引更新

kubectl krew search: 插件搜索

kubectl krew install get-all: 安装插件

kubectl krew list: 插件已装插件

kubectl krew info ns: 查看插件详情

kubectl krew upgrade : 升级安装的插件

kubectl krew remove view-secret : 卸载插件

krew自身也作为一个“kubectl 插件”,因此,可以使用命令kubectl krew upgrade命令来升级krew

https://cloud.tencent.com/developer/article/1543178

Kubeadm

Kubeadm 是一个提供了 kubeadm initkubeadm join 的工具, 作为创建 Kubernetes 集群的 “快捷途径” 的最佳实践。

kubeadm 通过执行必要的操作来启动和运行最小可用集群。 按照设计,它只关注启动引导,而非配置机器。同样的, 安装各种 “锦上添花” 的扩展,例如 Kubernetes Dashboard、 监控方案、以及特定云平台的扩展,都不在讨论范围内。

相反, kubeadm 之上构建更高级别以及更加合规的工具, 理想情况下,使用 kubeadm 作为所有部署工作的基准将会更加易于创建一致性集群。

常用命令

kubeadm init: 此命令初始化一个 Kubernetes 控制平面节点。

kubeadm join: 此命令用来初始化 Kubernetes 工作节点并将其加入集群。

当节点加入 kubeadm 初始化的集群时,我们需要建立双向信任。 这个过程可以分解为发现(让待加入节点信任 Kubernetes 控制平面节点)和 TLS 引导(让 Kubernetes 控制平面节点信任待加入节点)两个部分。

有两种主要的发现方案。 第一种方法是使用共享令牌和 API 服务器的 IP 地址。 第二种是以文件形式提供标准 kubeconfig 文件的一个子集。 发现/kubeconfig 文件支持令牌、client-go 鉴权插件(“exec”)、“tokenFile" 和 "authProvider"。该文件可以是本地文件,也可以通过 HTTPS URL 下载。 格式是 kubeadm join --discovery-token abcdef.1234567890abcdef 1.2.3.4:6443kubeadm join --discovery-file path/to/file.conf 或者 kubeadm join --discovery-file https://url/file.conf。 只能使用其中一种。 如果发现信息是从 URL 加载的,必须使用 HTTPS。 此外,在这种情况下,主机安装的 CA 包用于验证连接。

如果使用共享令牌进行发现,还应该传递 --discovery-token-ca-cert-hash 参数来验证 Kubernetes 控制平面节点提供的根证书颁发机构(CA)的公钥。 此参数的值指定为 ":", 其中支持的哈希类型为 "sha256"。哈希是通过 Subject Public Key Info(SPKI)对象的字节计算的(如 RFC7469)。 这个值可以从 "kubeadm init" 的输出中获得,或者可以使用标准工具进行计算。 可以多次重复 --discovery-token-ca-cert-hash 参数以允许多个公钥。

如果无法提前知道 CA 公钥哈希,则可以通过 --discovery-token-unsafe-skip-ca-verification 参数禁用此验证。 这削弱了 kubeadm 安全模型,因为其他节点可能会模仿 Kubernetes 控制平面节点。

kubeadm config:在 kubeadm init 执行期间,kubeadm 将 ClusterConfiguration 对象上传 到你的集群的 kube-system 名字空间下名为 kubeadm-config 的 ConfigMap 对象中。 然后在 kubeadm joinkubeadm resetkubeadm upgrade 执行期间读取此配置。

kubeadm config print 命令打印默认静态配置, kubeadm 运行 kubeadm init and kubeadm join 时将使用此配置

kubeadm config migrate 来转换旧配置文件

kubeadm config images list : 列出 kubeadm 所需的镜像。

kubeadm config images pull : 拉取 kubeadm 所需的镜像。

kubeadm token: kubeadm init创建了一个有效期为24小时的令牌,token命令用于令牌相关的操作

kubeadm token create: 创建一个引导令牌。 你可以设置此令牌的用途,"有效时间" 和可选的人性化的描述。

kubeadm token delete: 删除指定的引导令牌列表

kubeadm token generate: 此命令将打印一个随机生成的可以被 "init" 和 "join" 命令使用的引导令牌。

kubeadm token list: 列出服务器上的引导令牌

kubeadm certs: 处理Kubernetes证书的相关命令

kubeadm certs renew: 可以使用 all 子命令来续订所有 Kubernetes 证书,也可以选择性地续订部分证书。

kubeadm certs certificate-key: 此命令可用来生成一个新的控制面证书密钥。密钥可以作为 --certificate-key 标志的取值传递给 kubeadm initkubeadm join 命令,从而在添加新的控制面节点时能够自动完成证书复制

kubeadm certs check-expiration: 此命令检查 kubeadm 所管理的本地 PKI 中的证书是否以及何时过期。

kubeadm certs generate-csr: 此命令可用来为所有控制面证书和 kubeconfig 文件生成密钥和 CSR(签名请求)。 用户可以根据自身需要选择 CA 为 CSR 签名。

kubeadm upgrade: 一个对用户友好的命令,它将复杂的升级逻辑包装在一个命令后面,支持升级的规划和实际执行。

kubeadm upgrade plan: 检查可升级到哪些版本,并验证你当前的集群是否可升级。

kubeadm upgrade apply: 将Kubernetes集群升级到指定版本

kubeadm upgrade diff: 显示哪些差异将被应用于现有的静态 pod 资源清单

kubeadm upgrade node: 升级集群中某个节点的命令

kubeadm reset: 还原kubeadm init或者kubeadm join命令对主机所做的任何更改

kubeadm init phase: kubeadm init phase 能确保调用引导过程的原子步骤。 因此,如果希望自定义应用,则可以让 kubeadm 做一些工作,然后填补空白。

kubeadm join phase: kubeadm join phase 使你能够调用 join 过程的基本原子步骤。 因此,如果希望执行自定义操作,可以让 kubeadm 做一些工作,然后由用户来补足剩余操作。

kubeadm upgrade phase: 使用此阶段,可以选择执行辅助控制平面或工作节点升级的单独步骤。请注意,kubeadm upgrade apply 命令仍然必须在主控制平面节点上调用。

kubeadm reset phase: kubeadm reset phase 使你能够调用 reset 过程的基本原子步骤。 因此,如果希望执行自定义操作,可以让 kubeadm 做一些工作,然后由用户来补足剩余操作。

kubeadm alpha:尝试试验性功能

Kubebuilder

Kubebuilder 是一个使用 CRDs 构建 K8s API 的 SDK,主要是:

  • 提供脚手架工具初始化 CRDs 工程,自动生成 boilerplate 代码和配置;
  • 提供代码库封装底层的 K8s go-client;

方便用户从零开始开发 CRDs,Controllers 和 Admission Webhooks 来扩展 K8s。

GVK = GroupVersionKind,GVR = GroupVersionResource。

API Group 是相关 API 功能的集合,每个 Group 拥有一或多个 Versions,用于接口的演进。

每个 GV 都包含多个 API 类型,称为 Kinds,在不同的 Versions 之间同一个 Kind 定义可能不同, Resource 是 Kind 的对象标识(resource type),一般来说 Kinds 和 Resources 是 1:1 的,比如 pods Resource 对应 Pod Kind,但是有时候相同的 Kind 可能对应多个 Resources,比如 Scale Kind 可能对应很多 Resources:deployments/scale,replicasets/scale,对于 CRD 来说,只会是 1:1 的关系。

每一个 GVK 都关联着一个 package 中给定的 root Go type,比如 apps/v1/Deployment 就关联着 K8s 源码里面 k8s.io/api/apps/v1 package 中的 Deployment struct,我们提交的各类资源定义 YAML 文件都需要写:

  • apiVersion:这个就是 GV 。
  • kind:这个就是 K。

根据 GVK K8s 就能找到你到底要创建什么类型的资源,根据你定义的 Spec 创建好资源之后就成为了 Resource,也就是 GVR。GVK/GVR 就是 K8s 资源的坐标,是我们创建/删除/修改/读取资源的基础。

每一组 Controllers 都需要一个 Scheme,提供了 Kinds 与对应 Go types 的映射,也就是说给定 Go type 就知道他的 GVK,给定 GVK 就知道他的 Go type,比如说我们给定一个 Scheme: "tutotial.kubebuilder.io/api/v1".CronJob{} 这个 Go type 映射到 batch.tutotial.kubebuilder.io/v1 的 CronJob GVK,那么从 Api Server 获取到下面的 JSON:

{

"kind": "CronJob",

"apiVersion": "batch.tutorial.kubebuilder.io/v1",

...}

就能构造出对应的 Go type了,通过这个 Go type 也能正确地获取 GVR 的一些信息,控制器可以通过该 Go type 获取到期望状态以及其他辅助信息进行调谐逻辑。

Kubebuilder 的核心组件,具有 3 个职责:

  • 负责运行所有的 Controllers;
  • 初始化共享 caches,包含 listAndWatch 功能;
  • 初始化 clients 用于与 Api Server 通信。

Cache

Kubebuilder 的核心组件,负责在 Controller 进程里面根据 Scheme 同步 Api Server 中所有该 Controller 关心 GVKs 的 GVRs,其核心是 GVK -> Informer 的映射,Informer 会负责监听对应 GVK 的 GVRs 的创建/删除/更新操作,以触发 Controller 的 Reconcile 逻辑。

Controller

Kubebuidler 为我们生成的脚手架文件,我们只需要实现 Reconcile 方法即可。

Clients

在实现 Controller 的时候不可避免地需要对某些资源类型进行创建/删除/更新,就是通过该 Clients 实现的,其中查询功能实际查询是本地的 Cache,写操作直接访问 Api Server。

Index

由于 Controller 经常要对 Cache 进行查询,Kubebuilder 提供 Index utility 给 Cache 加索引提升查询效率。

Finalizer

在一般情况下,如果资源被删除之后,我们虽然能够被触发删除事件,但是这个时候从 Cache 里面无法读取任何被删除对象的信息,这样一来,导致很多垃圾清理工作因为信息不足无法进行,K8s 的 Finalizer 字段用于处理这种情况。在 K8s 中,只要对象 ObjectMeta 里面的 Finalizers 不为空,对该对象的 delete 操作就会转变为 update 操作,具体说就是 update deletionTimestamp 字段,其意义就是告诉 K8s 的 GC“在deletionTimestamp 这个时刻之后,只要 Finalizers 为空,就立马删除掉该对象”。

所以一般的使用姿势就是在创建对象时把 Finalizers 设置好(任意 string),然后处理 DeletionTimestamp 不为空的 update 操作(实际是 delete),根据 Finalizers 的值执行完所有的 pre-delete hook(此时可以在 Cache 里面读取到被删除对象的任何信息)之后将 Finalizers 置为空即可。

OwerReference

K8s GC 在删除一个对象时,任何 ownerReference 是该对象的对象都会被清除,与此同时,Kubebuidler 支持所有对象的变更都会触发 Owner 对象 controller 的 Reconcile 方法。

命令

kubebuilder init --domain edas.io

这一步创建了一个 Go module 工程,引入了必要的依赖,创建了一些模板文件。

kubebuilder create api --group apps --version v1alpha1 --kind Application

这一步创建了对应的 CRD 和 Controller 模板文件

源码

Kubebuilder 创建的 main.go 是整个项目的入口,逻辑十分简单

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)
func init() {
	appsv1alpha1.AddToScheme(scheme)
	// +kubebuilder:scaffold:scheme
}
func main() {
	...
        // 1、init Manager
	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Scheme: scheme, MetricsBindAddress: metricsAddr})
	if err != nil {
		setupLog.Error(err, "unable to start manager")
		os.Exit(1)
	}
        // 2、init Reconciler(Controller)
	err = (&controllers.ApplicationReconciler{
		Client: mgr.GetClient(),
		Log:    ctrl.Log.WithName("controllers").WithName("Application"),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr)
	if err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "EDASApplication")
		os.Exit(1)
	}
	// +kubebuilder:scaffold:builder
	setupLog.Info("starting manager")
        // 3、start Manager
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "problem running manager")
		os.Exit(1)
	}

可以看到在 init 方法里面我们将 appsv1alpha1 注册到 Scheme 里面去了,这样一来 Cache 就知道 watch 谁了,main 方法里面的逻辑基本都是 Manager 的:

  1. 初始化了一个 Manager;
  2. 将 Manager 的 Client 传给 Controller,并且调用 SetupWithManager 方法传入 Manager 进行 Controller 的初始化;
  3. 启动 Manager。

Manager初始化

// New returns a new Manager for creating Controllers.
func New(config *rest.Config, options Options) (Manager, error) {
	...
	// Create the cache for the cached read client and registering informers
	cache, err := options.NewCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod, Namespace: options.Namespace})
	if err != nil {
		return nil, err
	}
	apiReader, err := client.New(config, client.Options{Scheme: options.Scheme, Mapper: mapper})
	if err != nil {
		return nil, err
	}
	writeObj, err := options.NewClient(cache, config, client.Options{Scheme: options.Scheme, Mapper: mapper})
	if err != nil {
		return nil, err
	}
	...
	return &controllerManager{
		config:           config,
		scheme:           options.Scheme,
		errChan:          make(chan error),
		cache:            cache,
		fieldIndexes:     cache,
		client:           writeObj,
		apiReader:        apiReader,
		recorderProvider: recorderProvider,
		resourceLock:     resourceLock,
		mapper:           mapper,
		metricsListener:  metricsListener,
		internalStop:     stop,
		internalStopper:  stop,
		port:             options.Port,
		host:             options.Host,
		leaseDuration:    *options.LeaseDuration,
		renewDeadline:    *options.RenewDeadline,
		retryPeriod:      *options.RetryPeriod,
	}, nil
}

创建Cache

Cache初始化代码

// New initializes and returns a new Cache.
func New(config *rest.Config, opts Options) (Cache, error) {
	opts, err := defaultOpts(config, opts)
	if err != nil {
		return nil, err
	}
	im := internal.NewInformersMap(config, opts.Scheme, opts.Mapper, *opts.Resync, opts.Namespace)
	return &informerCache{InformersMap: im}, nil
}
// newSpecificInformersMap returns a new specificInformersMap (like
// the generical InformersMap, except that it doesn't implement WaitForCacheSync).
func newSpecificInformersMap(...) *specificInformersMap {
	ip := &specificInformersMap{
		Scheme:            scheme,
		mapper:            mapper,
		informersByGVK:    make(map[schema.GroupVersionKind]*MapEntry),
		codecs:            serializer.NewCodecFactory(scheme),
		resync:            resync,
		createListWatcher: createListWatcher,
		namespace:         namespace,
	}
	return ip
}
// MapEntry contains the cached data for an Informer
type MapEntry struct {
	// Informer is the cached informer
	Informer cache.SharedIndexInformer
	// CacheReader wraps Informer and implements the CacheReader interface for a single type
	Reader CacheReader
}
func createUnstructuredListWatch(gvk schema.GroupVersionKind, ip *specificInformersMap) (*cache.ListWatch, error) {
        ...
	// Create a new ListWatch for the obj
	return &cache.ListWatch{
		ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) {
			if ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot {
				return dynamicClient.Resource(mapping.Resource).Namespace(ip.namespace).List(opts)
			}
			return dynamicClient.Resource(mapping.Resource).List(opts)
		},
		// Setup the watch function
		WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) {
			// Watch needs to be set to true separately
			opts.Watch = true
			if ip.namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameRoot {
				return dynamicClient.Resource(mapping.Resource).Namespace(ip.namespace).Watch(opts)
			}
			return dynamicClient.Resource(mapping.Resource).Watch(opts)
		},
	}, nil
}

Cache 主要就是创建了 InformersMap,Scheme 里面的每个 GVK 都创建了对应的 Informer,通过 informersByGVK 这个 map 做 GVK 到 Informer 的映射,每个 Informer 会根据 ListWatch 函数对对应的 GVK 进行 List 和 Watch

创建Clients

创建Clients 很简单

// defaultNewClient creates the default caching client
func defaultNewClient(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
	// Create the Client for Write operations.
	c, err := client.New(config, options)
	if err != nil {
		return nil, err
	}
	return &client.DelegatingClient{
		Reader: &client.DelegatingReader{
			CacheReader:  cache,
			ClientReader: c,
		},
		Writer:       c,
		StatusClient: c,
	}, nil
}

读操作使用上面创建的 Cache,写操作使用 K8s go-client 直连。

Controller初始化

func (r *EDASApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
		err := ctrl.NewControllerManagedBy(mgr).
		For(&appsv1alpha1.EDASApplication{}).
		Complete(r)
		return err
}

使用的是 Builder 模式,NewControllerManagerBy 和 For 方法都是给 Builder 传参,最重要的是最后一个方法 Complete,其逻辑是

func (blder *Builder) Build(r reconcile.Reconciler) (manager.Manager, error) {
...
	// Set the Manager
	if err := blder.doManager(); err != nil {
		return nil, err
	}
	// Set the ControllerManagedBy
	if err := blder.doController(r); err != nil {
		return nil, err
	}
	// Set the Watch
	if err := blder.doWatch(); err != nil {
		return nil, err
	}
...
	return blder.mgr, nil
}

主要是看看 doController 和 doWatch 方法

doController方法

func New(name string, mgr manager.Manager, options Options) (Controller, error) {
	if options.Reconciler == nil {
		return nil, fmt.Errorf("must specify Reconciler")
	}
	if len(name) == 0 {
		return nil, fmt.Errorf("must specify Name for Controller")
	}
	if options.MaxConcurrentReconciles <= 0 {
		options.MaxConcurrentReconciles = 1
	}
	// Inject dependencies into Reconciler
	if err := mgr.SetFields(options.Reconciler); err != nil {
		return nil, err
	}
	// Create controller with dependencies set
	c := &controller.Controller{
		Do:                      options.Reconciler,
		Cache:                   mgr.GetCache(),
		Config:                  mgr.GetConfig(),
		Scheme:                  mgr.GetScheme(),
		Client:                  mgr.GetClient(),
		Recorder:                mgr.GetEventRecorderFor(name),
		Queue:                   workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), name),
		MaxConcurrentReconciles: options.MaxConcurrentReconciles,
		Name:                    name,
	}
	// Add the controller as a Manager components
	return c, mgr.Add(c)
}

该方法初始化了一个 Controller,传入了一些很重要的参数:

  • Do:Reconcile 逻辑;
  • Cache:找 Informer 注册 Watch;
  • Client:对 K8s 资源进行 CRUD;
  • Queue:Watch 资源的 CUD 事件缓存;
  • Recorder:事件收集。

doWatch方法

func (blder *Builder) doWatch() error {
	// Reconcile type
	src := &source.Kind{Type: blder.apiType}
	hdler := &handler.EnqueueRequestForObject{}
	err := blder.ctrl.Watch(src, hdler, blder.predicates...)
	if err != nil {
		return err
	}
	// Watches the managed types
	for _, obj := range blder.managedObjects {
		src := &source.Kind{Type: obj}
		hdler := &handler.EnqueueRequestForOwner{
			OwnerType:    blder.apiType,
			IsController: true,
		}
		if err := blder.ctrl.Watch(src, hdler, blder.predicates...); err != nil {
			return err
		}
	}
	// Do the watch requests
	for _, w := range blder.watchRequest {
		if err := blder.ctrl.Watch(w.src, w.eventhandler, blder.predicates...); err != nil {
			return err
		}
	}
	return nil
}

可以看到该方法对本 Controller 负责的 CRD 进行了 watch,同时底下还会 watch 本 CRD 管理的其他资源,这个 managedObjects 可以通过 Controller 初始化 Buidler 的 Owns 方法传入,说到 Watch 我们关心两个逻辑:

  1. 注册的 handler

可以看到 Kubebuidler 为我们注册的 Handler 就是将发生变更的对象的 NamespacedName 入队列,如果在 Reconcile 逻辑中需要判断创建/更新/删除,需要有自己的判断逻辑。

  1. 注册的流程
// Watch implements controller.Controller
func (c *Controller) Watch(src source.Source, evthdler handler.EventHandler, prct ...predicate.Predicate) error {
	...
	log.Info("Starting EventSource", "controller", c.Name, "source", src)
	return src.Start(evthdler, c.Queue, prct...)
}
// Start is internal and should be called only by the Controller to register an EventHandler with the Informer
// to enqueue reconcile.Requests.
func (is *Informer) Start(handler handler.EventHandler, queue workqueue.RateLimitingInterface,
	...
	is.Informer.AddEventHandler(internal.EventHandler{Queue: queue, EventHandler: handler, Predicates: prct})
	return nil
}

我们的 Handler 实际注册到 Informer 上面,这样整个逻辑就串起来了,通过 Cache 我们创建了所有 Scheme 里面 GVKs 的 Informers,然后对应 GVK 的 Controller 注册了 Watch Handler 到对应的 Informer,这样一来对应的 GVK 里面的资源有变更都会触发 Handler,将变更事件写到 Controller 的事件队列中,之后触发我们的 Reconcile 方法。

Manager启动

func (cm *controllerManager) Start(stop <-chan struct{}) error {
	...
	go cm.startNonLeaderElectionRunnables()
	...
}
func (cm *controllerManager) startNonLeaderElectionRunnables() {
	...
	// Start the Cache. Allow the function to start the cache to be mocked out for testing
	if cm.startCache == nil {
		cm.startCache = cm.cache.Start
	}
	go func() {
		if err := cm.startCache(cm.internalStop); err != nil {
			cm.errChan <- err
		}
	}()
        ...
        // Start Controllers
	for _, c := range cm.nonLeaderElectionRunnables {
		ctrl := c
		go func() {
			cm.errChan <- ctrl.Start(cm.internalStop)
		}()
	}
	cm.started = true
}

Cache启动

func (ip *specificInformersMap) Start(stop <-chan struct{}) {
	func() {
		...
		// Start each informer
		for _, informer := range ip.informersByGVK {
			go informer.Informer.Run(stop)
		}
	}()
}
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
        ...
        // informer push resource obj CUD delta to this fifo queue
	fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, s.indexer)
	cfg := &Config{
		Queue:            fifo,
		ListerWatcher:    s.listerWatcher,
		ObjectType:       s.objectType,
		FullResyncPeriod: s.resyncCheckPeriod,
		RetryOnError:     false,
		ShouldResync:     s.processor.shouldResync,
    // handler to process delta
		Process: s.HandleDeltas,
	}
	func() {
		s.startedLock.Lock()
		defer s.startedLock.Unlock()
    // this is internal controller process delta generate by reflector
		s.controller = New(cfg)
		s.controller.(*controller).clock = s.clock
		s.started = true
	}()
        ...
	wg.StartWithChannel(processorStopCh, s.processor.run)
	s.controller.Run(stopCh)
}
func (c *controller) Run(stopCh <-chan struct{}) {
	...
	r := NewReflector(
		c.config.ListerWatcher,
		c.config.ObjectType,
		c.config.Queue,
		c.config.FullResyncPeriod,
	)
	...
        // reflector is delta producer
	wg.StartWithChannel(stopCh, r.Run)
        // internal controller's processLoop is comsume logic
	wait.Until(c.processLoop, time.Second, stopCh)
}

Cache 的初始化核心是初始化所有的 Informer,Informer 的初始化核心是创建了 reflector 和内部 controller,reflector 负责监听 Api Server 上指定的 GVK,将变更写入 delta 队列中,可以理解为变更事件的生产者,内部 controller 是变更事件的消费者,他会负责更新本地 indexer,以及计算出 CUD 事件推给我们之前注册的 Watch Handler。

Controller启动

// Start implements controller.Controller
func (c *Controller) Start(stop <-chan struct{}) error {
	...
	for i := 0; i < c.MaxConcurrentReconciles; i++ {
		// Process work items
		go wait.Until(func() {
			for c.processNextWorkItem() {
			}
		}, c.JitterPeriod, stop)
	}
	...
}
func (c *Controller) processNextWorkItem() bool {
	...
	obj, shutdown := c.Queue.Get()
	...
	var req reconcile.Request
	var ok bool
	if req, ok = obj.(reconcile.Request); 
        ...
	// RunInformersAndControllers the syncHandler, passing it the namespace/Name string of the
	// resource to be synced.
	if result, err := c.Do.Reconcile(req); err != nil {
		c.Queue.AddRateLimited(req)
		...
	} 
        ...
}

Controller 的初始化是启动 goroutine 不断地查询队列,如果有变更消息则触发到我们自定义的 Reconcile 逻辑

Operator Framework

https://github.com/operator-framework/operator-sdk

集群联邦

集群联邦 Federation 的目的是实现单一集群统一管理多个kubernetes集群的机制。这些集群可以是跨地域的,跨云厂商的或者是用户内部自建集群。一旦集群建立联邦后,就可以使用集群 Federation API 来管理多个集群的 kubernetes API 资源。

kubernetes集群联邦主要是实现以下目标

1)简化管理多个联邦集群的Kubernetes API 资源

2)在多个集群之间分散工作负载(容器),以提升应用(服务)的可靠性

3)在不同集群中,能更快速更容易地迁移应用(服务)

4)跨集群的服务发现,服务可以就近访问,以降低延迟

5)实践多云(Multi-cloud)或混合云(Hybird Cloud)的部署

集群联邦最初是 v1 版本,因为方案设计问题,导致可扩展性比较差,已经被废弃,目前社区提出了新的方案 Federation V2

从上图架构中得知Federation v1 的设计沿用类似Kubernetes 模型,其主要组件有以下:

federation-apiserver :提供Federation API资源,只支持部分Kubernetes API resources。
federation-controller-manager :协调不同集群之间的状态,如同步Federated资源与策略,并建立Kubernetes组件至对应集群上。
etcd :储存Federation的状态。

该方案设计之初没有考虑到crd的特性,支持的 Federation API 资源类型都是写死的,无法有效的扩展,不能兼容新的api资源,不支持跨集群权限管理,如不支持RBAC。联邦层级的设定与策略依赖API 资源的Annotations 内容,这使得弹性不佳。

Federation V2

Federation V2 是Kubernetes SIG Multi-Cluster团队新提出的集群联邦架构( Architecture Doc与Brainstorming Doc ),新架构在Federation v1基础之上,简化扩展Federated API过程,并加强跨集群服务发现与编排的功能。

相较于V1,Federation V2移除了 federation-apiserver 组件,通过 crd 机制来完成 federated resource的扩充,kubefed-controller 组件通过监听crd的变化来完成联邦资源的同步和调度等功能。

集群联邦安装文档 DOC,集群成员注册后,会在管理集群创建一个 KubeFedCluster 资源来存储集群的基本信息,如API Endpoint、CA Bundle等,kubefed controller 通过这些信息来访问和管理联邦集群成员。

联邦化资源

Federation V2 可以联邦化任意资源,包括自定义的crd资源。对集群资源联邦化的实现主要是通过两种CRD来完成,分别是 FederatedTypeConfig 和 Federated ,FederatedTypeConfig定义了 Federated 和kubernetes api资源的关联关系。而 Federated 用来定义怎么去联邦化对应的kubernetes api资源。

KubeFed提供了一种自动化机制来将工作负载实例分散到不同的集群中,且能够基于总副本数与集群的定义策略来将Deployment或ReplicaSet资源进行编排。编排策略是通过创建ReplicaSchedulingPreference(RSP),再由KubeFed RSP Controller监听与获取RSP内容来将工作负载实例建立到指定的集群上。

管理K8s集群的工具

miniKube

Kind

Cloud Providers

Kube-DNS

KubeVela

KubeVela 是一个开箱即用的现代化应用交付与管理平台,它使得应用在面向混合云环境中的交付更简单、快捷。使用 KubeVela 的软件开发团队,可以按需使用云原生能力构建应用,随着团队规模的发展、业务场景的变化扩展其功能,一次构建应用,随处运行。

云原生技术的发展趋势正在朝着利用 Kubernetes 作为公共抽象层来实现高度一致的、跨云、跨环境的的应用交付而不断迈进。然而,尽管 Kubernetes 在统一底层基础架构细节方面表现出色,它并没有在混合的分布式部署环境之上提供应用层的软件交付模型和抽象。我们已经看到,这种缺乏统一上层抽象的软件交付过程,不仅降低了生产力、影响了用户体验,甚至还会导致生产中出现错误和故障。

然而,为现代微服务应用的交付过程建模是一个高度碎片化且充满挑战的事情。到目前为止,绝大多数试图解决上述问题的技术方案,要么过于简单以致于无法覆盖实际生产使用中的问题,要么过于复杂难以落地使用。云原生带来的基础设施能力爆发式增长也决定了新一代的应用管理平台不能以硬编码的方式做能力的集成和 UI 的构建,除了满足基础的功能和场景,平台本身的扩展能力成为了新时代应用管理平台的核心诉求。这就意味着平台不仅要简单易用,还要能够随着应用交付和管理的需求复杂度提升能够不断扩张,能够让开发者自助式的接入和使用,充分享受云原生生态的红利。

这也是 KubeVela 出现的核心价值:它既能够简化面向混合环境(多集群/多云/混合云/分布式云)的应用交付过程;同时又足够灵活可以随时满足业务不断高速变化所带来的迭代压力。它本身是一个面向混合交付环境同时又高可扩展的应用交付引擎,满足平台构建者的扩展和自建需求;同时又附加了一系列开箱即用的扩展组件,能够让开发者自助式的开发、交付云原生应用。

与其他系统对比

vs ci/cd(GitHub action gitlab jenkins)

KubeVela 是一个工作在 CI 流程下游的 CD 控制平面(Continuous Delivery Control Plane)。所以 KubeVela 希望你保持现有的 CI 流程,而在需要开始制品部署时让 KubeVela 接管 CD 流程。KubeVela 会给你的 CD 流程带来大量的现代化应用交付最佳实践,比如:声明式交付工作流、可编程的工作流步骤、Pull 模型、多云/多集群交付流程、统一的云服务部署和绑定等等。

如果你已经在 CD 环节中采纳了 GitOps 实践,KubeVela 会更容易跟你的 CI/CD 系统集成,因为 KubeVela 是完全声明式的。只需要把 KubeVela 的应用部署描述文件放置在你的配置仓库当中,所有的 KubeVela 特性(包括声明式交付工作流、多云/多集群交付流程等)就会立刻在你 的 GitOps 流程中出现。

Vs gitops(argoCD fluxCD)

KubeVela 可以基于你的 GitOps 流程,并在此之上增加跨云、跨环境的能力:

  • KubeVela 具有一个用户友好且可编程的工作流,可以让你集成现有的交付工具,包括通知和审批体系。
  • KubeVela 可以为你提供跨环境交付能力,让你在一个应用中描述多集群的差异化配置并统一的查看状态。

Vs PaaS

传统 PaaS 提供完整的应用程序部署和管理功能,旨在提高开发人员的体验和效率。在这个场景下,KubeVela 也有着相同的目标。

不过,KubeVela 和它们最大的区别在于其可扩展性

KubeVela 是可编程的。它的交付工作流乃至整个应用交付与管理能力集都是由独立的可插拔模块构成的,这些模块可以随时通过编写 CUE 模板的方式进行增/删/重定义且变更会即时生效。与这种机制相比,传统的 PaaS 系统的限制非常多:它们需要对应用类型和提供的能力进行各种约束来实现更好的用户体验,但随着应用交付需求的增长,用户的诉求就一定会超出 PaaS 系统的能力边界。这种情况在 KubeVela 平台中则永远不会发生。

此外,KubeVela 是一个独立于运行时集群的应用交付控制平面(这是我们认为的下一代 PaaS 系统的合理形态),而现有的 PaaS 则往往选择以插件形式部署在运行时集群当中。

vs Helm

Helm 是 Kubernetes 的包管理器,它能够以 Chart 为一个单元,提供打包、安装和升级的一组 YAML 文件的能力。

KubeVela 作为一个应用交付系统天然可以部署各种制品类型,Kustomize、Kubernetes Yaml 等,当然也包括 Chart。Helm 可以便捷的把 Chart 交付到一个集群,KubeVela 可以帮你把 Chart 交付到多个集群。

当然,KubeVela 还支持其他制品格式比如 Kustomize。

安装

安装Vela UI

$ vela addon enable ~/.vela/addons/velaux

访问

$ vela port-forward addon-velaux -n vela-system 8080:80

核心概念

每一个应用部署计划都由四个部分组成,分别是组件、运维能力、部署策略和工作流。其格式如下

这个 Application 对象会引用 componenttraitpolicy 以及 workflow step 的类型,这些类型背后是平台构建者(运维团队)维护的可编程模块。可以看到,这种抽象的方式是高度可扩展、可定制的

apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: <name>
spec:
  components:
    - name: <component name>
      type: <component type>
      properties:
        <parameter values>
      traits:
        - type: <trait type>
          properties:
            <traits parameter values>
    - name: <component name>
      type: <component type>
      properties:
        <parameter values>
  policies:
  - name: <policy name>
    type: <policy type>
    properties:
      <policy parameter values>
  workflow:
    - name: <step name>
      type: <step type>
      properties:
        <step parameter values>   
  • 组件(Component): 组件定义一个应用包含的待交付制品(二进制、Docker 镜像、Helm Chart...)或云服务。我们认为一个应用部署计划部署的是一个微服务单元,里面主要包含一个核心的用于频繁迭代的服务,以及一组服务所依赖的中间件集合(包含数据库、缓存、云服务等),一个应用中包含的组件数量应该控制在约 15 个以内。
  • 运维特征(Trait): 运维特征是可以随时绑定给待部署组件的、模块化、可拔插的运维能力,比如:副本数调整(手动、自动)、数据持久化、 设置网关策略、自动设置 DNS 解析等。
  • 应用策略(Policy): 应用策略负责定义指定应用交付过程中的策略,比如多集群部署的差异化配置、资源放置策略、安全组策略、防火墙规则、SLO 目标等。
  • 工作流步骤(Workflow Step): 工作流由多个步骤组成,允许用户自定义应用在某个环境的交付过程。典型的工作流步骤包括人工审核、数据传递、多集群发布、通知等。

以上这些概念的背后都是由一组称为模块定义(Definitions)的可编程模块提供具体功能。KubeVela 会像胶水一样基于 Kubernetes API 定义基础设施定义的抽象并将不同的能力组合起来

Cue

Cue是自动化配置的一种语言。Dagger也使用这种语言

Kubespray

Kubespray 是由若干 Ansible Playbook、 清单(inventory)、 制备工具和通用 OS/Kubernetes 集群配置管理任务的领域知识组成的。

Kubespray 提供:

  • 高可用性集群
  • 可组合属性(例如可选择网络插件)
  • 支持大多数流行的 Linux 发行版
  • 持续集成测试

Kubespray 能够自定义部署的许多方面:

  • 选择部署模式: kubeadm 或非 kubeadm
  • CNI(网络)插件
  • DNS 配置
  • 控制平面的选择:本机/可执行文件或容器化
  • 组件版本
  • Calico 路由反射器
  • 组件运行时选项

  • 证书生成方式

可以修改变量文件以进行 Kubespray 定制。 如果你刚刚开始使用 Kubespray,请考虑使用 Kubespray 默认设置来部署你的集群并探索 Kubernetes。

Kubespray 提供了一种使用 Netchecker 验证 Pod 间连接和 DNS 解析的方法。 Netchecker 确保 netchecker-agents Pod 可以解析 DNS 请求, 并在默认命名空间内对每个请求执行 ping 操作。 这些 Pod 模仿其他工作负载类似的行为,并用作集群运行状况指示器。

KubeKey

CAPI/LCM

云原生的核心之一是 Kubernetes,而 Kubernetes 的核心之一是集群生命周期管理(LCM)。解决了 LCM 的管理问题,可以一定程度上降低 Kubernetes 的使用成本。本文将主要探讨 LCM 相关问题。

LCM 包括但不局限于:

  • 集群创建
  • 集群删除
  • 集群扩缩容,增加或减少节点数
  • 集群升级,集群从低版本升级到更高的版本
  • 集群故障恢复,集群出现故障,例如某节点故障,修复节点故障恢复正常工作

社区目前常用 kubeadmkOpskubesprayRKEkubekey 等工具创建、扩容和升级集群。公有云和私有云则有自己的 Kubernetes 服务,但这些服务一般仅限本平台,对跨平台支持不友好。此外还有 RancherKubeSphere 等开源或商业化的容器平台,但它们没有通用的多平台支持,LCM 功能不丰富或是不够自动化。

以上 LCM 方案可能存在的问题:

  • 需要掌握一定的 Kubernetes 相关知识和经验
  • 不够自动化,手工管理,命令行工具或没有 UI,效率低,容易出错
  • 没有形成统一的技术标准,各自有自己的技术方案,可扩展性不强
  • 跨平台支持不够好

Cluster API(CAPI)是一个 Kubernetes 声明式 API 风格的多 Kubernetes 集群生命周期管理项目。CAPI 的目标是简化 Kubernetes LCM,使得 LCM 自动化,并支持不同的 IaaS(AWS、VMware 等)。

Kubernetes 声明式 API 通过 Resource + Controller 的模式实现。Resource 包括 Kubernetes 原生资源(Pod 等)和自定义资源(CRD)。每个资源对象包含 Spec 表示资源对象预期是什么样的,Status 表示预期资源对象当前的实际状态,Controller 则负责把资源达到预期的 Spec 状态。

在此基础上,Kubernetes 社区发展出了 Operator 模式,用来管理应用和基础设施资源(例如 prometheus-operator

Kubernetes 声明式 API 具有组合的特点,不同的 API 可以组合使用,达到功能扩展的目的。例如 Deployment + ReplicaSet + Pod 组合实现多版本应用部署管理。

目前常见通过声明式的方式管理分布式系统,而 Kubernetes 本身也是分布式系统,所以通过声明式管理 Kubernetes 集群是合适的。

CAPI 属于 Kubernetes Cluster Lifecycle 生态中的子项目,使用 Kubernetes 声明式风格也是比较天然的(Kube-On-Kube)。可以借用 Kubernetes 生态的优势,同时社区用户也更容易理解和使用。

声明式可以带来以下好处:

  • 使用配置文件描述最终状态,不需要考虑流程和目标环境的细节
  • 重复操作不会产生不一致的效果
  • 天然符合不可变基础设施的理念
  • 声明式自愈保证高可用

根据 Kubernetes 声明式 API 的特点,当我们需要一个 Kubernetes 集群的时候,可以通过一个 CRD 定义并描述我们的需求。CAPI 定义了 Cluster 用来描述 Kubernetes 集群

apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:  
	name: mycluster  
	namespace: default
spec: # 需要一个什么样的集群
status: # 当前集群的状态

CAPI controllers 根据 Cluster 创建并管理集群。

CAPI 部署所在的 Kubernetes 集群称为管控集群(Management Cluster),基于 CAPI 创建的 Kubernetes 集群称为工作负载集群(Workload Cluster)

管控集群主要由 CAPI 和 CAPI Providers 组成。

节点是集群的核心,节点的管理也是集群管理的重要部分。节点分为 Control Plane 和 Worker 节点,两者的角色和功能不一样,管理起来也有差异。

一个 Worker 节点包含多个 Kubernetes 组件程序(kubelet 等),集群升级过程会存在多个版本的节点同时存在的情况。这些特点和 Pod 有些相似之处,因此 CAPI 借鉴了 Deployment 的设计理念:MachineDeployment (MD) + MachineSet (MS) + Machine (Pod),每个 MachineSet 管理同一个版本的节点

Control Plane 节点是集群的控制面,业务逻辑和 Worker 节点不一样,除了有 kubelet 还有 APIServer、Etcd 等不同的组件。

Control Plane 节点目前常见以下几种管理方式:

  • 集群自身管理,例如 kubeadm 通过 static pods 运行 Control Plane 节点
  • 额外的 Kubernetes 集群部署,使用 Deployment 和 StatefulSet 的形式部署 Control Plane 节点
  • 第三方托管,例如 GKE、AKS、EKS 等

CAPI 提供了 ControlPlane Provider 的概念,我们可以根据不同的 Control Plane 节点

管理方式而选择不同的 Provider。CAPI 默认提供了 KubeadmControlPlane(KCP)管理 Control Plane 节点。

多平台

不同的 IaaS 管理 Kubernetes 集群会有差异。CAPI 封装了每个 IaaS 通用的 LCM 数据和逻辑,每个 IaaS 只需要处理自己特有的相关逻辑。为此 CAPI 提出了 Infrastructure Provider 的概念,每个 IaaS 按照规范实现即可(参考 Provider Implementers)。Provider 体现了 CAPI 利用 Kubernetes 声明式 API 抽象和组合的作用,带来了可以支持不同 IaaS 集群管理的可扩展性

CNI

原生K8s CNI 与第三方

https://github.com/containernetworking/cni

CRI、OCI、CRI-O、RUNC和containerd

CRI(Container Runtime Interface,容器运行时接口)是kubernetes定义的接口,定义了如何操作容器和镜像的统一规范,它主要包含ImageService和ContainerService。因为它已经是一个标准,所以你可以选择任何一个CRI的实现( containerd和CRI-O)来使用。

CRI-O也是一个CRI的实现,它来自于Red Hat/IBM等

OCI(Open Container Initialtive)提供了容器镜像和运行容器的规范。runc是OCI的一个实现,它是一个创建和运行容器进程的工具。

RUNC 实际上是从libcontainer演化过来的,并且是docker贡献给社区的第一个OCI参考实现,它就是用来创建和运行容器进行的工具。

containerd是容器虚拟化技术,从docker中剥离出来,形成开放容器接口(OCI)标准的一部分。

docker对容器的管理和操作基本都是通过containerd完成的。Containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性。Containerd 可以在宿主机中管理完整的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等。详细点说,Containerd 负责干下面这些事情

管理容器的生命周期(从创建容器到销毁容器) 拉取/推送容器镜像 存储管理(管理镜像及容器数据的存储) 调用 runC 运行容器(与 runC 等容器运行时交互) 管理容器网络接口及网络

Ansible

Ansible 是使用 Python 开发的自动化运维工具,如果这么说比较抽象的话,那么可以说 Ansible 可以让服务器管理人员使用文本来管理服务器,编写一段配置文件,在不同的机器上执行。

Ansible 使用 YAML 作为配置文件,YAML 是一个非常节省空间,并且没有丧失可读性的文件格式,其设计参考了很多语言和文件格式,包括 XML,JSON,C 语言,Python,Perl 以及电子邮件格式 RFC2822 等等。

Ansible 解决的问题正是在运维过程中多机器管理的问题。当有一台机器时运维比较简单,当如果要去管理 100 台机器,复杂度就上升了。使用 Ansible 可以让运维人员通过简单直观的文本配置来对所有纳入管理的机器统一进行管理。如果再用简单的话来概述 Ansible 的话,就是定义一次,无数次执行。

Ansible的主要工作

  • 定义目标机器列表,也就是需要管理的机器
  • 定义配置,使用 YAML 文件配置任务
  • 执行具体任务

Ansible的特性

  • 低学习成本
  • 无需在服务器中安装客户端,基于 SSH 工作,可并行执行
  • 无需服务端,直接终端命令即可
  • 管理的对象可以包括物理机,虚拟机,容器等等
  • 使用 YAML 格式文件编排 playbook

基本概念

Ansible 中的一些概念。

  • control node: 控制节点,可以在任何安装了 Python 环境的机器中使用 ansible,两个重要的可执行文件在 /usr/bin/ansible/usr/bin/ansible-playbook
  • managed node: 被控制的节点
  • inventory: 需要管理的节点,通常配置成 hostfile 文件 1
  • modules: ansible 进行自动化任务时调用的模块,社区提供了非常多 modules
  • Task: Ansible 的执行单元
  • playbook: 编排多个任务
  • roles: roles 是将 playbook 划分多个部分的机制
  • plugins: ansible 插件

工作流程:

  • 读取配置
  • 获取机器列表及分组配置
  • 确定执行模块和配置,modules 目录动态读取
  • Runner 执行
  • 输出

ubuntu安装

sudo apt update
sudo apt install software-properties-common
sudo apt-add-repository --yes --update ppa:ansible/ansible
sudo apt install ansible

配置

ansible.cfg 文件是 Ansible 中最主要的配置文件,ansible 寻找配置文件按照如下的优先级进行:

  • 由环境变量 ANSIBLE_CONFIG 指定的文件
  • ./ansible.cfg (ansible.cfg in the current directory)
  • ~/.ansible.cfg (.ansible.cfg in your home directory)
  • /etc/ansible/ansible.cfg

最简单的 ansible.cfg 配置示例:

[defaults]
hostfile = hosts
remote_user = root
remote_port = 22
host_key_checking = False
  • Hostfile

    文件指定了当前文件夹下的 hosts 文件。hosts 文件中会配置需要管理的机器 host

    • 配置 SSH 免密登录的文章可以参考之前的文章.
  • remote_user 配置默认操作的用户,如果没有配置,默认会使用当前用户
  • host_key_checking: 禁用 SSH key host checking

https://einverne.github.io/post/2020/05/ansible-introduction.html#%E5%B0%8F%E7%BB%93

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github