Statefulset の Pod's Ordinal Index をどうしても環境変数から取得したかった話

POD の Index がどうしても欲しい時ってありませんか。僕はありました。
github の [issue](https://github.com/kubernetes/kubernetes/issues/40651) で話題には上がっているみたいですが、2017年からOpenのままで、今後も Close される気配がありません。 

後述しますが、「環境変数から取得する」という点にこだわらなければ他の方法も紹介されてますが、僕は今回「どうしても環境変数」から取得したかったので、今回の解決策に至りました。

## TL;DR

**目的**
Statefulset の Pod の名前 (<statefulsetName>-<index>, e.g. somepod-0, somepod-1 ...) の 末尾にある `0` とか `1` を環境変数から取得したい。

**解決策**
k8s の Custom Admission Controller を作って、 Index を Pod に アノテートすることで環境変数から取得する

**ソースコード**
Git Hubに公開しています
https://github.com/mk811/sts-annotator

## 背景

*解決策に興味ある人は読み飛ばしOKです。 

AWS Kinesis がらみです。Kinesisのストリームに複数のShardに大量のイベントが均等分散するようにしていました。KinesisからイベントをConsumeするのに [Spring Cloudのライブラリ](https://github.com/spring-cloud/spring-cloud-stream-binder-aws-kinesis)を使っていました。しかし、このライブラリ、「各Shardに1台ずつConsumer Podを走らせる」ということには基本的には対応していませんでした。（Pod のレベルで負荷分散できないじゃん）

複数Shardに対応するためには、何台Consumer Podが動いてて、それぞれのConsumer Podに「自分が何台目のPodなのか」ということを設定する必要がありました。また、僕の場合、このPODは Aストリームでは3台、Bストリームでは1台それぞれ動かしたい。という要望もあり、複数Shardまたは単一Shardのみに対応したソースコードにする、というのはNG。条件分岐などでアプリのソースコードを少しいじればできのも事実ですが、余計なコードは増やしたくない。 `application.yaml` のコンフィグレベルで実現したい、という要望がありました。 そして、一番大きかったのは当時動いていたPodは単一Shard用でソースコードが出来上がっていて、スケジュールもきつくて変更を入れたくないタイミングだったということ。`application.yaml` で実現するのがもっともアプリに影響が少なく、PODごとに異なる値を動的に取得するには環境変数しかないので、今回の解決策にいたりました。

## 解決策

ここでは検討した解決策を紹介します

### 候補-1 Pod の `postStart` に export コマンドを追加する
詳細はこの [stackoverflow](https://stackoverflow.com/questions/50750672/is-there-a-way-to-get-ordinal-index-of-a-pod-with-in-kubernetes-statefulset-conf) に書かれていますが、下記のように Pod のLifecyleのPostStartでExportコマンドを挟むことで、環境変数として取得できる**可能性があります**

```sh:lifecycle
lifecycle:
  postStart:
    exec:
      command: ["/bin/sh", "-c", "export INDEX=${HOSTNAME##*-}"]
```

なぜ**可能性がある**という言い方をしたかというと、この`postStart`はコンテナの`Entrypoint`とどちらが先に実行されるかが保証されません。実際、僕のコンテナはこの export コマンドは無視されてました。

- Pros
    - k8s のコンフィグで完結し、でアプリのソースコード改変がない
    - コンフィグもPod Lifecycleのみであり、影響範囲が最小
- Cons
    - 全てのPodで機能するかどうかは保証されない (致命的)
    - k8s のコンフィグに定義されていない環境変数を使うことになり宣言的ではない

### 候補-2 Pod の初期処理のShell ScriptでPodのIndexをファイルに吐き出しておく

詳細はこの[issue](https://github.com/kubernetes/kubernetes/pull/68719#issuecomment-538494232)で紹介されていますが、`initContainer` を使ってPod の Indexをファイルに吐き出しておこう、というアプローチです。 `initContainer` とは、アプリのコンテナが動く前に、初期化用のコンテナを動かすことができる仕組みのことです。

```sh:initContainer
  command:
    - bash
    - "-c"
    - "[ -e /zookeeper/data/myid ] || echo $((${PODNAME##*-}+1)) > /zookeeper/data/myid"
```
このやり方では、アプリコンテナの `/zookeeper/data/myid` というテキストファイルに `0` や `1` というIndexが書き込まれることになります。これはこれでありなんですが、環境変数から読み取る、という目的が達成されません。また、アプリをローカルで動かす際に別途ファイルを用意する必要があったり、ソースコードに多少なりとも変更する必要があります。吐き出すファイルパスを変更すると、アプリのファイルパスも変更しないといけないなど、アプリとk8sのコンフィグに見えにくい依存関係が発生するのも地味に嫌な点です。

- Pros
    - 複雑度は低い
- Cons
    - 環境変数から読み込めない
    - ローカルで開発する際にファイルを作るという地味な一手間が発生する
        - 複数のPODをローカルで動かしてみたい時とか特にめんどくさい
    - アプリとk8sのコンフィグに見えにくい依存関係が発生する

### 候補-3 (採用) Kubernetes Admission Controller を活用する

k8s ではリソースを作成する際に様々な Processがあるんですが、そこに自分の望む処理をInjectすることができます。これにより、カスタムポリシー (Quata制限とか)をデプロイすることもできます。(余談ですが、CNCFでも登録されている [Open Policy Agent](https://www.openpolicyagent.org/)の中身の仕組みはこのAdmission Controllerです）。

今回は、僕は Statefulset の Pod なら、全て Pod の Index と Pod の数がアノテートされるような Controller を作りました。

**Admission Controller Phases**  
<img width="639" alt="admission-controller-phases.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252218/69d43da0-6623-76ae-a635-1f9c030ac738.png">

このControllerをデプロイすることで、下記のようなPODには、`sts-annotator/pod-index: 0`, `sts-annotator/pod-replicas: 3` というアノテーションがつきます。
- Statefulset Name:  `api-somethings-v1`
- PodReplicas: `3`
- POD Name: `api-somethings-v1-0`

![annotations.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252218/ee5c2ed2-0009-37a8-9d50-4fd881ca25f2.png)

Annotationから環境変数を読み込むときは、Statefulsetの `env` を下記のように設定します。

```yaml:Statefulset
- env:
    - name: POD_INDEX
      valueFrom:
        fieldRef:
          fieldPath: metadata.annotations['sts-annotator/pod-index']
    - name: POD_REPLICAS
      valueFrom:
        fieldRef:
          fieldPath: metadata.annotations['sts-annotator/pod-replicas']
```

これにより、PODが環境変数を正しく読み込むことができます :tada: 

```sh:api-somethings-v1-0
$ env | grep POD
POD_REPLICAS=0
POD_INDEX=3
```

- Pros
    - アプリのソースコードに影響なく、コンフィグだけで完結
    - 環境変数から読み込める
    - ローカルで開発するときは `application.yaml` に固定値を入れておけばいいので簡単
    - 環境変数がk8s のリソースに宣言されている
- Cons
    - 上記の解決策の中では群を抜いて複雑。環境変数から読み込むだけのためにここまでやるかは疑問



## 最後に

アプリのソースコードへの影響の極小化を最優先に考えた結果、Admission Controllerを活用することになりました。複雑度的に「どうなん？」とも思いましたが、アプリ開発者にはとても喜ばれたのでまあよかったかなと思います。
Admission Controller は k8s の活用の幅を広げてくれるので、使いこなせるととても便利です。


## 2020/05/05 追記

Javaなら下記のやり方でも実現できるとコメントをいただきましたので追記します。
Javaアプリのためにサクッと実装するならこれがいいですね。

```yaml:Statefulset
// in k8s statefulset yaml
command: ["sh"]
args: ["-c", "ENV_ORDINAL=${HOSTNAME##*-} java -jar uber.jar"]
```