はじめに
本記事は ラクスパートナーズアドベントカレンダー 2023 2日目 の投稿です。
数年前から話題だったようですが、私は最近プロジェクトで触れる機会があり、遅ればせながら KubernetesOperator に入門したので個人的な備忘もかねて投稿します。
私はバリバリインフラ系のため、Go については完全素人でしたが後述の operator-sdk があったおかげで何とか作り上げることができました。ありがたや。。
そもそも KubernetesOperator とは
ざっくり簡単に言うと
「Kubernetes リソースの作成を自動化してくれるプログラム」です。
CustomResource をアプライすることで Cluster 内に起動した KubernetesOperator がそれを検知し
KubernetesOperator 内で定義してある Deployment や NameSpace 等のリソースを自動でクラスターに起動してくれます。
Operatorhub で様々なプロダクトが開発している KubernetesOperator があるので使用したことがある人も多いと思います。(PrometheusOperator 等。。)
- ユーザーが CustomResource をアプライする。
- KubernetesOperator が CustomResource のアプライを検知する。
- KubernetesOperator が各リソースをアプライする。
詳しくは RedHat 様の記事 を読んでいただくとより理解していただけると思います。
KubernetesOperator の作成方法はいろいろあるようですが、今回は上記 Redhat 様の記事で紹介されている 「operator-sdk」を使用して KubernetesOperator を作成していきます。
operator-sdk とは
KubernetesOperator の開発、管理を容易にする SDK で、
Go, Ansible, Helm のいずれかを使用した開発に対応しています。
今回はプロジェクトで触れた Go での開発を記載いたします。
※ 筆者は KubernetesOperator で初めて Go に触れましたがそれでも operator-sdk を使うことでかなり簡単に開発をすることができました。
実際に作成してみる
ここから実際に簡単な内容で KubernetesOperator を作成していきます。
淡々と作成していくので都度 operator-sdk のドキュメント を読んで補完していただくとより分かりやすいかもしれません。
(ただの手順書になってしまった。。)
〇 環境情報
プラットフォーム:GCP GKE
KubernetesVersion:1.27.5-gke.200
KubectlVersion:v1.27.5
Go Version:1.19.13
(operator-sdk の前提条件で Go の 1.19.~ が必要です。)
1 operator-sdk のインストール
operator-sdk のドキュメント を確認して環境に合ったインストール方法を選択します。
今回は Git リポジトリからクローンしてインストールします。
-
リポジトリをクローン
$ git clone https://github.com/operator-framework/operator-sdk
-
クローンしたリポジトリに移動する
$ cd operator-sdk
-
make コマンドを使用してインストールする
少し時間かかります。。$ make install
2 operator プロジェクトの作成
operator プロジェクトを作成して各種必要な資材を作成します。
-
プロジェクト作成
--domain
や--repo
は実在するものでなくても問題ありません。$ operator-sdk init --domain demo.com --repo github.com/example/demo-operator
-
Operator Api の作成
--kind
で指定した文字列が CustomResource の kind になります。$ operator-sdk create api --group demo --version v1alpha1 --kind Demo --resource --controller
3 CustomResourceDefinition の作成
前手順で作成した資材の中から、CustomResourceDefinition の設定にかかわる部分から修正していきます。
-
~types.go の修正
<kind>Spec
の構造体に必要な値を追加する$ vim api/v1alpha1/demo_types.go
(修正イメージ)
DemoSpec
等の文字列は前段のcreate api
で指定した--kind
によって変わりますので適宜読み替えてください。
※ デフォルトのコメントアウトは消しています。type DemoSpec struct { - Foo string `json:"foo,omitempty"` // CustomResource の manifest の値を読み取るために、json:"キー名" を紐づける。 // omitempty を付けないと必須の値となり、マニフェストで指定していなければアプライ時にエラーが出力される。 + Namespace string `json:"namespace"` + Name string `json:"name"` + Image string `json:"image"` + Size int32 `json:"size"` }
-
CustomResourceDefinition マニフェストの作成
~types.go
修正後に以下コマンドを実行すると
~types.go
の内容に合った CustomResource のアプライに必要なファイルを作成/修正してくれるため修正後は必ず実行します。$ make generate
$ make manifests
4 Reconcile に処理を追加
KubernetesOperator のメイン処理 (deployment, namespace を作ったり) を記載する部分なので、開発作業の大半はこのファイルをいじることになります。
-
~controller.go の修正
$ vi controllers/demo_controller.go
↓ の Reconcile メソッドにメインの処理を追記します。(もちろん別の関数にしたり、別の pkg から呼び出したりもできますが今回は簡単に Reconcile メソッドのみ修正します。。)
func (r *DemoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // TODO(user): your logic here return ctrl.Result{}, nil }
(修正イメージ)
func (r *DemoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // 要所要所でログを出力したいので logger を定義 logger := log.FromContext(ctx) // CustomResourceDefinition がなければ処理を行わない cr := &demov1alpha1.Demo{} err := r.Get(ctx, req.NamespacedName, cr) if errors.IsNotFound(err) { logger.Info("Demo Resource Not Found") return ctrl.Result{}, nil } else if err != nil { // 取得自体が失敗したらエラーを出力 logger.Error(err, "Failed to Get Demo Resource") } // それっぽいログを出力 logger.Info("Create Namespace") // Namespace 作成用の構造体を定義 // 他のリソースを作成したい場合は Go のドキュメントを参照ください。 ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: cr.Spec.Namespace, }, } // すでに対象リソースがあれば処理を行わない err = r.Get(ctx, client.ObjectKey{Name: cr.Spec.Namespace}, ns) if errors.IsNotFound(err) { // 対象リソースがなければここで作成 if err := r.Client.Create(ctx, ns); err != nil { logger.Error(err, "Failed to Create Namespace") } } else if err != nil { logger.Error(err, "Failed to Get Namespace") } logger.Info("Namespace creation successfully") logger.Info("Create Deployment") // Deployment 作成用の構造体を定義 deploy := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: cr.Spec.Name, Namespace: cr.Spec.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &cr.Spec.Size, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"demo": "true"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: cr.Spec.Name + "-pod", Labels: map[string]string{"demo": "true"}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: cr.Spec.Name + "-container", Image: cr.Spec.Image, }, }, }, }, }, } // Namespace と同じく対象リソースがすでにあれば作成しない err = r.Get(ctx, client.ObjectKey{Name: cr.Spec.Name, Namespace: cr.Spec.Namespace}, deploy) if errors.IsNotFound(err) { if err := r.Client.Create(ctx, deploy); err != nil { logger.Error(err, "Failed to Create Deployment") } } else if err != nil { logger.Error(err, "Failed to Get Deployment") } // 最後にまたそれっぽいログを出力して処理完了 logger.Info("Deployment creation successfully") return ctrl.Result{}, nil }
-
静的チェック
(go vet でも同様のチェックができるのでお好きなほうで。。)
何も出力されなければ OK$ make vet
5 動作確認
ローカルでオペレーターを起動
-
CustomResourceDefinition のアプライ
$ make install
-
KubernetesOperator のアプライ
Starting workers
と出力されていれば問題なく起動できている。$ make run
(出力例)
~~ snip ~~ 2023-12-01T13:51:56Z INFO controller-runtime.metrics Metrics server is starting to listen {"addr": ":8080"} 2023-12-01T13:51:56Z INFO setup starting manager 2023-12-01T13:51:56Z INFO Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"} 2023-12-01T13:51:56Z INFO Starting server {"kind": "health probe", "addr": "[::]:8081"} 2023-12-01T13:51:56Z INFO Starting EventSource {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "source": "kind source: *v1alpha1.Demo"} 2023-12-01T13:51:56Z INFO Starting Controller {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo"} 2023-12-01T13:51:56Z INFO Starting workers {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "worker count": 1} ~~ snip ~~
-
CustomResource のアプライ
operator-sdk で用意してくれているサンプルマニフェストを少し修正し Operator の CustomResource をアプライする。
make run
を実行しているターミナルとは別のターミナルを開いて行う。$ vim config/samples/demo_v1alpha1_demo.yaml
(修正イメージ)
~types.go で指定したキーを入れていく。apiVersion: demo.demo.com/v1alpha1 kind: Demo metadata: labels: app.kubernetes.io/name: demo app.kubernetes.io/instance: demo-sample app.kubernetes.io/part-of: demo-operator app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: demo-operator name: demo-sample spec: # TODO(user): Add fields here + namespace: demo-namespace + name: demo-deployment + image: nginx + size: 3
$ kubectl apply -f config/samples/demo_v1alpha1_demo.yaml
CustomResource をアプライすると Reconcile メソッドに追加したそれっぽいログが出力される。
2023-12-01T14:23:49Z INFO Create Namespace {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"} 2023-12-01T14:23:50Z INFO Namespace creation successfully {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"} 2023-12-01T14:23:50Z INFO Create Deployment {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"} 2023-12-01T14:23:50Z INFO Deployment creation successfully {"controller": "demo", "controllerGroup": "demo.demo.com", "controllerKind": "Demo", "Demo": {"name":"demo-sample","namespace":"default"}, "namespace": "default", "name": "demo-sample", "reconcileID": "9ae44549-3b16-4087-bd42-e1d5feaae1e2"}
さらに定義した Namespace と Deployment も問題なくアプライされている。
$ kubectl get ns NAME STATUS AGE default Active 2d10h demo Active 24h demo-namespace Active 2m5s gmp-public Active 2d10h gmp-system Active 2d10h kube-node-lease Active 2d10h kube-public Active 2d10h kube-system Active 2d10h $ kubectl get all -n demo NAME READY STATUS RESTARTS AGE pod/demo-c9fd6db65-kxg64 1/1 Running 0 23h pod/demo-c9fd6db65-vlhl5 1/1 Running 0 23h pod/demo-c9fd6db65-z6scj 1/1 Running 0 23h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/demo 3/3 3 3 24h NAME DESIRED CURRENT READY AGE replicaset.apps/demo-c9fd6db65 3 3 3 24h
-
動作確認まで完了したので最後にイメージを作成してクラスター内で起動する。
イメージをビルドする前にもうひと手間加えます。
Reconciler 構造体の下にあるこの部分。//+kubebuilder:rbac:groups=demo.demo.com,resources=demoes,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/status,verbs=get;update;patch //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/finalizers,verbs=update
初見時はぱっと見コメントだと思いましたが、ここに rbac の設定を記載しておけばビルド時に rbac のリソースをよしなに作ってくれます。便利!
今回作成したいのは Namespace と Deployment なのでこんな感じで追記。//+kubebuilder:rbac:groups=demo.demo.com,resources=demoes,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/status,verbs=get;update;patch //+kubebuilder:rbac:groups=demo.demo.com,resources=demoes/finalizers,verbs=update //+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch
追記したら
make
コマンドを使ってイメージをビルドする。$ make docker-build
ビルドしたら適当なレジストリにプッシュする。
その後以下のコマンドを実行して、オペレーターをクラスター内に起動する。$ make deploy IMG=<IMAGEPATH>:<TAG>
$ kubectl get ns NAME STATUS AGE default Active 2d12h ↓ ここにオペレーターのリソースが入っている。 demo-operator-system Active 22m gmp-public Active 2d12h gmp-system Active 2d12h kube-node-lease Active 2d12h kube-public Active 2d12h kube-system Active 2d12h $ kubectl get all -n demo-operator-system NAME READY STATUS RESTARTS AGE pod/demo-operator-controller-manager-68fc6f6cc8-v6znr 2/2 Running 0 23m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demo-operator-controller-manager-metrics-service ClusterIP 10.60.4.79 <none> 8443/TCP 23m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/demo-operator-controller-manager 1/1 1 1 23m NAME DESIRED CURRENT READY AGE replicaset.apps/demo-operator-controller-manager-68fc6f6cc8 1 1 1 23m
再度 CustomResource をアプライする。
$ kubectl apply -f config/samples/demo_v1alpha1_demo.yaml
$ kubectl get ns NAME STATUS AGE default Active 2d13h demo-namespace Active 51s demo-operator-system Active 60s gmp-public Active 2d13h gmp-system Active 2d13h kube-node-lease Active 2d13h kube-public Active 2d13h kube-system Active 2d13h $ kubectl get all -n demo-namespace NAME READY STATUS RESTARTS AGE pod/demo-deployment-6db7ddf7d9-7px8s 1/1 Running 0 62s pod/demo-deployment-6db7ddf7d9-7v8kx 1/1 Running 0 62s pod/demo-deployment-6db7ddf7d9-xrd2s 1/1 Running 0 62s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/demo-deployment 3/3 3 3 62s NAME DESIRED CURRENT READY AGE replicaset.apps/demo-deployment-6db7ddf7d9 3 3 3 62s
問題なく作成できているのでヨシ!!
以上!!
簡潔な内容ですが以上です。
正直 KubernetesOperator が刺さる要件ってそこまで多くない気はしていますが
私がまだまだ Go と仲良くなれていないため、KubernetesOperator の良さを引き出せていない。というのもあるので今後も修行し続ければもっと良い使い方が見つかるかも。。
とりあえず Kubernetes を頻繁に触っている方や、"自動化" が大好物な方はかなり楽しいので触ってみることをお勧めします。
本当は CustomResource の削除時に自動で削除とか、エコシステムのクライアント使ってリソース作成とかいろいろ書こうと思っていましたが長くなってしまうのでまたそのうち書きます。。
参考文献