Kubernetes APIのdeprecation/removalを事前に検出する

`apps/v1beta1 Deployment`等のKubernete APIのdeprecation/removalを検出するための方法について紹介します。

**特にKubernetes v1.16以上に移行する方々**
Kubernetes v1.16ではDaemonSetやDeploymentの古いAPIバージョン(ex. `extensions/v1beta1`, `apps/v1beta2`)がremoval対象となっているため影響が大きいです。v1.16以上のクラスタに移行する予定がある方は注意しましょう。

# 背景

Kubernetesは新しいバージョンで特定のKubernetes API(ex. apps/v1beta1 Deployment)がdeprecated/removedとなります。詳細なDeprecation Policyについては[公式ドキュメント](https://kubernetes.io/docs/reference/using-api/deprecation-policy/)を参照してください。

Kubernetesでは各リソースのAPIバージョンを`Internal Version -> Storage Version`に変換する仕組みがあります(Internal/Storage Versionに興味がある人は[こちらのスライド](https://speakerdeck.com/uesyn/k8s-storage-version-migration)等を参照)。このためKubernetesクラスタ上に登録されているリソースはdeprecated/removedされていないAPIバージョンに変換済みで、Kubernetesクラスタをアップグレードしても直ちに問題は発生しないかもしれません。

しかしアップグレード後の運用を考慮した場合、以下の3つの課題に遅かれ早かれ当たることになります。

> NOTE: 以降で登場するManifestファイルとは、Pod, Deployment, ConfigMap等を記述したYAMLファイルを指します。`kubectl create -f`や`kubectl apply -f`等で指定するあのファイルです。

1. **Helm Chart、Manifestファイル**: deprecated/removed APIを使用したままの場合、Kubernetesクラスタ上のリソースを追加したり更新する時に失敗する可能性がある。
2. **Kubernetesクラスタ上のリソース**: KubernetesによってInternal/Storage Versionに変換された場合、期待しないデフォルト値が設定されて想定外の挙動を引き起こす可能性がある。
    - 例えばDaemonSetではAPIバージョンが`extensions/v1beta1`の時は`spec.updateStrategy.type`のデフォルト値はOnDeleteだったが、`apps/v1`ではデフォルト値がRollingUpdateに変更されている。
3. **Controller/Operator**: Kubernetesクラスタで利用しているカスタムのController/Operatorはそのプログラムの中で特定バージョンのKubernetes APIのリソースを作成したりアクセスしている場合がある。もしも該当するKubernetes APIがremovedとなった場合、該当するKubernetes APIリソースを操作する時に失敗するなどの問題が発生する可能性がある。
    - Controller/Operator以外にもKubernetes APIリソースを作成したり登録するようなCLIツール等についても基本的に同じことが言えます。 

これらの課題の困ったところは**「アップグレード直後は問題は発覚せず、運用中のある操作を行ったりした時だったり、何度もアップグレードを重ねた時に初めて発覚する可能性がある」**という点です。

このためKubernetesクラスタのアップグレード前にKubernetes APIのdeprecation/removalを検出して適切な変更を行う必要があります。しかし現在のKubernetesはそのような検出するための仕組みやツールは公式に提供されていません。しかし以降で紹介する各種ツール等を活用することでAPI deprecation/removalを検出できます。

# 検出方法

各パターンのAPI deprecation/removalを検出するためのツールについて紹介します。
インストールや使用方法については、各ツールの公式レポジトリや関連URLを参照してください。

## Helm Chart/Manifestファイル

結論: Plutoを使用する。

Helm Chart、Manifestファイル(YAML)のAPI deprecadtion/removalを検出するためのツールはいくつか存在します。基本的にPlutoがお勧めですが、汎用性や柔軟性の高さを重視する場合はconftestの方が良いかもしれません。

- **[conftest](https://github.com/open-policy-agent/conftest)**
    - Open Policy Agentベースのチェックツール。幅広い用途で利用できる。
    - [swade1987/deprek8ion](https://github.com/swade1987/deprek8ion)で各Kubernetesバージョンのdeprecation/removal検出ポリシーが提供されてる。([関連記事](https://medium.com/@swade1987/deprek8ion-staying-ahead-of-k8s-api-deprecations-a378aed8d0a4))
    - Helm Chartのチェックには対応していない。
        - `helm template`を併用すればチェック可能。
- **[Pluto](https://github.com/FairwindsOps/pluto)**
    - Kubernetes API deprecation/removal検出に特化したツール。
    - Helm Chart(v2/v3)とManifestファイルの両方に対応。
        - Helm Chartのチェックは`helm template`を利用するため、helm CLIのインストールが必須。
    - HelmについてはKubernetesクラスタ上のリソースチェックも可能。
    - CLIバイナリのみで利用可能。
    - API deprecation/removal情報はハードコーディングされてるため柔軟性は低い。


## Kubernetesクラスタ上のリソース (非推奨)

結論: kubentを使用する。ただし非推奨。

Helm ChartやManifestファイルをCI/CDの中で自動生成してそのまま適用するような場合、Kubernetesクラスタ上にしかリソースがないため、こちらをチェックする必要があります。

[kubent](https://github.com/doitintl/kube-no-trouble)を使用することで、Kubernetesクラスタ上のリソースのAPI deprecation/removalを検出できます。Helm(v2/v3)および`kubectl apply`で登録したリソースに対してチェックをかけることができます。

ただしここで注意して欲しいのは`kubectl apply`ではなく**`kubectl create`で登録したリソースに対してはチェックができないことです。このためこれらのリソースについてはAPI deprecation/removalの検出はできません**。
これはkubentの制約というよりも、補足に記載しているKubernetes自体に起因するものです。このためクラスタ上のリソースに対しては極力チェックしなくても済むようにGitOps等をベースにしたCI/CD環境を整えて、Helm ChartやManifestファイルのチェックを行えるようにすることをお勧めします。

### 補足: 何故kubectl applyだと検出できるのか？

Kubernetesではリソースを登録すると、そのリソースはデフォルトのAPIバージョンに変換されます。例えば`extensions/v1beta1:DaemonSet`のリソースを登録しても、DaemonSetのデフォルトAPIバージョンが`apps/v1`であれば`apps/v1:DaemonSet`に変換されてリソースが登録されます。つまりこの時点でKubernetesサーバ側のetcd上ではもともとの`extensions/v1beta1`というAPIバージョン情報は存在しないことになります。これは`kubectl create`でも`kubectl apply`でも同様です。

しかし`kubectl apply`の場合は、リソースを登録する時に登録時点のManifest情報がそのリソースの`kubectl.kubernetes.io/last-applied-configuration`アノテーションに設定されます。このManifest情報には登録時のapiVersion等も含まれるため、前述したデフォルトAPIバージョンへのリソース変換が実施されてもアノテーションとしてAPIバージョン情報が残ります。

kubentは`kubectl.kubernetes.io/last-applied-configuration`アノテーションを参照しているため、適切にAPI deprecation/removalを検出できるわけです。

## Controller/Operator

結論: Go言語 + kubernetes/client-goの実装であれば[yoichiwo7/k8sdepr](https://github.com/yoichiwo7/k8sdepr/tree/wip)を使用する。(ただしツールはwip=WorkInProgress)

> NOTE:
> 既により良いものがあるとか、k8sdeprの問題点や改善に関する指摘があればお願いします。

個人的にKubernetes API deprecation/removal検出が最も難しいのがController/Operatorだと考えています。

自身で開発しているController/Operatorであれば各Kubernetesバージョンのクラスタをkind(kubernetes in docker)で用意してテストを動かすことで検出したりできます。しかしそこまでテスト構成を整えるのはそれなりの規模でないと厳しいのが実情だと思います。
またKubernetes上にインストールしてあるサードパーティのController/Operatorである場合、その全てのプロジェクトに対してそのようなテストを追加してもらうことは現実的ではありません。

このためコード静的解析によるAPI deprecation/removalの検出が最も現実的な落としどころの一つとなります。このようなツールであれば開発やCIに組み込むことも可能ですし、サードパーティのController/Operatorに対してチェックをかけることも比較的容易です。自分で探した範囲ではそのようなツールは存在しなかったため、Go言語のKubernetes APIのdeprecation/removalを検出するためのツール(Go Analyzer)を自作して使用しています。([yoichiwo7/k8sdepr@wip](https://github.com/yoichiwo7/k8sdepr/tree/wip))

Go言語プロジェクトに対してツールを実行することにより、Kubernetes APIのdeprecation/removalがあるかを検出してくれます。例として、サードパーティのOperatorであるspotahome/redis-operator v0.5.0に対してAPI deprecation/removalの検出チェックを実行した結果の抜粋を以下に示します。


```bash
# Kubernetes v1.16.0を対象バージョンとして指定
$ k8sdepr -targetVersion v1.16.0 ./...
/tmp/redis-operator/service/k8s/deployment.go:18:42: apps/v1beta2:Deployment is removed. Migrate to apps/v1:Deployment. {deprecated=v1.9.0, removed=v1.16.0} 
...
/tmp/redis-operator/service/k8s/statefulset.go:18:43: apps/v1beta2:StatefulSet is removed. Migrate to apps/v1:StatefulSet. {deprecated=v1.9.0, removed=v1.16.0}
```

上記のようにどのKubernetesバージョンでdeprecated/removedされたかの情報、新しく使用すべきKubernetes APIも併せて出力するようになっています。

> NOTE:
> ちなみにspotahome/redis-operator v0.5.0等のようにGo Module未対応のプロジェクトの場合は、`go mod init`等を実行した上で一回Go Moduleプロジェクトに変換した状態でツールを実行する必要があります。Go Moduleの普及は進んでいますが、まだまだ未対応のプロジェクトは多いので留意しておく必要があります。

なおGo言語 + kubernetes/client-go以外で実装されてるような本流から外れたController/Operatorのチェックには対応していません。Go言語以外で実装されている場合は地道にgrepしたりして目視確認するか、コード静的解析によるツールをその言語向けに実装する必要があります。

### 補足: Go言語とKubernetesエコシステムに寄り添う大切さ

ちなみにyoichiwo7/k8sdeprは以下の前提/割り切りをもとに作成してます。

- 主要なController/OperatorはGo言語 + kubernetes/client-goで実装されている
- Go言語は静的解析のためのモジュール群が豊富であるため実装が比較的容易
- Kubernetes APIのパッケージ構成が体系化されている
    - 基本的に`k8s.io/api/`配下に各種apiVersionおよびkindが配置されている

このような前提があるため、Go言語およびKubernetesエコシステムから大きく離れた形で実装されたController/Operatorに対しては適切な検出ができない可能性が高いです。

Controller/OperatorをJavaやPython等で実装するためのプロジェクトがいくつか存在しますが、それを選択した時点でGo言語およびKubernetesエコシステムの恩恵を受けられなくなる、または恩恵を受けるために独自の工夫が必要となる可能性が高くなります。慣れ親しんだ言語を使えるメリットと、恩恵を受けられなくなるデメリットのトレードオフは慎重に検討しましょう。
特に中長期にController/Operatorを開発運用することを想定する場合、このデメリットは大きな技術負債となる可能性が高いです。