なぜBashスクリプトは限界を迎えるのか
YAMLファイルのデプロイは簡単です。しかし、本番環境で複雑なアプリケーションの健全性を維持するのは一筋縄ではいきません。多くのチームが、データベースのスキーママイグレーション、シャードの再バランシング、証明書の更新といった運用ロジックを、BashスクリプトやJenkinsパイプライン、あるいは手動のkubectl操作の継ぎ接ぎで処理しているのをよく目にします。
最大の悩みは、スクリプトが「命令形(imperative)」であることです。これらは一連の手順を実行して終了します。もしネットワークの一時的な瞬断で10ステップ中4ステップ目でスクリプトが停止してしまったら、中途半端なデプロイ状態のまま午前2時に障害対応の電話を受けることになります。スクリプトには「記憶」がありません。実行終了から5分後に環境が変化しても、それを修正する仕組みがないのです。これが「運用債務」を生み、エンジニアは新機能の開発ではなく、自動化ツールの監視に追われることになります。
標準コントローラーにおける「知能」の欠如
Kubernetesは**制御ループ(Control Loop)**に依存しています。DeploymentやStatefulSet用の標準コントローラーは、現在の状態を常に監視し、期待される状態(Desired State)と一致するように24時間稼働しています。しかし、これらのコントローラーは汎用的です。Podが「実行中(Running)」であるべきことは知っていますが、特定のアプリがトラフィックを受け入れる前に30秒のウォームアップ期間が必要であることや、特定のAPI登録呼び出しが必要であることまでは把握していません。
標準のリソースには、特定の技術スタック特有ের「ドメイン知識」が欠けています。このギャップを外部スクリプトで埋めようとすると、Kubernetesの持つ宣言的な魔法が失われてしまいます。クラスターに対して、私たちのアプリケーションをネイティブなリソースと同じように管理する方法を「教える」必要があるのです。
解決策:Kubernetes Operators
Operatorとは、本質的には「人間の運用知識をソフトウェアとしてパッケージ化したもの」です。これは、**カスタムリソース定義(CRD)**と**カスタムコントローラー**を組み合わせたものです。スクリプトを実行する代わりに、YAMLファイルで「期待される状態」を定義します。するとOperatorが、その状態を実現するために継続的に動作します。
これを効率的に構築するために、controller-runtimeライブラリを使用します。これは**Operator SDK**や**Kubebuilder**を支えるエンジンそのものです。API通信、リソースのキャッシュ、イベントキューイングといった退屈な配線作業を処理してくれるため、開発者はビジネスロジックに集中できます。私の経験では、このモデルに切り替えることで、復旧時間(MTTR)を手動介入による15分から、自動修復による500ミリ秒未満に短縮できました。
ハンズオン:初めてのOperator構築
AppServiceというカスタムリソースのための基本的なOperatorを構築してみましょう。このOperatorは、すべてのAppServiceが自身のDeploymentとConfigMapを自動的に維持するように制御します。
1. プロジェクトの初期化
GoとOperator SDKが必要です。まず、新しいディレクトリを作成し、雛形(スキャフォールディング)を初期化します:
mkdir app-operator
cd app-operator
operator-sdk init --domain itfromzero.com --repo github.com/itfromzero/app-operator
2. APIの作成
次に、カスタムリソースを定義します。これにより、Kubernetes APIサーバーに新しいオブジェクトタイプを教えます。
operator-sdk create api --group apps --version v1alpha1 --kind AppService --resource --controller
3. 期待される状態の定義
api/v1alpha1/appservice_types.goを開きます。リソースが期待するフィールド(レプリカ数や設定メッセージなど)をKubernetesに伝える必要があります。
type AppServiceSpec struct {
Size int32 `json:"size"`
Message string `json:"message"`
}
type AppServiceStatus struct {
Nodes []string `json:"nodes"`
}
構造体を更新したら、make generateとmake manifestsを実行します。これにより、自動生成されたコードとCRDを定義するYAMLファイルが更新されます。
4. リコンサイル(調整)ループの実装
internal/controller/appservice_controller.goにあるReconcile関数こそが、魔法の舞台です。これはAppServiceに変更があるたびにトリガーされます。ロジックは「**観察(Observe) -> 分析(Analyze) -> 反応(React)**」と考えると分かりやすいでしょう。
func (r *AppServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// 1. AppServiceインスタンスを取得
appService := &appsv1alpha1.AppService{}
err := r.Get(ctx, req.NamespacedName, appService)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. Deploymentが存在するか確認し、なければ作成
found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: appService.Name, Namespace: appService.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
dep := r.deploymentForAppService(appService)
log.Info("新しいDeploymentを作成中", "Namespace", dep.Namespace, "Name", dep.Name)
err = r.Create(ctx, dep)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}
// 3. デプロイメントのサイズをスペックと同期
size := appService.Spec.Size
if *found.Spec.Replicas != size {
found.Spec.Replicas = &size
err = r.Update(ctx, found)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
5. デプロイとテスト
ローカルテストには**Kind**(Kubernetes in Docker)を使用します。以下のコマンドでCRDをインストールし、コントローラーを実行します:
make install
make run
別のターミナルで、サンプルリソースを適用します:
apiVersion: apps.itfromzero.com/v1alpha1
kind: AppService
metadata:
name: example-app
spec:
size: 3
message: "Operatorからのこんにちは"
kubectl applyを実行したら、ログを確認してください。コントローラーが即座に新しいリソースを検出し、Podを立ち上げます。
自己修復のメリット
なぜこれほどの手間をかけるのでしょうか? 答えは**自己修復(Self-Healing)**にあります。もしジュニアエンジニアが誤ってDeploymentを削除してしまっても、リコンサイルループが即座にそれを検知します。欠落したリソースを観察し、不一致を分析して、数ミリ秒以内に再作成します。CronベースのBashスクリプトでもいつかは修正されるかもしれませんが、Operatorはリアルタイムの復元力を提供します。
Goを使用することでenvtestや堅牢なユニットテストフレームワークを利用できます。APIのタイムアウトやネットワーク分断のような複雑な失敗をシミュレートし、クラスターにデプロイする前にロジックが本番環境に対応できるかを確認できます。
まとめ
手動スクリプトからカスタムOperatorへの移行は、あらゆるDevOpsチームにとって大きな節目となります。それは、ベテランエンジニアの頭の中に閉じ込められた「暗黙知」を、信頼性の高い、バージョン管理されたコードへと変換します。GoやKubernetes APIの学習には労力が必要ですが、それによって得られる安定性と拡張性は、学習に費やす時間の価値が十分にあります。

