POD の Index がどうしても欲しい時ってありませんか。僕はありました。
github の issue で話題には上がっているみたいですが、2017年からOpenのままで、今後も Close される気配がありません。
後述しますが、「環境変数から取得する」という点にこだわらなければ他の方法も紹介されてますが、僕は今回「どうしても環境変数」から取得したかったので、今回の解決策に至りました。
TL;DR
目的
Statefulset の Pod の名前 (-, 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のライブラリを使っていました。しかし、このライブラリ、「各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 に書かれていますが、下記のように Pod のLifecyleのPostStartでExportコマンドを挟むことで、環境変数として取得できる可能性があります
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で紹介されていますが、initContainer
を使ってPod の Indexをファイルに吐き出しておこう、というアプローチです。 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の中身の仕組みはこのAdmission Controllerです)。
今回は、僕は Statefulset の Pod なら、全て Pod の Index と Pod の数がアノテートされるような Controller を作りました。
この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
Annotationから環境変数を読み込むときは、Statefulsetの env
を下記のように設定します。
- 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が環境変数を正しく読み込むことができます
$ env | grep POD
POD_REPLICAS=0
POD_INDEX=3
- Pros
- アプリのソースコードに影響なく、コンフィグだけで完結
- 環境変数から読み込める
- ローカルで開発するときは
application.yaml
に固定値を入れておけばいいので簡単 - 環境変数がk8s のリソースに宣言されている
- Cons
- 上記の解決策の中では群を抜いて複雑。環境変数から読み込むだけのためにここまでやるかは疑問
最後に
アプリのソースコードへの影響の極小化を最優先に考えた結果、Admission Controllerを活用することになりました。複雑度的に「どうなん?」とも思いましたが、アプリ開発者にはとても喜ばれたのでまあよかったかなと思います。
Admission Controller は k8s の活用の幅を広げてくれるので、使いこなせるととても便利です。
2020/05/05 追記
Javaなら下記のやり方でも実現できるとコメントをいただきましたので追記します。
Javaアプリのためにサクッと実装するならこれがいいですね。
// in k8s statefulset yaml
command: ["sh"]
args: ["-c", "ENV_ORDINAL=${HOSTNAME##*-} java -jar uber.jar"]