Skip to content

필사 모드: Kubebuilder で初めての Operator を作る — プロジェクト作成からデプロイまで

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

前の記事で Operator パターンの原理を見ました。今回は実際に手を動かす番です。Kubebuilder を使って、シンプルですが本当に動く Operator を最初から作ってみます。作る対象は `Guestbook` という CRD で、ユーザーが望む replica 数とイメージを宣言すると、それに合った Deployment と Service を自動で作り調整する Operator です。

本記事の目標は「魔法なしで」全体の流れを理解することです。スキャフォールディングが何を作るのか、reconcile 関数に何を埋めるべきか、RBAC と CRD はどう生成されるのか、そしてローカルとクラスターでそれぞれどう動かすのかを最後まで追っていきます。controller-runtime v0.24.x が基準です。

事前準備

作業を始める前に、次のツールが必要です。

- **Go 1.26 以上**: Kubebuilder が生成するモジュールは最新の Go ツールチェーンを前提とします。

- **Kubebuilder CLI**: プロジェクトのスキャフォールディングとコード生成を担います。

- **kubectl**: クラスターと通信する標準 CLI です。

- **ローカルまたはリモートのクラスター**: kind や minikube で作ったローカルクラスターで十分です。

- **Docker または同等のコンテナビルダー**: コントローラーイメージをビルドしてデプロイするときに必要です。

インストールが終わったらバージョンを確認します。

go version

kubebuilder version

kubectl version --client

プロジェクト作成: init と create api

kubebuilder init

まず空のディレクトリでプロジェクトを初期化します。`--domain` は API グループの接尾辞になり、`--repo` は Go モジュールのパスです。

mkdir guestbook-operator && cd guestbook-operator

kubebuilder init --domain example.com --repo example.com/guestbook-operator

このコマンドは `main.go`、`Makefile`、`Dockerfile`、`config/` ディレクトリ(マニフェスト)、そして依存関係が埋められた `go.mod` を生成します。この時点でプロジェクトは何の API もない空のマネージャー(manager)に過ぎません。

kubebuilder create api

次に API とコントローラーを追加します。

kubebuilder create api --group webapp --version v1 --kind Guestbook

プロンプトで Resource(型定義)と Controller(reconcile ロジック)の両方を生成するか尋ねられます。両方 yes を選びます。すると次が作られます。

- `api/v1/guestbook_types.go` — Spec/Status などの API 型定義

- `internal/controller/guestbook_controller.go` — reconcile ロジックが入るコントローラー

- `config/crd/`、`config/rbac/`、`config/samples/` — 関連マニフェスト

プロジェクト構造を見てみる

スキャフォールディングが作ったディレクトリ構造を一目で理解すると、その後の作業がずっと楽になります。核心ディレクトリだけまとめると次のとおりです。

guestbook-operator/

api/v1/ # API 型定義 (Spec/Status、マーカー)

internal/controller/ # reconcile ロジック

config/

crd/ # 生成された CRD マニフェスト

rbac/ # 生成された RBAC (role.yaml など)

manager/ # コントローラーマネージャー Deployment

samples/ # サンプル CR

cmd/main.go # マネージャーの入口 (スキーム登録、マネージャー起動)

Makefile # build/run/deploy などの標準ターゲット

Dockerfile # コントローラーイメージのビルド

- **api/**: 私たちが直接手を入れる場所です。型とマーカーをここで定義します。

- **internal/controller/**: 同じく reconcile を埋める場所です。

- **config/**: ほとんど自動生成されるので手で直すことは少ないです。マーカーを変えて make manifests を回す方式で管理します。

- **cmd/main.go**: マネージャーを作り、スキームに型を登録し、コントローラーをマネージャーにつなぐブートストラップコードです。新しいコントローラーを追加すると、通常自動でここに登録コードが入ります。

この構造の核心的な哲学は「自分が書くコード(api、controller)」と「生成される成果物(config)」を明確に分けることです。生成物は直接直さず、常にソース(型とマーカー)を直したあと再生成する流れを守れば、一貫性が保たれます。

API 型の定義: Spec と Status

`api/v1/guestbook_types.go` を開いて、欲しいフィールドを定義します。Spec はユーザーが宣言する「望ましい状態」、Status はコントローラーが埋める「観測された状態」です。

package v1

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

)

// GuestbookSpec はユーザーが望む状態を定義します。

type GuestbookSpec struct {

// 望ましい replica 数です。

// +kubebuilder:validation:Minimum=1

// +kubebuilder:validation:Maximum=10

Replicas int32 `json:"replicas"`

// 使用するコンテナイメージです。

// +kubebuilder:validation:MinLength=1

Image string `json:"image"`

// サービスが公開するポートです。

// +kubebuilder:default=80

Port int32 `json:"port,omitempty"`

}

// GuestbookStatus は観測された状態を保持します。

type GuestbookStatus struct {

// 準備できた replica 数です。

ReadyReplicas int32 `json:"readyReplicas"`

// 状態条件の一覧です。

Conditions []metav1.Condition `json:"conditions,omitempty"`

}

// +kubebuilder:object:root=true

// +kubebuilder:subresource:status

// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.spec.replicas`

// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas`

// Guestbook は私たちのカスタムリソースです。

type Guestbook struct {

metav1.TypeMeta `json:",inline"`

metav1.ObjectMeta `json:"metadata,omitempty"`

Spec GuestbookSpec `json:"spec,omitempty"`

Status GuestbookStatus `json:"status,omitempty"`

}

// +kubebuilder:object:root=true

// GuestbookList は Guestbook の一覧です。

type GuestbookList struct {

metav1.TypeMeta `json:",inline"`

metav1.ListMeta `json:"metadata,omitempty"`

Items []Guestbook `json:"items"`

}

func init() {

SchemeBuilder.Register(&Guestbook{}, &GuestbookList{})

}

ここで `// +kubebuilder:` で始まる行を **マーカー(marker)** と呼びます。マーカーはコード生成ツールが読む指示文です。たとえば `validation:Minimum` は CRD の OpenAPI スキーマに検証ルールを追加し、`subresource:status` は status サブリソースを有効化し、`printcolumn` は `kubectl get` に追加列を表示します。

reconcile 実装の全コード

いよいよ核心の reconcile 関数を埋めます。`internal/controller/guestbook_controller.go` を次のように書きます。このコントローラーは Guestbook を読み、望ましい Deployment と Service を冪等に作り、最後に status を更新します。

package controller

"context"

appsv1 "k8s.io/api/apps/v1"

corev1 "k8s.io/api/core/v1"

"k8s.io/apimachinery/pkg/api/errors"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"k8s.io/apimachinery/pkg/intstr"

"k8s.io/apimachinery/pkg/runtime"

ctrl "sigs.k8s.io/controller-runtime"

"sigs.k8s.io/controller-runtime/pkg/client"

"sigs.k8s.io/controller-runtime/pkg/log"

webappv1 "example.com/guestbook-operator/api/v1"

)

type GuestbookReconciler struct {

client.Client

Scheme *runtime.Scheme

}

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

logger := log.FromContext(ctx)

// 1. 対象の Guestbook を読む。

var gb webappv1.Guestbook

if err := r.Get(ctx, req.NamespacedName, &gb); err != nil {

if errors.IsNotFound(err) {

// すでに削除済み。owner reference のおかげで子リソースは GC が片付ける。

return ctrl.Result{}, nil

}

return ctrl.Result{}, err

}

// 2. 望ましい Deployment を作り冪等に調整する。

desiredDeploy := r.buildDeployment(&gb)

if err := ctrl.SetControllerReference(&gb, desiredDeploy, r.Scheme); err != nil {

return ctrl.Result{}, err

}

var existingDeploy appsv1.Deployment

err := r.Get(ctx, client.ObjectKeyFromObject(desiredDeploy), &existingDeploy)

if errors.IsNotFound(err) {

logger.Info("Deployment を作成", "name", desiredDeploy.Name)

if err := r.Create(ctx, desiredDeploy); err != nil {

return ctrl.Result{}, err

}

} else if err != nil {

return ctrl.Result{}, err

} else {

// すでに存在するなら望ましい姿に合わせる。

existingDeploy.Spec = desiredDeploy.Spec

if err := r.Update(ctx, &existingDeploy); err != nil {

return ctrl.Result{}, err

}

}

// 3. 望ましい Service を調整する。

desiredSvc := r.buildService(&gb)

if err := ctrl.SetControllerReference(&gb, desiredSvc, r.Scheme); err != nil {

return ctrl.Result{}, err

}

var existingSvc corev1.Service

err = r.Get(ctx, client.ObjectKeyFromObject(desiredSvc), &existingSvc)

if errors.IsNotFound(err) {

if err := r.Create(ctx, desiredSvc); err != nil {

return ctrl.Result{}, err

}

} else if err != nil {

return ctrl.Result{}, err

}

// 4. status を更新する。

gb.Status.ReadyReplicas = existingDeploy.Status.ReadyReplicas

if err := r.Status().Update(ctx, &gb); err != nil {

return ctrl.Result{}, err

}

return ctrl.Result{}, nil

}

func (r *GuestbookReconciler) buildDeployment(gb *webappv1.Guestbook) *appsv1.Deployment {

labels := map[string]string{"app": gb.Name}

replicas := gb.Spec.Replicas

return &appsv1.Deployment{

ObjectMeta: metav1.ObjectMeta{

Name: gb.Name,

Namespace: gb.Namespace,

},

Spec: appsv1.DeploymentSpec{

Replicas: &replicas,

Selector: &metav1.LabelSelector{MatchLabels: labels},

Template: corev1.PodTemplateSpec{

ObjectMeta: metav1.ObjectMeta{Labels: labels},

Spec: corev1.PodSpec{

Containers: []corev1.Container{{

Name: "app",

Image: gb.Spec.Image,

Ports: []corev1.ContainerPort{{ContainerPort: gb.Spec.Port}},

}},

},

},

},

}

}

func (r *GuestbookReconciler) buildService(gb *webappv1.Guestbook) *corev1.Service {

labels := map[string]string{"app": gb.Name}

return &corev1.Service{

ObjectMeta: metav1.ObjectMeta{

Name: gb.Name,

Namespace: gb.Namespace,

},

Spec: corev1.ServiceSpec{

Selector: labels,

Ports: []corev1.ServicePort{{

Port: gb.Spec.Port,

TargetPort: intstr.FromInt32(gb.Spec.Port),

}},

},

}

}

func (r *GuestbookReconciler) SetupWithManager(mgr ctrl.Manager) error {

return ctrl.NewControllerManagedBy(mgr).

For(&webappv1.Guestbook{}).

Owns(&appsv1.Deployment{}).

Owns(&corev1.Service{}).

Complete(r)

}

`SetupWithManager` の `Owns` が重要です。こう登録すると、コントローラーが作った Deployment や Service が外部から変更されたときにも reconcile がトリガーされます。つまり誰かが手で replica を変えても、コントローラーがすぐに望ましい値に戻します。これが自己修復の原理です。

RBAC マーカー

コントローラーが Deployment と Service を作るには、それに見合った権限が必要です。Kubebuilder では 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=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete

// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete

これらのマーカーはコード生成時に `config/rbac/role.yaml` の ClusterRole に変換されます。RBAC の YAML を手で書く代わりに、コードの隣で権限の意図を宣言する方式なので、権限とコードが一緒に保たれます。必要以上の権限を与えないよう verbs を最小化するのが良いプラクティスです。

CRD 生成: make manifests

API 型とマーカーを書き終えたらマニフェストを生成します。

make manifests

make generate

- `make manifests` はマーカーを読んで `config/crd/` の CRD と `config/rbac/` の RBAC を生成します。

- `make generate` は `DeepCopy` のようなボイラープレートの Go コードを生成します。

生成された CRD をクラスターにインストールします。

make install

kubectl get crd guestbooks.webapp.example.com

ローカル実行対クラスターデプロイ

ローカル実行(開発ループに適する)

開発中はコントローラーをクラスターの外、つまり自分のノート PC で直接実行するのが最も速いです。kubeconfig を通じてクラスターに接続します。

make run

コードを直して再度 `make run` すれば即座に反映されるので、反復が速いです。ではサンプル CR を適用してみます。

kubectl apply -f config/samples/webapp_v1_guestbook.yaml

kubectl get guestbook

kubectl get deployment,service

サンプル CR の replica を 5 に変えて再適用すると、Deployment の replica が 5 に追従するのが見えます。手で Deployment の replica を 3 に変えると、コントローラーがすぐに元の値に戻します。

クラスターデプロイ(運用に適する)

実際の運用ではコントローラーもクラスター内のポッドとして動かします。イメージをビルドしてプッシュしたあとデプロイします。

make docker-build docker-push IMG=registry.example.com/guestbook-operator:v0.1.0

make deploy IMG=registry.example.com/guestbook-operator:v0.1.0

`make deploy` は RBAC、CRD、そしてコントローラー Deployment を一度に適用します。デプロイ後はコントローラーマネージャーのポッドのログで動作を確認します。

kubectl -n guestbook-operator-system get pods

kubectl -n guestbook-operator-system logs deploy/guestbook-operator-controller-manager

テスト: envtest の概念

Operator を本気で作るにはテストが必要です。Kubebuilder は **envtest** というツールを提供します。envtest は実際のクラスターなしで etcd と kube-apiserver のバイナリだけを立ち上げ、コントローラーが本物の API Server とやり取りするかのようにテストできるようにします。

make test

envtest の利点は fake client より現実的なことです。CRD 検証、status サブリソース、owner reference のような API Server レベルの動作を実際に検証できます。テストでは通常、CR を作ったあと、reconcile が望ましい Deployment を作ったかをポーリングで確認するパターンを使います。

よくある間違い

実際に初めて Operator を作るときによく遭遇する落とし穴をまとめます。

| 間違い | 症状 | 解決 |

| --- | --- | --- |

| RBAC マーカー漏れ | 「forbidden」エラーでリソース作成失敗 | 必要なグループ/リソース/verb マーカーを追加後 make manifests |

| owner reference 未設定 | CR を削除しても子リソースが残る | SetControllerReference を呼ぶ |

| 非冪等な reconcile | 「already exists」エラー、無限ループ | get 後なければ create、あれば update のパターン |

| status を Update で更新 | status 衝突または無視される | Status().Update を使う(サブリソース) |

| Owns 漏れ | 子リソース変更に反応しない | SetupWithManager に Owns を追加 |

| make generate 忘れ | DeepCopy 関連のコンパイルエラー | 型変更後に make generate を実行 |

特に冪等性と owner reference は前の記事で強調した 2 つの基本です。この 2 つさえきちんと守れば、ほとんどの初歩的なバグを避けられます。

2026 年の文脈

2026 年基準で Kubebuilder は Kubernetes 1.36 と Go 1.26 をサポートし、内部的に controller-runtime v0.24.x と controller-tools v0.21.x を使います。かつてスキャフォールディングに含まれていた kube-rbac-proxy サイドカーは削除され、代わりに controller-runtime の認証・認可ミドルウェア(WithAuthenticationAndAuthorization)でメトリクスエンドポイントを保護します。新規に作ったプロジェクトならこの方式が既定なので、追加コンテナなしでも安全なメトリクス公開が可能です。

おわりに

Kubebuilder で Operator を作る流れは、結局次の 5 ステップに要約されます。init でプロジェクトを作り、create api で型とコントローラーを追加し、Spec/Status とマーカーで API を定義し、reconcile に冪等な調整ロジックを埋め、make manifests/install/run で動かしてみることです。

最初はスキャフォールディングが生み出すファイルが多くて途方に暮れて見えますが、核心は結局 reconcile 関数 1 つです。「望ましい状態を作り、実際の状態と比較して冪等に合わせる」という一文をコードに移すことが Operator 開発の本質です。次の記事では、この reconcile ループを性能とエラー処理の観点からさらに深く掘り下げます。

参考資料

- [Kubebuilder Book — Quick Start](https://book.kubebuilder.io/quick-start.html)

- [Kubebuilder Book — Tutorial](https://book.kubebuilder.io/cronjob-tutorial/cronjob-tutorial.html)

- [Kubebuilder マーカーリファレンス](https://book.kubebuilder.io/reference/markers.html)

- [controller-runtime (pkg.go.dev)](https://pkg.go.dev/sigs.k8s.io/controller-runtime)

- [Operator SDK ドキュメント](https://sdk.operatorframework.io/)

- [kubernetes-sigs/kubebuilder (GitHub)](https://github.com/kubernetes-sigs/kubebuilder)

- [kubernetes-sigs/controller-runtime (GitHub)](https://github.com/kubernetes-sigs/controller-runtime)

- [envtest 利用ガイド](https://book.kubebuilder.io/reference/envtest.html)

- [Operator パターン(Kubernetes 公式ドキュメント)](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)

현재 단락 (1/256)

前の記事で Operator パターンの原理を見ました。今回は実際に手を動かす番です。Kubebuilder を使って、シンプルですが本当に動く Operator を最初から作ってみます。作る対象は ...

작성 글자: 0원문 글자: 11,245작성 단락: 0/256