Kubernetes Controllerにおける
対象読者
- Kubernetes Controller を Go (controller-runtime) で実装している方
- tombstone(DeletedFinalStateUnknown)にハマったことがある/テストで妙な NULL を踏んだことがある方
TL;DR
起因 | どんな時に起こる? | 見分け方 | 取るべき対策 |
---|---|---|---|
DeletedFinalStateUnknown | オブジェクトがキャッシュに載る前に削除された | Event の型が cache.DeletedFinalStateUnknown
|
tombstone 専用ハンドラで Key 解析 or 無視 |
フェイククライアント | テスト用 fake.NewClientBuilder() に渡したオブジェクトに metadata.namespace を書き忘れた |
テストだけで再現する/APIServerを経由していない |
obj.SetNamespace(...) or client.Create() で検証を通す |
スコープ不一致 | Cluster-scoped 型を誤って Watch している | CRD の scope: Namespaced/Cluster を確認 |
スコープ専用 RBAC & unit test で早期検知 |
YAML の namespace 抜け | POST /apis/... を直叩き or GitOps で namespace 省略 | kubectl 経由では再現しない | CI でスキーマ検証 or admission webhook |
1. DeletedFinalStateUnknown ―― "無縁仏" Tombstone イベント
コントローラが Informer から受け取る削除イベントには2種類あります。
種類 | キャッシュに実体がある? | 典型的な流れ |
---|---|---|
DeleteEvent | ある | Add → Update → Delete |
DeletedFinalStateUnknown | ない | Add が届く前に物理削除 → キャッシュに無いまま Delete だけ届く |
DeletedFinalStateUnknown.Obj には「ゼロ値」のオブジェクトが挿入されるため、
if tomb, ok := ev.Object.(cache.DeletedFinalStateUnknown); ok {
key := tomb.Key // => "default/foo"
// → key から namespace/name をパースする
}
のように Key 文字列 から情報を復元するか、"存在しない前提"で早期 return するのが安全です。
2. フェイククライアントで namespace を入れ忘れた
fake.NewClientBuilder()
に渡すオブジェクトは APIServer バリデーションが走りません。
テストデータを手書きする際に metadata.namespace
を忘れると、そのまま ""
が保存されます。
obj := &ipamv1.Subnet{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
// Namespace: "default", ← 忘れると空に!
},
}
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(obj). // → ここで検証されない
Build()
ベストプラクティス
- 必ず
obj.SetNamespace("default")
などで埋める - もしくは
client.Create(ctx, obj)
経由で登録し、fakeClient 内部のスキーマ検証を利用する
3. スコープ不一致(Cluster-scoped 型を誤って扱う)
- CRD を Namespaced → Cluster に変更した
- Watch 対象の型と RBAC が食い違った
……といった場合、Informer から届くオブジェクトは当然 Namespace == "" です。
CI で スキーマ&RBAC テスト を走らせておくと早期に検知できます。
# controller-gen の lint
controller-gen paths=./... crd:crdVersions=v1 output:stdout \
| kubeconform -strict -
4. YAML の metadata.namespace が抜けたまま直 API コール
kubectl -n default apply -f ...
なら補完されますが、
GitOps で Raw Manifest を API に流し込む場合は 空のまま登録 されることがあります。
- Admission Webhook (CEL / kyverno) で必須フィールドチェック
- CI で kubeconform + OpenAPI 検証を推奨
5. 実装例:安全なイベントハンドリングの雛形
func (r *Reconciler) handleEvent(obj client.Object) {
// tombstone を区別
if tomb, ok := obj.(cache.DeletedFinalStateUnknown); ok {
// (a) 無視する
return
// --- または (b) Key をパースする ---
// ns, name := cache.SplitMetaNamespaceKey(tomb.Key)
// ...
}
// Namespace を必ずチェック
if obj.GetNamespace() == "" {
// cluster-scoped? それとも異常?
log.Info("empty namespace; skip", "name", obj.GetName())
return
}
// 以降は安心して Reconcile
}
まとめ
- "空 namespace" はバグではなく "例外イベント or テスト環境" が主犯
- tombstone かどうかを判定し、Key 解析 or 無視で安全に捌く
- フェイククライアント・GitOps マニフェストの namespace 欠落は自分で防御
- スコープ不一致は CI と RBAC テストで早期発見
この4点を押さえておけば、controller-runtime の "空ネームスペース事件" でハマる確率はほぼゼロになります。