この記事は Kubernetes Advent Calendar 2018 20日目の記事です。
今回はkubebuilderで超絶簡単なカスタムコントローラを作る手順を紹介しようと思う。
kubebuilderとは
kubebuilderはGoでKubernetes APIを実装するためのSDKだ。
Railsなどのフレームワークの様に、kubebuilderも様々なリソースやファイルなどを自動的に生成し、開発を手助けしてくれる。
kubebuilderのインストール
GithubのRleaseから取ってくる。
Releases · kubernetes-sigs/kubebuilder
ここから自分のOSのBinary選択して落としてきてパスの通ってるところにも配置しよう。
Linuxであればだいたい以下のようになるだろう。
1
2
3
4
5
6
7
8
|
version=1.0.5
arch=amd64
curl -L -O https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/kubebuilder_${version}_linux_${arch}.tar.gz
tar -zxvf kubebuilder_${version}_linux_${arch}.tar.gz
mkdir -p ~/.local/bin
mv kubebuilder_${version}_linux_${arch}/bin/* ~/.local/bin/
export PATH=$PATH:$HOME/.local/bin
|
tar.gzを展開すると分かるが、結構いろいろなバイナリを配置することになる。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
kubebuilder_1.0.5_linux_amd64/bin/lient-gen
kubebuilder_1.0.5_linux_amd64/bin/onversion-gen
kubebuilder_1.0.5_linux_amd64/bin/eepcopy-gen
kubebuilder_1.0.5_linux_amd64/bin/efaulter-gen
kubebuilder_1.0.5_linux_amd64/bin/tcd
kubebuilder_1.0.5_linux_amd64/bin/en-apidocs
kubebuilder_1.0.5_linux_amd64/bin/nformer-gen
kubebuilder_1.0.5_linux_amd64/bin/ube-apiserver
kubebuilder_1.0.5_linux_amd64/bin/ubebuilder
kubebuilder_1.0.5_linux_amd64/bin/ube-controller-manager
kubebuilder_1.0.5_linux_amd64/bin/ubectl
kubebuilder_1.0.5_linux_amd64/bin/ister-gen
kubebuilder_1.0.5_linux_amd64/bin/penapi-gen
|
自分でちゃんと管理したい方は適宜配置する場所を変えるといいだろう。
あと、以下のものを適宜インストールしておく。このページを見に来るような方であれば入っていることだろう。
kubebuilderの使う
kubebuilderのプロジェクトの作成
さて、まずはプロジェクトの作成をする。
kubebuilderを実行するには $GOPATH/src/<package>
以下で作業を行う必要がある。
ここでは $GOPATH/src/github.com/cstoku/kubebuilder-test-controller
で作業します。
cstoku/kubebuilder-test-controller
辺りは適宜変えて実行してください。
1
2
|
mkdir -p $GOPATH/src/github.com/cstoku/kubebuilder-test-controller
cd $GOPATH/src/github.com/cstoku/kubebuilder-test-controller
|
それではプロジェクトを作成しよう。 kubebuilder init
で行う。
--domain
: APIグループのドメインを指定(割となんでも良い)
--license
: ソフトウェアライセンスを選択。 apache2
か none
の2択
--owner
: このソフトウェアのオーナーを指定。Copyrightに差し込んでくれる
1
|
kubebuilder init --domain cstoku.dev --license apache2 --owner cstoku
|
途中 dep ensure
実行するけどいい?と聞いてくるので y
を選択して実行させよう。
以下のような出力があったはずだ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Run `dep ensure` to fetch dependencies (Recommended) [y/n]?
y
dep ensure
Running make...
make
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/crds'
RBAC manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/rbac'
go test ./pkg/... ./cmd/... -coverprofile cover.out
? github.com/cstoku/kubebuilder-test-controller/pkg/apis [no test files]
? github.com/cstoku/kubebuilder-test-controller/pkg/controller [no test files]
? github.com/cstoku/kubebuilder-test-controller/pkg/webhook [no test files]
? github.com/cstoku/kubebuilder-test-controller/cmd/manager [no test files]
go build -o bin/manager github.com/cstoku/kubebuilder-test-controller/cmd/manager
Next: Define a resource with:
$ kubebuilder create api
|
さて、何が作成されたかだけ確認しておこう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ ls -a1
.
..
bin
cmd
config
cover.out
Dockerfile
.gitignore
Gopkg.lock
Gopkg.toml
hack
Makefile
pkg
PROJECT
vendor
|
リソースの定義
さて、次は出力にもあった通りAPI作成してリソースの定義をしていく。
kubebuilder create api
で行う。
--group
: APIグループを指定
--version
: APIのバージョンを指定。 ここ を参考に付けると良いだろう
--kind
: APIの名前を指定
1
|
kubebuilder create api --group trial --version v1alpha1 --kind EchoField
|
今回は --group
にお試し版ということもあって trial
を指定。 --version
に初期段階ということで v1alpha1
を指定。 --kind
は今回作成するリソース名( EchoField
)を指定した。
実行するとリソースとコントローラを pkg
以下に作成していいか聞かれるので y
をそれぞれ入力しよう。
以下のような出力があったはずだ。Testでこけているが一旦ここでは気にしない 😝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
Create Resource under pkg/apis [y/n]?
y
Create Controller under pkg/controller [y/n]?
y
Writing scaffold for you to edit...
pkg/apis/trial/v1alpha1/echofield_types.go
pkg/apis/trial/v1alpha1/echofield_types_test.go
pkg/controller/echofield/echofield_controller.go
pkg/controller/echofield/echofield_controller_test.go
Running make...
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/crds'
RBAC manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/rbac'
go test ./pkg/... ./cmd/... -coverprofile cover.out
? github.com/cstoku/kubebuilder-test-controller/pkg/apis [no test files]
? github.com/cstoku/kubebuilder-test-controller/pkg/apis/trial [no test files]
2018/12/21 01:45:51 failed to start the controlplane. retried 5 times
FAIL github.com/cstoku/kubebuilder-test-controller/pkg/apis/trial/v1alpha1 0.013s
? github.com/cstoku/kubebuilder-test-controller/pkg/controller [no test files]
2018/12/21 01:45:52 failed to start the controlplane. retried 5 times
FAIL github.com/cstoku/kubebuilder-test-controller/pkg/controller/echofield 0.018s
? github.com/cstoku/kubebuilder-test-controller/pkg/webhook [no test files]
? github.com/cstoku/kubebuilder-test-controller/cmd/manager [no test files]
make: *** [Makefile:9: test] Error 1
2018/12/21 01:45:52 exit status 2
|
ローカル環境での実行
それでは、初期状態でのControllerの実行してみる。
ローカルといっても手元のconfigから接続出来るKubernetes Clusterに導入を行う。
ないようであればこちらを参考にローカル環境を立てるのが良いだろう。
準備ができたら make install
を実行しよう。
1
2
3
4
5
6
|
$ make install
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/crds'
RBAC manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/rbac'
kubectl apply -f config/crds
customresourcedefinition.apiextensions.k8s.io/echofields.trial.cstoku.dev created
|
ログを見る感じ、 controller-gen
を実行してCRDを適用したようだ。
次は make run
で実際に実行してみる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
go run ./cmd/manager/main.go
{"level":"info","ts":1545324635.4268575,"logger":"entrypoint","msg":"setting up client for manager"}
{"level":"info","ts":1545324635.4326477,"logger":"entrypoint","msg":"setting up manager"}
{"level":"info","ts":1545324635.4595976,"logger":"entrypoint","msg":"Registering Components."}
{"level":"info","ts":1545324635.4596338,"logger":"entrypoint","msg":"setting up scheme"}
{"level":"info","ts":1545324635.459897,"logger":"entrypoint","msg":"Setting up controller"}
{"level":"info","ts":1545324635.4599698,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"echofield-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1545324635.4600892,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"echofield-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1545324635.4601424,"logger":"entrypoint","msg":"setting up webhooks"}
{"level":"info","ts":1545324635.460151,"logger":"entrypoint","msg":"Starting the Cmd."}
{"level":"info","ts":1545324635.5639021,"logger":"kubebuilder.controller","msg":"Starting Controller","controller":"echofield-controller"}
{"level":"info","ts":1545324635.664216,"logger":"kubebuilder.controller","msg":"Starting workers","controller":"echofield-controller","worker count":1}
|
さて、何やら実行されたようだ。実はここまでの数コマンドでコントローラーのビルドと実行が出来ており、先程作成したCRDのコントローラーとして機能しているのだ。
別のターミナルからサンプルで作成されているManifestを適用してみよう。
1
2
|
$ kubectl apply -f config/samples/trial_v1alpha1_echofield.yaml
echofield.trial.cstoku.dev/echofield-sample created
|
するとコントローラーを実行している方でログが出力されているはずだ。
色々と kubectl get
で取得してみよう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ kubectl get echofield
NAME AGE
echofield-sample 2m
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/echofield-sample-deployment-6bdd78b879-g9v5v 1/1 Running 0 2m14s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d12h
service/nginx ClusterIP 10.108.165.92 <none> 80/TCP 3d12h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/echofield-sample-deployment 1/1 1 1 2m14s
NAME DESIRED CURRENT READY AGE
replicaset.apps/echofield-sample-deployment-6bdd78b879 1 1 1 2m14s
|
どうやら今実行しているコントローラーはDeploymentを作ってくれているらしい。
コントローラーの実装を変更する
突然Deploymentを生成されても困るので、実装を変更しよう。
コントローラーのロジックを実装したい場合は、途中の出力にもあったが pkg/controller/echofield/echofield_controller.go
にあるファイルに実装していけば良い。
開いてみると重要な箇所がいくつかある。見ていこう。
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("echofield-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to EchoField
err = c.Watch(&source.Kind{Type: &trialv1alpha1.EchoField{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
// TODO(user): Modify this to be the types you create
// Uncomment watch a Deployment created by EchoField - change this for objects you create
err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType: &trialv1alpha1.EchoField{},
})
if err != nil {
return err
}
return nil
}
|
Watch APIを呼んでいるようだ。今は EchoField
だけ見てればいいので Deployment
の方は消してしまおう。
次に実際を処理している部分を見ていく。
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
func (r *ReconcileEchoField) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// Fetch the EchoField instance
instance := &trialv1alpha1.EchoField{}
err := r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
// TODO(user): Change this to be the object type created by your controller
// Define the desired Deployment object
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-deployment",
Namespace: instance.Namespace,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"deployment": instance.Name + "-deployment"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"deployment": instance.Name + "-deployment"}},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
},
},
}
if err := controllerutil.SetControllerReference(instance, deploy, r.scheme); err != nil {
return reconcile.Result{}, err
}
// TODO(user): Change this for the object type created by your controller
// Check if the Deployment already exists
found := &appsv1.Deployment{}
err = r.Get(context.TODO(), types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
log.Printf("Creating Deployment %s/%s\n", deploy.Namespace, deploy.Name)
err = r.Create(context.TODO(), deploy)
if err != nil {
return reconcile.Result{}, err
}
} else if err != nil {
return reconcile.Result{}, err
}
// TODO(user): Change this for the object type created by your controller
// Update the found object and write the result back if there are any changes
if !reflect.DeepEqual(deploy.Spec, found.Spec) {
found.Spec = deploy.Spec
log.Printf("Updating Deployment %s/%s\n", deploy.Namespace, deploy.Name)
err = r.Update(context.TODO(), found)
if err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
|
Deployment
オブジェクトを作って見つからなければ作成するし、既にあって差分がある場合は更新している。
この処理のおかげで突如 Deployment
が作成されたわけだ。こんな処理取っ払ってしまおう 💥
一旦以下のような状態にした。
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
func (r *ReconcileEchoField) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// Fetch the EchoField instance
instance := &trialv1alpha1.EchoField{}
err := r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
|
さて、今回は EchoField
の spec.field
に指定されたものをそのまま status.field
にセットする処理を実装しよう。
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
func (r *ReconcileEchoField) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// Fetch the EchoField instance
instance := &trialv1alpha1.EchoField{}
err := r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
desire := instance.DeepCopy()
if desire.Status.Field != instance.Spec.Field {
desire.Status.Field = instance.Spec.Field
log.Printf("Updating EchoField %s/%s\n", instance.Namespace, instance.Name)
err = r.Update(context.TODO(), desire)
if err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{}, nil
}
|
DeepCopyでオブジェクトを複製して、 spec.field
と status.field
を比較している。差分があったら desire
を更新して Update
を呼んでいる。
さて、 field
というフィールドを作成したので、Structも更新しよう。 pkg/apis/trial/v1alpha1/echofield_types.go
にある以下の部分だ。
27
28
29
30
31
32
33
34
35
36
|
type EchoFieldSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// EchoFieldStatus defines the observed state of EchoField
type EchoFieldStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
|
この2つの構造体に field
を足そう。
27
28
29
30
31
32
33
34
35
36
37
38
|
type EchoFieldSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
Field string `json:"field"`
}
// EchoFieldStatus defines the observed state of EchoField
type EchoFieldStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Field string `json:"field,omitempty"`
}
|
さて、 コメントに書いてあるように make
を実行してから make install
と make run
を実行してみよう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
$ make
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/crds'
RBAC manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/rbac'
go test ./pkg/... ./cmd/... -coverprofile cover.out
? github.com/cstoku/kubebuilder-test-controller/pkg/apis [no test files]
? github.com/cstoku/kubebuilder-test-controller/pkg/apis/trial [no test files]
2018/12/21 03:11:55 failed to start the controlplane. retried 5 times
FAIL github.com/cstoku/kubebuilder-test-controller/pkg/apis/trial/v1alpha1 0.026s
? github.com/cstoku/kubebuilder-test-controller/pkg/controller [no test files]
2018/12/21 03:11:55 failed to start the controlplane. retried 5 times
FAIL github.com/cstoku/kubebuilder-test-controller/pkg/controller/echofield 0.011s
? github.com/cstoku/kubebuilder-test-controller/pkg/webhook [no test files]
? github.com/cstoku/kubebuilder-test-controller/cmd/manager [no test files]
make: *** [Makefile:9: test] Error 1
$ make install
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/crds'
RBAC manifests generated under '/home/cs_toku/go/src/github.com/cstoku/kubebuilder-test-controller/config/rbac'
kubectl apply -f config/crds
customresourcedefinition.apiextensions.k8s.io/echofields.trial.cstoku.dev configured
$ make run
make run
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
go run ./cmd/manager/main.go
{"level":"info","ts":1545329632.4990416,"logger":"entrypoint","msg":"setting up client for manager"}
{"level":"info","ts":1545329632.5055401,"logger":"entrypoint","msg":"setting up manager"}
{"level":"info","ts":1545329632.5347497,"logger":"entrypoint","msg":"Registering Components."}
{"level":"info","ts":1545329632.534778,"logger":"entrypoint","msg":"setting up scheme"}
{"level":"info","ts":1545329632.5348616,"logger":"entrypoint","msg":"Setting up controller"}
{"level":"info","ts":1545329632.5348947,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"echofield-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1545329632.5349865,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"echofield-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1545329632.535034,"logger":"entrypoint","msg":"setting up webhooks"}
{"level":"info","ts":1545329632.535041,"logger":"entrypoint","msg":"Starting the Cmd."}
{"level":"info","ts":1545329632.6352468,"logger":"kubebuilder.controller","msg":"Starting Controller","controller":"echofield-controller"}
{"level":"info","ts":1545329632.7355602,"logger":"kubebuilder.controller","msg":"Starting workers","controller":"echofield-controller","worker count":1}
|
改めて、 EchoField
のリソースを作成してみよう。以下のようなManifestを作成した。
1
2
3
4
5
6
|
apiVersion: trial.cstoku.dev/v1alpha1
kind: EchoField
metadata:
name: echofield-sample
spec:
field: "hello!!"
|
適用して取得してみる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
kubectl apply -f echofield.yaml
echofield.trial.cstoku.dev/echofield-sample created
kubectl get -f echofield.yaml -o yaml
apiVersion: trial.cstoku.dev/v1alpha1
kind: EchoField
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"trial.cstoku.dev/v1alpha1","kind":"EchoField","metadata":{"annotations":{},"name":"echofield-sample","namespace":"default"},"spec":{"field":"hello!!"}}
creationTimestamp: 2018-12-20T18:25:22Z
generation: 2
name: echofield-sample
namespace: default
resourceVersion: "371823"
selfLink: /apis/trial.cstoku.dev/v1alpha1/namespaces/default/echofields/echofield-sample
uid: 9ca1c900-0484-11e9-adf0-dc51cfa8f3cb
spec:
field: hello!!
status:
field: hello!!
|
status.field
に spec.field
の値が入っていることが分かる。
最後に
このような流れで簡単にカスタムコントローラーの実装を始めることが出来る。ついでに上記で作ったコードは以下に置いておく。
cstoku/kubebuilder-test-controller
今回は本当に簡単でシンプルなコントローラーを書いたが、もちろん他にも色々な機能が提供されているので、それらについてはまた別の記事(きっとある。)で解説できればと思う。
それでは!