4

建议版本:

  • docker 18.06
  • k8s 1.14 (如果要开发删除时的webhook,需要升级到1.15)

1. Install

安装kubebuilder

执行下面的脚本:

os=$(go env GOOS)
arch=$(go env GOARCH)

# download kubebuilder and extract it to tmp
curl -sL https://go.kubebuilder.io/dl/2.0.0-beta.0/${os}/${arch} | tar -xz -C /tmp/

# move to a long-term location and put it on your path
# (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else)
mv /tmp/kubebuilder_2.0.0-beta.0_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin

即可

安装kustomize

2. Dev

创建project

使用kubebuilder前,必须要确保本地的go版本为1.11或以上,开启了go module (export GO111MODULE=on

我们在GOPATH下新建一个目录。并在其中执行kubebuilder init --domain netease.com
会发现报错:

go: sigs.k8s.io/controller-runtime@v0.2.0-beta.4: unrecognized import path "sigs.k8s.io/controller-runtime" (https fetch: Get https://sigs.k8s.io/controller-runtime?go-get=1: dial tcp 35.201.71.162:443: i/o timeout)
go: error loading module requirements

无法直接clone sigs.k8s.io/controller-runtime,那么我们可以通过对go mod配置proxy,走代理下载: export GOPROXY=https://goproxy.io

将目录清理,并重新执行kubebuilder init --domain netease.com

# kubebuilder init --domain my.domain
go mod tidy
Running make...
make
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.0-beta.4
go: finding sigs.k8s.io/controller-tools/cmd/controller-gen v0.2.0-beta.4
go: finding sigs.k8s.io/controller-tools/cmd v0.2.0-beta.4
/root/mygo/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths=./api/...
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Next: Define a resource with:
$ kubebuilder create api
# ls
bin  config  Dockerfile  go.mod  go.sum  hack  main.go    Makefile  PROJECT

init之后生成了一些代码、hack脚本、dockerfile、makefile,这意味着kubebuilder帮我们生成了一个operator需要的基础代码内容。另外bin目录下编译出了一个manager二进制文件。这就是kubebuilder最终帮我们制作的程序,它包括:

  • 一个metrics接口(for prometheus)
  • 运行0或多个controller
  • 运行0或多个webhook server

创建一个API (和控制器)

执行:kubebuilder create api --group ops --version v1 --kind Playbook

# kubebuilder create api --group ops --version v1 --kind Playbook
Create Resource [y/n]
y
Create Controller [y/n]      # 如果我们只需要创建一个api,不需要开发一个控制器,这里选n即可
y
Writing scaffold for you to edit...
api/v1/guestbook_types.go
controllers/guestbook_controller.go
Running make...
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.0-beta.4
go: finding sigs.k8s.io/controller-tools/cmd/controller-gen v0.2.0-beta.4
go: finding sigs.k8s.io/controller-tools/cmd v0.2.0-beta.4
/root/mygo/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths=./api/...
go fmt ./...
go vet ./...
go: downloading github.com/onsi/ginkgo v1.8.0
go: downloading github.com/onsi/gomega v1.5.0
go: extracting github.com/onsi/gomega v1.5.0
go: extracting github.com/onsi/ginkgo v1.8.0
go build -o bin/manager main.go
# ls
api  bin  config  controllers  Dockerfile  go.mod  go.sum  hack  main.go  Makefile  PROJECT

上述操作执行完后,需要关注两个地方

  • api/v1/playbook_types.go

这个文件包含了对Playbook这个CRD的定义,我们可以在里面进行修改,加上我们需要的字段。

比如我们给Playbook.Spec增加一个字段Abstract,类型为string,表示该Playbook的摘要;给Playbook.Status增加一个字段Progress,类型为int32,表示这本Playbook看到百分几。

  • controllers/playbook_controller.go

这个文件里,有如下函数:

func (r *PlaybookReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
        _ = context.Background()
        _ = r.Log.WithValues("playbook", req.NamespacedName)

        // your logic here

        return ctrl.Result{}, nil
}

这就是控制器的处理函数,每当集群中有Playbook资源的变动(CRUD),都会触发这个函数进行协调。

比如我们在Reconcile中获取进入协调的Playbook对象,检查它的status.progress是否等于100,如果是,就直接调用Delete接口删除它。

我们开发完毕后,可以直接运行make run, 会重新编译出一个/bin/manager并运行,由于我们在一开始指定要生成controller,kubebuilder会在manager中注入controller,当manager运行起来后,会运行起我们开发的controller。

需要注意的是,直接make run可能会报错提示8080端口已经被占用,检查是否你的apisever监听在该端口,或者直接修改main.go中的代码,将metrics接口服务监听在别的端口上

当我们看到如下的日志时,说明manager中的控制器已经在工作了:

2019-09-09T17:43:35.290+0800    INFO    controller-runtime.controller    Starting Controller    {"controller": "playbook"}

创建一个api的webhook

我们如果需要在Playbook CRUD 时进行操作合法性检查, 可以开发一个webhook实现。webhook的脚手架一样可以用kubebuilder生成:

# kubebuilder create webhook --group ops --version v1 --kind Playbook --programmatic-validation --defaulting
Writing scaffold for you to edit...
api/v1/playbook_webhook.go

执行后会生成api/v1/playbook_webhook.go文件,由于我们指定了参数--programmatic-validation(可以在创建、更新guestbook时触发的字段验证函数)和--defaulting(创建或更新时,用于配置guestbook某些字段默认值的函数)
我们可以对上述函数进行自定义的修改。体现在代码中就是如下三个方法:

func (r *Playbook) ValidateCreate() error 
func (r *Playbook) ValidateUpdate(old runtime.Object) error
func (r *Playbook) Default()

比如我们在这里对playbook.Spec.Abstract的内容进行检查,如果包含了单词"yellow",我们认为这是本黄书,进行报错。

webhook的本地运行和测试

测试和部署的前提是我们在开发环境中安装了k8s。这一步不做说明。

  • 执行make,可以进行main.go编译
  • 执行make install 可以将我们生成的crd注册到集群中。
  • 执行make run,可以前台运行manager,我们经过上面的编辑,manager中注入了一个controller和一个webhook。

但是webhook现在编译会失败,报错是找不到webhook server使用的证书文件。我们需要用k8s的ca签发一个webhookserver的证书和key,当然我们也可以直接用k8s的管理员证书。 手动使用k8s的ca给我们的webhook-server签发一个证书,可以见下文手动签发证书

cp /etc/kubernetes/pki/apiserver.crt /tmp/k8s_webhook-server/serving-certs/tls.crt
cp /etc/kubernetes/pki/apiserver.key /tmp/k8s_webhook-server/serving-certs/tls.key

再次make run,这下运行起来了。我们看到终端打印出了如下两行日志:

2019-09-09T17:48:06.967+0800    INFO    controller-runtime.builder    Registering a mutating webhook    {"GVK": "ops.netease.com/v1, Kind=Playbook", "path": "/mutate-ops-netease-com-v1-playbook"}
2019-09-09T17:48:06.967+0800    INFO    controller-runtime.builder    Registering a validating webhook    {"GVK": "ops.netease.com/v1, Kind=Playbook", "path": "/validate-ops-netease-com-v1-playbook"}

但此时apiserver并不知道运行了这么一个webserver,此时我们创建一个crd资源对象,webhook是不会主动去进行default或validate的。

那么此时webhook如何才能被调用呢?

make webhook enable

我们需要创建webhookcfg,让apiserver自己感知到某些资源(Playbook)的某些请求(CREATE,UPDATE)需要访问某个服务(service,或者一个url)

幸运的是,kubebuilder帮我们生成了webhookcfg。在/config/webhook/目录下,记录了manifests.yaml 和 service.yaml。

service.yaml是一个service模板,对应的是我们设计的webhook

manifests.yaml的内容如下:

---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  creationTimestamp: null
  name: mutating-webhook-configuration
webhooks:
- clientConfig:
    caBundle: Cg==
    service:
      name: webhook-service
      namespace: system
      path: /mutate-ops-netease-com-v1-playbook
  failurePolicy: Fail
  name: mplaybook.kb.io
  rules:
  - apiGroups:
    - ops.netease.com
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - playbooks

---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  creationTimestamp: null
  name: validating-webhook-configuration
webhooks:
- clientConfig:
    caBundle: Cg==
    service:
      name: webhook-service
      namespace: system
      path: /validate-ops-netease-com-v1-playbook
  failurePolicy: Fail
  name: vplaybook.kb.io
  rules:
  - apiGroups:
    - ops.netease.com
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - playbooks

很显然,这里记录的就是webhookcfg。因为webhook有多种,我们上面创建时指定了两种(--programmatic-validation--defaulting),对应创建了一个ValidatingWebhookConfiguration和一个MutatingWebhookConfiguration

在本地测试的过程中,我们可能不想每次代码修改都要打包成镜像,并发布成deployment+service。那么我们就可以修改以此处的

clientConfig:
    caBundle: Cg==
    service:
      name: webhook-service
      namespace: system
      path: /validate-ops-netease-com-v1-playbook
  • 去掉service部分,改为url: https://${server}:443/validate-ops-netease-com-v1-playbook ,注意与service.path对应。
  • webhook必须使用https,所以需要申明caBundle,这样 apiserver 才可以信任 webhook server 提供的 TLS 证书。将集群的ca.crt内容(可以通过kubectl config view --raw -o json | jq -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '"'拿到)填入到caBundle的值中;
  • url中的server值必须是一个webhook证书的有效地址。如果我们在开发机器上部署了k8s,那么可以简单地使用机器自身的ip(master节点的ip)。
  • 创建出MutatingWebhookConfiguration后,我们前台运行着的manager程序中的mutating webhook将会在k8s集群中生效;
  • 创建出ValidatingWebhookConfiguration后,我们前台运行着的manager程序中的validating webhook将会在k8s集群中生效;

注意到,不管是ValidatingWebhookConfiguration还是MutatingWebhookConfiguration,他们的webhooks是一个数组,我们如果在开发过程中需要设计多个CRD,那么多次执行kubebuilder create api **** 后,我们可以按照自己的设计,在两种(或其中一种)WebhookConfitration的webhooks里增加一个项目,只要填充正确的name,clientConfig.url,rules即可。

这里介绍的是本地开发环境便于调试的部署方式,测试、线上集群的部署,应该做到标准化和规范化,那么如何进行标准化部署呢?

3. Deploy

部署整个manager

  • 安装cert-manager。cert-manager可以动态地为我们开发的服务配置tls证书。安装步骤见https://docs.cert-manager.io/...
  • 进行dockerbuild。

    • 修改Makefile,将docker-build 后面的test去掉
    • 修改Dockerfile。一是在拷贝go.mod, go.sum后增加两个env:

      ENV GO111MODULE=on
      ENV GOPROXY=https://goproxy.io

      二是将FROM gcr.io/distroless/static:latest 替换为FROM golang:1.12.5;三是如果我们只写了个webhook,没有写controller,那么Dockerfile中的COPY controllers/ controllers/要去掉

    • 修改/config/default/kustomization.yaml, 将webhook、certmanager相关的注释去掉并将patches改为patchesStrategicMerge
    • 修改config/crd/kustomization.yaml,将webhook、certmanager相关的注释去掉
    • 修改config/default/manager_auth_proxy_patch.yaml文件中的image,改为quay.io/coreos/kube-rbac-proxy:v0.4.0
    • 如果要自定义生成的镜像名,可以修改Makefile文件中的IMG变量值
    • 执行 make docker-build
    • 如果需要push镜像, 执行 make docker-push
  • 进行deploy:

    • 执行make deploy

执行完毕后,我们在集群中可以找到${项目名}-system namespace下运行了我们开发的服务,包括一个deployment,两个service:

# kubectl  get all -n testop3-system 
NAME                                              READY   STATUS         RESTARTS   AGE
pod/testop3-controller-manager-6f44ddbd68-q7m24   1/2     ErrImagePull   0          15s

NAME                                                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/testop3-controller-manager-metrics-service   ClusterIP   10.107.195.212   <none>        8443/TCP   15s
service/testop3-webhook-service                      ClusterIP   10.110.243.20    <none>        443/TCP    15s

NAME                                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/testop3-controller-manager   0/1     1            0           15s

NAME                                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/testop3-controller-manager-6f44ddbd68   1         1         0       15s

注意事项

webhookconfiguration

我们从上文中了解到,要让一个webhook在k8s集群内可用,需要创建对应的webhookconfiguration,即validatingwebhookconfigurationmutatingwebhookconfiguration.k8s apiserver会根据集群中已经创建的所有validatingwebhookconfigurationmutatingwebhookconfiguration逐一进行调用,所以,当我们创建一个crd对象失败,且日志显示访问某个webhook失败时,我们可以:

确认集群中的webhookconfiguration是否为我们需要的webhook并且要检查每一个webhookconfiguration中,配置的服务地址是否可达。

特别是:当我们使用本地make run测试,且使用make deploy 测试,会需要不同的webhookconfiguration,两种测试方式建议只选择一种,不要来回切换,否则要确保webhookconfiguration的配置,比较费时。

手动签发证书

k8s集群部署时会自动生成一个CA(证书认证机构),当然这个CA是我们自动生成的,并不具有任何合法性。k8s还提供了一套api,用于对用户自主创建的证书进行认证签发。

准备

  • 安装k8s集群
  • 安装cfssl工具,从这里下载cfssl和cfssljson

创建你的证书

执行下面的命令,生成server.csr和server-key.pem。

cat <<EOF | cfssl genkey - | cfssljson -bare server
{
  "hosts": [
    "my-svc.my-namespace.svc.cluster.local",
    "my-pod.my-namespace.pod.cluster.local",
    "192.0.2.24",
    "10.0.34.2"
  ],
  "CN": "my-pod.my-namespace.pod.cluster.local",
  "key": {
    "algo": "ecdsa",
    "size": 256
  }
}
EOF

这里你可以修改文件里的内容,主要是:

  • hosts。 服务地址,你可以填入service的域名,service的clusterIP,podIP等等
  • CN 。对于 SSL 证书,一般为网站域名;而对于代码签名证书则为申请单位名称;而对于客户端证书则为证书申请者的姓名
  • key。加密算法和长度。一般有ecdsa算法和rsa算法,rsa算法的size一般是2048或1024

这一步生成的server-key.pem是服务端的私钥,而server.csr则含有公钥、组织信息、个人信息(域名)。

创建一个CSR资源

执行如下脚本:

cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: my-svc.my-namespace
spec:
  request: $(cat server.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF

在k8s集群中创建一个csr资源。注意要将第一步中创建的server.csr内容进行base64编码,去掉换行后填入spec.request中。spec.usages中填入我们对证书的要求,包括数字签名、密钥加密、服务器验证。一般填这三个就够了。

之后我们通过kubectl describe csr my-svc.my-namespace 可以看到:

Name:                   my-svc.my-namespace
Labels:                 <none>
Annotations:            <none>
CreationTimestamp:      Tue, 21 Mar 2017 07:03:51 -0700
Requesting User:        yourname@example.com
Status:                 Pending
Subject:
        Common Name:    my-svc.my-namespace.svc.cluster.local
        Serial Number:
Subject Alternative Names:
        DNS Names:      my-svc.my-namespace.svc.cluster.local
        IP Addresses:   192.0.2.24
                        10.0.34.2
Events: <none>

认证csr

注意到,csr的status是pending,说明还没有被CA认证。在k8s集群中,如果是node上kubelet创建的CSR,kube-controller-manager会自动进行认证,而我们手动创建的证书,需要进行手动认证:
kubectl certificate approve

也可以拒绝:kubectl certificate deny

之后我们再检查csr,发现已经是approved了:

kubectl get csr
NAME                  AGE       REQUESTOR               CONDITION
my-svc.my-namespace   10m       yourname@example.com    Approved,Issued

我们可以通过

kubectl get csr my-svc.my-namespace -o jsonpath='{.status.certificate}' | base64 --decode > server.crt

命令,得到server的证书。之后你就可以使用server.crt和server-key.pem作为你的服务的https认证

流程总结

  1. 编写一个json文件,描述server的信息,包括域名(或IP),CN,加密方式
  2. 执行cfssl命令生成server的密钥,和认证请求文件server.csr
  3. 将server.csr内容编码,在k8s中创建一个server的CSR资源
  4. 手动对该CSR资源进行认证签发
  5. 将k8s生成的server.crt 即服务端证书拷贝下来。
  6. server.crt 和server-key.pem 即server的https服务配置

kubebuilder 2.3.0使用说明更新

创建crd时直接指明scope

执行kubebuilder create api时直接带上--namespaced=false可以将该对象设计为集群级别(类似node、pv)

使用validateDelete

在执行kubebuilder create webhook之后,生成的webhook yaml中我们可以看到有ValidateDelete方法,要使该方法生效,我们需要在上面的kubebuilder注释中,在verb中补充一个delete`,如:

// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-network-netease-com-v1-ipallocation,mutating=false,failurePolicy=fail,groups=network.netease.com,resources=ipallocations,versions=v1,name=vipallocation.kb.io

早先版本的kube-apiserver中,发送delete操作给webhook时,不会带上要删除的资源的object,这会导致我们在开发validatedelete后,测试时报错:

Error from server: admission webhook "vippool.kb.io" denied the request: there is no content to decode

解决办法: k8s升级到1.15

相关的pr

参考资料:


fzu_huang
293 声望126 粉丝