Kubernetes | Custom Resource

2021 01 13, Wed

前两天搞定了Tcp Ingress,但是用的是比较挫的ConfigMap的方法。留了一个问题是有没有更好的方法。能这么问当然是有啦。不过这玩意儿Nginx Ingress Controller也没打算加。我们先来看下解决的办法本身:Custom Resource。

这两天项目里面同事被调走赶另一个项目,我暂时没有任务,赶紧多学一点。看到Oreilly有免费10天会员就赶紧多薅薅,打算把Programming Kubernetes看完,有机会了再买本实体书报答作者……太贵了。

Kubernetes API

K8S是以API为核心管理集群的系统,也就是apiserver为核心,etcd作为存储,kubelet根据apiserver操作容器,用户通过kubelet之类的工具(换句话说就是kubernetes sdk)和apiserver交互。

K8S的API以Resource Definition为形式,有两部分,一部分是Kind(定义),一部分是Object(实例)。有些资料会把两部分分为Schema Level和Object Level,类似于编程语言中的类/类型和变量/实例,只不过K8S中用yaml就可以定义两者。Resource Definition为了避免命名冲突,有一个APIVersion来表明是哪里来的(Group)、什么版本的API(Version),类似于编程语言中的包、模块或者命名空间。

APIServer自带一部分可以让K8S work Out of box的资源定义,APIVersion通常是<version>或者*.k8s.io/<version>。比如说v1Pod或者rbac.authorization.k8s.io/v1RoleBinding。其他的Resource Definition一般叫Custom Resource Definition,APIVersion不和K8S的冲突就好,比如说cert-manager中cert-manager.io/v1CertificateRequest

一般创建了Resource Definition,K8S会村到etcd里面。对Resource做一些操作后(增加删除修改),由一个Controller来根据Resource做出响应。比如说我们增加了Ingress,Nginx Ingress Controller就会更新Nginx的配置。对于大部分的K8S APIServer自带的API,API Server就是Controller,会和相关的组件进行交互,比如说操作Pod啊什么的。

Kubernetes的API可以从自动生成的文档查到。

Custom Resource

APIServer自带的Resource Definition以外,用户建立的Resource Definition都叫 CRDs ( Custom Resource Definitions ) ,想知道自己的集群有哪些crd的话,kubectl get crds就行。

Programming Kubernetes给了一个例子是At,长这个样子:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  group: cnat.programming-kubernetes.info
  names:
    kind: At
    listKind: AtList
    plural: ats
    singular: at
  scope: Namespaced
  subresources:
    status: {}
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true

但是这个例子有点老了,在新的K8S集群上会警告。

$ kubectl apply -f at-crds.yaml
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
customresourcedefinition.apiextensions.k8s.io/ats.cnat.programming-kubernetes.info created

v1beta1的CustomResourceDefinition在Kubernetes 1.16中已经被标记不推荐,不是不能用,我就是喜欢新的。查了一下文档,稍微改一下就好了,区别不大。v1中subresources这个field在versions中,并且默认可以不写。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  group: cnat.programming-kubernetes.info
  names:
    kind: At
    listKind: AtList
    plural: ats
    singular: at
  scope: Namespaced
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true

定义好crd以后,就可以正常定义具体的cr了:

apiVersion: cnat.programming-kubernetes.info/v1alpha1
kind: At
metadata:
  name: cnrex
spec:
  schedule: "2019-07-03T02:00:00Z"
  containers:
  - name: shell
    image: centos:7
    command:
    - "bin/bash"
    - "-c"
    - echo "Kubernetes native rocks!"

kubectl get ats也可以获取到资源

$ kubectl get ats.cnat.programming-kubernetes.info
NAME    AGE
cnrex   12m

其中有两个问题:

  • 一个是在crd中,我们并没有规定fields长什么样子,所以At的spec可以容纳任意类型的数据,但是我们一个类型一定是有一个特定的结构的,每个部分有自己的类型,所以看起来不大行,我们需要定义一下类型。
  • 另一个问题是我们只是定义了CRD,有一个CR,但是实际上定义了就定义了,就跟摆在墙角的小白菜一样没有人理会,不会有任何影响,谁都不处理它,我们需要想办法让集群对这个资源做出响应。

Schema

刚才提到RD/CRD属于Schema Level,因为类型的定义要定义信息的结构,也就是Schema。CRD用OpenAPI。给At加上Schema以后大概长这个样子:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: ats.cnat.programming-kubernetes.info
spec:
  group: cnat.programming-kubernetes.info
  names:
    kind: At
    listKind: AtList
    plural: ats
    singular: at
  scope: Namespaced
  versions:
  - name: v1alpha1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            required:
            - schedule
            properties:
              schedule:
                type: string
                pattern: "^\\d{4}-([0]\\d|1[0-2])-([0-2]\\d|3[01])..."
              containers:
                type: array
                items:
                  type: object
                  required:
                  - name
                  - image
                  - command
                  properties:
                    name:
                      type: string
                    image:
                      type: string
                    command:
                      type: array
                      items:
                        type: string
            type: object
          status:
            type: object

通过schema就可以定义一个对象应该长什么样子了。如果这个时候我们不按照定义写At,kubectl就会验证不通过。

比如说我们少写了schedule,就会报错:

error: error validating "test.yaml": error validating data: ValidationError(At.spec): missing required field "schedule" in info.programming-kubernetes.cnat.v1alpha1.At.spec; if you choose to ignore these errors, turn validation off with --validate=false

或者schedule字符串不满足pattern定义的正则:

The At "cnrex" is invalid: spec.schedule: Invalid value: "2019-07-0qwer3T02:00:00Z": spec.schedule in body should match '^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])...'

Printer Columns

但是kubectl get ats的时候感觉很冷……什么信息都没有:

kubectl get ats.cnat.programming-kubernetes.info
NAME    AGE
cnrex   4m8s

根据K8S文档,加上additionalPrinterColumns就好了

# apiVersion: apiextensions.k8s.io/v1
# kind: CustomResourceDefinition
# metadata:
#   name: ats.cnat.programming-kubernetes.info
# spec:
#   group: cnat.programming-kubernetes.info
#   names:
#     kind: At
#     listKind: AtList
#     plural: ats
#     singular: at
#   scope: Namespaced
  versions:
  - name: v1alpha1
    additionalPrinterColumns:
    - name: Schedule
      type: date
      description: Time to Execute
      jsonPath: .spec.schedule
    - name: Age
      type: date
      jsonPath: .metadata.creationTimestamp
#     served: true
#     storage: true
#     schema:
#       openAPIV3Schema:
#         type: object
#         properties:
#           apiVersion:
#             type: string
#           kind:
#             type: string
#           metadata:
#             type: object
#           spec:
#             required:
#             - schedule
#             properties:
#               schedule:
#                 type: string
#                 pattern: "^\\d{4}-([0]\\d|1[0-2])-([0-2]\\d|3[01])..."
#               containers:
#                 type: array
#                 items:
#                   type: object
#                   required:
#                   - name
#                   - image
#                   - command
#                   properties:
#                     name:
#                       type: string
#                     image:
#                       type: string
#                     command:
#                       type: array
#                       items:
#                         type: string
#             type: object
#           status:
#             type: object

加上additionalPrinterColumns以后,kubectl get ats就会变成

kubectl get ats
NAME    SCHEDULE               AGE
cnrex   2019-07-03T02:00:00Z   10m

如果Schedule的type设置为date,就会是(写博客时,是2020年1月13日)

kubectl get ats
NAME    SCHEDULE   AGE
cnrex   559d       11m

Controller

我们要解决的另一个问题就是,这个at资源我们建好以后,k8s是不认识的,我们需要有一个程序通过API去处理这类资源。

一般这种可以和API直接交互的app,我们叫做Controller。Ingress Controller就是一个例子,它可以处理K8S中的Ingress资源,输入Ingress、输出Nginx配置和对应的Service。Controller换个说法也是APIServer的客户端。

这玩意儿说来话长。我们过两天再搞。