Kubernetes Operator - 講義
概要
Kubernetes Operatorは、Kubernetesのカスタムリソース定義(CRD)とコントローラーを組み合わせて、複雑なアプリケーションのライフサイクルを自動化する強力なパターンです。この技術は、クラウドネイティブエコシステムにおいて、インフラストラクチャのコード化(Infrastructure as Code)を次のレベルに引き上げ、運用の自動化と一貫性を実現します。
Operatorパターンは、Kubernetesの「宣言的な設定」という哲学を拡張し、アプリケーション固有の知識をコードとして表現します。これにより、データベースのバックアップ、証明書の更新、スケーリング戦略の実装など、従来は手動で行っていた複雑な運用タスクを自動化できます。
学習目標
この講義を通じて、以下のスキルを習得します:
- Kubernetes基礎の深い理解 - Pod, Deployment, Service, ConfigMap, Secretなどの基本リソースと、それらの相互作用
- カスタムリソース定義(CRD) - 独自のKubernetesリソースの設計と実装
- コントローラーパターン - Reconciliation loopの実装と、宣言的な状態管理
- Operator SDK/Kubebuilder - 効率的なOperator開発のためのツールチェーン
- 本番運用ノウハウ - 監視、ロギング、障害対応、パフォーマンスチューニング
- テスト駆動開発(TDD) - Envtest, Kind, E2Eテストを用いた品質保証
- GitOps統合 - CI/CDパイプラインとの連携、バージョン管理戦略
- Go言語: 中級レベル(Goroutine, Channel, Interface, Context の理解)
- Kubernetes: 基本概念(Pod, Deployment, Service, Namespace, Label/Selector)
- Docker/コンテナ: イメージビルド、レジストリ操作、基本的なトラブルシューティング
- YAML: Kubernetes マニフェストの読み書き
- Git: バージョン管理の基本操作
- 複雑な運用ロジックの自動化不足: データベースのマスター選出、バックアップ、リストアなどの操作を、標準的なKubernetesリソースだけでは表現できなかった
- アプリケーション固有の知識の欠如: Kubernetesのコントローラーは汎用的で、特定のアプリケーションのベストプラクティスを理解していなかった
- Day 2 Operations(運用フェーズ)の自動化: デプロイだけでなく、アップグレード、スケーリング、障害復旧などの継続的な運用タスクを自動化する必要があった
前提知識
この講義を最大限に活用するために、以下の知識を前提とします:
---
1. 技術の歴史と発展
1.1 Kubernetes Operatorパターンの誕生
Kubernetes Operatorパターンは、2016年にCoreOS(現Red Hat)によって正式に提唱されました。当時、Kubernetesはステートレスアプリケーションの管理には優れていましたが、データベースやメッセージキューなどのステートフルアプリケーションの運用には課題がありました。
背景にあった課題:
1.2 etcd-operator: 最初の実用的Operator
CoreOSは、分散Key-Valueストアであるetcdの運用を自動化するetcd-operatorを最初の実装例として公開しました。これにより、以下のような複雑な運用タスクが自動化されました:
apiVersion: "etcd.database.coreos.com/v1beta2"
kind: "EtcdCluster"
metadata:
name: "example-etcd-cluster"
spec:
size: 3
version: "3.2.13"
このシンプルな宣言だけで、Operatorが以下を自動実行:
1.3 技術の進化と標準化
2017-2018年: ツールチェーンの発展
controller-runtimeライブラリの成熟化2019年以降: エコシステムの拡大
2020年代: クラウドネイティブの標準パターンへ
Kubernetes Operatorは、CNCF(Cloud Native Computing Foundation)エコシステムの中核技術として確立され、以下のような広範な用途で使われるようになりました:
- データベース管理(PostgreSQL, MySQL, MongoDB)
- メッセージング(Kafka, RabbitMQ)
- 監視(Prometheus, Grafana)
- セキュリティ(cert-manager, Vault)
- マルチクラスター管理(Crossplane, ArgoCD)
---
2. 実社会での活用事例
2.1 Prometheus Operator
提供元: CoreOS/Red Hat 用途: Kubernetes上でのPrometheus監視スタックの管理
実装の特徴:
apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus
spec:
replicas: 2
serviceMonitorSelector:
matchLabels:
team: frontend
企業での活用:
- Spotify: 数千のマイクロサービスの監視を、Prometheus Operatorで一元管理
- GitLab: SaaSプラットフォームの全体的な可観測性(Observability)を実現
- Adobe: マルチテナント環境での監視設定の自動化
- ServiceMonitor CRD: サービスディスカバリーの自動化
- AlertManager統合: アラートルールの宣言的管理
- 動的な設定リロード: Prometheusの再起動なしで設定変更
技術的メリット:
2.2 cert-manager
提供元: Jetstack(現Venafi) 用途: TLS証明書の自動発行・更新
実装の特徴:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com
spec:
secretName: example-com-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
企業での活用:
- Shopify: 数万のマーチャント向けカスタムドメインのTLS証明書を自動管理
- Zalando: マイクロサービス間mTLS(mutual TLS)の完全自動化
- Bloomberg: 内部CA統合による企業証明書管理
- Let's Encrypt統合: ACME protocolによる無料証明書の自動取得
- 証明書更新の自動化: 有効期限30日前に自動更新
- 複数CA対応: Vault, Venafi, AWS PCAなど
技術的メリット:
2.3 ArgoCD Application Controller
提供元: Intuit(CNCF卒業プロジェクト) 用途: GitOpsによる継続的デリバリー
実装の特徴:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
spec:
source:
repoURL: https://github.com/argoproj/argocd-example-apps.git
targetRevision: HEAD
path: guestbook
destination:
server: https://kubernetes.default.svc
namespace: guestbook
syncPolicy:
automated:
prune: true
selfHeal: true
企業での活用:
- Intuit: 数千のマイクロサービスのデプロイ自動化
- Red Hat: OpenShift GitOpsの基盤技術
- Tesla: 車両向けOTAアップデートのインフラストラクチャ管理
- Git as Single Source of Truth: すべての変更がGitで追跡可能
- 自動同期とセルフヒーリング: ドリフト検出と自動修復
- マルチクラスター対応: 単一のArgoCDで複数クラスター管理
技術的メリット:
2.4 Crossplane
提供元: Upbound(CNCF incubatingプロジェクト) 用途: クラウドリソースのKubernetes API化
実装の特徴:
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
metadata:
name: production-db
spec:
forProvider:
region: us-west-2
dbInstanceClass: db.t3.medium
engine: postgres
engineVersion: "13.7"
masterUsername: adminuser
allocatedStorage: 100
writeConnectionSecretToRef:
name: db-conn-secret
企業での活用:
- GitLab: マルチクラウド環境のインフラストラクチャ管理統一
- Upbound: 自社のManaged Crossplane製品の基盤
- VSHN: スイスのマネージドKubernetesプロバイダーでの全自動インフラ
- Composition: 複数のクラウドリソースを抽象化した独自APIの作成
- マルチクラウド対応: AWS, Azure, GCP, Alibabaなど
- Infrastructure as Code: Terraform的な機能をKubernetes APIで提供
技術的メリット:
2.5 Strimzi Kafka Operator
提供元: Red Hat/CNCF Sandbox 用途: Kubernetes上でのApache Kafka運用
実装の特徴:
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: my-cluster
spec:
kafka:
version: 3.4.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
- name: tls
port: 9093
type: internal
tls: true
storage:
type: persistent-claim
size: 100Gi
zookeeper:
replicas: 3
storage:
type: persistent-claim
size: 10Gi
企業での活用:
- Aiven: マネージドKafkaサービスの基盤技術
- Red Hat: OpenShift Streamsでの採用
- 欧州の金融機関: コンプライアンス要件を満たすKafka運用
- 完全なKafkaライフサイクル管理: ブローカー、Zookeeper、Kafka Connectなど
- セキュリティ設定の自動化: TLS、SASL、OAuth統合
- ローリングアップデート: ダウンタイムなしのバージョンアップ
技術的メリット:
2.6 その他の注目すべきOperator
| Operator | 提供元 | 用途 | 採用企業例 |
|---|---|---|---|
| **Elasticsearch Operator** | Elastic | Elasticsearch/Kibanaクラスター管理 | Walmart, Tinder |
| **PostgreSQL Operator** | Zalando | PostgreSQL HA構成の自動化 | Zalando, HelloFresh |
| **Vault Operator** | Banzai Cloud | HashiCorp Vault管理 | 金融機関、ヘルスケア企業 |
| **Istio Operator** | Google/IBM | サービスメッシュ管理 | eBay, AutoTrader |
| **Velero** | VMware | バックアップとディザスタリカバリ | Adobe, GitHub |
---
3. なぜ学ぶ価値があるか
3.1 クラウドネイティブ時代の必須スキル
現代のソフトウェア開発において、Kubernetes Operatorは以下の理由で必須スキルとなっています:
1. インフラストラクチャのプログラマブル化
従来の「インフラチームが手動で構築」から「開発者が宣言的に記述」へのパラダイムシフト:
// 従来: シェルスクリプトで手動構築
./setup-database.sh --replicas=3 --backup-enabled
// Operator時代: 宣言的な定義
apiVersion: db.example.com/v1
kind: Database
spec:
replicas: 3
backupEnabled: true
2. マルチクラウド・ハイブリッドクラウド戦略
単一のKubernetes APIで、あらゆるクラウドプロバイダーのリソースを管理できるスキルは、企業のベンダーロックイン回避戦略において重要です。
3. SREとDevOpsの融合
Operatorは、Site Reliability Engineering(SRE)のプラクティスをコードとして表現します:
3.2 SREとしてのキャリアパス
Kubernetes Operatorのスキルは、以下のようなキャリアパスを開きます:
レベル1: Platform Engineer(年収800万〜1200万円)
- 社内向けPaaS(Platform as a Service)の構築
- 開発チームのセルフサービス化支援
- Operatorを用いた標準化されたデプロイパイプライン構築
レベル2: Senior SRE(年収1200万〜1800万円)
- 大規模Kubernetesクラスターの設計と運用
- カスタムOperatorの開発とメンテナンス
- インシデント対応の自動化とポストモーテム文化の確立
レベル3: Staff/Principal Engineer(年収1800万〜2500万円+)
- クラウドネイティブアーキテクチャの技術戦略策定
- OSSへの貢献(Kubernetes, CNCF projects)
- 組織横断的な技術標準の策定
レベル4: Consulting/Freelance(プロジェクトベース)
- 大手企業のKubernetes移行支援(月額150万〜300万円)
- Operator開発のトレーニング提供
- アーキテクチャレビューとベストプラクティス策定
- 分散システム設計: Consensus algorithm, CAP定理, Eventual consistency
- Go言語の実践的活用: Concurrency pattern, Testing, Performance tuning
- Kubernetes内部機構の深い理解: API Server, Controller Manager, Scheduler
- クラウドネイティブパターン: Circuit breaker, Retry, Backoff, Rate limiting
3.3 技術的スキルの横展開
Operatorの学習を通じて得られる副次的スキル:
---
4. 市場価値と求人トレンド
4.1 年収相場(日本市場、2024年時点)
ジュニアレベル(1-3年経験)
- Kubernetes基礎 + Operator使用経験: 600万〜850万円
- 簡単なOperator実装経験あり: 700万〜950万円
ミドルレベル(3-6年経験)
- Operator開発・運用経験: 900万〜1400万円
- 複数のOperator本番運用: 1000万〜1600万円
シニアレベル(6年以上)
- カスタムOperator設計・実装: 1300万〜2000万円
- OSSコントリビューション実績: 1500万〜2500万円
外資系企業・グローバルリモート
- Staff Engineer相当: 2000万〜3500万円
- Principal Engineer相当: 3000万〜5000万円+ストックオプション
- 需要の高い技術スタック
4.2 求人市場のトレンド
2023年の求人分析(Indeed, LinkedIn調査):
- 業界別需要
- 地理的な需要
4.3 スキルセットの組み合わせ価値
以下の組み合わせは特に市場価値が高い:
| 技術スタック | 市場価値 | 典型的な年収レンジ |
|---|---|---|
| Kubernetes Operator + AWS EKS | ★★★★★ | 1000万〜2000万円 |
| Operator + ArgoCD + Terraform | ★★★★★ | 1100万〜2200万円 |
| Operator + Istio + Observability | ★★★★☆ | 1000万〜1800万円 |
| Operator + Crossplane + Multi-cloud | ★★★★★ | 1200万〜2500万円 |
| Operator + Security (OPA, Vault) | ★★★★★ | 1100万〜2000万円 |
---
5. 実務での運用考慮点
5.1 本番環境でのOperator運用
デプロイ戦略:
- 段階的ロールアウト
# Development環境で2週間
# Staging環境で1週間
# Production環境へCanary deployment
- バージョン管理
- High Availability構成
spec:
replicas: 3 # 複数レプリカでの冗長化
leaderElection:
enabled: true # リーダー選出の有効化
5.2 障害対応と復旧手順
典型的な障害シナリオと対処法:
シナリオ1: Reconciliation loopの無限ループ
// 問題のあるコード
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 毎回Statusを更新 → 無限ループ発生
obj.Status.LastUpdate = time.Now()
r.Status().Update(ctx, obj)
return ctrl.Result{}, nil
}
// 修正後
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if obj.Status.Condition != "Ready" {
obj.Status.Condition = "Ready"
obj.Status.LastUpdate = time.Now()
return ctrl.Result{}, r.Status().Update(ctx, obj)
}
return ctrl.Result{}, nil // 変更なければ何もしない
}
シナリオ2: リソースリーク
// Owner Referenceを設定し忘れると、親リソース削除時に子が残る
func (r *Reconciler) createDeployment(ctx context.Context, app *MyApp) error {
dep := &appsv1.Deployment{...}
// これを忘れるとリソースリーク
if err := ctrl.SetControllerReference(app, dep, r.Scheme); err != nil {
return err
}
return r.Create(ctx, dep)
}
シナリオ3: APIサーバーの過負荷
// 悪い実装: 頻繁なリスト操作
func (r *Reconciler) Reconcile(...) (ctrl.Result, error) {
var podList corev1.PodList
r.List(ctx, &podList) // 毎回すべてのPodをリスト
// ...
return ctrl.Result{RequeueAfter: 1 * time.Second}, nil // 1秒ごとに実行
}
// 良い実装: Watchとキャッシュを活用
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&MyApp{}).
Owns(&corev1.Pod{}). // Watchを設定
Complete(r)
}
5.3 監視とアラート
重要なメトリクス:
- Reconciliation metrics
controller_runtime_reconcile_total: Reconcile実行回数
- controller_runtime_reconcile_errors_total: エラー回数
- controller_runtime_reconcile_time_seconds: 処理時間- Workqueue metrics
workqueue_depth: キューの深さ(溜まりすぎに注意)
- workqueue_retries_total: リトライ回数- Custom metrics
var (
customResourceCount = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "myoperator_custom_resource_count",
Help: "Number of custom resources managed",
},
)
)
Prometheus + Grafanaでのダッシュボード例:
apiVersion: v1
kind: ServiceMonitor
metadata:
name: myoperator-metrics
spec:
selector:
matchLabels:
control-plane: controller-manager
endpoints:
- port: https
scheme: https
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
5.4 パフォーマンスチューニング
1. Rate Limiting
// Controller Managerの設定
ctrl.Options{
RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(
5*time.Millisecond, // 基準時間
1000*time.Second, // 最大待機時間
),
}
2. 並列処理の制御
ctrl.Options{
MaxConcurrentReconciles: 3, // 同時実行数を制限
}
3. Cacheの最適化
// 特定のNamespaceのみキャッシュ
cache.Options{
DefaultNamespaces: map[string]cache.Config{
"my-namespace": {},
},
}
---
6. 開発手法:TDD(Test-Driven Development)
6.1 Operatorにおけるテスト戦略
テストピラミッド:
/\
/E2E\ <- 少数(本番環境に近い統合テスト)
/------\
/ 統合 \ <- 中程度(Envtestを使用)
/----------\
/ ユニット \ <- 多数(ロジックの単体テスト)
/--------------\
6.2 Envtestによる統合テスト
package controller_test
import (
"context"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
myappv1 "example.com/myoperator/api/v1"
"example.com/myoperator/controllers"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var ctx context.Context
var cancel context.CancelFunc
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
ctx, cancel = context.WithCancel(context.TODO())
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
var err error
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
err = myappv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
// Controllerの起動
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).ToNot(HaveOccurred())
err = (&controllers.MyAppReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
go func() {
defer GinkgoRecover()
err = k8sManager.Start(ctx)
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
}()
})
var _ = AfterSuite(func() {
cancel()
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
var _ = Describe("MyApp Controller", func() {
Context("When creating a MyApp resource", func() {
It("Should create a Deployment", func() {
ctx := context.Background()
myApp := &myappv1.MyApp{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: myappv1.MyAppSpec{
Replicas: 3,
Image: "nginx:latest",
},
}
Expect(k8sClient.Create(ctx, myApp)).Should(Succeed())
// Deploymentが作成されるまで待機
deployment := &appsv1.Deployment{}
Eventually(func() error {
return k8sClient.Get(ctx, client.ObjectKey{
Name: "test-app",
Namespace: "default",
}, deployment)
}, time.Second*10, time.Millisecond*250).Should(Succeed())
Expect(*deployment.Spec.Replicas).Should(Equal(int32(3)))
})
})
})
6.3 CI/CDパイプライン統合
GitHub Actionsの例:
name: Operator CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install tools
run: |
make install-tools
- name: Run tests
run: make test
- name: Run envtest
run: make test-integration
- name: Build Docker image
run: make docker-build IMG=myoperator:${{ github.sha }}
e2e:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Create Kind cluster
uses: helm/kind-action@v1.8.0
- name: Deploy Operator
run: |
make deploy IMG=myoperator:${{ github.sha }}
- name: Run E2E tests
run: make test-e2e
---
7. ソフトスキル:チーム開発での実践
7.1 Operator設計のレビューポイント
設計レビューチェックリスト:
- API設計
- Reconciliation ロジック
- パフォーマンス
7.2 プルリクエストのベストプラクティス
良いPRの例:
## Summary
MyAppリソースに自動スケーリング機能を追加
## Changes
- HorizontalPodAutoscaler (HPA) の自動作成機能を実装
- `spec.autoscaling`フィールドをCRDに追加
- HPAのReconciliationロジックを追加
## Testing
- Envtestで単体テスト追加
- Kindクラスターでの手動E2Eテスト実施
- 負荷テストでスケーリング動作を確認
## Migration Guide
既存のMyAppリソースには影響なし(後方互換性あり)
新規にautoscaling機能を使う場合:
yaml
spec:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 80
## Checklist
- [x] Tests added/updated
- [x] Documentation updated
- [x] CHANGELOG updated
- [x] API version bumped (if needed)
7.3 ドキュメンテーション戦略
必須ドキュメント:
- README.md
- API Reference
// MyAppSpec defines the desired state of MyApp
type MyAppSpec struct {
// Replicas is the desired number of replicas
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
// +kubebuilder:default=1
Replicas int32 `json:"replicas,omitempty"`
// Image is the container image to use
// +kubebuilder:validation:Required
Image string `json:"image"`
}
- Operations Guide
- Architecture Decision Records (ADR)
# ADR-001: Reconciliation戦略の選択
## Status
Accepted
## Context
複数のリソース(Deployment, Service, Ingress)を管理する必要がある
## Decision
Single Reconcilerパターンを採用し、すべてのリソースを1つのReconcile関数で管理
## Consequences
- Pros: ロジックが集中し、依存関係が明確
- Cons: Reconcile関数が大きくなる可能性
---
8. よくある失敗と回避策
8.1 Reconciliation loopの無限ループ
問題:
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var myApp MyApp
r.Get(ctx, req.NamespacedName, &myApp)
// 毎回タイムスタンプを更新 → Statusの変更 → 再度Reconcileが呼ばれる
myApp.Status.LastReconcile = metav1.Now()
r.Status().Update(ctx, &myApp)
return ctrl.Result{}, nil
}
解決策:
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var myApp MyApp
if err := r.Get(ctx, req.NamespacedName, &myApp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 実際に状態が変わった時だけStatusを更新
originalStatus := myApp.Status.DeepCopy()
// Reconciliationロジック
r.reconcileDeployment(ctx, &myApp)
// 変更があった場合のみ更新
if !reflect.DeepEqual(originalStatus, &myApp.Status) {
myApp.Status.LastReconcile = metav1.Now()
if err := r.Status().Update(ctx, &myApp); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
8.2 リソースリークとOwner Reference
問題:
func (r *Reconciler) createService(ctx context.Context, app *MyApp) error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
},
Spec: corev1.ServiceSpec{...},
}
// Owner Reference設定忘れ → 親リソース削除時に子が残る
return r.Create(ctx, svc)
}
解決策:
func (r *Reconciler) createService(ctx context.Context, app *MyApp) error {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: app.Name,
Namespace: app.Namespace,
},
Spec: corev1.ServiceSpec{...},
}
// Owner Referenceを設定(必須)
if err := ctrl.SetControllerReference(app, svc, r.Scheme); err != nil {
return fmt.Errorf("failed to set owner reference: %w", err)
}
return r.Create(ctx, svc)
}
8.3 エラーハンドリングの不適切な実装
問題:
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 外部APIへの呼び出しが失敗した場合、何も返さない
if err := r.callExternalAPI(); err != nil {
log.Error(err, "external API call failed")
return ctrl.Result{}, nil // エラーを無視 → 再試行されない
}
return ctrl.Result{}, nil
}
解決策:
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 一時的なエラーの場合はRequeue
if err := r.callExternalAPI(); err != nil {
if isTemporaryError(err) {
log.Error(err, "external API call failed, will retry")
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
// 恒久的なエラーの場合はStatusに記録
return ctrl.Result{}, fmt.Errorf("permanent error: %w", err)
}
return ctrl.Result{}, nil
}
func isTemporaryError(err error) bool {
// ネットワークエラー、タイムアウトなど
return errors.Is(err, context.DeadlineExceeded) ||
errors.Is(err, syscall.ECONNREFUSED)
}
8.4 Watchの設定ミス
問題:
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&MyApp{}).
// Ownsを設定していない → 子リソースの変更を検知できない
Complete(r)
}
解決策:
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&MyApp{}).
Owns(&appsv1.Deployment{}). // Deploymentの変更を検知
Owns(&corev1.Service{}). // Serviceの変更を検知
Watches(
&source.Kind{Type: &corev1.ConfigMap{}},
handler.EnqueueRequestsFromMapFunc(r.findObjectsForConfigMap),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
).
Complete(r)
}
func (r *Reconciler) findObjectsForConfigMap(cm client.Object) []reconcile.Request {
// ConfigMapの変更に関連するMyAppを検索
var requests []reconcile.Request
var myAppList MyAppList
r.List(context.Background(), &myAppList, client.InNamespace(cm.GetNamespace()))
for _, app := range myAppList.Items {
if app.Spec.ConfigMapRef == cm.GetName() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: app.Name,
Namespace: app.Namespace,
},
})
}
}
return requests
}
---
9. まとめ:学習ロードマップ
レベル1: 基礎(1-2ヶ月)
- Kubernetesの基本リソースを完全に理解
- 簡単なCRDとControllerを実装(Kubebuilderのチュートリアル)
- Kindでのローカル開発環境構築
レベル2: 実践(3-6ヶ月)
- 本番レベルのOperatorを1つ実装
- Envtestでのテスト駆動開発
- CI/CDパイプライン統合
レベル3: マスター(6-12ヶ月)
- 複数のOperatorの本番運用経験
- OSSへの貢献(バグ修正、機能追加)
- アーキテクチャ設計と技術選定のリード
この講義を通じて、単なる「Operatorを動かせる」レベルから、「本番環境で運用できる」「アーキテクチャを設計できる」レベルへと成長していきましょう。