Docker Compose で、ベースイメージが更新された場合のみ再 up する

Pocket

本記事は Docker/コンテナ仮想環境 Advent Calendar 2023 の3日目の記事だ。

2日目は @ko-he-8 氏の /var/lib/docker/が肥大化していたけどdocker system prune --volumesで解決しなかったので調べてみた #Docker だった。
なんかの拍子にコンテナが管理外になってしまっていて docker system prune で削除できないときのお話になっている。
開発用の仮想マシンをリセットせずに使い続けていたりすると、こういう状況に良くなるよね…


Docker Compose だけで雑に本番運用する際に、定期的にリビルドしたい

信頼できるオフィシャルイメージを元に、カスタムイメージをビルドして本番運用する場合、 CI 等でビルドしたイメージをコンテナレジストリに push し、それを本番環境側で pull するのが一般的なベストプラクティスだ。

しかし、クラスタも構築せず VM 1台でいくつかのコンテナを動かすような小規模な運用ほ場合、わざわざ CI やレジストリを準備するのも面倒だ。
このため、運用環境上で Docker Compose を使ってイメージのビルドやコンテナの起動を行ってしまう単純な構成にしたくなる。

そんな時、ベースイメージのセキュリティ更新に追従するために、定期的にイメージをリビルドした上で、必要な場合に限ってコンテナを再 create & up させる方法を考える。

BuildX 既定化による弊害

以前は、 docker-compose pull --no-paralleldocker-compose build --pull の標準出力のいずれかから、 新しいイメージが pull されたことを示す "Downloaded newer image" の文字列があるかどうかを確認し、 docker-compose up すればよかった。

ところが、 docker 23.0.0 にて、 Linux 環境における既定のビルダーが BuildKit というものに切り替わってしまった。
旧来の docker-compose V1 の docker-compose build ないし、 Docker Compose V2 の docker compose build 実行時は docker CLI を通じてビルドされる ため、この仕様変更の影響を受ける。
このため、規定の設定のままだと build の仕組みが変わってしまい、上述のベースイメージのダウンロードを検出できなくなった。

BuildKit は規定では出力をいい感じにまとめてしまうので、そのままだとベースイメージの pull があったかどうか判別できない。

環境変数で DOCKER_BUILDKIT=0 を設定しておけば、旧来の Legacy Builder でビルドをしてくれるが、いつまでサポートされるかわからないので、 BuildKit を使う方法でなんとかしたい。

幸い、 docker build --progress=plain の出力の指定 は、 docker compose build でも働いてくれる。

この出力から ^#[0-9]+\s+resolve\s+ の正規表現がヒットする場所を探してくれば検出できそうだ。

Docker Compose V2 に対する出力オプション

また、既に廃止済みの docker-compose V1 の代替として、 Docker Engine 20.10.13Docker Compose V2 プラグイン がインストールされるようになって以降、現在はこちらの docker compose コマンド使う必要がある。

Compose V2 は、多くの面では docker-compose V1 と互換があり、作成されるコンテナ名のセパレータが一致しない事 (これも --compatibility フラグ で互換性を維持できる)以外はおおむねそのまま利用できる。

ただ、コマンドの UI 出力の内容や、それに纏わるオプションは V1 から大きく変わっているので、そこらへんは調整してやる必要がある。

--ansi never オプションを指定すると、ANSI エスケープコードが無効化され、ターミナルの色がつかなくなったり、パイプ時に余計な文字コードが確実につかなくなる…はずなのだが、指定しても up した時のログぐらいにしか効果が出ないような気はする。
規定値だと auto なので、パイプ処理された場合はそもそも ANSI エスケープコードがつかないはずではあるが、とりあえずオプションの方でも never を強制しといた方が良さそうなので設定しておく。

--progress plain オプションを指定すると、インタラクティブな出力が無くなり、以下のようにパイプで処理しやすいプレーンなテキストが出力されるようになる。
Docker Engine 24.0.3 に同梱されている Docker Compose 2.19.0 移行で compose のグローバルオプションに昇格したオプションであるため、それより低いバージョンだと指定できない点に注意。

$ docker compose --progress plain pull
 alpine-built1 Skipped - No image to be pulled
 alpine-built2 Skipped - No image to be pulled
 alpine-pure2 Skipped - Image is already being pulled by alpine-pure1
 alpine-pure1 Pulling
 070eb51debd9 Pulling fs layer
 070eb51debd9 Downloading [>                                                  ]   28.1kB/2.808MB
 070eb51debd9 Downloading [==================================>                ]  1.911MB/2.808MB
 070eb51debd9 Verifying Checksum
 070eb51debd9 Download complete
 070eb51debd9 Extracting [>                                                  ]  32.77kB/2.808MB
 070eb51debd9 Extracting [==================================================>]  2.808MB/2.808MB
 070eb51debd9 Pull complete
 alpine-pure1 Pulled

docker compose build, up のステータスの出力は、標準出力ではなくて標準エラーの方に出るので、パイプラインつなぐときは注意。

実際のコード例

compose.yaml が存在するディレクトリを /var/lib/docker-compose-dir とした場合、以下のようにできるだろう。

cd /var/lib/docker-compose-dir && { \
  /usr/bin/docker compose --ansi never --progress=plain pull --no-parallel; \
  /usr/bin/docker compose --ansi never --progress=plain build --pull; \
} 2>&1 | \
grep -E "Downloaded newer image|\s+Pull complete\s*$|^#[0-9]+\s+resolve\s+" && \
/usr/bin/docker compose --ansi never up -d

--

さて、こういうコードは cron 等で定期的に実行させたい。
そういった場合、あとから状況を確認しやすくするために、 docker compose の各出力は journald にも書き込んでおきたい。

条件分岐を行うための grep と、 journald に記録するコマンド (logger 等) の両方にパイプしたい場合、 bash や BusyBox ash 等が持つ プロセス置換 が使えそうだ。
crontab が起動するコマンドは、 sh という名前で POSIX 標準互換モードで動かすディストリビューションが多い (debian (Ubuntu) 系, fedora (RHEL, CentOS) 系) ので、明示的に bash (または ash) を呼び出し、その中でプロセス置換を行う方が安全だろう。

例えば Ubuntu の crontab 内で定期的にイメージのリビルドを行う場合、以下のようにすると良いだろう。

cd /var/lib/docker-compose-dir && { \
  /usr/bin/docker compose --ansi never --progress=plain pull --no-parallel; \
  /usr/bin/docker compose --ansi never --progress=plain build --pull; \
} 2>&1 | \
bash -c "tee >( logger -i -t user/docker-auto-rebuild -p user.info )" | \
grep -E "Downloaded newer image|\s+Pull complete\s*$|^#[0-9]+\s+resolve\s+" && { \
  /usr/bin/docker compose --ansi never up -d 2>&1 | \
  logger -i -t user/docker-auto-recreate -p user.info; \
}

なお、実際には cron 内のコマンドでは改行できないので、改行は削除して記述しよう。

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください