Dockerコンテナ内のプロセスを終了シグナルで止める + α(:躓いた話とか)

# はじめに

今回はdockerコンテナとDockerfile内のcmd, entrypointを使ったシェル起動、終了プロセスについて学んだことを書く。
dockerやシェル、バッチ内で動かしているプロセスは、プロセス内のstopやexitみたいなコマンドで正常に終了できるものがある。

Dockerコンテナをentrypoint ["sh"]で起動した際に終了シグナルを受け取る方法と躓いた点を簡単にまとめた。

## 状況

Dockerコンテナを建てる際、何らかの処理をしてからメインプロセスを実行し、終了シグナルを受けとって終了したかった。

## 解決策

1. enptypointでシェルスクリプトを呼び出し、シェルスクリプト側で何らかの処理を行う
1. シェルスクリプトを呼び出すときは引数を設定できるが、変数展開のためには最初にシェルを呼び出しその引数にスクリプトファイルとそれに渡す引数をセットで渡す
1. このままだと終了シグナルを受け取るプロセスがentrypointで指定したシェルスクリプトなので、シェルスクリプト内でtrapコマンドを使いシグナルを検出、メインプロセスを安全にkillする

```Dockerfile:Dockerfile
# base image
ARG BASE_IMAGE
FROM ${BASE_IMAGE}

# 作業ディレクトリの設定
WORKDIR /root/hoge

# 必要なパッケージのインストール
RUN apt-get update && apt-get install -y curl

# メインプロセスの準備

# 実行用のスクリプトをコンテナにコピー
COPY ./execute.sh ./
RUN chmod +x ./execute.sh

# スクリプト側で処理したい値を取得
ARG VAL
ENV VAL=${VAL}

ENTRYPOINT ["/bin/bash", "-c", "./execute.sh ${VAL}"]
```

```bash:execute.sh
#!/bin/bash

# 停止シグナルのハンドル
stop_handler(){
    echo "Terminating process with PID: ${MAIN_PID}..."
    kill -s TERM $MAIN_PID
    wait $MAIN_PID
    echo "Process with PID: ${MAIN_PID} has been terminated."
}

# シグナルハンドリング
trap stop_handler SIGTERM

# 引数を受け取る
VAL=${1}
echo "\$VAL: ${VAL}"

# 何らかの処理

# メインプロセス(ここではjarファイルをjavaで動かす。このmain.jarは終了プロセスを受け取ったとき正常終了する。)を実行
java -jar main.jar &
# メインプロセスのPIDを取得
MAIN_PID=$!
echo "\$MAIN_PID: ${MAIN_PID}"

# 子プロセスの終了を待つ
wait $MAIN_PID
```

なお、`ENTRYPOINT`を`CMD`に置き換えても同じ挙動っぽい。

## 躓いた点

1. シェルスクリプトに値を渡す方法
    1. シェルを指定し、スクリプトファイルを渡す
    1. スクリプトファイル自体に渡したい引数
2. 終了シグナルのハンドラーが効かない

1-1:スクリプトファイルを実行
ごにょごにょしてからメインのプロセスを実行したいのでDockerfileからシェルスクリプトを起動する。
このときスクリプトファイルに値を渡したいが補足にも書いている通りexec形式ではシェル環境を利用できず変数が展開できないため、このようにシェルを明示した。

```Dockerfile:Dockerfile
# NG
ENTRYPOINT ["./execute.sh", "${VAL}"]

# OK
ENTRYPOINT ["/bin/bash", "-c", "./execute.sh ${VAL}"]
```

1-2: 引数の扱い方
このようにスクリプトファイルに渡したい値を切り離して書いてしまうと"/bin/bash"に対しての第三引数となってしまう。

```Dockerfile
# NG
ENTRYPOINT ["/bin/bash", "-c", "./execute.sh", "${VAL}"]
```

2: シグナルがスクリプトファイルに対して届かない問題
前提としてdockerでコンテナに終了シグナルを送るコマンド`docker compose stop`は、コンテナのPID:1のプロセスに対して終了シグナル(TERM)を送り、デフォルトでその10秒後に強制終了(KILL)する。

shell形式やシェルを指定したexec形式では、シェルを親プロセスとしてその上にメインプロセスを子プロセスとして動かす。
そのためPID:1が`/bin/bash ./execute.sh val_data`のようにシェルになる。

今回正常に終了させたいプロセスはmain.jarだが、これは`bash -c execute.sh`の子プロセス。
スクリプトファイル内ではハンドラーを定義し、スクリプトファイルに対して終了シグナルが届くとその子プロセスであるmain.jarに終了プロセスを送る形にする。

ここで注意なのが`ENTRYPOINT`で指定したシェルと`execute.sh`のシェバンが一致している必要があること。

## 補足

### shell形式とexec形式

ENTRYPOINTやCMDコマンドには書き方が二種類ある

```Dockerfile
# shell形式
ENTRYPOINT command arg1 arg2
CMD command arg1 arg2

# exec形式
ENTRYPOINT [ "command", "arg1", "arg2" ]
CMD [ "command", "arg1", "arg2" ]
```

shell形式ではシェル環境の上で実行される。
シェルの変数を展開して使用したければshell形式か、exec形式で明示的にshell環境を指定する。

```Dockerfile
# shell形式 シェル環境の上なのでcommandだけでなくエイリアスも使える
ENTRYPOINT command ${val}
CMD command${val}

# exec形式 shell環境を指定
ENTRYPOINT [ "/bin/sh", "-c", "command", "arg1", "arg2" ]
CMD [ "/bin/sh", "-c", "arg1", "arg2" ]
```

### ENTRYPOINTとCMD

`ENTRYPOINT`と`CMD`はそれぞれ一回ずつしか使えず2回使った場合は最後の行のみ有効。
- `CMD`は`ENTRYPOINT`が定義されていないときのデフォルトの挙動で、`ENTRYPOINT`が定義されているときは`ENTRYPOINT`の引数に追加される
また、コンテナ起動時にコマンドラインから置き換えることができる
- `ENTRYPOINT`はコンテナ起動時に必ず実行される挙動を定義しておく

ENTRYPOINTで基本的に変わらなさそうな起動コマンドを定義し、CMDでその引数を柔軟に決めるということらしい。

## 参考

PID1のプロセス: [Docker上のサーバーを強制終了ではなく正常に終了する　PID1になるプロセスとは？を理解してみた](https://note.shiftinc.jp/n/ndc63c45d9f97)
ENTRYPOINTでシェル変数の展開: [Dockerfile リファレンス#entrypoint](https://docs.docker.jp/engine/reference/builder.html#entrypoint)
ENTRYPOINTとCMDの使い分け: [Docker CMDとENTRYPOINTの使い方](https://www.grandream.jp/blog/docker-cmd-entrypoint)