Docker コンテナ内で cron を実行し実行ログを出力する方法

Docker

はじめに

Docker Compose を使って構築している環境にスケジューリングされたジョブ実行の仕組みがほしいという相談をいただきました。

AWS や GCP といったクラウドサービスを利用している場合は CloudWatch Events や Google Scheduler の利用も選択肢に入ります。
しかし今回の環境は Linux サーバに Docker デーモンが用意されているだけでしたので、cron を利用することにしました。

このときに構築した Docker 内で cron を実行する方法と、cron 実行結果を標準出力、標準エラー出力に流して docker logs コマンドで確認する方法を紹介します。

検証環境

$ uname -moi
arm64 unknown Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.2.2(1)-release (aarch64-apple-darwin21.6.0)

$ docker --version
Docker version 20.10.17, build 100c701

今回利用した Docker ベースイメージ

今回の検証で利用した Docker のベースイメージは Alpine です。

以下のタグの Alpine イメージを利用しました。

  • alpine:3.16.2

準備

まずは cron の本体であるcrondが実行可能な Docker イメージを用意します。

Alpine にはすでにcrondがインストールされているされているため、インストール作業は不要です。

crond を実行するコンテナを起動

crond を起動するコンテナを起動してみます。

起動時には-d オプションを付与してバックグラウンド(デーモン)起動します。

# 正常に起動できれば、コンテナIDが出力されます。
$ docker run -d alpine:3.16 crond -f
0d16132769db5000466887906837e03ab1ca3bc02a38b307da75424528cc0a76

起動できたようなので、コンテナ ID を指定してコンテナ実行時に出力される標準出力、標準エラー出力を監視します。

# 先程のコンテナIDの先頭6文字だけ(省略形)を指定
$ docker logs -f 0d1613

こちらのコマンドを実行したターミナルウィンドウは閉じずにそのままおいておきます。
後ほど動作確認時に使用します。

crontab にジョブを登録する

次に crontab に実行ジョブを登録します。

ここでは簡単なジョブを 2 つ登録します。

  • 1 つは正常終了するジョブ。
  • 1 つは異常終了するジョブ。

そのために、まずは crond 実行中のコンテナにアタッチします。

# コンテナ内に接続
$ docker exec -it 0d1613 sh
/ #

次に crontab -e コマンドで crontab をエディタで開きます。
( ちなみに設定ファイルの実体は /var/spool/cron/crontabs/root です。 )

# crontab -e コマンドを実行すると、crontab設定ファイルをエディタで開きます
/ # crontab -e

ファイルには以下の 2 行を追記します。

*/1     *       *       *       *       date 1>>/proc/1/fd/1 2>>/proc/1/fd/2
*/1     *       *       *       *       hoge 1>>/proc/1/fd/1 2>>/proc/1/fd/2

1 行目は date コマンドを実行しています。
2 行目は hoge という、存在しないコマンドを実行しています。

結果、1 行目の実行結果は正常終了し、2 行目の実行結果は異常終了します。

標準出力は、 プロセス ID = 1 つまり crond の標準出力と同じファイル記述子に出力します。
標準エラー出力は、やはり プロセス ID = 1 つまり crond の標準エラー出力と同じファイル記述子に出力します。

動作確認のため、いずれのジョブも 1 分ごとに起動します。

ちなみに、 1>>/proc/1/fd/1 2>>/proc/1/fd/2 という記述の意味については後述します

それでは動作確認に進みます。

動作確認

はじめにdocker logs -f を実行したウィンドウを確認してみましょう。

設定がうまく行っていれば、1 分ごとにスケジューリングしたジョブの実行ログが出力されているはずです。

$ docker logs -f 0d1613
j/bin/ash: hoge: not found
Tue Oct 11 10:52:00 UTC 2022
/bin/ash: hoge: not found
Tue Oct 11 10:53:00 UTC 2022
/bin/ash: hoge: not found
Tue Oct 11 10:54:00 UTC 2022
/bin/ash: hoge: not found
Tue Oct 11 10:55:00 UTC 2022
/bin/ash: hoge: not found
Tue Oct 11 10:56:00 UTC 2022
/bin/ash: hoge: not found
Tue Oct 11 10:57:00 UTC 2022
/bin/ash: hoge: not found
Tue Oct 11 10:58:00 UTC 2022
Tue Oct 11 10:59:00 UTC 2022
/bin/ash: hoge: not found

date コマンドが実行されたことにより、日時が出力されていることがわかります。

また、 hoge という存在しないコマンドが実行され、 /bin/ash: hoge: not found というエラーも出力されていることがわかります。

もし 1>>/proc/1/fd/1 2>>/proc/1/fd/2 という記述がなかったらどうなるの?

crontab に記述されたコマンドが実行されると、標準出力、標準エラー出力の内容はホスト内の sendmail コマンドを使ってメール送信されます。

メール送信されることを確認するために、 crontab を以下のように書き換えます。

# crontabを開く
/ # crontab -e
MAILTO=""
*/1     *       *       *       *       date
*/1     *       *       *       *       hoge

docker logs -f 実行中のターミナルウィンドウを確認すると、 cronjob 実行結果ログが以下のように変化していることがわかります。

$ docker logs -f
...
sendmail: can't connect to remote host (127.0.0.1): Connection refused
sendmail: can't connect to remote host (127.0.0.1): Connection refused
sendmail: can't connect to remote host (127.0.0.1): Connection refused
sendmail: can't connect to remote host (127.0.0.1): Connection refused

標準出力、標準エラー出力が cronjob から出力された場合、メール送信をしようとしていることがわかります。(ただし、失敗していますね。)

正しくメール送信されるような環境が整っていればよいです。
しかし、そうでなければ cronjob が正常終了したのか異常終了したのかわかりません。

Docker の標準出力、標準エラー出力に実行ジョブのログが出力されることでこの問題を解決できます。

ひとこと

はじめに触れたとおり、 Web API として実行基盤を用意して、CloudWatch Events や Google Scheduler で起動する、という仕組みを作ることばかりでした。
そのため、今まで Docker コンテナ内で cronjob を実行したことはほとんどありませんでした。

こういう方法もあるんだな、と勉強になりました。

Docker