Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

Dockerより高速に開発用一時データベースを一括セットアップする方法(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Dockerより高速に開発用一時データベースを一括セットアップする方法(翻訳)

私たちのRailsEventStoreでは、非常に広範囲なテストスイートを用いてサポートするすべてのデータベースがスムーズに動作することを確かめています。サポート対象データベースには、さまざまなバージョンのPostgreSQL、MySQL、SQLiteも含まれており、最新バージョンはもちろん、サポート範囲で最も古いリリースも含まれています。

これほど多くのデータベースをさまざまなバージョンで使い捨てとしてセットアップすることについては、CI上でほぼ解決済みであり、個別のテストは独自の隔離された環境で実行されるようになっています。しかし開発作業では、少なくともmacOSがらみで少しだけ微妙な点が残っていました。

この問題を少し詳しく見てみましょう。PostgreSQL 11用のアダプタとPostgreSQL 15用のアダプタを利用してテストスイートを実行する必要がある場合、いくつかの選択肢が考えられます。

  1. brewコマンド(Homebrew)を使うと手間がかかります。
    最初に必要なバージョンのPostgreSQLをインストールし、続いてリンクを現在選択しているバージョンに切り替え、データベースサービスをバックグラウンドで起動し、PostgreSQLのヘッダーファイルがパスに存在することを確認してpg gemをコンパイル可能にし...と多くの作業を行うことになります。
    最終的に、データベースに集約されたデータも管理しなければならなくなります。

  2. dockerはこの問題をおそらく解決してくれますよね?
    使いたいバージョンごとにたくさんのDockerfileファイルでデータベースサービスを記述するか、さもなければ1個のDockerfileで多数のデータベースを起動し、それぞれに異なる外部ポート番号を設定することになります。コンテナが終了すればデータベースのステートも破棄されるのも嬉しい点です。これらだけでも既にbrewコマンドよりずっと多くのメリットが得られます。
    唯一の泣き所はおそらくパフォーマンスです。ひどいというほどではありませんが、素晴らしく速いわけでもありません。

実は第3の選択肢があるとしたらどうでしょうか?しかもUNIX系システムにそうしたデータベースエンジンが既に組み込み済みだとしたらどうでしょうか?

🔗 1: UNIX流の方法

種明かしの前に、材料を簡単に紹介しておきます。

  1. 一時ファイルと一時ディレクトリ
    mktempユーティリティの便利な機能を用いて、衝突の起きない一意のパスをディスク上に生成します。作成場所を/tmpパーティションにしておけば、OSが定期的にクリーンアップしてくれるという嬉しいおまけも付いてきます。

  2. UNIXソケット
    ソケット(socket)はプロセス間データ交換のメカニズムで、ソケットのアドレスはファイルシステム上に配置されます。TCPソケットではhost:portでアドレスを指定でき、ここでの通信はIPスタックとルーティングを経由します。ここでの「接続」は、ディスク上のパスに対して行われ、このアクセスはディスクのパーミッションでも制御されます。アドレスは、/tmp/tmp.iML7fAcubUのようになります。

  3. 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の上にスタック的な振る舞いを構築する形で実装できます。

modernish/modernish - GitHub

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を使う方法は広く普及しているので、一時データベースはもう解決済みではないかと主張する人もいるでしょう。私もおおむね賛成です。

しかし私は随分前にnix1を知って心に平和が訪れました。以下の多くの貢献やイニシアチブ↓のおかげで、nixをmacOS上でbrewと同じぐらいシンプルに利用できます。

参考: Nix 🖤 macOS - Open Collective

NixOS/nix - GitHub

nix managernix-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!

関連記事

Bash: .bashrcと.bash_profileの違いを今度こそ理解する


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。