スクエニ ITエンジニア ブログ

Kubernetes の custom controller を実装してみる

ホシイです。

拡張性が高い Kubernetes。様々な機能・ソフトウェアが、Kubernetes が備える機能拡張の仕組みである operator や controller で提供されるのを見るようになりました。GitLab Operator のような複雑な application や、MySQL Operator のように database 機能を提供するものもあります。より単純な機能としては、Pod などを監視しつつ条件に応じて scaling や backup するなどの機能が考えられます。

Kubernetes の Operator pattern に説明されている Operators は、Kubernetes 自体に手をいれることなく controller や custom resource を使い、cluster での deploy や workload 管理を拡張するものとされています。

この強力な機能が活用できると非常に心強そうですが、何しろ敷居が高そうなのもたしかです。今日はその敷居を少しでも下げるため、custom controller の実装を体験してみたいと思います。

事前準備: Dev Container と kind

controller を開発するにあたり、それを実行する Kubernetes cluster が必要です。ここでは、kind を使います。kind は Kubernetes 自体の開発にも使われているもので、軽量な Kubernetes cluster を実行できます。

もしすでに他の Kubernetes cluster 環境をお持ちであれば、そちらを利用することもできると思います。ご自身の環境に合わせてセットアップしてください。

今回は、開発環境構築に Visual Studio Code の Dev Container 機能を使用します。Dev Container は、container を使用して開発環境を構築する強力な機能です。開発環境構築をコード化して共有したり、開発環境を隔離してホストをきれいに保つことができます。このブログでは他にも Dev Container に関する記事 をいくつか書いているので、ぜひ見てみてください。

.devcontainer/devcontainer.json に以下のように feature を入れるだけで kind が使えるようになります。

{
	"name": "kubebuilder-test",
	"build": {
		"dockerfile": "Dockerfile"
	},
	"features": {
		"ghcr.io/devcontainers/features/docker-in-docker:2": {},
		"ghcr.io/devcontainers-contrib/features/kind:1": {}
	}
}

上記では docker-in-docker を使っているので、host 側に Docker が必要です。他に類似のものとして Docker を利用した開発時に便利な docker-outside-of-docker もありますが、そちらだと cluster endpoint が host 側に開いてしまって不便なため、 docker-in-docker を使うようにしています。

.devcontainers/Dockerfile として以下を保存します。

FROM ubuntu:22.04
SHELL [ "/bin/bash", "-c" ]

RUN apt update && DEBIAN_FRONTEND=noninteractive apt -y install --no-install-recommends \
        sudo git curl openssh-client ca-certificates \
        golang-1.21 build-essential && \
    echo "done."

ENV PATH="${PATH}:/usr/lib/go-1.21/bin"

RUN groupadd devs && \
    useradd hoshy -g devs -m -s /bin/bash && \
    echo "hoshy ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
    # https://book.kubebuilder.io/quick-start.html#installation
    curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)" && \
    chmod +x kubebuilder && mv kubebuilder /usr/local/bin/ && \
    # kubectl https://kubernetes.io/docs/tasks/tools/
    curl -o /usr/local/bin/kubectl -L "https://dl.k8s.io/release/$(curl -LS https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
    chmod +x /usr/local/bin/kubectl && \
    echo "done."

# 以降一般 user
USER hoshy

RUN echo && \
    mkdir -p $HOME/.kube && \
    echo "done."

Dev Container を使用しない場合は、上記 Dockerfile などを参考に環境を構築してください。

cluster の準備

VS Code 上で上記の準備ができたら、Dev Container を起動します。
起動したら Terminal を開き、以下のようにして kind cluster をつくっておきます。

$ kind create cluster
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:39523
CoreDNS is running at https://127.0.0.1:39523/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
$ kubectl config get-contexts 
CURRENT   NAME        CLUSTER     AUTHINFO    NAMESPACE
*         kind-kind   kind-kind   kind-kind

Kubernetes Controller 開発 framework の種類

Controller を実装する際に利用できる SDK にはいろいろ種類があるようです。

  • Kubebuilder
    • Go 言語で Kubernetes API を開発する framework。controller-runtime、controller-tools を用い、Kubernetes の標準的な技術に基づいている。
  • Operator SDK
    • Go に加えて Ansible や Helm による高レベルでの開発方法が提供されている。Kubebuilder を library として利用している。
  • Metacontroller
    • custom controller を実装する Kubernetes add-on。Python や JavaScript も利用できる。
  • client-go
    • Kubernetes の client library。controller 開発用の utility も提供されている。

他にもありそうですが、今回検討したのはこれくらいです。Controller の実装はいずれでも可能なようですが、今回は web などでも使用例・情報が多い Kubebuilder を使用します。

Kubebuilder を使って controller を実装する

Quick Start に沿ってやってみます。
以下、実行するコマンドを抜粋しますが、詳細は上記ページを参照してください。

まずは kubebuilder コマンドを使用して、実装する controller (を含む) のひな型を生成します。

mkdir guestbook
cd $_
kubebuilder init --domain example.com --repo example.com/guestbook
kubebuilder create api --group webapp --version v1 --kind Guestbook --resource --controller

結構な数の directory、file が生成されます。この後迷わないように軽く眺めておくとよさそうです。

ここで、Quick Start ページに記述されているように guestbook/api/v1/guestbook_types.go を編集しておきます。

ここから Kubernetes cluster (とそこへの接続) が必要なので、記事冒頭に書いた kind cluster の用意をしておいてください。

CRD を install します。

$ make install 
  :
customresourcedefinition.apiextensions.k8s.io/guestbooks.webapp.example.com created
$ kubectl get crds
NAME                              CREATED AT
guestbooks.webapp.example.com   2024-05-08T06:32:42Z

Makefile を見ると、make install$(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - していることがわかります。

実装した controller を手元で実行します。

make run 

サンプルの CRD を apply し、guestbook (custom resource) を生成します。

$ kubectl apply -k config/samples/
$ kubectl get guestbooks
NAME               AGE
guestbook-sample   9s

ここまでで、実装から実行してみるまでの流れは完了です。
あとは、controller に求める機能を追加実装していきます。

開発が完了したら、cluster へ deploy するために以下のようにして container image を build します。引数の IMG= で image name:tag を指定できます。指定を省略すると controller:latest になるようです。

$ make docker-build IMG=test/controller:test
$ docker image ls
REPOSITORY        TAG       IMAGE ID       CREATED         SIZE
test/controller   test      aaf29ab9250a   4 seconds ago   52.7MB
kindest/node      <none>    1cf551538f7d   2 months ago    960MB

(二行目に出ている kindest/node は kind の node 用の image です)

image が build できたら、以下のようにして cluster に deploy できます。

$ make deploy IMG=test/controller:test
/workspaces/kube-build-test/guestbook/bin/controller-gen-v0.14.0 rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
cd config/manager && /workspaces/kube-build-test/guestbook/bin/kustomize-v5.3.0 edit set image controller=test/controller:test
/workspaces/kube-build-test/guestbook/bin/kustomize-v5.3.0 build config/default | kubectl apply -f -
namespace/guestbook-system created
customresourcedefinition.apiextensions.k8s.io/guestbooks.webapp.example.com unchanged
serviceaccount/guestbook-controller-manager created
role.rbac.authorization.k8s.io/guestbook-leader-election-role created
clusterrole.rbac.authorization.k8s.io/guestbook-guestbook-editor-role created
clusterrole.rbac.authorization.k8s.io/guestbook-guestbook-viewer-role created
clusterrole.rbac.authorization.k8s.io/guestbook-manager-role created
clusterrole.rbac.authorization.k8s.io/guestbook-metrics-reader created
clusterrole.rbac.authorization.k8s.io/guestbook-proxy-role created
rolebinding.rbac.authorization.k8s.io/guestbook-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/guestbook-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/guestbook-proxy-rolebinding created
service/guestbook-controller-manager-metrics-service created
deployment.apps/guestbook-controller-manager created

controller の deployment 以外に、service や role/rolebinding の類も生成している様子が窺えます。
deploy したものを削除するにはおなじように、make undeploy ... を実行します。

実装を読む

どのように実装されているか、すこしコードを読んでみましょう。controller の実装は guestbook/internal/controller/guestbook_controller.go にあります。

以下の部分で、Guestbook resource の生成を watch するように設定しています。

func (r *GuestbookReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&webappv1.Guestbook{}).
		Complete(r)
}

controller が resource を監視できるように、以下のように RBAC marker comment が書かれており、これにより RBAC 設定が生成されます。

//+kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks/finalizers,verbs=update

resouce 状態が変更されたときに呼び出される Reconcile メソッドに処理を追加してみましょう。

たとえば以下のように書くと、Guestbook resource に変更があり、かつその Guestbook resource が存在しなかったとき (要するに delete されたとき) に log が出力されます。create されたときにも Reconcile() は呼び出されますが、if 条件にあたらないので log は出力されません。create 時にも log 出力を追加する方法は簡単に想像できますね。以下、TODO コメント行以降が追加した部分です。

func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)

	// TODO(user): your logic here
	var gb webappv1.Guestbook
	if err := r.Get(ctx, req.NamespacedName, &gb); err != nil {
		if errors.IsNotFound(err) {
			log.Log.Info("Guestbook not found", "name", req.NamespacedName.Name)
			return ctrl.Result{}, nil
		}
	}

この変更を入れた controller を make run で実行しておき、別の shell から kubectl apply -k config/samples/kubectl delete -k config/samples/ を繰り返すことで log 出力が確認できます。

複数の resource を監視するときは以下のようにも書けます。 Owns() は、指定された resource type の event を watch し、それらの変更があったときに reconcile を trigger します。

// (Microsoft Copilot in Bing により生成)
func (r *MyReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&corev1.ConfigMap{}). // 主に監視するリソース
        Owns(&corev1.Pod{}).      // 監視する従属リソース
        Owns(&corev1.Service{}).  // 別の監視する従属リソース
        Complete(r)
}

また、 Watches() を使用すると、custom event handler や predicate を指定することもできます。これにより、特定の条件下でのみ event を監視したり、外部の custom resource を監視してそれを契機とした処理を追加したりできます。

// (Microsoft Copilot in Bing により生成)
import (
    "sigs.k8s.io/controller-runtime/pkg/source"
    "sigs.k8s.io/controller-runtime/pkg/event"
    "sigs.k8s.io/controller-runtime/pkg/predicate"
)

// ...

func (r *MyReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&corev1.ConfigMap{}).
        Watches(
            &source.Kind{Type: &appsv1.Deployment{}},
            &handler.EnqueueRequestForObject{},
            builder.WithPredicates(predicate.Funcs{
                CreateFunc: func(e event.CreateEvent) bool {
                    // 作成イベントのフィルタリングロジック
                    return true // または false
                },
                UpdateFunc: func(e event.UpdateEvent) bool {
                    // 更新イベントのフィルタリングロジック
                    return true // または false
                },
                // 他のプレディケート関数...
            }),
        ).
        Complete(r)
}

さらには、こちらの Tutorial を進めると具体的な実装や defaulting/validating webhook などの他の要素についても学べます。

あとは要件次第で肉付けしていくことで、様々な機能が実装できそうです。build を含む iteration もじゅうぶんに早くて、開発体験はよいと感じました。

片付け

以下のようにして後片付けをしておきます。

make undeploy
make uninstall

kind cluster も削除 (停止) するときは以下のようにします。

kind delete cluster

これらも非常に早く完了します。よくわからなくなったときの環境リセットもかんたんにできるのが非常によいですね。

まとめ

Kubebuilder を使用して、Kubernetes controller の実装を体験してみました。kubebuilder には様々な開発段階をサポートする機能が詰まっていて、さすが開発が盛んなソフトウェアという印象を受けました。

custom controller の実装は、既存の機能で満たせない要件があれば検討することになりますが、運用や将来のメンテナンスを考えると安易に選択できるケースは少なく、慎重に進める必要があります。Kubernetes 自体の開発はまだまだ盛んで、大きめの変更が入ることもあります。upgrade への追従は具体的にどういった作業になるのか、どれくらいたいへんなのかを予測するのは難しそうです。

かたや、こうしていつもと違った角度から触れることで、Kubernetes 自体の理解をより深めることができそうです。controller の実装までは必要になることがなかったとしても、きっとこの知識・経験が活きることがあるでしょう。

Troubleshooting

試しているときにあたった問題とその解決をいくつか書いておきます。

go version が古い (と言われる)

Kubebuilder には記事作成時点で 1.21+ が必要とありますが、使用した Ubuntu 22.04 では apt install golang とすると 1.18 が入るようです。そちらは削除し、apt で golang-1.21 を入れ、以下のように $PATH に追加しました。

export PATH=$PATH:/usr/lib/go-1.21/bin

cross-compile 関係のエラー

go: cannot install cross-compiled binaries when GOBIN is set

$GOARCH が設定されてたりして arch がややこしいことになっているかもしれません。今回は試行錯誤のときに設定したものが原因だったので、unset して解決しました。

kubebuilder init や create api をやりなおすとき

kubebuilder 実行に必要なコマンドなどが揃っていない (install が不足している) 時、中途半端に file が生成されて途中で止まってしまうことがあります。手で変更する前であれば生成されたものをすべて削除して再実行しましょう。kubebuilder に --force をつけて実行すると既存のものを上書きするようです。いずれの場合も、保存していない変更が失われないように注意しましょう。

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!