建议版本:
- 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,即validatingwebhookconfiguration
或mutatingwebhookconfiguration
.k8s apiserver
会根据集群中已经创建的所有validatingwebhookconfiguration
或mutatingwebhookconfiguration
逐一进行调用,所以,当我们创建一个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认证
流程总结
- 编写一个json文件,描述server的信息,包括域名(或IP),CN,加密方式
- 执行cfssl命令生成server的密钥,和认证请求文件server.csr
- 将server.csr内容编码,在k8s中创建一个server的CSR资源
- 手动对该CSR资源进行认证签发
- 将k8s生成的server.crt 即服务端证书拷贝下来。
- 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 。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。