Hello Custom Controller!
2019年はカスタムコントローラーや!ということで2018年末頃から少しづつ調べている。まずは非常に簡単なものを動かしてみる。
Custom Controller is…
CRDについてのメモにも書いたが、CRD自体はただのデータなので、それだけでは何も起こらない。Kubernetesのdeclarative APIを活用するためにはコントローラーの作成が必要。なお、本記事は以下のページを参考にしています。
- Custom Resources
- [KubernetesのCRD(Custom Resource Definition)とカスタムコントローラーの作成](https://qiita.com/ Attsun /items/785008ef970ad82c679c)
- Extending Kubernetes with Custom Resources and Operator Frameworks
- Kubernetesを拡張するにはデータとロジックが必要
- データはCustomResourceDefinition
- ロジックはController
- Operator Framework
- operator-sdk
- kubebuilder
- kubernetes/sample-controller
Foo
というカスタムリソースを定義する- このリソースは
Deployment
を定義するためのカスタムリソース- 名前とレプリカ数を指定できる
- client-goライブラリを使っている
- KubernetesのCRDまわりを整理する。
作っていく
カスタムコントローラーを作っていくわけだが、いきなり複雑なものを作るのは難しい。そこで、まずはカスタムリソースを読み込み、ログに出力するだけのコントローラーを作る。カスタムコントローラー用のSDKやBuilderがいろいろあるようだがこれも使わず、KubernetesのAPI用コードを出力するだめの code-generator を用いて素朴に作ってみる。code-generatorはいくつかの機能を持っている。
- deepcopy-gen – 各型についてDeepCopy用のメソッドを生成
- client-gen – カスタムリソースを扱うためのクライアントセットを生成
- informer-gen – カスタムリソースの変更に対応するためのイベントベースインターフェースを生成
- lister-gen – GetとListについて、読み取り用キャッシュレイヤを扱うコードを生成
本格的にカスタムリソースを扱うためにはinformer-genやlister-genが必要になるが、今回はdeepcopy-genとclient-genのみ。deepcopy-genも不要な気がするが、これなしでうまく動かす方法がちと分からなかった。
生成元のコードを用意する
まずは apis/foo/v1alpha/doc.go
。
// +k8s:deepcopy-gen=package
// Package v1alpha is the v1alpha version of the API.
// +groupName=samplecontroller.k8s.io
package v1alpha
次にapis/foo/v1alpha/register.go
。
package v1alpha
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: "samplecontroller.k8s.io", Version: "v1alpha"}
// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Foo{},
&FooList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
最後にapis/foo/v1alpha/types.go
。
package v1alpha
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Foo is a specification for a Foo resource
type Foo struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Status FooStatus `json:"status"`
Spec FooSpec `json:"spec"`
}
type FooStatus struct {
Name string `json:"name"`
}
// FooSpec is the spec for a Workflow resource
type FooSpec struct {
Name string `json:"name"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// FooList is a list of Workflow resources
type FooList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []Foo `json:"items"`
}
コード生成
これらのファイルを用意したら、code-generatorでコードを生成する。なお、code-generatorのバージョンは kubernetes-1.12.3
を使用。
$ bash ~/src/k8s.io/code-generator/generate-groups.sh client,deepcopy github.com/takaishi/hello2019/hello-custom-controller/pkg/client github.com/takaishi/hello2019/hello-custom-controller/pkg/apis foo:v1alpha
Generating deepcopy funcs
Generating clientset for foo:v1alpha at github.com/takaishi/hello2019/hello-custom-controller/pkg/client/clientset
カスタムコントローラーを実装
これで、GoからFooリソースを扱うことが出来る。カスタムコントローラー用のコードを書いていく:
package main
import (
clientset "github.com/takaishi/hello2019/hello-custom-controller/pkg/client/clientset/versioned"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"fmt"
"github.com/urfave/cli"
"log"
"os"
"time"
)
func main() {
log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime)
app := cli.NewApp()
app.Flags = []cli.Flag{}
app.Action = func(c *cli.Context) error {
return action(c)
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func action(c *cli.Context) error {
cfg, err := rest.InClusterConfig()
if err != nil {
log.Printf(err.Error())
}
client, err := clientset.NewForConfig(cfg)
if err != nil {
log.Printf(err.Error())
}
for {
foos, err := client.SamplecontrollerV1alpha().Foos("default").List(v1.ListOptions{})
if err != nil {
log.Printf(err.Error())
continue
}
fmt.Printf("%+v\n", foos)
time.Sleep(10 * time.Second)
}
return nil
}
コントローラー用のマニフェスト。defaultアカウントがオブジェクトを取得できるようにRBACの設定も行っている:
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: foo-reader
namespace: default
rules:
- apiGroups: ["samplecontroller.k8s.io"]
verbs: ["get", "list"]
resources: ["foos"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: foo-reader-rolebinding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: foo-reader
subjects:
- kind: ServiceAccount
name: default
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-main
spec:
selector
matchLabels:
app: controller-main
replicas: 1
template:
metadata:
labels:
app: controller-main
spec:
containers:
- name: controller-main
image: rtakaishi/sample-controller-main:latest
imagePullPolicy: IfNotPresent
デプロイして動作確認する
カスタムコントローラーのビルドからKubernetesへのデプロイまで行うためのMakefile:
date := $(shell date +'%s')
default:
env GOOS=linux GOARCH=amd64 go build -o controller-main main.go
docker build . -t rtakaishi/sample-controller-main
kubectl apply -f ./deploy-controller-main.yaml
kubectl patch deploy controller-main -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(date)\"}}}}}"
デプロイする:
$ eval (minikube docker-env)
$ make
カスタムコントローラーのログを見ると、Fooリソースを取得してログに出力していることが確認できた:
$ stern controller-main
+ controller-main-66f5cb49df-4bhq6 › controller-main
controller-main-66f5cb49df-4bhq6 controller-main &{TypeMeta:{Kind: APIVersion:} ListMeta:{SelfLink:/apis/samplecontroller.k8s.io/v1alpha/namespaces/default/foos ResourceVersion:98881 Continue:} Items:[{TypeMeta:{Kind:Foo APIVersion:samplecontroller.k8s.io/v1alpha} ObjectMeta:{Name:foo-001 GenerateName: Namespace:default SelfLink:/apis/samplecontroller.k8s.io/v1alpha/namespaces/default/foos/foo-001 UID:77506f4f-0e40-11e9-95ac-263ada282756 ResourceVersion:3649 Generation:1 CreationTimestamp:2019-01-02 03:42:45 +0000 UTC DeletionTimestamp:<nil> DeletionGracePeriodSeconds:<nil> Labels:map[] Annotations:map[kubectl.kubernetes.io/last-applied-configuration:{"apiVersion":"samplecontroller.k8s.io/v1alpha","kind":"Foo","metadata":{"annotations":{},"name":"foo-001","namespace":"default"},"spec":{"deploymentName":"deploy-foo-001","replicas":1}}
controller-main-66f5cb49df-4bhq6 controller-main ] OwnerReferences:[] Initializers:nil Finalizers:[] ClusterName:} Status:{Name:} Spec:{Name:}} {TypeMeta:{Kind:Foo APIVersion:samplecontroller.k8s.io/v1alpha} ObjectMeta:{Name:foo-002 GenerateName: Namespace:default SelfLink:/apis/samplecontroller.k8s.io/v1alpha/namespaces/default/foos/foo-002 UID:775194f8-0e40-11e9-95ac-263ada282756 ResourceVersion:93582 Generation:1 CreationTimestamp:2019-01-02 03:42:45 +0000 UTC DeletionTimestamp:<nil> DeletionGracePeriodSeconds:<nil> Labels:map[] Annotations:map[kubectl.kubernetes.io/last-applied-configuration:{"apiVersion":"samplecontroller.k8s.io/v1alpha","kind":"Foo","metadata":{"annotations":{},"name":"foo-002","namespace":"default"},"spec":{"deploymentName":"deploy-foo-002","hoge":"huga","replicas":1}}
controller-main-66f5cb49df-4bhq6 controller-main ] OwnerReferences:[] Initializers:nil Finalizers:[] ClusterName:} Status:{Name:} Spec:{Name:}}]}
非常にシンプル、というかとくに何もしていないが、カスタムコントローラーができたといっていいだろう。理屈ではログ出力の部分をロジックに置き換えればいいはず。実際にはinformerを用いてイベントベースで処理を行うようなコードになるのだと思う。次はログ出力だけではなく別リソースを扱うようにしていきたい。
ソースコードなどはtakaishi/hello2019にあります。