Dockerコンテナ内でinotifywaitとawkでファイルを監視する

Pythonエンジニア見習いです。今回はDockerコンテナ内でファイルの編集を自動的に検知して対応するユニットテストを実行する方法を紹介します。

まず、ファイルを監視して、スクリプトを実行してくれるプロダクトは以下のとおりです。

  • inotifywait(C) + awk
  • watchman(C)
  • guard(Ruby)
  • gulp-watch(Node.js)
  • watchdog(Python)

Dockerで様々な環境のコンテナを作っていくと、このプロダクトではRubyを使うけれど、あのプロダクトでは使っていないという現象が頻繁に起こります。例えば、guard(Ruby)でファイル監視スクリプトを組んだとしてもPythonしかインストールされていない環境では、新たにRubyをインストールしなければなりません。さらに通常パッケージマネージャーなどでインストールできるバージョンは変更される可能性があり、安定性に難があります。そこで、rbenv等でバージョンを固定するのですが、

  • 毎回ビルドする時間がけっこう増える
  • コンテナの容量が増加する
  • ビルドツールをインストールしなければならない
  • rbenvのパスをとおさなければならない

等のデメリットを考えるとあまりいい手ではありません。

そこで、ファイル監視にinotifywaitを用いることで、上記の問題を解決できます(パッケージマネージャーでインストール可能かつ枯れている)。しかし、inotifywaitはファイルの変更を検知してくれるだけで、ファイルの実行まではやってくれません。そこで、inotifywaitの出力を読み取って、スクリプトを起動するものが必要になります。しかし、RubyやPython、Perl等の高級なスクリプト言語は、Alpine linux等の軽量イメージにはインストールされていなことがしばしばあります。そこでAWKを使うことでこの問題を解決します。

例えば、以下のディレクトリ構造があったとして、Pythonのコードが編集されたときにテストが走るようにしたいとします。また、Pythonのユニットテストはnosetestsで実行するものとします。

  • module/
    • a/
      • foo.py
    • bar.py
  • test/
    • a/
      • test_foo.py
    • test_bar.py
  • run-python-test

inotifywaitは、module/a/foo.pyが編集されたとすると

module/a/ MODIFY foo.py

と出力します。

これを踏まえると監視スクリプトは以下のようにワンライナーで書くことが出来ます。

inotifywait -m -e modify -r module | awk '$3 ~ /\.py$/ {system("nosetests")}'

一応これでも動くのですが、これでは、各ファイルを編集するごとにすべてのテストが動いてしまい、時間の無駄です。そこで編集したファイルに対応するテストのみ動くようにしたものが以下のものです。

inotifywait -e modify -m -r module test |
  awk '$3 ~ /\.py$/ {system("./run-python-test "$1" "$3)}'

run-python-test

DIR_NAME=$(echo $1 | sed 's/^module/test/')
FILE_NAME=$(echo $2 | sed 's/^test_//')
nosetests ${DIR_NAME}test_${FILENAME}

また、その他の言語のファイルについても監視を増やしたい場合は以下のように増やすことが出来ます。

inotifywait -e modify -m -r module test |
  awk ' \
    $3 ~ /\.py$/ {system("./run-python-test "$1" "$3)} \
    $3 ~ /\.js$/ {system("./run-javascript-test "$1" "$3)} \
    $3 ~ /\.rb$/ {system("./run-ruby-test "$1" "$3)}'

結論

inotifywaitawkを使えば、様々な環境で、ファイルを監視することが出来ます。さらに、余計なものをインストールしなくても良いので、ビルド時間の短縮やコンテナの容量の節約等のご利益があります。