Kubernetesコントローラー開発におけるFinalizerと削除処理の詳細解説

# Kubernetesコントローラー開発におけるFinalizerと削除処理の詳細解説

Kubernetesのカスタムコントローラーを開発する際、リソースの作成や更新だけでなく、**削除時の適切な処理**が重要です。特に、関連する外部リソースのクリーンアップを確実に行うためには、 **Finalizer（Finalizer）** の活用が不可欠です。

本記事では、`HelmReleaseProxy`コントローラーにおける削除処理を詳しく解説し、このパターンが一般的なコントローラー開発にも適用可能であることを説明します。また、実装レベルでの詳細や、リソースのクリーンアップが確実に行われるための工夫についても触れます。

## はじめに

`HelmReleaseProxy`は、Kubernetes上でHelmチャートを管理するためのカスタムリソースです。このリソースは、`HelmChartProxy`を経由して作成されます。リソースの削除時には、関連するHelmリリースや外部リソースを適切にクリーンアップする必要があります。

この削除処理の詳細な実装は、以下のソースコードで確認できます：

- [helmreleaseproxy_controller.go](https://github.com/kubernetes-sigs/cluster-api-addon-provider-helm/blob/main/controllers/helmreleaseproxy/helmreleaseproxy_controller.go)

## 削除処理の流れと実装詳細

### 1. 削除状態の確認

まず、リソースが削除中かどうかを確認します。

```go
if helmReleaseProxy.ObjectMeta.DeletionTimestamp.IsZero() {
    // リソースは削除されていない
} else {
    // リソースは削除中
}
```

- **`DeletionTimestamp`がゼロ値の場合**：リソースは削除されていません。
- **`DeletionTimestamp`が設定されている場合**：リソースは削除プロセス中です。

### 2. Finalizerの追加

削除されていないリソースに対して、以下の条件でFinalizerを追加します。

- **条件**：

  - `DeletionTimestamp`が設定されていない。
  - リソースの`finalizers`に、指定したFinalizerが含まれていない。

**実装**：

```go
if !controllerutil.ContainsFinalizer(helmReleaseProxy, finalizerName) {
    controllerutil.AddFinalizer(helmReleaseProxy, finalizerName)
    if err := patchHelmReleaseProxy(ctx, patchHelper, helmReleaseProxy); err != nil {
        return ctrl.Result{}, err
    }
}
```

- **目的**：Finalizerを追加することで、削除時に必要なクリーンアップ処理が完了するまでリソースの削除を遅延させます。
- **注意点**：Finalizerの追加は、コントローラーがリソースを処理をする一番最初のステップで行う必要があります。例えばリソースを処理した後にFinalizerを追加する様な処理の場合だと、Finalizerを追加する前のステップでコントローラーが強制終了されて起動しない状態になったら、Finalizerが追加できなくなり、クリーンアップ処理が実行できなくなる可能性が出てきます。

### 3. 削除中の処理とクリーンアップ

リソースが削除中で、かつFinalizerが設定されている場合、関連する外部リソースのクリーンアップを行います。

**実装**：

```go
if controllerutil.ContainsFinalizer(helmReleaseProxy, finalizerName) {
    // クラスタの取得
    if err := r.Client.Get(ctx, clusterKey, cluster); err == nil {
        // クラスタが存在する場合
        // クラスタのkubeconfigを取得
        restConfig, err := remote.RESTConfig(ctx, "caaph", r.Client, clusterKey)
        if err != nil {
            return ctrl.Result{}, err
        }
        // 外部リソース（Helmリリース）の削除
        if err := r.reconcileDelete(ctx, helmReleaseProxy, r.HelmClient, restConfig); err != nil {
            return ctrl.Result{}, err
        }
    } else if apierrors.IsNotFound(err) {
        // クラスタが存在しない場合、クリーンアップは不要
    } else {
        // その他のエラー処理
        return ctrl.Result{}, err
    }

    // Finalizerの削除
    controllerutil.RemoveFinalizer(helmReleaseProxy, finalizerName)
    if err := patchHelmReleaseProxy(ctx, patchHelper, helmReleaseProxy); err != nil {
        return ctrl.Result{}, err
    }
}
```

#### ポイント

- **クラスタの存在確認**：
  - クラスタが存在する場合のみ、外部リソースの削除を実行します。
  - クラスタが既に削除されている場合、関連する外部リソース（Helmリリース）も削除できないため、その旨をログで記録し、finalizerを完了させます。

### 4. Runtime hooksとの連携

**問題点**：

- クラスタが先に削除されると、関連するHelmリリースが残ったままになる可能性があります。

**解決策**：

- **Runtime hooksを利用**：`HelmChartProxy`コントローラーをクラスタのライフサイクルにフックさせ、クラスタ削除前に関連するリソースのクリーンアップを行います。
- **例**：

  - クラスタの削除要求を検知した際に、関連する`HelmChartProxy`や`HelmReleaseProxy`の削除処理を優先的に実行します。

このproposalの詳細は、以下で確認できます：
[20220414-runtime-hooks.md · kubernetes-sigs/cluster-api](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220414-runtime-hooks.md)

### 5. Finalizerの削除

クリーンアップ処理が完了したら、Finalizerを削除します。これにより、Kubernetesはリソースの最終的な削除を進めることができます。

**実装**：

```go
controllerutil.RemoveFinalizer(helmReleaseProxy, finalizerName)
if err := patchHelmReleaseProxy(ctx, patchHelper, helmReleaseProxy); err != nil {
    return ctrl.Result{}, err
}
```

### 6. Reconciliationの終了

```go
return ctrl.Result{}, nil
```

- 削除処理が完了したため、Reconciliationを終了します。

## 一般的なコントローラー開発への適用

この削除処理のモデルは、他のKubernetesコントローラー開発でも広く適用できます。

### Finalizer活用の手順（実装レベル）

1. **削除状態の確認**：

   ```go
   if resource.ObjectMeta.DeletionTimestamp.IsZero() {
       // リソースは削除されていない
   } else {
       // リソースは削除中
   }
   ```

2. **Finalizerの追加**：

   - **条件**：

     - `DeletionTimestamp`が設定されていない。
     - `finalizers`に対象のFinalizerが含まれていない。

   - **実装**：

     ```go
     if !controllerutil.ContainsFinalizer(resource, finalizerName) {
         controllerutil.AddFinalizer(resource, finalizerName)
         // リソースの更新
     }
     ```

3. **削除中のクリーンアップ処理**：

   - **条件**：`DeletionTimestamp`が設定されており、Finalizerが含まれている場合。

   - **実装**：

     ```go
     if controllerutil.ContainsFinalizer(resource, finalizerName) {
         // 外部リソースのクリーンアップ
         // Finalizerの削除
     }
     ```

### クリーンアップの確実性とリソースの整合性

- **問題点**：関連リソースが削除される順序によっては、クリーンアップが完全には実行できない場合があります。

### `HelmChartProxy`との関係

- **`HelmReleaseProxy`の生成**：`HelmReleaseProxy`は`HelmChartProxy`を経由して作成されます。

## 実装上のポイント

- **エラーハンドリング**：外部リソースの削除中にエラーが発生した場合、適切にログを記録し、リソースの状態を更新して再試行を促します。

- **リソースの状態管理**：Conditionsを活用して、リソースの現在の状態やエラー情報を明確にします。

- **依存関係の管理**：リソース間の依存関係を明確にし、削除順序を制御します。

## まとめ

Kubernetesコントローラー開発において、リソース削除時の適切な処理とFinalizerの活用は、外部リソースの整合性とシステムの健全性を維持する上で不可欠です。本記事で解説した`HelmReleaseProxy`コントローラーの削除処理は、その一例です。

このモデルは、他のコントローラー開発でも広く適用可能です。実装レベルでの詳細を把握し、自身のコントローラーに適用することで、より堅牢なシステムを構築できます。

---

**参考リンク**

- [HelmReleaseProxy Controller ソースコード](https://github.com/kubernetes-sigs/cluster-api-addon-provider-helm/blob/main/controllers/helmreleaseproxy/helmreleaseproxy_controller.go)
- [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
- [Runtime hooks](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20220414-runtime-hooks.md)

---

**注意**：本記事で取り上げたコード例は[Apache License 2.0](https://github.com/kubernetes-sigs/cluster-api-addon-provider-helm/blob/main/LICENSE)の下で提供されています。