kubernetes podのSIGTERM問題

:::note info
この記事は[ZOZO AdventCalender 2023](https://qiita.com/advent-calendar/2023/zozo)シリーズ5の10日目の記事です。
:::

# podが意図せず強制終了されてしまっていた
kubernetes上で稼働させているjavaで実装されたアプリケーションのpod終了時、
SIGTERMを受信すれば動くはずの終了処理が動作せず強制終了されてしまう事象が発生したので原因と対処方法を共有したいと思います。

## 原因
通常、podがTerminatingになるとコンテナのメインプロセス(pid=1)にSIGTERMが送信されます。
今回の事象もkubeletからjavaアプリのコンテナにSIGTERM自体は正常に送信されていましたが、後述する理由によりjavaアプリまでSIGTERMが届かない状態になっていました。

### SIGTERMがjavaアプリまで届かない理由
該当のjavaアプリのDockerfileでのjavaプロセスの起動に関して以下のように記述されていました。
```Dockerfile
ENTRYPOINT java -Xmx${JAVA_HEAP_XMX} -Xms${JAVA_HEAP_XMS} -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar
```
この記法は[shell形式](https://docs.docker.jp/engine/reference/builder.html#shell-form-entrypoint-example)と呼ばれ、実際のコマンドは`/bin/sh -c`の中で実行されることになります。
実行中のコンテナでpsコマンドによりプロセスを確認すると以下のように起動されていました。
```bash
root@worker-5dc87f4dd7-mzxpm:/# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 07:19 ?        00:00:00 /bin/sh -c java -Xmx${JAVA_HEAP_XMX} -Xms${JAVA_HEAP_XMS} -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar
root         7     1 44 07:19 ?        00:00:21 java -Xmx6000m -Xms6000m -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar
root        35     0  0 07:19 pts/0    00:00:00 bash
root       351    35  0 07:20 pts/0    00:00:00 ps -ef
root@worker-5dc87f4dd7-mzxpm:/#
```
`/bin/sh -c`のプロセスがルートプロセス(pid=1)として起動し、javaプロセスはその子プロセスとして起動されているのがわかるかと思います。
pod終了時にSIGTERMが送信されるのは、メインプロセス(pid=1)である`/bin/sh`のプロセスになります。
ところがSIGTERMを受け取った`/bin/sh`は子プロセスにシグナルを伝搬するような気の利いた処理はしてくれません。
※伝搬させたければtrapコマンドなどでハンドリングする必要があります。
そのため`/bin/sh`でシグナルをハンドルしていないと結果としてSIGTERMは無視されることになります。
その結果、javaプロセスにSIGTERMが送信されず終了処理を実行することなく最終的に強制終了されてしまっていました。

## 解決方法
### ENTRYPOINTをexec形式で書く
[exec形式](https://docs.docker.jp/engine/reference/builder.html#exec-entrypoint)でENTRYPOINTを記述すればjavaコマンドがそのまま実行されるため、
`/bin/sh`がルートプロセスになることなくpod終了時のSIGTERMがjavaプロセスに送信されます。
ただし、exec形式ではコマンドの変数展開がされないため、今回のケースではヒープに関する設定を環境変数から取得する箇所がコマンド中にあり使えませんでした。
※変数展開はシェルの機能なのでexecでシェルを挟まないと展開されません。

### 起動コマンドをシェルスクリプトにしてtrapコマンドを使う
exec形式で書きつつ変数展開をしたい場合、起動コマンドを`entrypoint.sh`といったようなシェルスクリプトにして以下のようにtrapコマンドを使ってSIGTERMをハンドリングするやり方があります。
```bash
#!/bin/sh
signalhandler() {
    kill -TERM ${pid}
    wait ${pid}
}
trap signalhandler SIGTERM
java -Xmx${JAVA_HEAP_XMX} -Xms${JAVA_HEAP_XMS} -XX:FlightRecorderOptions=stackdepth=256 -javaagent:/dd-java-agent.jar -jar app.jar &
pid=$!
wait ${pid}
```
Dockerfile側は以下のようにします。
```Dockerfile
ENTRYPOINT ["./entrypoint.sh"]
```
### Deploymentのcontainer.lifecycle.preStopを使う
kubernetesのコンテナには終了処理の直前に呼ばれるPreStopという[コンテナフック](https://kubernetes.io/ja/docs/concepts/containers/container-lifecycle-hooks/)があります。
shell形式で記述すると`/bin/sh`がルートプロセスになることは避けられないため、PreStopフックで子プロセスのpidを取得し、直接SIGTERMを送信することでjavaプロセスを正常に停止させることができます。
該当のjavaアプリのDeploymentに以下のPreStopフックを定義することで、javaアプリを正常終了させることができました。
```
lifecycle:
  preStop:
    exec:
      command:
        - sh
        - -c
        - kill -TERM $(pidof java)
```

以上がpod終了時のSIGTERM問題の原因と対処方法です。
強制終了されてしまっていても明確なエラーが出なかったりして、意外と気づかずにこのような状態になってしまっているケースはあるかと思います。
自分の管理しているアプリケーションはちゃんと正常終了できているか、一度確認することをオススメします。