如何基于 Knative 開發(fā) 自定義controller
1. 為什么要開發(fā) 自定義 controller?
開源版本的 Knative 提供了擴(kuò)縮容及事件驅(qū)動(dòng)的架構(gòu),對(duì)于大部分場(chǎng)景的 Serverless 已經(jīng)滿足了,不過對(duì)于商業(yè)版本的 Serverless 平臺(tái)來說,免不了要添加一些增強(qiáng)特性。
通常情況下,In-Tree 形式的增強(qiáng)不推薦,而且這種方式也會(huì)因開源版本升級(jí)帶來不小的適配工作量。
Out-Of-Tree 形式的 自定義 controller 是一種很好特性增強(qiáng)方式,而且社區(qū)本身對(duì)于周邊組件的解耦也是通過 controller 來對(duì)接的。比如:
- net-contour : 對(duì)接 Contour 七層負(fù)載的網(wǎng)絡(luò)插件
- net-kourier : 對(duì)接 Kourier 七層負(fù)載的網(wǎng)絡(luò)插件
- net-istio : 對(duì)接 Istio 七層負(fù)載的網(wǎng)絡(luò)插件
上述提到的幾個(gè)網(wǎng)絡(luò)插件都是通過 自定義
Controller結(jié)合Kingress這個(gè)CRD資源來實(shí)現(xiàn)

2. How?
對(duì)于 有過 Kubernetes operator 開發(fā)經(jīng)驗(yàn)的同學(xué)來說,可能對(duì) Kubebuilder 更熟悉一些,其實(shí) Knative 自定義 控制器的開發(fā)更簡(jiǎn)單,下面一步一步介紹怎么開始
2.1 Fork 社區(qū) Template
社區(qū) 項(xiàng)目地址在 https://github.com/knative-sandbox/sample-controller,直接 fork 到個(gè)人倉(cāng)庫(kù)。
2.2 sample-controller介紹
代碼下載到本地,目錄如下,如下(此處省略掉不重要的文件):
sample-controller
├── cmd
│ ├── controller
│ │ └── main.go # controller 的啟動(dòng)入口文件
│ ├── schema
│ │ └── main.go # 生成 CRD 資源的 工具
│ └── webhook
│ └── main.go # webhook 的入口文件
├── config # controller 和webhook 的部署文件(deploy role clusterrole 等等,此處省略)
│ ├── 300-addressableservice.yaml
│ ├── 300-simpledeployment.yaml
├── example-addressable-service.yaml # CR 資源的示例yaml
├── example-simple-deployment.yaml # CR 資源的示例yaml
├── hack
│ ├── update-codegen.sh # 生成 informer,clientset,injection,lister 等
│ ├── update-deps.sh
│ ├── update-k8s-deps.sh
│ └── verify-codegen.sh
├── pkg
│ ├── apis
│ │ └── samples
│ │ ├── register.go
│ │ └── v1alpha1 # 此處需編寫 CRD 資源的types
│ ├── client # 執(zhí)行 hack/update-codegen.sh 后自動(dòng)生成的文件
│ │ ├── clientset
│ │ ├── informers
│ │ ├── injection
│ │ └── listers
│ └── reconciler # 此處是控制器的主要邏輯,示例中實(shí)現(xiàn)了兩個(gè)控制器,每個(gè)控制器包含主控制器入口(controller.go) 和對(duì)應(yīng)的 reconcile 邏輯
│ ├── addressableservice
│ │ ├── addressableservice.go
│ │ └── controller.go
│ └── simpledeployment
│ ├── controller.go
│ └── simpledeployment.go
目錄介紹:
-
cmd: 包含
controller和webhook的入口main函數(shù),以及生成 crd 的 schema 工具(這也是筆者的社區(qū)貢獻(xiàn)之一) - config: controller 和webhook 的部署文件(本文只關(guān)注 controller)
-
hack:是 程序自動(dòng)生成代碼的腳本,其中的
update-codegen.sh最常用,是生成informer,clientset,injection,lister的工具 - pkg/apis: 此處是 CRD 定義的 types 文件
-
pkg/client: 這里是 執(zhí)行
hack/update-codegen.sh后自動(dòng)生成的,包含 clienset,informers, injection(常用的是其中的 reconfiler 框架,框架中 lister 和 informer 可以從 context 中獲取,這也是 injection 的含義) ,lister。 -
pkg/reconciler: 這里是控制器的主要邏輯,包括控制器主入口
controller.go和對(duì)應(yīng)的reconciler邏輯
2.3 CRD 資源定義
1. 確定 GKV,即資源的 Group、Kind、Version
此處實(shí)例中,有兩個(gè) crd 資源,本文主要以 AddressableService 為例講解。
- Group 為
samples.knative.dev, - Kind 為
AddressableService(實(shí)例中有兩個(gè)類型,取一個(gè)介紹), - Version 為
v1alpha1
2.編寫 CRD types 文件
目錄按照 /pkg/apis/<kind 一般取 groupname 第一個(gè)逗號(hào)前的單詞>/<version>
Group 和 Version及其注冊(cè)
# pkg/apis/samples/register.go#20
package samples
const (
GroupName = "samples.knative.dev"
)
在 addKnownTypes 中將 Kind 注冊(cè)
# pkg/apis/samples/v1alpha1/register.go#27
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: samples.GroupName, Version: "v1alpha1"}
// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&AddressableService{},
&AddressableServiceList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
為 CRD types 編寫對(duì)應(yīng)的 spec 和 status, 注意其中的注解,這是 hack/update-codegen.sh 執(zhí)行生成 clientset 和 reconciler 的關(guān)鍵
// +genclient
// +genreconciler
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
# pkg/apis/samples/v1alpha1/addressable_service_types.go#32
// +genclient
// +genreconciler
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type AddressableService struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Spec holds the desired state of the AddressableService (from the client).
// +optional
Spec AddressableServiceSpec `json:"spec,omitempty"`
// Status communicates the observed state of the AddressableService (from the controller).
// +optional
Status AddressableServiceStatus `json:"status,omitempty"`
}
3 CRD 資源的 配置
可以看到,對(duì)于每個(gè) CRD 資源,除了 xxxtypes.go 外,還有以下幾個(gè)文件
- xxx_validation.go: 用于
webhook校驗(yàn) - _xxx__lifecycle.go: 用于
status狀態(tài)的設(shè)置 - xxx_defaults.go: 用于 默認(rèn)值的設(shè)置
可在 xxx_types 文件中 聲明如下,校驗(yàn)是否實(shí)現(xiàn)了對(duì)應(yīng)的接口
// Check that AddressableService can be validated and defaulted.
_ apis.Validatable = (*AddressableService)(nil)
_ apis.Defaultable = (*AddressableService)(nil)
_ kmeta.OwnerRefable = (*AddressableService)(nil)
// Check that the type conforms to the duck Knative Resource shape.
_ duckv1.KRShaped = (*AddressableService)(nil)
4. hack/update-codegen.sh 文件配置
# 1. knative.dev/sample-controller/pkg/client 表示生成代碼的目標(biāo)位置
# 2. knative.dev/sample-controller/pkg/apis 表示 CRD 資源定義的文件位置
# 3. `samples:v1alpha1` 表示 crd 的kind 版本
# 4. `deepcopy,client,informer,lister` 表示生成對(duì)應(yīng)的方法
${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \
knative.dev/sample-controller/pkg/client knative.dev/sample-controller/pkg/apis \
"samples:v1alpha1" \
--go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt
group "Knative Codegen"
# injection 這是生成 reconciler 的關(guān)鍵
# Knative Injection
${KNATIVE_CODEGEN_PKG}/hack/generate-knative.sh "injection" \
knative.dev/sample-controller/pkg/client knative.dev/sample-controller/pkg/apis \
"samples:v1alpha1" \
--go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt
5. 編寫完畢,執(zhí)行 bash hack/update-codegen.sh
執(zhí)行完畢沒出錯(cuò)的話,就可以進(jìn)行下一步編寫控制器主邏輯了
如果是 mac 用戶,這里一定要升級(jí) bash 版本到 v4(執(zhí)行 bash --version 查看),不然會(huì)出現(xiàn)如下問題,升級(jí)方法請(qǐng)自行百度
bash hack/update-codegen.sh
hack/../vendor/knative.dev/hack/library.sh: line 25: conditional binary operator expected
2.4 控制器邏輯介紹
controller 入口文件
# cmd/controller/main.go
func main() {
sharedmain.Main("controller",
addressableservice.NewController,
simpledeployment.NewController,
)
}
sharedmain.Main 函數(shù)傳入 controller 的初始化方法,該方法會(huì)返回一個(gè) controller 的實(shí)現(xiàn) controller.impl ,impl 的定義如下
# https://github.com/knative/pkg
# knative.dev/pkg/controller/controller.go#188
type Impl struct {
// 控制器的名字
Name string
// Reconciler 是主要實(shí)現(xiàn)邏輯,實(shí)現(xiàn)了接口 Reconcile(ctx context.Context, key string) error
// Reconciler 會(huì)調(diào)用
Reconciler Reconciler
// 工作隊(duì)列
workQueue *twoLaneQueue
}
# knative.dev/pkg/controller/controller.go#65
type Reconciler interface {
Reconcile(ctx context.Context, key string) error
sharedmain.Main 會(huì)執(zhí)行以下事情:
- 啟動(dòng)各種
informer,啟動(dòng) 所有controller,knative.dev/pkg/injection/sharedmain/main.go#238 - 執(zhí)行工作流
processNextWorkItem,knative.dev/pkg/injection/sharedmain/main.go#468 - 調(diào)用
Reconciler接口的Reconcile(ctx context.Context,key string) err函數(shù) -
Reconcile(ctx context.Context,key string) err函數(shù)調(diào)用 具體的 Reconciler 的實(shí)現(xiàn)接口 (這里就是用戶自己實(shí)現(xiàn)的代碼了)sample-controller/pkg/client/injection/reconciler/samples/v1alpha1/addressableservice/reconciler.go#181FinalizeKind(ctx context.Context, o v1alpha1.AddressableService) reconciler.EventFinalizeKind(ctx context.Context, o v1alpha1.AddressableService) reconciler.Event
- 接下來就是上述第 4點(diǎn)說的自己實(shí)現(xiàn)的代碼了
2.5 控制器邏輯編寫
代碼主要在 如下兩個(gè)文件:
- sample-controller/pkg/reconciler/addressableservice/addressableservice.go
- sample-controller/pkg/reconciler/addressableservice/controller.go
addressableservice.go 實(shí)現(xiàn) AddressableService 的 ReconcileKind 接口,如果刪除 CR 資源時(shí)要做清理動(dòng)作,可以實(shí)現(xiàn) Finalizer 的 FinalizeKind 接口,可通過以下聲明 確保接口的實(shí)現(xiàn)(IDE 一鍵生成函數(shù)框架)
// Check that our Reconciler implements Interface
var _ addressableservicereconciler.Interface = (*Reconciler)(nil)
var _ addressableservicereconciler.Finalizer = (*Reconciler)(nil)
- controller 中 代碼如下
# pkg/reconciler/addressableservice/controller.go#
// 借助 injection 從 context 中獲取 informer
addressableserviceInformer := addressableserviceinformer.Get(ctx)
svcInformer := svcinformer.Get(ctx)
// 實(shí)例化 addressableservice 的 Reconciler
r := &Reconciler{
ServiceLister: svcInformer.Lister(),
}
// 實(shí)例化 controller.impl 返回 供 controller 框架調(diào)用
impl := addressableservicereconciler.NewImpl(ctx, r)
r.Tracker = tracker.New(impl.EnqueueKey, controller.GetTrackerLease(ctx))
logger.Info("Setting up event handlers.")
// 添加 informer 的hander 函數(shù)
addressableserviceInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))
svcInformer.Informer().AddEventHandler(controller.HandleAll(
// Call the tracker's OnChanged method, but we've seen the objects
// coming through this path missing TypeMeta, so ensure it is properly
// populated.
controller.EnsureTypeMeta(
r.Tracker.OnChanged,
corev1.SchemeGroupVersion.WithKind("Service"),
),
))
-
handler函數(shù)
為 informer 添加 函數(shù)除了實(shí)例中的 Informer().AddEventHandler,還可以 通過 Informer().AddEventHandlerWithResyncPeriod 確保除了 watch 之外,周期性將 CR 全量加入 工作隊(duì)列中處理。
-
filter函數(shù)
還可以添加如下 filter 函數(shù),過濾進(jìn)入 工作隊(duì)列的 資源,(在資源數(shù)量巨大時(shí)能優(yōu)化性能)
domainInformer.Informer().AddEventHandlerWithResyncPeriod(cache.FilteringResourceEventHandler{
FilterFunc: controller.FilterControllerGK(v1beta1.Kind("Function")),
Handler: controller.HandleAll(impl.EnqueueControllerOf),
}, ControllerResyncPerion)
// k8s don't allow cross namespace owerreferences, so filter resource with label
k8ssvcInformer.Informer().AddEventHandlerWithResyncPeriod(cache.FilteringResourceEventHandler{
FilterFunc: FilterLabelKeyExists(api.FuncNameLabelKey),
Handler: controller.HandleAll(impl.EnqueueLabelOfNamespaceScopedResource(api.FuncNameSpaceLabelKey, api.FuncNameLabelKey)),
}, ControllerResyncPerion)
2.6 Reconciler 邏輯編寫
參考 sample-controller/pkg/reconciler/addressableservice/addressableservice.go 文件即可,其中注意
status 在 reconciler 中調(diào)用 xxx_lifecycle.go 中的 狀態(tài)設(shè)置函數(shù)可以,controller 框架會(huì)在 reconcile 流程結(jié)束后將 CR 資源的狀態(tài) 通過 kube-apiserver 更新到 etcd 中
# sample-controller/pkg/reconciler/addressableservice/addressableservice.go#76
o.Status.MarkServiceAvailable()
o.Status.Address = &duckv1.Addressable{
URL: &apis.URL{
Scheme: "http",
Host: network.GetServiceHostname(o.Spec.ServiceName, o.Namespace),
},
}
2.7 調(diào)試
1. 生成 CRD 描述文件 并 apply 到集群
- 在
sample-controller/cmd/schema/main.go中注冊(cè),如下:
func main() {
registry.Register(&v1alpha1.AddressableService{})
if err := commands.New("knative.dev/sample-controller").Execute(); err != nil {
log.Fatal("Error during command execution: ", err)
}
}
- 執(zhí)行命令
go run cmd/schema/main.go dump AddressableService
將生成的 yaml 粘貼到 sample-controller/config/300-addressableservice.yaml 中的
spec.versions.schema.openAPIV3Schema 下
- apply crd yaml,在 k8s 集群中執(zhí)行
kubectl apply -f config/300-addressableservice.yaml
- IDE 中 debug
如果是在 mac 中的 IDE 調(diào)試,將 k8s 集群中的 config 文件 復(fù)制一份,放在 mac 地址的 ~/.kube 目錄下,window linux 類似,config 放在用戶目錄下的 .kube目錄下:
為程序添加 環(huán)境變量 SYSTEM_NAMESPACE ,主要是用于 controller 選主,不設(shè)置會(huì) panic。
接下來,直接 debug sample-controller/cmd/controller/main.go 中的 main 函數(shù)即可 !
請(qǐng)關(guān)注 Knative 微信公眾號(hào),了解更多 Knative Serverless相關(guān)資訊