まとめ
- PostgreSQL の Docker公式イメージは、複数のDB作成をサポートしています。
- ただし、複数DBのユーザー名、パスワード等をDBと利用側アプリの双方に渡すのは手間がかかります。
- コンテナ生成時に mitamae を利用すると良い感じにDBが作成できます。
- mitamae は、イメージ作成時にも利用できます。
1つのpostgresコンテナで複数のDBを使用したい
DockerHub上の PostgreSQL の 公式イメージ では、環境変数 POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD を指定することで DB を簡単に作成することができますが、DBの数は1つだけに限られます。
一方で公式ではpostgresコンテナ上に複数のDB作成にも対応できる仕組を用意しています(事例をいろいろ検索していると複数のアプリのために複数のpostgresのコンテナとボリュームを立ち上げている人とかもいましたが)。
公式の "Initialization scripts" の節から引用します。
このイメージで追加の初期化を行いたい場合は、
/docker-entrypoint-initdb.d
の下に1つ以上の*.sql、.sql.gz、または.shスクリプトを追加してください(必要に応じてディレクトリを作成してください)。
エントリポイントが initdb を呼び出してデフォルトの postgres ユーザとデータベースを作成した後、サービス開始の前にさらなる初期化を行うために、そのディレクトリ内で見つけた*.sqlファイルを実行し、実行可能な *.shスクリプトを実行し、実行不可能な *.shスクリプトをソースにします。
すでに、 シェルスクリプトと環境変数の組み合わせで複数DBを生成する、という 事例 が公開されています。
ただし、各DBのユーザーのパスワードをどう設定するか、といったことまでは踏み込んでいません。
各DBのパスワードを管理したい
上記の事例で複数のDBを作成できることは分かりました。しかし、DBを作成するとき同時にユーザーのパスワードを設定したいし、パスワードを決め打ちで git にコミットするといったことは避けたいものです。また、複数のDBを作成したいというケースでは、複数のアプリケーションのコンテナのためそれぞれにDBを作成するわけですが、アプリケーション側にも同じユーザー名とパスワードを渡してあげる必要があります。
アプリケーション側(ここでは同じ docker-compose 上で稼働するコンテナという前提にします)のDBユーザー名、パスワードも環境変数で渡すのが一般的ですが、その環境変数名はバラバラです。
このあたりを良い感じで管理するために、yaml ファイルを作成し、パスワードだけは別ファイルに保存しておく、という方法を検討します。
ディレクトリ構成
Windows上で、Docker Desktop 2.3.0.4 でテストしています。
├── pg-init-scripts コンテナにマウントするディレクトリ
│ ├── create-multiple-postgresql-databases.sh PostgreSQL が実行するシェルスクリプト
│ ├── dbenv.rb mitamae (後述)が参照する rubyスクリプト
│ ├── dbenv.yaml DB の構成を記述した yamlファイル
│ ├── dbpass.yaml DB のパスワード
│ └── mitamae
├── .gitignore
├── dbenv.ps1 環境変数ファイルを生成するスクリプト(Windows環境なので Powershell を使っています)
├── .app1.env アプリの環境変数(自動生成)
├── .app2.env アプリの環境変数(自動生成)
├── .postgres.env postgresの環境変数(自動生成)
└── docker-compose.yaml
version: '3.7'
services:
app1:
container_name: app1
image: app1:latest
depends_on:
- postgres
env_file:
- ./.app1.env # dbenv.yaml, dbpass.yaml から生成します。後述。
ports:
- 8080:8080
app2:
container_name: app1
image: app2:latest
depends_on:
- postgres
env_file:
- ./.app2.env # dbenv.yaml, dbpass.yaml から生成します。後述。
ports:
- 8081:8080
postgres:
container_name: postgres
image: postgres:latest
env_file:
- ./.postgres.env # dbenv.yaml, dbpass.yaml から生成します。後述。
environment:
DB_PARAM: /docker-entrypoint-initdb.d/dbenv.yaml
DB_PASS: /docker-entrypoint-initdb.d/dbpass.yaml
volumes:
# 初期化スクリプトのディレクトリは、ホストと共有
- ./pg-init-scripts:/docker-entrypoint-initdb.d
- data:/var/lib/postgresql/data
ports:
- 5432:5432
volumes:
data:
各DBの名、ユーザー名、環境変数名等を独立した yaml ファイルで管理します。キーの名前は、docker-compose.yamlのサービス名に合わせます。postgres本体のユーザー名もここに記載します。
# postgres自身のユーザー名、パスワード
postgres:
USERNAME:
key: POSTGRES_USER
val: postgres
PASSWORD:
key: POSTGRES_PASSWORD
# app1 の DB名、ユーザー名、パスワード
app1:
DB_NAME:
key: APP1_DB_DATABASE
val: app1_postgres
USERNAME:
key: APP1_DB_USERNAME
val: app1_postgres
PASSWORD:
key: APP1_DB_PASSWORD
# app2 では、ユーザー名がそのままDB名になるという考え方
# 当然、環境変数のネーミングルールもバラバラ
app2:
USERNAME:
key: DATABASE_USERNAME
val: app2_postgres
PASSWORD:
key: DATABASE_PASSWORD
パスワードファイルは別管理です。こちらは、バージョン管理システムにコミットしないように注意しましょう。
postgres: postgres
app1: postgres_pass1
app2: postgres_pass2
mitamae の利用
世の中には、yaml を bash でパースする剛の者もいるようですが、ここでは mitamae を使用します。
mitamae は、mruby で作成されたプロビジョニングツールです。rubyで作成された Itamae が母体となっていますが、シングルバイナリのツールで rubyのインストールは不要です。yaml のパース、テンプレートエンジンの使用などの機能が取り込まれていてサイズが2.15MB(ver1.11.7の時点で)というコンパクトな点も魅力です。
mitamae は、./pg-init-scripts/ に保存しておきます(コンテナ内で実行するのでコンテナの環境に合わせた実行形式をダウンロードする必要があります。記事の環境では、mitamae-x86_64-linuxを使用しています)。DB追加のための rubyスクリプトも同じディレクトリに保存します。
スクリプトの改行コードは、LFであることに注意してください。CRLFで保存しているとヒアドキュメントが動きません。私はWindows環境なのでここで少しハマりました。
param = open(ENV["DB_PARAM"], 'r') { |f| YAML.load(f.read) }
pass = open(ENV["DB_PASS"] , 'r') { |f| YAML.load(f.read) }
param.each_pair do |key, value|
db_name = value['DB_NAME'] ? value['DB_NAME']['val'] : value['USERNAME']['val']
username = value['USERNAME']['val']
password = pass[key]
next if db_name === 'postgres'
execute "create DB #{db_name}" do
command <<-CMD.gsub(/^ */, "")
psql -v ON_ERROR_STOP=1 --username "#{ ENV["POSTGRES_USER"] }" <<-EOSQL
CREATE USER #{username} WITH PASSWORD '#{password}';
CREATE DATABASE #{db_name};
GRANT ALL PRIVILEGES ON DATABASE #{db_name} TO #{username};
EOSQL
CMD
end
end
PostgreSQL が実行するスクリプト本体はこちらです。
#!/bin/bash
set -eu && \
cd /docker-entrypoint-initdb.d && \
./mitamae local ./dbenv.rb
コンテナ生成時に mitamae を使用するというアイデアは、 「Dockerコンテナでの設定ファイル生成にテンプレートとしてERBを使う」を参考にさせていただきました(こちらはイメージ作成時に使っています)。
DB利用側の環境変数ファイルの生成
docker-compose.yaml に記載したように、各コンテナの env_file 欄に指定されたファイルは dbenv.yaml と dbpass.yaml から生成するようにした方が間違いがありません。Windows環境で開発しているので Powershell で書いています(ConvertFrom-Yaml は外部モジュールです。外部モジュールの利用に抵抗がある場合、上記設定ファイルも含めてjsonを使う方法もあります)。mitamaeのダウンロードも、このスクリプトでやっておきましょう。
# postgresのアプリ別のDBのユーザー名、パスワードを、postgres
# 設定用、各アプリ設定用の環境変数を設定するためのスクリプト
# yaml を読み込み、.key名.env のファイル名で環境変数=値の内容を
# 書き込む
$obj = Get-Content '.\pg-init-scripts\dbenv.yaml' -Raw | ConvertFrom-Yaml
$pass = Get-Content '.\pg-init-scripts\dbpass.yaml' -Raw | ConvertFrom-Yaml
$obj.Keys | ForEach-Object {
$o = $_
$first = $true
$content = $obj[ $o ].Keys| ForEach-Object {
if ($first) {
"# 自動生成されたファイルです。gitにコミットしないで下さい。`n"
$first = $false
}
$k = $_
if ($k -eq 'PASSWORD') {
'{0}={1}' -f $obj[ $o ][ $k ]['key'], $pass[ $o ]
} else {
'{0}={1}' -f $obj[ $o ][ $k ]['key'], $obj[ $o ][ $k ]['val']
}
}
$filename = Join-Path (Convert-Path .) ('.{0}.env' -f $o)
[IO.File]::WriteAllLines($filename, $content)
}
# postgres 初期化用のディレクトリに mitamae をダウンロードする。
$mitamae = '.\pg-init-scripts\mitamae'
$url = 'https://github.com/itamae-kitchen/mitamae/releases/latest/download/mitamae-x86_64-linux'
if (!(Test-Path $mitamae)) {
Invoke-WebRequest -Uri $url -Outfile $mitamae
}
実行結果
実行してみましょう。
.\dbenv.ps1
docker-compose up -d
そして、ログを覗いてみましょう。
docker-compose logs -f
ログはこちら
server started
/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/create-multiple-postgresql-databases.sh
INFO : Starting mitamae...
INFO : Recipe: /docker-entrypoint-initdb.d/dbenv.rb
INFO : execute[create DB app1_postgres] executed will change from 'false' to 'true'
INFO : execute[create DB app2_postgres] executed will change from 'false' to 'true'
/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/dbenv.rb
/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/dbenv.yaml
/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/dbpass.yaml
/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/mitamae
waiting for server to shut down....2020-08-xx xx:xx:xx.xxx UTC [46] LOG: received fast shutdown request
2020-08-xx xx:xx:xx.xxx UTC [46] LOG: aborting any active transactions
2020-08-xx xx:xx:xx.xxx UTC [46] LOG: background worker "logical replication launcher" (PID 53) exited with exit code 1
2020-08-xx xx:xx:xx.xxx UTC [48] LOG: shutting down
2020-08-xx xx:xx:xx.xxx UTC [46] LOG: database system is shut down
done
server stopped
PostgreSQL init process complete; ready for start up.
良い感じです。PostgreSQL自身からは、*.sh 以外のファイルは無視されているのが分かります。app1_postgres と app2_postgres が作成されました。