てっく・ざ・ぶろぐ!

とある大学院生のテックブログ

Dockerオブジェクトに自動でラベル付与(#3)

はじめに

#1では、肥大化するDockerのストレージの対策としてSSDの増設と/var/lib/dockerディレクトリからの移行を紹介しました。

今回は4TBのSSDを増設しましたが、容量は決して無限ではありません。特にML系のイメージはサイズが大きく、作成したDockerオブジェクト(イメージ、コンテナ、ボリューム、etc.)を放置すると数年後には同じ運命を辿ってしまいます。

本記事では、持続可能*1なDockerオブジェクトの管理を目的として、管理者ラベルの自動付与スクリプトの設定を紹介します。

1. 要件定義

目的は、研究室サーバー上で作成されたDockerイメージ・コンテナの管理者を明確にすることです。

そもそも、#1でDockerのストレージ容量がひっ迫してしまった原因は主に以下の2点だと考えられます。

  1. 単純に使用済みのイメージ・コンテナを消し忘れる
  2. 自分が作成したイメージ・コンテナかどうか怖くて削除できない

1.については、人間なので仕方ないと思います。今は使わないイメージ・コンテナがあっても、近いうちに利用する可能性があれば消さずに取っておきたいものですし、研究に集中しすぎると研究以外のことはなおざりになってしまいがちです。

問題があると考えたのは、2.です。研究室のNotionには、以前からDockerイメージ・コンテナ作成時のテンプレートのようなものが紹介されていました。何割かの学生はテンプレートに沿って自身のユーザー名をコンテナ名に含めていましたが、一方でイメージの管理者が不明であったり、ルールが十分に浸透していなかったりという不完全さがありました。

手っ取り早い対策として、Notion上のドキュメントを完全なものにする(e.g., イメージ名とコンテナ名にユーザー名を含めるように明記する)ということも考えられますが、ターミナルとNotionを頻繁に行き来するのは依然として面倒、かつ認知負荷が高いままです。

また、ルールを策定したとしても浸透率が100%になることはないですし、ルールに違反するイメージ・コンテナを監視してSlackに通知する方法はアラート疲れの原因となりそうです。

多くのアイデアを比較検討した結果、2章で紹介するラベルをメタデータとして、3章で紹介するエイリアスによって自動的に付与する方法を採用しました。4章では、それでもカバーできないケースの対応を行っています。

2. Dockerオブジェクトのラベル

ラベルは、 Dockerオブジェクトに対してメタデータを設定する仕組みです。

docs.docker.com

ラベルは名前やタグと違って、そのオブジェクトが存在する間は不変なので、意図せず上書きされることも無さそうです。

以下はコンテナにラベルを付与する例です。

$ docker run -d --name busybox-container --label "foo=bar" busybox sleep 3600
5e19a20ba247ccaee2c6bbf18a853b3cc0717650f8b764376a25754aa09ae7b1
$ docker inspect busybox-container | jq '.[0].Config.Labels'
{
  "foo": "bar"
}
$ docker ps --filter "label=foo=bar"
CONTAINER ID   IMAGE     COMMAND        CREATED              STATUS              PORTS     NAMES
5e19a20ba247   busybox   "sleep 3600"   About a minute ago   Up About a minute             busybox-container

イメージについては、docker buildコマンドの--labelオプションだけでなく、Dockerfileに以下のように記述することでラベルを付与することもできます。

LABEL foo=bar

フィルタリング機能を利用すれば、CLI上で特定のユーザーが作成したイメージ・コンテナを手軽に確認できるだけでなく、Docker Engine SDKsを利用してより複雑な条件で絞り込むこともできそうです。

3. ラベル自動付与のスクリプト

ラベルを付与すると、簡単にフィルタリングできて便利だということが分かりました。しかし、先述したラベルの付与方法をドキュメント(e.g., Notion)に記載し、全員に意識してもらうのはベストプラクティスとは程遠いと考えました。

ChatGPTとの長きにわたる壁打ちの結果、以下のようなスクリプトdockerコマンドのラッパーを作成することにしました。

#!/bin/bash

USER_NAME=$(whoami)

if [[ $1 == "build" ]]; then
  shift
  exec docker build --label "maintainer.xxxlab=$USER_NAME" "$@"
elif [[ $1 == "run" ]]; then
  shift
  exec docker run --label "maintainer.xxxlab=$USER_NAME" "$@"
else
  exec docker "$@"
fi

maintainerキーは単純で重複する可能性が高いので、maintainer.xxxlabをキーとしました。名前の衝突を避けるため、一般的には接頭辞に逆DNS表記が利用される*2そうです。

今回は、イメージ作成時のdocker buildコマンドとコンテナ作成・実行時のdocker runコマンドを対象として--labelオプションを追加しています。

引数を1つずらすshiftコマンドや、"$1" "$2" …と等価である"$@"は使用したことがなかったので勉強になりました。

先述のスクリプト/usr/local/bin/docker-wrapperとして保存し、実行権限を付与します。それから、ユーザー全体に適用されるようにスタートアップファイル*3として追加します。

$ chmod +x /usr/local/bin/docker-wrapper
$ echo "alias docker=/usr/local/bin/docker-wrapper" > /etc/profile.d/docker_alias.sh
$ exit

再度シェルにログインして、明示的にラベルは付与せずにDockerイメージ・コンテナを1つずつ作成してみます。

$ alias
alias docker='/usr/local/bin/docker-wrapper'
# (省略)
$ docker build -t my-ubuntu - <<EOF
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl
CMD ["bash"]
EOF
$ docker run -d --name busybox-container --label "foo=bar" busybox sleep 3600

次に、ユーザー名でフィルタリングを行います。

$ docker images --filter "label=maintainer.xxxlab=azuma"
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
my-ubuntu    latest    f538b2998179   3 minutes ago   125MB
$ docker ps --filter "label=maintainer.xxxlab=azuma"
CONTAINER ID   IMAGE     COMMAND        CREATED              STATUS              PORTS     NAMES
e330e1ee116c   busybox   "sleep 3600"   About a minute ago   Up About a minute             busybox-container

無事にユーザー名のラベルを自動付与することができました。

4. ラベルが自動付与されないケースの対処

先述したdockerコマンドラッパーを数週間ほどテストしてみましたが、どうやら指定ラベルが付与されていないイメージ・コンテナが作成されているようでした。

原因を調査したところ、そのようなオブジェクトはDocker Composeを利用して作成されていました。完全に考慮し忘れていました。

また、一般的にエイリアスシェルスクリプトのような非インタラクティブシェルで展開されません。つまり、シェルスクリプトを利用してdockerコマンドを実行しているユーザーは自動ラベル付与の恩恵を受けられないということです。man bashの中に以下のような一文があります。

Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt (see the description of shopt under SHELL BUILTIN COMMANDS below).

スクリプトshopt -s expand_aliasesを追加するか、bash -o expand_aliasesとオプション実行することで非インタラクティブモードでも無理矢理エイリアスを展開することができます。さらに、bashコマンドに-lオプションを付けることで、Bashをログインシェルとして起動したかのように動作させる(/etc/profileを読み込ませる)ことができます。

makeでdockerコマンドを実行しているユーザーの場合は、さらにMakefileSHELL=/bin/bash -lを追記してシェルを指定しなければいけません。

これは完全にオーバーエンジニアリングであり、避けるべきです。

dockerコマンドをインタラクティブシェルで実行しているユーザーには自動ラベル付与を提供し、それ以外のユーザーにはシンプルに以下の一行をDockerfileに追加してもらうことにしました。

LABEL maintainer.xxxlab=<Linuxユーザー名>

結局2章の方法に回帰してしまい悔しいですが、全てのケースに対応するのは想定よりも遥かに難しいタスクだったため断念しました。

まとめ

持続可能なDockerオブジェクトの管理を目標に掲げ、(一般的なdockerコマンドのユーザーには)ユーザー名を値とした管理者ラベルを自動で付与することができました。

完全なツールを作成することはできませんでしたが、オーバーエンジニアリングにならない範囲でDockerオブジェクトの運用方針を周知していこうと思います。

Bashをはじめとするシェルは非常に奥が深く、勉強になりました。

*1:持続可能なエンジニアリングの象徴として、各記事の冒頭には見覚えのある雰囲気のアイコンが掲載されています

*2:Docker object labels | Docker Docs

*3:The Bash Shell Startup Files