docker-stopでコンテナ停止時にスクリプトを実行する

# はじめに
この投稿は、Dockerのコンテナ停止時に、コンテナ内でスクリプトを実行する方法を試行錯誤しながらまとめたものです。

# なぜやるのか
職場で
「コンテナ停止時にデータのバックアップが取りたい」
という要望があったので調べていました。
「Volumeを使ってマウントすればいいじゃん」というツッコミはご遠慮ねがいます…。

# 先に結論
- 実行したいスクリプトをシェルのtrap機能で呼び出す
- CMD命令はJSON形式で書く
- フォアグラウンドのプロセスはsleepの無限ループにする

# 環境
```bash
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04 LTS"

$ docker version
Client:
 Version:      1.11.2
 API version:  1.23
 Go version:   go1.5.4
 Git commit:   b9f10c9
 Built:        Wed Jun  1 22:00:43 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.11.2
 API version:  1.23
 Go version:   go1.5.4
 Git commit:   b9f10c9
 Built:        Wed Jun  1 22:00:43 2016
 OS/Arch:      linux/amd64
```

# 最初にDockerのrun（start）とstopについておさらい
## docker-runとDockerfileのCMDの関係
dockerサービスは```docker-run```の実行時に指定されたイメージの
CMDに記述されたコマンドを```PID:1```としてコンテナ内で実行します。
※CMDに書かれた内容を上書きして実行する方法もあります。
詳しくは[Dockerfile リファレンス — Docker-docs-ja 1.11.0 ドキュメント] (http://docs.docker.jp/v1.11/engine/reference/builder.html?highlight=cmd#cmd)を参照してください。

## docker-stopで何が起こるか
dockerサービスは```docker-stop```の実行時に指定されたコンテナで
```kill -SIGTERM 1```を実行します。
つまり、```docker-run```で実行されたCMDのプロセスに対して```kill```を実行することになります。
この時SIGTERMでプロセスが停止できると```Exited (0)```とステータスが表示されます。

```bash
$ docker ps -a
CONTAINER ID  IMAGE  COMMAND       CREATED        STATUS                    PORTS  NAMES
9d7823cc3cbc  hoge   "/hoge.bash"  3 minutes ago  Exited (0) 2 minutes ago         hoge
```

# どうやってdocker-stopでスクリプトを実行するか？
ここからが本題です。
まずコンテナ内で実行したいサービスやコマンドをシェルスクリプトにまとめます。

```bash
#!/bin/bash

# すべてデーモンやバックグラウンドのプロセスとして起動する
/path/to/service/fuga-service start

sudo -b -u piyouser /path/to/command/piyo-command
```

スクリプト内でSIGTERMをtrapします。
参考：

- [Dockerコンテナー内のbashからアプリケーション停止処理を実施するTIPS - めもめも] (http://enakai00.hatenablog.com/entry/20140824/1408855090)
- [Bashのシグナルトラップ - 双六工場日誌] (http://sechiro.hatenablog.com/entry/20130216)

```bash
trap_TERM() {
  echo 'SIGTERM ACCEPTED.'
  exit 0
}
trap 'trap_TERM' TERM
```

さらに、スクリプトの最後にsleepを無限ループするように書きます。

```bash
while :
do
  sleep 5
done
```

Dockerfileでは作ったシェルスクリプトをCMDで実行するように書きます。

```bash
FROM ubuntu:16.04
.....
COPY hoge.bash /hoge.bash
CMD ["/hoge.bash"]
```

こうすると```docker-run```で起動した```hoge.bash```が、
```docker-stop```によって実行される```kill -SIGTERM```を受け取ります。
そうすると```trap```がTERM受け取るように指定されているので、
trap内の```echo```コマンドが実行されます。

この例では標準出力が使われるので、

```bash
$ docker logs hoge
SIGTERM ACCEPTED.
```
と、```docker-logs```コマンドから確認ができます。

# はまったところ
## フォアグラウンドのプロセスがsleepではなくtailだった
バックグラウンドで実行するプロセスしかない場合、

```bash
tail -f /dev/null
```
でフォアグラウンドのプロセスを残す例をよく見ますよね。

### tailをフォアグラウンドにする問題点
この場合だとtailがSIGTERMで停止できず、
1. ```docker-stop```のタイムアウト（デフォルト10秒）まで待つ
2. ```kill -SIGKILL```が実行される
となります。
この場合trapに掛からないので想定した動きになりません。

## DockerfileのCMDをシェル形式で書いていた
CMD命令は複数の書き方があります。最初はこう書いていました。

```bash
CMD "/hoge.bash"
```
### シェル形式の問題点
シェル形式で書いた上の例の場合は

```bash
/bin/sh -c '/hoge.bash'
```
と展開された後で実行されます。
そうするとDockerコンテナ内では下のようになります。
```PID:1```のプロセスに対してSIGTERMを実行しても、```/bin/sh```にはtrapを設定していないので、何も実行されず終わってしまいます。
※実際には```PID:5```のプロセスが残るので、タイムアウト待ちになります。

```bash
$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 05:42 ?        00:00:00 /bin/sh -c "/hoge.bash"
root         5     1  0 05:42 ?        00:00:00 bash /hoge.bash
root        17     5  0 05:42 ?        00:00:00 sleep 5
```


## sleepの時間がdocker-stopのタイムアウトよりも長かった
これは単なる凡ミスです。
「スリープは長いほうがいいよね」と何も考えずに

```bash
while :
do
  sleep 100
done
```
としていました。
これだと、sleepの実行が終わって制御が返ってくるよりも
```docker-stop```のタイムアウトの方が早くきてしまうので、
```kill -SIGKILL```が実行されてしまいます。

# まとめ
コンテナ停止時にスクリプトを実行する方法についてまとめました。
試行錯誤していく過程で、コンテナの開始時と停止時に何が起きているのか少しだけ理解できた気がします。
今回は試さなかったData volume containerを使ったバックアップ方法なども考えていきたいです。

# その他参考にしたページ
https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/