Dockerより高速に開発用一時データベースを一括セットアップする方法(翻訳)
私たちのRailsEventStoreでは、非常に広範囲なテストスイートを用いてサポートするすべてのデータベースがスムーズに動作することを確かめています。サポート対象データベースには、さまざまなバージョンのPostgreSQL、MySQL、SQLiteも含まれており、最新バージョンはもちろん、サポート範囲で最も古いリリースも含まれています。
これほど多くのデータベースをさまざまなバージョンで使い捨てとしてセットアップすることについては、CI上でほぼ解決済みであり、個別のテストは独自の隔離された環境で実行されるようになっています。しかし開発作業では、少なくともmacOSがらみで少しだけ微妙な点が残っていました。
この問題を少し詳しく見てみましょう。PostgreSQL 11用のアダプタとPostgreSQL 15用のアダプタを利用してテストスイートを実行する必要がある場合、いくつかの選択肢が考えられます。
brew
コマンド(Homebrew)を使うと手間がかかります。
最初に必要なバージョンのPostgreSQLをインストールし、続いてリンクを現在選択しているバージョンに切り替え、データベースサービスをバックグラウンドで起動し、PostgreSQLのヘッダーファイルがパスに存在することを確認してpg
gemをコンパイル可能にし...と多くの作業を行うことになります。
最終的に、データベースに集約されたデータも管理しなければならなくなります。-
docker
はこの問題をおそらく解決してくれますよね?
使いたいバージョンごとにたくさんのDockerfile
ファイルでデータベースサービスを記述するか、さもなければ1個のDockerfile
で多数のデータベースを起動し、それぞれに異なる外部ポート番号を設定することになります。コンテナが終了すればデータベースのステートも破棄されるのも嬉しい点です。これらだけでも既にbrew
コマンドよりずっと多くのメリットが得られます。
唯一の泣き所はおそらくパフォーマンスです。ひどいというほどではありませんが、素晴らしく速いわけでもありません。
実は第3の選択肢があるとしたらどうでしょうか?しかもUNIX系システムにそうしたデータベースエンジンが既に組み込み済みだとしたらどうでしょうか?
🔗 1: UNIX流の方法
種明かしの前に、材料を簡単に紹介しておきます。
- 一時ファイルと一時ディレクトリ
mktemp
ユーティリティの便利な機能を用いて、衝突の起きない一意のパスをディスク上に生成します。作成場所を/tmp
パーティションにしておけば、OSが定期的にクリーンアップしてくれるという嬉しいおまけも付いてきます。 -
UNIXソケット
ソケット(socket)はプロセス間データ交換のメカニズムで、ソケットのアドレスはファイルシステム上に配置されます。TCPソケットではhost:port
でアドレスを指定でき、ここでの通信はIPスタックとルーティングを経由します。ここでの「接続」は、ディスク上のパスに対して行われ、このアクセスはディスクのパーミッションでも制御されます。アドレスは、/tmp/tmp.iML7fAcubU
のようになります。 -
OSのプロセス
ここで用いる最小の分離単位はプロセスです。プロセスはPIDで識別されます。識別子を知っていれば、プロセスをバックグラウンドに移動した後でも制御できるようになります。
以上を押さえたところで、ソリューションの生の部分を紹介します。
TMP=$(mktemp -d)
DB=$TMP/db
SOCKET=$TMP
initdb -D $DB
pg_ctl -D $DB\
-l $TMP/logfile\
-o "--unix_socket_directories='$SOCKET'"\
-o "--listen_addresses=''\'''\'"\
start
createdb -h $SOCKET rails_event_store
export DATABASE_URL="postgresql:///rails_event_store?host=$SOCKET"
最初にmktemp -d
でベースとなる一時ディレクトリを作成します。
ここから得られるパスは、/tmp/tmp.iML7fAcubU
のようにランダムかつ一意なものになります。このベースディレクトリの下に「UNIXソケット」「バックグラウンド実行中にデータベースプロセスが生成するデータベースストレージファイルとログ」が置かれます。
次にinitdb
を実行して、指定のディレクトリでデータベースストレージをseedする必要があります。
次にpg_ctl
を実行して、postgresプロセスをバックグラウンドで起動します。
このコマンドの設定は、コマンドラインスイッチだけで十分行えます。この設定によって、「ログの出力先」「指定のパスで他のプロセスと通信するときのUNIXソケット」「TCPソケットを利用しないこと」を指定します。つまり、異なるプロセス間で同じhost:port
ペアを指定しても競合は発生しません。
分離したデータベースエンジンユニットが無事に動くようになったら、アプリケーション環境も準備しておくと便利です。
PostgreSQL CLIのcreatedb
コマンドでデータベースを作成します。このコマンドはUNIXソケットも理解できます。
最後に、DATABSE_URL
環境変数にデータベースのパスを設定して、データベースの場所をアプリケーションに知らせます。ここで指定するURLは postgresql:///rails_event_store?host=/tmp/tmp.iML7fAcubU
のようになっており、指定のバージョンのデータベースエンジンの特定のインスタンスをこれで完全に記述できます。
テストの実行が完了したら、一時データベースを破棄します。
以下のように、最初にバックグラウンドのプロセスを終了し、次にプロセスが動作していた一時ディレクトリのルートを削除します。
pg_ctl -D $DB stop
rm -rf $TMP
このスクリプトでやっていることはだいたい以上です。
🔗 2: 大いに役立つ小さな自動化
こんなシェル関数もあると便利そうです: バックグラウンドで一時データベースエンジンを生成して、DATABASE_URL
が設定済みのシェルで使えるようにし、終了時に自動的にクリーンアップするシェル関数です。
足りない材料は、シェル終了時のフック機能だけです。これは、modernishでやっているように、trap
の上にスタック的な振る舞いを構築する形で実装できます。
pushtrap () {
test "$traps" || trap 'set +eu; eval $traps' 0;
traps="$*; $traps"
}
この自動化部分のコード全体は以下のとおりです。
with_postgres_15() {
(
pushtrap() {
test "$traps" || trap 'set +eu; eval $traps' 0;
traps="$*; $traps"
}
TMP=$(mktemp -d)
DB=$TMP/db
SOCKET=$TMP
/path_to_pg_15/initdb -D $DB
/path_to_pg_15/pg_ctl -D $DB\
-l $TMP/logfile\
-o "--unix_socket_directories='$SOCKET'"\
-o "--listen_addresses=''\'''\'"\
start
/path_to_pg_15/createdb -h $SOCKET rails_event_store
export DATABASE_URL="postgresql:///rails_event_store?host=$SOCKET"
pushtrap "/path_to_pg_15/pg_ctl -D $DB stop; rm -rf $TMP" EXIT
$SHELL
)
}
これで、PostgreSQL 15を実行しているシェルに入る必要があるときは、with_postgres_15
シェル関数を実行するだけでできるようになります。
🔗 3: nix
で仕上げる
Docker
を使う方法は広く普及しているので、一時データベースはもう解決済みではないかと主張する人もいるでしょう。私もおおむね賛成です。
しかし私は随分前にnix
1を知って心に平和が訪れました。以下の多くの貢献やイニシアチブ↓のおかげで、nix
をmacOS上でbrew
と同じぐらいシンプルに利用できます。
参考: Nix 🖤 macOS - Open Collective
nix managerとnix-shell
ユーティリティを用いることで、以下のコマンド一発で一時データベースを生成できるようになります。
nix-shell ~/Code/rails_event_store/support/nix/postgres_15.nix
このコマンドは以下のように、前述の自動化スクリプトに加えて、指定バージョンのPostgreSQLバイナリがシステム上にない場合は、nixリポジトリからバイナリをフェッチします。これで、Dockerのあらゆる便利な点をDockerの欠点抜きで特定用途向けに利用できるようになります。
with import <nixpkgs> {};
mkShell {
buildInputs = [ postgresql_14 ];
shellHook = ''
${builtins.readFile ./pushtrap.sh}
TMP=$(mktemp -d)
DB=$TMP/db
SOCKET=$TMP
initdb -D $DB
pg_ctl -D $DB\
-l $TMP/logfile\
-o "--unix_socket_directories='$SOCKET'"\
-o "--listen_addresses=''\'''\'"\
start
createdb -h $SOCKET rails_event_store
export DATABASE_URL="postgresql:///rails_event_store?host=$SOCKET"
pushtrap "pg_ctl -D $DB stop; rm -rf $TMP" EXIT
'';
}
RailsEventStoreのリポジトリには、PostgreSQL版、MySQL版、Redis版のnixファイルを多数用意しています↓。これらは既に開発で便利に使われており、最終的には私たちのCIでも活用する予定です。
参考: rails_event_store/support/nix at master · RailsEventStore/rails_event_store
Happy experimenting!
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。