operator 之旅(一)

环境准备

依赖版本

MAC M1

kubernetes: 1.18.3

go: 1.17.6

kubebuilder:3.1.0

知识必备

Kubernetes的Group、Version、Resource、Kind浅解

Kubernetes是以资源为中心的系统:Group、Version、Resource、Kind 等资源相关的数据结构显得格外重要。

Group

Group即资源组,在kubernetes对资源进行分组时,对应的数据结构就是Group,源码路径:staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go
,如下,可见Group有自己的名称和版本:

type APIGroup struct {
	TypeMeta `json:",inline"`
	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
	Versions []GroupVersionForDiscovery `json:"versions" protobuf:"bytes,2,rep,name=versions"`
	PreferredVersion GroupVersionForDiscovery `json:"preferredVersion,omitempty" protobuf:"bytes,3,opt,name=preferredVersion"`
	ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs,omitempty" protobuf:"bytes,4,rep,name=serverAddressByClientCIDRs"`
}

在kubernetes中有两种资源组:有组名资源组和无组名资源组(也叫核心资源组Core Groups),它们都很常见:
例子:deployment有组名,pod没有组名,把它俩的OpenAPI放在一起对比就一目了然了

Version

Version即版本,这个好理解,kubernetes的版本分为三种:

Alpha:内部测试版本,如v1alpha1

Beta:经历了官方和社区测试的相对稳定版,如v1beta1

Stable:正式发布版,如v1、v2

如下图红框,资源组batch之下有v1和v2alpha1两个版本,每个版本下都有多个资源:

数据结构源码还是在staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go文件中,如下:

type APIVersions struct {
	TypeMeta `json:",inline"`
	Versions []string `json:"versions" protobuf:"bytes,1,rep,name=versions"`
	ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs" protobuf:"bytes,2,rep,name=serverAddressByClientCIDRs"`
}

Resource

Resource资源在kubernetes中的重要性是不言而喻的,常见的pod、service、deployment这些都是资源。

在kubernetes环境被实例化的资源即资源对象(ResourceObject)

资源被分为持久性(Persistent Entity)和非持久性(Ephemeral Entity),持久性如deployment,创建后会在etcd保存,非持久性如pod

kubernetes为资源准备了8种操作:create、delete、deletecollection、get、list、patch、update、watch,每一种资源都支持其中的一部分,这在每个资源的API文档中可以看到

增:

create:Resource Object 创建
删:

delete:单个 Resource Object 删除

deletecollection:多个 Resource Objects 删除

改:
patch:Resource Object 局部字段更新

update:Resource Object 整体更新

查:

get:单个 Resource Object 获取

list:多个 Resource Objects 获取

watch:Resource Objects 监控

资源支持以命名空间(namespace)进行隔离

资源对象描述文件在日常操作中频繁用到,一共由五部分组成:apiVersion、kind、metadata、spec、status,下图是官方的deployment描述文件,用于创建3个nginx pod,对着红框和文字就了解每个部分的作用了


上图并没有status,该部分是用来反应当前资源对象状态的,体现在资源的数据结构中,如下所示:

type Deployment struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
	Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
	Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

数据结构源码还是在staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go文件中,如下:

type APIResource struct {
	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
	SingularName string `json:"singularName" protobuf:"bytes,6,opt,name=singularName"`
	Namespaced bool `json:"namespaced" protobuf:"varint,2,opt,name=namespaced"`
	Group string `json:"group,omitempty" protobuf:"bytes,8,opt,name=group"`
	Version string `json:"version,omitempty" protobuf:"bytes,9,opt,name=version"`
	Kind string `json:"kind" protobuf:"bytes,3,opt,name=kind"`
	Verbs Verbs `json:"verbs" protobuf:"bytes,4,opt,name=verbs"`
	ShortNames []string `json:"shortNames,omitempty" protobuf:"bytes,5,rep,name=shortNames"`
	Categories []string `json:"categories,omitempty" protobuf:"bytes,7,rep,name=categories"`
	StorageVersionHash string `json:"storageVersionHash,omitempty" protobuf:"bytes,10,opt,name=storageVersionHash"`
}

Kind

Kind 与 Resource 属于同一级概念,Kind 用于描述 Resource 的种类

Scheme

每种资源的都需要有对应的Scheme,Scheme结构体包含gvkToType和typeToGVK的字段映射关系,APIServer 根据Scheme来进行资源的序列化和反序列化。

GV & GVK & GVR

GV: Api Group & Version

API Group 是相关 API 功能的集合

每个 Group 拥有一或多个 Versions

GVK: Group Version Kind

每个 GV 都包含 N 个 api 类型,称之为 Kinds,不同 Version 同一个 Kinds 可能不同

GVR: Group Version Resource

Resource 是 Kind 的对象标识,一般来 Kind 和 Resource 是 1:1 的,但是有时候存在 1:n 的关系,不过对于 Operator 来说都是 1:1 的关系

举个例子,我们在 k8s 中的 yaml 文件都有下面这么两行:

apiVersion: apps/v1 # 这个是 GV,G 是 apps,V 是 v1
kind: Deployment    # 这个就是 Kind
sepc:               # 加上下放的 spec 就是 Resource了
  ...

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

查看当前环境的所有资源,及其相关属性
# ctl api-resources -o wide
查看特定group下面的资源,例如:apps
# ctl api-resources --api-group apps -o wide
查看指定资源的详情,例如:configmap
# ctl explain configmap
查看所有Group和Version的命令
# ctl api-versions

官方文档速查

在实际学习和开发中,对指定资源做增删改查操作时,官方文档是我们最可靠的依赖,地址:https://kubernetes.io/docs/reference/kubernetes-api/

打开deployment的文档,如下图:

另外还有API文档也是必不可少的,最新的是1.19版本,地址:https://v1-22.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/

下图是deployment的api接口文档,可见示例、path、请求响应参数都有详细的说明:

APIResources数据结构

APIResource是个常用的数据结构了,可以用来描述资源,例如kubernetes/pkg/controller/resourcequota/resource_quota_controller_test.go中有对其的使用:

func TestDiscoverySync(t *testing.T) {
	serverResources := []*metav1.APIResourceList{
		{
			GroupVersion: "v1",
			APIResources: []metav1.APIResource{
				{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
			},
		},
	}
	unsyncableServerResources := []*metav1.APIResourceList{
		{
			GroupVersion: "v1",
			APIResources: []metav1.APIResource{
				{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
				{Name: "secrets", Namespaced: true, Kind: "Secret", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
			},
		},
	}

如何对Kubernetes进行扩展

Kubernetes 是一个可移植的、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态系统。Kubernetes 的服务、支持和工具广泛可用。
虽然现在 Kubernetes 已经是容器编排的事实标准,其本身的功能也非常丰富并且灵活,但是也不能满足所有人的需求,在遇到 Kubernetes 提供的能力无法满足我们需求的时候,我们就可以利用其强大的扩展能力进行定制。
在实际工作中,对kubernetes的资源执行各种个性化配置和控制是很常见的需求,例如自定义镜像的pod如何控制副本数、主从关系,以及各种自定义资源的控制等。

Kubernetes 有哪些扩展点


如上图所示,从客户端到底层容器运行时,绝大部分地方 Kubernetes 都为我们预留了扩展点,我们从上往下一个一个的来看

kubectl

kubectl 是我们平时和 Kubernetes 交互使用的最多的客户端工具,常见的运维操作都会通过 kubectl 来完成,kubectl 为我们提供了插件机制来方便扩展。

kubectl 插件其实就是以kubectl-为前缀的任意可执行文件 ,执行 kubectl 插件的时候可以通过 kubectl 插件名 参数 的方式运行插件。

就像 Ubuntu 使用 apt 管理软件,mac 可以使用 brew 一样,kubectl 也有类似的插件管理工具 krew ,同时我们可以从 https://krew.sigs.Kubernetes.io/plugins/ 查找是否已经存在我们需要的插件

APIServer
聚合层

从 Kubernetes v1.7 版本之后 APIServer 引入了聚合层的功能,这个功能可以让每个开发者都能够实现聚合 API 服务暴露它们需要的接口,这个过程不需要重新编译 Kubernetes 的任何代码。

如果我们将下面这个资源提交给 Kubernetes 之后,用户在访问 API 服务器的 /apis/metrics.Kubernetes.io/v1beta1 路径时,会被转发到集群中的 metrics-server.kube-system.svc 服务上

apiVersion: apiregistration.Kubernetes.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.Kubernetes.io
spec:
  service:
    name: metrics-server
    namespace: kube-system
  group: metrics.Kubernetes.io
  version: v1beta1
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 100
  versionPriority: 100
准入控制

除此之外无论是从 kubectl 还是 client-go 等其他客户端发起的请求都会发送到 APIServer 经过 认证 -> 鉴权 -> 准入控制 的步骤,这其中的每一步我们都可以对其进行扩展,而这其中用的最多的就是准入控制的扩展。

准入控制当中又会先经过,变更准入控制 MutatingAdmissionWebhook,然后再经过验证准入控制 ValidatingAdmissionWebhook,任何一个准入控制器返回了错误这个请求都会失败,例如这两个准入控制器我们可以做很多事情,例如注入 sidecar,验证资源,调整 pod 的配额等等。

Kubernetes 资源

我们常用的 Deployment、Pod、Node 等都是 Kubernetes 官方提供的内置资源,但是有的时候内置的资源无法满足我们的需求的时候,就可以使用 CR(CustomResource) 也就是自定义资源。自定义资源常常会和 Controller 一起配合使用,不过需要注意的是使用自定义资源的时候需要思考一下如果只是一些配置可能 ConfigMap 会更加适合,不要滥用这个特性。

Controller 控制器

Kubernetes 中资源的状态的维护都是 Controller 来实现的,Controller 会不断的尝试将一个资源调整为我们描述的状态,这其实也就是我们常说的声明式 api,声明式 api 背后具体的活都是 Controller 干的。Controller 一般会配合着 CRD(Custom Resource Definition) 一起使用

Schedule 调度器

调度器是一种特殊的控制器,负责监视 Pod 变化并将 Pod 分派给节点,调度器可以被直接替换掉或者是使用多个调度器,除此之外官方默认的调度器也支持 WebHook。

CNI 网络插件

CNI 网络插件,全称 Container Network Interface(容器网络接口)包含一组用于开发插件去配置 Linux 容器中网卡的接口和框架。一般我们不会去对网络插件做定制开发,而是采用开源的组件,例如 Flannel、Cilium,如果使用云服务的 Kubernetes 还会遇到一些定制的网络插件, 例如阿里云有 Terway

CSI 存储插件

CSI 存储插件,全称 Container Storage Interface,可以通过 CSI 接口支持不同的存储类型

CRI 容器运行时

CRI 容器运行时,全称 Container Runtime Interface,是一组用于管理容器运行时和镜像的 gRPC 接口,利用这个接口可以支持 docker、containerd 等不同的容器运行时

kustomize

看到 Kustomize 我的第一反应是这个东西和 helm 有什么区别,Kustomize 没有模板语法,只需要一个二进制命令就可以生成对应的 yaml 文件非常的轻量,而 helm 支持 GoTemplate,组件上也要多一些,并且 helm 通过 chart 包来进行发布相对来说还是要重量级一些。个人觉得 Kustomize 更适合做 gitops 而 helm 更合适做应用包的分发。

那么kustomize是什么,有什么用,怎么用?

简介

kustomize 是一个通过 kustomization 文件定制 kubernetes 对象的工具,它可以通过一些资源生成一些新的资源,也可以定制不同的资源的集合。

一个比较典型的场景是有一个应用,在不同的环境例如生产环境和测试环境,它的 yaml 配置绝大部分都是相同的,只有个别的字段不同,这时候就可以利用 kustomize 来解决,kustomize 也比较适合用于 gitops 工作流。


如上图所示,有一个 ldap 的应用,/base目录保存的是基本的配置,/overlays里放置的不同环境的配置,例如 /dev、/staging,/prod这些就是不同环境的配置,/base等文件夹下都有一个 kustomization .yml 文件,用于配置。

执行 kustomize build dir的方式就可以生成我们最后用于部署的 yaml 文件,也就是进行到了我们上图的第四步,然后通过 kubectl apply -f命令进行部署。

布局

├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── dev
    │   ├── kustomization.yaml
    │   └── patch.yaml
    ├── prod
    │   ├── kustomization.yaml
    │   └── patch.yaml
    └── staging
        ├── kustomization.yaml
        └── patch.yaml

一个常见的项目 kustomize 项目布局如上所示,可以看到每个环境文件夹里面都有一个 kustomization.yaml 文件,这个文件里面就类似配置文件,里面指定源文件以及对应的一些转换文件,例如 patch 等

kustomization.yml

一个常见的 kustomization.yml 如下所示,一般包含 apiVsersion 和 kind 两个固定字段

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- manager.yaml

configMapGenerator:
- files:
  - controller_manager_config.yaml
  name: manager-config

kustomize 提供了比较丰富的字段选择,除此之外还可以自定义插件,下面会大概列举一下每个字段的含义,当我们需要用到的时候知道有这么个能力,然后再去 Kustomize 官方文档 (https://kubectl.docs.kubernetes.io/zh/guides/) 查找对应的 API 文档就行了

resources: 表示 k8s 资源的位置,这个可以是一个文件,也可以指向一个文件夹,读取的时候会按照顺序读取,路径可以是相对路径也可以是绝对路径,如果是相对路径那么就是相对于 kustomization.yml的路径

crds: 和 resources 类似,只是 crds 是我们自定义的资源

namespace: 为所有资源添加 namespace

images: 修改镜像的名称、tag 或 image digest ,而无需使用 patches

replicas: 修改资源副本数

namePrefix: 为所有资源和引用的名称添加前缀

nameSuffix: 为所有资源和引用的名称添加后缀

patches: 在资源上添加或覆盖字段,Kustomization 使用 patches 字段来提供该功能。

patchesJson6902: 列表中的每个条目都应可以解析为 kubernetes 对象和将应用于该对象的 JSON patch。

patchesStrategicMerge: 使用 strategic merge patch 标准 Patch resources.

vars: 类似指定变量

commonAnnotations: 为所有资源加上 annotations 如果对应的 key 已经存在值,这个值将会被覆盖

commonAnnotations:
  app.lailin.xyz/inject: agent

resources:
- deploy.yaml

commonLabels: 为所有资源的加上 label 和 label selector 注意:这个操作会比较危险

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

commonLabels:
  app: bingo

configMapGenerator 可以生成 config map,列表中的每一条都会生成一个 configmap

secretGenerator 用于生成 secret 资源

generatorOptions 用于控制 configMapGenerator 和 secretGenerator 的行为

CRD

CRD:自定义资源定义,Kubernetes 中的资源类型。

CR:Custom Resource,对使用 CRD 创建出来的自定义资源的统称

CRD 是用来扩展 Kubernetes 最常用的方式,在 Service Mesh 和 Operator 中也被大量使用。因此如果想在 Kubernetes 上做扩展和开发的话,是十分有必要了解 CRD 的。

官方文档:https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/

创建 CRD(CustomResourceDefinition)

创建新的 CustomResourceDefinition(CRD)时,Kubernetes API Server 会为您指定的每个版本创建新的 RESTful 资源路径。CRD 可以是命名空间的,也可以是集群范围的,可以在 CRD scope 字段中所指定。与现有的内置对象一样,删除命名空间会删除该命名空间中的所有自定义对象。CustomResourceDefinition 本身是非命名空间的,可供所有命名空间使用。

参考下面的 CRD,将其配置保存在 resourcedefinition.yaml 文件中:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # 名称必须符合下面的格式:<plural>.<group>
  name: crontabs.stable.example.com
spec:
  # REST API使用的组名称:/apis/<group>/<version>
  group: stable.example.com
  # REST API使用的版本号:/apis/<group>/<version>
  versions:
    - name: v1
      # 可以通过 served 来开关每个 version
      served: true
      # 有且仅有一个 version 开启存储
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cronSpec:
                  type: string
                image:
                  type: string
                replicas:
                  type: integer
  # Namespaced或Cluster
  scope: Namespaced
  names:
    # URL中使用的复数名称: /apis/<group>/<version>/<plural>
    plural: crontabs
    # CLI中使用的单数名称
    singular: crontab
    # CamelCased格式的单数类型。在清单文件中使用
    kind: CronTab
    # CLI中使用的资源简称
    shortNames:
    - ct

然后在以下位置创建一个新的命名空间 RESTful API 端点:

/apis/stable.example.com/v1/namespaces/*/crontabs/...

然后,此端点 URL 可用于创建和管理自定义对象。上面的 CRD 中定义的类型就是 CronTab。

可能需要几秒钟才能创建端点。可以监控 CustomResourceDefinition 中 Established 的状态何时为 true,或者查看 API 资源的发现信息中是否显示了资源。

创建自定义对象

创建 CustomResourceDefinition 对象后,您可以创建自定义对象。自定义对象可包含自定义字段。这些字段可以包含任意 JSON。在以下示例中, cronSpec 和 image 自定义字段在自定义对象中设置 CronTab。CronTab 类型来自您在上面创建的 CustomResourceDefinition 对象的规范。

如果您将以下 YAML 保存到 my-crontab.yaml:

apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
  name: my-new-cron-object
spec:
  cronSpec: "* * * * */5"
  image: my-awesome-cron-image

并创建它:

kubectl create -f my-crontab.yaml

然后,您可以使用 kubectl 管理 CronTab 对象。例如:

kubectl get crontab

应该打印这样的列表:

NAME                 AGE
my-new-cron-object   6s

...
https://jimmysong.io/kubernetes-handbook/concepts/crd.html

https://liqiang.io/post/kubernetes-all-about-crd-part01-crd-introduction-fb14d399

Operator简介

Kubernetes 是一个高度可扩展的系统,虽然它的扩展点这么多,但是一般来说我们接触的比较多的还是 自定义资源,控制器,准入控制,有些还会对 kubectl 和 调度器做一些扩展,其他的大部分使用成熟的开源组件就可以了。

官方对Operator的介绍:https://kubernetes.io/zh/docs/concepts/extend-kubernetes/operator/ ,Operator模式的执行流程如下图所示:

Operator 遵循 Kubernetes 的理念,它利用自定义资源管理应用及其组件, Operator 模式会封装你编写的任务自动化代码。

Operator 常见使用范围包括:

按需部署应用

获取/还原应用状态的备份

处理应用代码的升级以及相关改动。例如,数据库 schema 或额外的配置设置

发布一个 service,要求不支持 Kubernetes API 的应用也能发现它

模拟整个或部分集群中的故障以测试其稳定性

在没有内部成员选举程序的情况下,为分布式应用选择首领角色

从 Operator 理念的提出到现在已经有了很多工具可以帮助快速低成本的开发,其中最常用的就是 CoreOS 开源的 operator-sdk 和 k8s sig 小组维护的 kubebuilder。

除了自己开发之外还可以在 https://operatorhub.io/ 上找到别人开发的现成的 Operator 进行使用

kubebuilder

kubebuilder 初体验

#  mkdir kubedev   
# cd kubedev      
# go mod init kubedev
go: creating new go.mod: module kubedev
#  kubebuilder init --domain zise.feizhu
Error: failed to initialize project: unable to run pre-scaffold tasks of "base.go.kubebuilder.io/v3": go version 'go1.17.6' is incompatible because 'requires 1.13 <= version < 1.17'. You can skip this check using the --skip-go-version-check flag 

这种情况下可以添加 --skip-go-version-check 忽略这个错误,但是还是建议使用官方推荐的版本

# kubebuilder init --domain zise.feizhu --skip-go-version-check
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.8.3
Update dependencies:
$ go mod tidy
Next: define a resource with:
$ kubebuilder create api

在当前目录下新增以下内容,可见这是个标准的go module工程:

.
├── Dockerfile
├── Makefile                                                    # 这里定义了很多脚本命令,例如运行测试,开始执行等
├── PROJECT                                                     # 这里是 kubebuilder 的一些元数据信息
├── config                                                      # config目录下获得启动配置
│   ├── default                                           # 一些默认配置
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager                                           # 部署 crd 所需的 yaml
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus                                        # 监控指标数据采集配置
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   └── rbac                                              # 部署所需的 rbac 授权 yaml
│       ├── auth_proxy_client_clusterrole.yaml
│       ├── auth_proxy_role.yaml
│       ├── auth_proxy_role_binding.yaml
│       ├── auth_proxy_service.yaml
│       ├── kustomization.yaml
│       ├── leader_election_role.yaml
│       ├── leader_election_role_binding.yaml
│       ├── role_binding.yaml
│       └── service_account.yaml
├── go.mod                                                      # 一个新的Go模块,与我们的项目相匹配,具有基本的依赖项
├── go.sum
├── hack                                                        # 脚本
│   └── boilerplate.go.txt
└── main.go

6 directories, 24 files

创建API(CRD和Controller)

# kubebuilder create api --group apps --version v1 --kind Application
Create Resource [y/n]               # 是否创建资源
y
Create Controller [y/n]             # 是否创建控制器
y

执行之后我们可以发现项目结构发生了一些变化

├── api
│   └── v1
│       ├── application_types.go                            # crd定义
│       ├── groupversion_info.go                            # 主要存放schema和我们的gvk的定义
│       └── zz_generated.deepcopy.go                        # 序列化和反序列化我们的crd资源。这个一般不需要关注
├── bin
│   └── controller-gen
├── config
│   ├── crd                                                      # 自动生成的 crd 文件,不用修改这里,只需要修改了 v1 中的 go 文件之后执行 make generate 即可
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches
│   │       ├── cainjection_in_applications.yaml
│   │       └── webhook_in_applications.yaml
│   ├── rbac
│   │   ├── application_editor_role.yaml
│   │   ├── application_viewer_role.yaml
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── role_binding.yaml
│   │   └── service_account.yaml
│   └── samples                                            # 这里是 crd 示例文件,可以用来部署到集群当中
│       └── apps_v1_application.yaml
├── controllers
│   ├── application_controller.go                          # 自定义控制器主要实现cr的reconcile方法,通过拿到cr资源信息,我们真正业务逻辑存放的地方
│   └── suite_test.go                                      # 测试组件
    
13 directories, 37 files

构建和部署CRD

kubebuilder提供的Makefile将构建和部署工作大幅度简化,执行以下命令会将最新构建的CRD部署在kubernetes上

#  make install  
......
customresourcedefinition.apiextensions.k8s.io/applications.apps.zise.feizhu created

编译和运行controller

以体验基本流程为主,不深入研究源码,所以对代码仅做少量修改,用于验证是否能生效
main.go

if err = (&controllers.ApplicationReconciler{
		Client: mgr.GetClient(),
		Log:    ctrl.Log.WithName("controllers").WithName("Application"),                   // add this line
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Application")
		os.Exit(1)
	}

./controllers/application_controller.go


type ApplicationReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)
	_ = r.Log.WithValues("application",req.NamespacedName)
	// your logic here
	r.Log.Info("1. %v",req)  				// 打印入参
	r.Log.Info("2. %s",debug.Stack())		// 打印堆栈

	return ctrl.Result{}, nil
}
...

执行以下命令,会编译并启动刚才修改的controller:

# make run
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
main.go:35:2: package kubedev/controllers imports github.com/go-logr/logr from implicitly required module; to add missing requirements, run:
        go get github.com/go-logr/logr@v0.3.0
Error: not all generators ran successfully
run `controller-gen crd:trivialVersions=true,preserveUnknownFields=false rbac:roleName=manager-role webhook paths=./... output:crd:artifacts:config=config/crd/bases -w` to see all available markers, or `controller-gen crd:trivialVersions=true,preserveUnknownFields=false rbac:roleName=manager-role webhook paths=./... output:crd:artifacts:config=config/crd/bases -h` for usage
make: *** [manifests] Error 1
#  go get github.com/go-logr/logr@v0.3.0
# make run

创建Application资源的实例

现在kubernetes已经部署了Application类型的CRD,而且对应的controller也已正在运行中,可以尝试创建Application类型的实例了(相当于有了pod的定义后,才可以创建pod);

kubebuilder已经自动创建了一个类型的部署文件 ./config/samples/apps_v1_application.yaml ,内容如下,很简单,接下来就用这个文件来创建Application实例:

apiVersion: apps.zise.feizhu/v1
kind: Application
metadata:
  name: application-sample
spec:
  # Add fields here
  foo: bar
执行 apply, 去controller所在控制台,可以看到新增和修改的操作都有日志输出,新增的日志都在里面,代码调用栈一目了然
make run            
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
I0210 15:26:27.993087   31662 request.go:655] Throttling request took 1.031351708s, request: GET:https://192.168.101.45:6443/apis/storage.k8s.io/v1beta1?timeout=32s
2022-02-10T15:26:29.262+0800    INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": ":8080"}
2022-02-10T15:26:29.263+0800    INFO    setup   starting manager
2022-02-10T15:26:29.263+0800    INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
2022-02-10T15:26:29.263+0800    INFO    controller-runtime.manager.controller.application       Starting EventSource    {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "source": "kind source: /, Kind="}
2022-02-10T15:26:29.368+0800    INFO    controller-runtime.manager.controller.application       Starting Controller     {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application"}
2022-02-10T15:26:29.368+0800    INFO    controller-runtime.manager.controller.application       Starting workers        {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "worker count": 1}
2022-02-10T15:26:36.742+0800    INFO    controllers.Application 1. default/application-sample
2022-02-10T15:26:36.743+0800    INFO    controllers.Application 2. goroutine 420 [running]:
runtime/debug.Stack()
/Users/zisefeizhu/go/go1.17.6/src/runtime/debug/stack.go:24 +0x88
kubedev/controllers.(*ApplicationReconciler).Reconcile(0x140007443c0, {0x10399f3d8, 0x140006c5680}, {{{0x140003ac869, 0x7}, {0x1400071cc30, 0x12}}})
/Users/zisefeizhu/linkun/goproject/kubedev/controllers/application_controller.go:58 +0x184
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).reconcileHandler(0x1400055e140, {0x10399f330, 0x14000632600}, {0x10383dfc0, 0x14000443f20})
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.8.3/pkg/internal/controller/controller.go:298 +0x2ac
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).processNextWorkItem(0x1400055e140, {0x10399f330, 0x14000632600})
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.8.3/pkg/internal/controller/controller.go:253 +0x1d8
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func1.2({0x10399f330, 0x14000632600})
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.8.3/pkg/internal/controller/controller.go:216 +0x44
k8s.io/apimachinery/pkg/util/wait.JitterUntilWithContext.func1()
/Users/zisefeizhu/go/pkg/mod/k8s.io/apimachinery@v0.20.2/pkg/util/wait/wait.go:185 +0x38
k8s.io/apimachinery/pkg/util/wait.BackoffUntil.func1(0x14000025748)
/Users/zisefeizhu/go/pkg/mod/k8s.io/apimachinery@v0.20.2/pkg/util/wait/wait.go:155 +0x68
k8s.io/apimachinery/pkg/util/wait.BackoffUntil(0x1400052ff48, {0x10397a2e0, 0x140005585a0}, 0x1, 0x14000026300)
/Users/zisefeizhu/go/pkg/mod/k8s.io/apimachinery@v0.20.2/pkg/util/wait/wait.go:156 +0x94
k8s.io/apimachinery/pkg/util/wait.JitterUntil(0x14000025748, 0x3b9aca00, 0x0, 0x1, 0x14000026300)
/Users/zisefeizhu/go/pkg/mod/k8s.io/apimachinery@v0.20.2/pkg/util/wait/wait.go:133 +0x88
k8s.io/apimachinery/pkg/util/wait.JitterUntilWithContext({0x10399f330, 0x14000632600}, 0x14000720140, 0x3b9aca00, 0x0, 0x1)
/Users/zisefeizhu/go/pkg/mod/k8s.io/apimachinery@v0.20.2/pkg/util/wait/wait.go:185 +0x88
k8s.io/apimachinery/pkg/util/wait.UntilWithContext({0x10399f330, 0x14000632600}, 0x14000720140, 0x3b9aca00)
/Users/zisefeizhu/go/pkg/mod/k8s.io/apimachinery@v0.20.2/pkg/util/wait/wait.go:99 +0x50
created by sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func1
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.8.3/pkg/internal/controller/controller.go:213 +0x308

删除实例并停止controller

不再需要Application实例的时候,执行以下命令即可删除:
# ctl delete -f config/samples/
不再需要controller的时候,去它的控制台使用Ctrl+c中断即可

不过实际生产环境中controller一般都会运行在kubernetes环境内,像上面这种运行在kubernetes之外的方式就不合适了,来试试将其做成docker镜像然后在kubernetes环境运行

将controller制作成docker镜像

执行以下命令构建docker镜像并推送到aliyun,镜像名为

# docker login --username=zisefeizhu registry.cn-shenzhen.aliyuncs.com
Password: 
Login Succeeded
# docker build -t registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001 .
# docker push registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001

在kubernetes环境部署controller

# make deploy IMG=registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
cd config/manager && /Users/zisefeizhu/linkun/goproject/kubedev/bin/kustomize edit set image controller=registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001
/Users/zisefeizhu/linkun/goproject/kubedev/bin/kustomize build config/default | kubectl apply -f -
namespace/kubedev-system created
customresourcedefinition.apiextensions.k8s.io/applications.apps.zise.feizhu configured
serviceaccount/kubedev-controller-manager created
role.rbac.authorization.k8s.io/kubedev-leader-election-role created
clusterrole.rbac.authorization.k8s.io/kubedev-manager-role created
clusterrole.rbac.authorization.k8s.io/kubedev-metrics-reader created
clusterrole.rbac.authorization.k8s.io/kubedev-proxy-role created
rolebinding.rbac.authorization.k8s.io/kubedev-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kubedev-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kubedev-proxy-rolebinding created
configmap/kubedev-manager-config created
service/kubedev-controller-manager-metrics-service created
deployment.apps/kubedev-controller-manager created

# kubectl get pods -n kubedev-system -w
NAME                                          READY   STATUS              RESTARTS   AGE
kubedev-controller-manager-776899d98c-9xdfg   2/2     Running   0          9s

将kube-rbac-proxy 改为阿里云镜像地址

卸载和清理

把前面创建的资源和CRD全部清理掉,可以执行以下命令

# make uninstall

简单实现 Controller

定义 CR

api/v1/application_types.go

	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// Product 该应用所属的产品
	Product string `json:"product,omitempty"`
}

修改之后执行一下 make manifests generate 可以发现已经生成了相关的字段,并且代码中的字段注释也就是 yaml 文件中的注释

# make manifests generate
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."

config/crd/bases/apps.lailin.xyz_applications.yaml

			properties:
              product:
                description: Product 该应用所属的产品
                type: string

实现 controller

kubebuilder 已经实现了 Operator 所需的大部分逻辑,所以只需要在 Reconcile 中实现业务逻辑就行了

./controllers/application_controller.go

func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)
	_ = r.Log.WithValues("application", req.NamespacedName)
	// your logic here
	r.Log.Info("app changed", "ns", req.Namespace)
	//r.Log.Info(fmt.Sprintf("1. %v", req))           // 打印入参
	//r.Log.Info(fmt.Sprintf("2. %s", debug.Stack())) // 打印堆栈

	return ctrl.Result{}, nil
}

逻辑修改好之后,先执行 make install 安装 CRD,然后执行 make run运行 controller

make run
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
I0210 16:23:51.408872   35770 request.go:655] Throttling request took 1.029677459s, request: GET:https://192.168.101.45:6443/apis/storage.k8s.io/v1beta1?timeout=32s
2022-02-10T16:23:52.683+0800    INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": ":8080"}
2022-02-10T16:23:52.684+0800    INFO    setup   starting manager
2022-02-10T16:23:52.684+0800    INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
2022-02-10T16:23:52.684+0800    INFO    controller-runtime.manager.controller.application       Starting EventSource    {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "source": "kind source: /, Kind="}
2022-02-10T16:23:52.786+0800    INFO    controller-runtime.manager.controller.application       Starting Controller     {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application"}
2022-02-10T16:23:52.786+0800    INFO    controller-runtime.manager.controller.application       Starting workers        {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "worker count": 1}

部署一个测试的 crd kubectl apply -f config/samples/apps_v1_application.yaml

apiVersion: apps.lailin.xyz/v1
kind: Application
metadata:
  name: application-sample
spec:
  # Add fields here
  product: test

然后可以看到之前写的日志逻辑已经触发

022-02-10T16:25:27.946+0800    INFO    controllers.Application app changed     {"ns": "default"}

Kubebuilder 注释

在生成的代码当中我们可以看到很多 //+kubebuilder:xxx 开头的注释,对 Go 比较熟悉的同学应该知道这些注释是给对应的代码生成器服务的,在 Go 中有一个比较常用的套路就是利用 go gennerate生成对应的 go 代码。

kubebuilder 使用 controller-gen(https://github.com/kubernetes-sigs/controller-tools/blob/master/cmd/controller-gen/main.go)生成代码和对应的 yaml 文件,这其中主要包含 CRD 生成、验证、处理还有 WebHook 的 RBAC 的生成功能,下面我简单介绍一下,完整版可以看 kubebuilder 的官方文档(https://master.book.kubebuilder.io/quick-start.html)

CRD 生成

//+kubebuilder:subresource:status 开启 status 子资源,添加这个注释之后就可以对 status进行更新操作了

//+groupName=nodes.lailin.xyz 指定 groupname

//+kubebuilder:printcolumn 为 kubectl get xxx 添加一列,这个挺有用的

……

CRD 验证,利用这个功能,只需要添加一些注释,就给可以完成大部分需要校验的功能

//+kubebuilder:default:= 给字段设置默认值

//+kubebuilder:validation:Pattern:=string 使用正则验证字段

……

Webhook

//+kubebuilder:webhook 用于指定 webhook 如何生成,例如我们可以指定只监听 Update 事件的 webhook

RBAC 用于生成 rbac 的权限

//+kubebuilder:rbac

参考文档:

https://lailin.xyz/post/operator-01-overview.html
https://blog.csdn.net/boling_cavalry/article/details/113089414

posted @ 2022-02-22 14:15  紫色飞猪  阅读(1107)  评论(0编辑  收藏  举报