はじめに
SapeetでSWEをやっている久保田です。
今回はプロダクトの立ち上げに際してDBスキーマ管理にsqldefを採用した経緯と、sqldefをCIへ統合し、RDSへのマイグレーションを簡単に適用するプラクティスを紹介します。
sqldefを使うことで得ることができる最高の開発者体験
特徴
- 宣言的マイグレーション管理ツールであるsqldefは、実際のDBと比較して差分(ALTER等)を適用するためのDDLを自動生成します
- MySQL, PostgreSQL, SQLiteなど様々なRDBMSに対応しています
- 単一ファイル、
create tableだけで完結させることが可能なため、パッと見で最新のテーブル定義を確認できます - 開発中の頻繁な変更に対しても、複雑で面倒なスキーマ管理が不要になります
- DryRunによる実行前に差分を安全に確認したり、オプションで明示的にDropを許可しないと破壊的な変更が実行されないなど柔軟なオプションの選択が可能です
実行例
すでに適用されたスキーマに対してカラムを追加する変更を加え、適用すると以下のような結果が得られます。
今回のプロジェクトでは、PostgreSQLを採用しておりpsqldefを使ってマイグレーションを実行しています。
create table sample (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
↓ カラム追加して再実行すると、差分を適用するためのDDLを生成してくれます
create table sample (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
date DATE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
$ PGPASSWORD="your-db-password" psqldef \
-U "db-user" \
-h "db-hostname" \
-p "db-port" \
"db-name" \
< migration/schema.sql
-- Apply --
BEGIN;
ALTER TABLE "public"."sample" ADD COLUMN "description" text;
ALTER TABLE "public"."sample" ADD COLUMN "date" date;
COMMIT;
実行結果からわかる通り、最新の schema.sql と現在のDBの状態を比較し、必要な差分だけが適用されます。
冪等性が担保されており差分がなければ何も起きないため、何度コマンドを叩いても安全に処理されます。
この宣言的なアプローチにより、複雑なスキーマ変更もシンプルかつ安全に管理でき、開発スピードも両立できる二度美味しいツールです。
CodeBuildへの統合:セキュアでサーバーレスな実行環境
CodeBuildへの統合
データベースには、AWS/RDSを採用しているため、マイグレーションのCI/CD環境としてAWS CodeBuildを使用します。
CodeBuildを採用することで以下の恩恵を得ることも可能です。
- VPC内実行による閉域網アクセス -> CodeBuildをVPC内部で実行するよう構成することで、PrivateSubnet内のRDSへアクセス可能です。これにより、マイグレーションのためだけに踏み台サーバーを用意したりしなくてよくなります
-
IAM認証とSecretsManager/SSMによるクレデンシャル管理 -> DBへのアクセス情報は
Secrets ManagerやAWS Systems Manager Parameter Store (SSM)で一元管理しています。 CodeBuildには、それらの値を読み取るIAM権限のみを付与し、実行時にのみ動的にクレデンシャルを取得することで、セキュアなパイプラインを実現できます
※構成管理にはTerraformを採用しており、SecurityGroupやVPC設定はコード化されていますが、アプリケーション側のパイプラインは最低これだけの記述で完結します -> 本記事ではTerraform側の構成は割愛しています
RDSへApplyする
version: 0.2
phases:
install:
runtime-versions:
nodejs: 22
commands:
- echo "Installing psqldef..."
- wget -q "https://github.com/sqldef/sqldef/releases/download/v3.6.5/psqldef_linux_amd64.tar.gz"
- tar -xzf psqldef_linux_amd64.tar.gz
- chmod +x psqldef
- mv psqldef /usr/local/bin/
- psqldef --version
pre_build:
commands:
- echo "Fetching database connection parameters from SSM Parameter Store..."
# 各DB接続パラメータを個別に取得
- |
export DB_USER=$(aws ssm get-parameter ...
- |
export DB_PASS=$(aws ssm get-parameter ...
- |
export DB_HOST=$(aws ssm get-parameter ...
- |
export DB_PORT=$(aws ssm get-parameter ...
- |
export DB_NAME=$(aws ssm get-parameter ...
build:
commands:
- echo "Running database migration with psqldef..."
- |
PGPASSWORD="$DB_PASS" psqldef \
-U "$DB_USER" \
-h "$DB_HOST" \
-p "$DB_PORT" \
"$DB_NAME" \
< migration/schema.sql
- echo "Database migration completed successfully"
RDSへマイグレーションを実行するために必要なところだけ抜粋していますが、上述のようにバイナリをダウンロードし、クレデンシャルを参照するだけでCodeBuild側の準備も完了します。
あとはこのCodeBuildをキックしてあげれば、sqldef による RDSへのマイグレーションが実行可能です。
Typescriptプロジェクトでも sqldef
今回のプロジェクトは TypeScript × Hono という構成でスタートしました。 このスタックであれば、DrizzleやPrismaといったDBマイグレーションも統合されたORMを採用するのが王道かもしれません。
しかし、最終的には以下の理由からスキーマ管理にsqldef、DBアクセス層にはKyselyという構成を選択しました。
※ Kyselyに関しては後日紹介記事書くかもしれないし、書かないかもしれません。特段取り上げるほどでもなくシンプルで使い勝手の良い軽量コネクション管理&SQLビルダーで構文から吐き出されるSQLが予測可能なので、そういったものが好みな人には向いていると思っています。
ご紹介までにKysely
この構成を取ることで得られる恩恵
- 学習コストの最小化 -> メンバーが増えた際、特定のORM独自のマイグレーションDSLや作法を覚える学習コストは一定かかるものです。このシンプルな構成であれば、学習コストも一定抑えつつ、アプリケーションのシンプルさを保つことも可能です
-
ツールとしてのシンプルさ -> 多機能なORMは便利ですが、ブラックボックスになりがちです。今回はアプリケーション層には軽量なSQLビルダーである
Kyselyを採用し、型安全性を確保しつつも発行されるクエリの透明性を重視しました。同様にスキーマ管理も、宣言的でシンプルな sqldef に任せることで、各レイヤーが単一の責任を持つ疎結合な構成を目指しました。(Kyselyでは、DBから型情報を生成することも可能です)
シンプルでクセのないDBを中心とした開発体験を優先した結果、sqldef は最高のパートナーなりました。
この構成は、複雑になりがちなDBスキーマ管理とチームの認知負荷を抑えつつ、スピード感の求められる開発にも対応することが可能という恩恵を受けることができます。
次の開発のおともに、いかがでしょうか?