6.2 - PostgreSQLデータベースの構築
PostgreSQLデータベースの構築
本書のこの時点まで、私たちはJSONファイルを使用してToDo項目を保存してきました。これは、今のところうまく機能しています。実際、本書の残りの部分でも、JSONファイルを使用してタスクを完了することができない理由はありません。しかし、本番のプロジェクトでJSONファイルを使用する場合、いくつかの欠点に出くわすことになります。
きちんとしたデータベースを使うべき理由
JSONファイルへの読み書きが増えると、同時並行性の問題やデータの破損に直面する可能性があります。また、データの種類をチェックすることもできません。したがって、別の開発者がJSONファイルに異なるデータを書き込む関数を書いても、何も支障はない。
また、マイグレーションにも問題があります。ToDoアイテムにタイムスタンプを追加したい場合、これはJSONファイルに挿入する新しいToDoアイテムにのみ影響します。そのため、ToDo項目の中にはタイムスタンプがあるものとないものがあり、アプリにバグをもたらすことになります。また、JSONファイルには、フィルタリングの面でも限界があります。
現在は、データファイル全体を読み込み、データセット全体の項目を変更し、データセット全体をJSONファイルに書き込むだけです。これでは効率的とは言えず、拡張性もありません。また、これらのToDo項目を別のデータモデルのようなユーザーにリンクさせることも阻害される。さらに、今はステータスを使った検索しかできない。もし、ユーザーテーブルとToDoデータベースをリンクしたSQLデータベースを使えば、ユーザー、ステータス、タイトルに基づいてToDoアイテムをフィルタリングすることができます。また、これらの組み合わせも可能です。データベースを実行する際には、Dockerを使用します。では、なぜDockerを使う必要があるのでしょうか?
なぜDockerを使うのか?
なぜDockerを使うのかを理解するために、まずDockerが何なのかを理解する必要があります。Dockerは基本的に仮想マシンのように動作するコンテナを持っていますが、より具体的で粒度の高い方法で動作します。Dockerコンテナは、単一のアプリケーションと、そのアプリケーションのすべての依存関係を分離します。アプリケーションはDockerコンテナの内部で実行されます。Dockerコンテナは、互いに通信することができます。Dockerコンテナは単一の共通オペレーティングシステム(OS)を共有するため、互いに、またOS全体から区分けされており、Dockerアプリケーションは仮想マシンと比較してより少ないメモリを使用することになります。Dockerコンテナのおかげで、私たちはアプリケーションの移植性を高めることができます。Dockerコンテナが自分のマシンで動作していれば、Dockerを搭載した別のマシンでも動作します。また、アプリケーションをパッケージ化することもできます。つまり、アプリケーションを実行するために特別に必要なパッケージは、OSレベルの依存関係も含めて、別途インストールする必要がありません。その結果、Dockerを使えば、サーバーやデータベースをローカルマシンでシミュレートできるため、Web開発において大きな柔軟性を発揮します。
Dockerを使ってデータベースを動かす方法
このようなことを考えると、SQLデータベースをセットアップして実行するために必要な余分なステップを踏むことは理にかなっていると言えます。そのために、コンテナの作成と利用を支援するツールであるDockerを使用することにします。コンテナ自体はLinuxの技術で、アプリケーションとその実行環境全体をパッケージ化して分離するものです。コンテナは技術的には分離されたファイルシステムですが、本章で行っていることを視覚化するために、ミニ軽量仮想マシンと考えることができます。これらのコンテナは、Docker Hubからダウンロードできるイメージから作られています。次の図に見られるように、コンテナをスピンアップする前に、これらのイメージに独自のコードを挿入することができます。
図6.1 「Dockerイメージとコンテナの関係
Dockerを使えば、PostgreSQLデータベースなどのイメージをダウンロードし、開発環境で実行することができます。Dockerを使えば、複数のデータベースやアプリを立ち上げて、必要な時に必要なだけシャットダウンすることができる。まず、ターミナルで以下のコマンドを実行して、コンテナの在庫を確認する必要があります。
Dockerがフレッシュインストールであれば、次のような出力が得られます。
見ての通り、容器がない。また、イメージの在庫を確認する必要があります。これは、以下のターミナルコマンドを実行することで行うことができます。
先のコマンドでは、次のような出力が得られます。
繰り返しになりますが、Dockerが新規インストールであれば、コンテナは存在しません。
Dockerでデータベースを作成する方法は、他にもあります。例えば、OSや構成を定義した独自のDockerfileを作成することができます。しかし、ここではdocker-composeをインストールしています。docker-composeを使うことで、データベースの定義が簡単になります。また、コンテナやサービスを追加することも可能になります。PostgreSQLデータベースを定義するために、ルートディレクトリにあるdocker-compose.ymlファイルに、以下のYAMLコードを記述します。
version: "3.7"
services:
postgres:
container_name: 'to-do-postgres'
image: 'postgres:11.2'
restart: always
ports:
- '5432:5432'
environment:
- 'POSTGRES_USER=username'
- 'POSTGRES_DB=to_do'
- 'POSTGRES_PASSWORD=password'
先のコードでは、ファイルの先頭で、バージョンを定義しています。2や1などの古いバージョンでは、ファイルのレイアウトが異なるスタイルになっています。また、バージョンによってサポートする引数も異なります。本書を執筆している時点では、バージョン3が最新版となります。各docker-composeのバージョン間の変更点は、以下のURLで網羅されています: https://docs.docker.com/compose/compose-file/compose-versioning/.
次に、postgresタグの下にネストされたデータベースサービスを定義します。postgresやservicesなどのタグは辞書を表し、リストは各要素に-を付けて定義します。このdocker-composeファイルをJSONに変換すると、次のような構造になります。
{
"version": "3.7",
"services": {
"postgres": {
"container_name": "to-do-postgres",
"image": "postgres:11.2",
"restart": "always",
"ports": [
"5432:5432"
],
"environment": [
"POSTGRES_USER=username",
"POSTGRES_DB=to_do",
"POSTGRES_PASSWORD=password"
]
}
}
}
先のコードでは、サービスが各サービスを表す辞書であることがわかります。したがって、同じ辞書のキーを2つ持つことができないように、同じ名前のタグを2つ持つことはできないと推論できる。また、先のコードは、独自のパラメータを持つサービスタグを積み重ね続けることができることも教えてくれる。
Dockerでデータベースを実行する
データベースサービスでは、名前があるので、コンテナを見たときに、サーバーやデータベースといったサービスに関連して、各コンテナが何をしているのかがわかります。データベースの設定と構築に関しては、幸運にも公式のpostgresイメージを引っ張り出すことができました。このイメージにはすべてが設定されており、Dockerはそれをリポジトリから引っ張ってきます。イメージは青写真のようなものです。このイメージは設計図のようなもので、Dockerはこのイメージから独自のパラメータを持つ複数のコンテナをスピンアップさせることができます。そして、いつも通り再起動ポリシーを定義します。つまり、コンテナの再起動ポリシーは、コンテナが終了したときに発動することになります。また、障害や停止に基づく再起動のみを定義することも可能です。
Dockerコンテナには、マシンに対して開放されていない独自のポートがあることに注意する必要があります。しかし、コンテナポートを公開し、公開されたポートをDockerコンテナ内の内部ポートにマッピングすることは可能です。これらの機能を考慮し、ポートを定義することができます。
しかし、この例では、定義をシンプルにします。ポート5432のDockerコンテナへの着信トラフィックを受け入れ、内部ポート5432にルーティングすることを明記することにします。次に、環境変数として、ユーザー名、データベース名、およびパスワードを定義します。本書では、一般的で覚えやすいパスワードとユーザ名を使用していますが、本番環境に移行する場合は、より安全なパスワードとユーザ名に変更することをお勧めします。docker-compose ファイルがあるルートディレクトリに移動して、以下のコマンドを実行することで、システムのスピンアップを構築することができます。
このコマンドは、リポジトリからpostgresイメージを取り込み、データベースの構築を開始します。ログメッセージが次々と表示された後、ターミナルには次のような出力が表示されるはずです。
LOG: listening on IPv4 address "0.0.0.0", port 5432
LOG: listening on IPv6 address "::", port 5432
LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
LOG: database system was shut down at 2022-04-23 17:36:45 UTC
LOG: database system is ready to accept connections
ご覧のように、日付と時間は異なります。しかし、ここで言われているのは、データベースが接続を受け入れる準備ができたということです。そう、本当に簡単なことなのです。したがって、Dockerの採用は止められないのです。Ctrl + Cをクリックすると、docker-composeが停止し、postgresコンテナがシャットダウンされます。
ここで、以下のコマンドですべてのコンテナをリストアップしてみます。
先のコマンドでは、次のような出力が得られます。
CONTAINER ID IMAGE COMMAND
c99f3528690f postgres:11.2 "docker-entrypoint.s…"
CREATED STATUS PORTS
4 hours ago Exited (0) About a minute ago
NAMES
to-do-postgres
先の出力では、すべてのパラメータが存在することがわかります。しかし、サービスを停止したため、ポートが空になっています。
Dockerでルーティングとポートを探索する
もし私たちが再びサービスを開始し、別のターミナルでコンテナをリストアップするとしたら、ポート5432はPORTSタグの下にあるはずです。DockerコンテナへのCONTAINER IDの参照は、コンテナごとにユニークで異なる/ランダムであるため、メモしておく必要があります。ログにアクセスする際には、これらを参照する必要があります。docker-compose upを実行すると、基本的に以下のような構成になります。
図6.2 - docker-composeがデータベースを提供する様子
図6.2では、docker-composeが一意のプロジェクト名を使用して、コンテナとネットワークをネームスペースに保持していることがわかります。注意しなければならないのは、私たちのコンテナはlocalhost上で動作していることです。したがって、docker-composeが管理するコンテナに電話をかける場合は、localhostリクエストを行う必要があります。ただし、docker-composeから開放されているポートに呼び出す必要があり、docker-composeはdocker-compose.ymlファイルに定義されているポートにルーティングします。例えば、以下のようなymlファイルで2つのデータベースがあるとします。
version: "3.7"
services:
postgres:
container_name: 'to-do-postgres'
image: 'postgres:11.2'
restart: always
ports:
- '5432:5432'
environment:
- 'POSTGRES_USER=username'
- 'POSTGRES_DB=to_do'
- 'POSTGRES_PASSWORD=password'
postgres_two:
container_name: 'to-do-postgres_two'
image: 'postgres:11.2'
restart: always
ports:
- '5433:5432'
environment:
- 'POSTGRES_USER=username'
- 'POSTGRES_DB=to_do'
- 'POSTGRES_PASSWORD=password'
先のコードでは、両方のデータベースがポート5432を通じてコンテナへのトラフィックを受け入れていることがわかります。しかし、衝突してしまうので、一緒に開くポートの1つはポート5433で、これは2番目のデータベースコンテナのポート5432にルーティングされ、以下のレイアウトになります。
図6.3 - docker-compose が複数のデータベースを提供する様子
このルーティングにより、複数のコンテナを実行する際の柔軟性を確保することができます。ToDoアプリケーションで複数のデータベースを実行するつもりはないので、postgres_twoサービスを削除する必要があります。postgres_twoサービスを削除したら、再びdocker-composeを実行し、以下のコマンドでコンテナをリストアップします。
先のコマンドで、次のような出力が得られるようになりました。
先の出力では、イメージがpostgresのリポジトリから引き出されたことがわかります。また、イメージのユニークでランダムなIDと、イメージが作成された日付があります。
これでデータベースの立ち上げ方の基本がわかったので、次のコマンドでdocker-composeをバックグラウンドで実行することができます。
先のコマンドでは、どのコンテナがスピンアップされたかを以下の出力で伝えるだけです。
コンテナをリストアップすると、以下のような出力でステータスを確認することができます。
先ほどの出力では、他のタグも同じですが、STATUSタグからコンテナの稼働時間や、どのポートを占有しているのかがわかりますね。docker-composeはバックグラウンドで動作していますが、何が起こっているのかがわからないわけではありません。logsコマンドを呼び出し、以下のコマンドでコンテナのIDを参照することで、いつでもコンテナのログにアクセスすることができます。
先のコマンドは、標準的なdocker-composeのupコマンドと同じ出力になるはずです。docker-composeを停止するには、次のようにstopコマンドを実行します。
先のコマンドは、docker-composeのコンテナを停止させるものです。これは、次のように示されるdownコマンドとは異なることに注意する必要があります。
downコマンドは、私たちのコンテナも停止させます。しかし、downコマンドはコンテナを削除してしまいます。私たちのデータベースコンテナが削除された場合、私たちはすべてのデータも失うことになります。
ボリュームという設定パラメータがあり、コンテナが削除されたときに我々のデータが削除されるのを防ぐことができます。しかし、これは我々のコンピュータでのローカル開発には不可欠なものではありません。実際、ラップトップからコンテナやイメージを定期的に削除したくなるはずです。私は以前、ノートパソコンから使わなくなったコンテナやイメージを削除したところ、23GBが解放されました。
私たちのローカル開発マシン上のDockerコンテナは、一時的なものとして扱われるべきです。Dockerコンテナは標準的な仮想マシンよりも軽量ですが、無料ではありません。ローカルマシン上でDockerを実行するアイデアは、アプリケーションの実行がサーバ上でどのようになるかをシミュレートすることができるというものです。特にKubernetesのようなDockerオーケストレーションツールで管理されている場合、ノートPC上でDockerが動作すれば、サーバー上でも動作することが確実となります。
BashスクリプトでDockerをバックグラウンドで実行する
Dockerは、一貫したテストや開発にも役立ちます。私たちは、テストを実行するたびに同じ結果を得られるようにしたいと思うでしょう。また、他の開発者を簡単に取り込み、開発をサポートするコンテナのティアダウンやスピンアップを迅速かつ簡単に行えるようにしたいと思います。私は個人的に、簡単なティアダウンとスピンアップの手順をサポートしていない場合、開発が遅れるのを見たことがあります。例えば、複雑なアプリケーションに取り組んでいるとき、追加してテストしているコードが、データベースに傷をつけるかもしれません。元に戻すことはできないかもしれませんし、データベースを削除してもう一度始めるのは、このデータを再構築するのに長い時間がかかるため、苦痛です。開発者は、最初にどのようにデータを構築したかさえ覚えていないかもしれません。このような事態を防ぐ方法は複数あり、第9章「アプリケーションのエンドポイントとコンポーネントをテストする」で説明します。
とりあえず、バックグラウンドでデータベースを起動し、データベースへの接続が可能になるまで待機し、データベースを破棄するスクリプトを作成します。これにより、開発を開始するためのパイプライン、テスト、オンボーディングパッケージを構築するための基盤ができます。そのために、Rust Webアプリケーションのルートディレクトリにscriptsというディレクトリを作成します。そして、scripts/wait_for_database.sh ファイルに以下のコードを格納します。
#!/bin/bash
cd ..
docker-compose up -d
until pg_isready -h localhost -p 5432 -U username
do
echo "Waiting for postgres"
sleep 2;
done
echo "docker is now running"
docker-compose down
先ほどのコードで、スクリプトの現在の作業ディレクトリをscriptsディレクトリからルートディレクトリに移動しています。そして、バックグラウンドでdocker-composeを起動します。次に、pq_isreadyコマンドを使用してポート5432にpingを打ち、データベースが接続を受け入れる準備が整うまでループします。
重要なお知らせ
pg_isready Bashコマンドは、お使いのコンピュータで利用できないかもしれません。pg_isreadyコマンドは通常、PostgreSQLクライアントのインストールに付属しています。また、pg_isreadyの代わりに、以下のDockerコマンドを使用することもできます。
docker run -it postgres --add-host host.docker.internal:host-gateway docker.io/postgres:14-alpine -h localhost -U username pg_isready まで。
ここでは、postgres Dockerイメージを使ってデータベースチェックを実行し、データベースが接続を受け入れる準備が整っていることを確認しているのです。
データベースが稼働したら、コンソールにデータベースが稼働していることを出力し、docker-composeをテアダウンしてデータベースコンテナを破棄します。wait_for_database.sh Bashスクリプトを実行するコマンドを実行すると、次のように出力されます。
bash
❯ sh wait_for_database.sh
[+] Running 0/0
⠋ Network web_app_default Creating 0.2s
⠿ Container to-do-postgres Started 1.5s
localhost:5432 - no response
Waiting for postgres
localhost:5432 - no response
Waiting for postgres
localhost:5432 - accepting connections
docker is now running
[+] Running 1/1
⠿ Container to-do-postgres Removed 1.2s
⠿ Network web_app_default Removed
この出力から、ループの繰り返しごとに2秒間スリープするように指示していることを考えると、新しく起動したデータベースが接続を受け付けるまでにおよそ4秒かかったと推測できます。これで、Dockerでローカルデータベースを管理するための基本的な能力は身についたと言えるでしょう。
このセクションでは、私たちの環境をセットアップしました。また、Dockerの基本を十分に理解し、いくつかの簡単なコマンドだけでデータベースを構築、監視、シャットダウン、削除することができました。さて、次のセクションに進み、Rustとdiesel crateを使ってデータベースと対話することができます。