スペースマーケット、宣言的スキーマ管理はじめるってよ
はい、ということでこれまでスペースマーケットでは ActiveRecord マイグレーションを使って差分によるマイグレーションを行ってきたのですが、これからは スキーマのあるべき形を管理 するようにしますよというお話です!
宣言的スキーマ管理とは
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL DEFAULT '',
`name` varchar(255),
PRIMARY KEY (`id`)
)
というユーザーテーブルがあり、ここに年齢のカラムを追加したいとなった場合、ActiveRecordではマイグレーションコマンドにより以下のようなファイルを作る必要があります。
class AddAgeToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :age, :integer
end
end
これに対し、あるべき形を管理できるようになると上記のSQLファイルに1行追加するだけです。
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL DEFAULT '',
`name` varchar(255),
+ `age` int(11),
PRIMARY KEY (`id`)
)
どうです?簡単でしょう?
わたしたちが抱えていたもの
スペースマーケットでは ActiveRecord マイグレーションの他にも 一部のデータベースで Knex.js を利用したマイグレーションを行っており、修正方法やマイグレーション方法が統一されておりません。
マイクロサービスアーキテクチャを採用している場合は各サービスが独立していることもありでマイグレーション方法が異なっていてもよいと思いますが、分散モノリスの構成となっているスペースマーケットではスキーマ管理を統一することで開発効率の向上を図りたいと考えました。
この取り組みによって得たい効果は以下の通りです。
- 最新のスキーマ状態が管理できておらず、どういったカラムがあるのかを把握しやすくする
- ActiveRecord の場合は schema.rb に最新の状態が反映されるのですが、Knex.js の場合は自前で作り込む必要がありデータベースを覗かないとわからないという状態です 😭
- マイグレーションの実行を自動化する
- 残念ながら現在の運用はEC2のインスタンスに入ったあと
rake db:migrate
を打つ必要があり手動運用の手間があります 🤧
- 残念ながら現在の運用はEC2のインスタンスに入ったあと
これらを実現するために sqldef を採用することにしました。
sqldef にした理由
Rails を利用している場合は Ridgepole が第一候補に挙がるケースが多いと思いますが、sqldef 自体が Ridgepole にインスパイアされたツールであることや、わたし自身が以前から sqldef を利用していたということが大きな理由です。
その他にも、
- DDLで管理できること
DSL を覚える必要はもうないのです 😉 - バイナリで動くこと
RubyやNodeなど言語に依存した実行環境を作らなくて済むんです 💪 - 1回の実行で済むこと
差分による方法では、環境構築時に過去のマイグレーションファイルすべて適用する必要があり時間がかかってしまいます 😤
がメリットとして挙げられます。
自動化のための構成
ファイル構成はこのようにしています。
PJ_Root
├ .github
├ deployments: infra構成のterraformコード
├ schema: 各DBのDDLを格納
│ ├ db1.sql
│ ├ db2.sql
│ └ ...
├ scripts
│ └ migrate.sh: マイグレーションを行うbash shell
└ Dockerfile
Dockerfile の ENTRYPOINT
として scripts/migrate.sh
を指定しているため
docker run --rm migate --db={db} [--dry-run]
のように実行が可能です。
システムの構成はざっくりと以下のようになります。
mainブランチに変更をマージすることで GitHub Actions が実行され、sqldef によるマイグレーションが実行されます。
また、 sqldef では --dry-run
オプションを利用することで実行前に差分を確認することができます。
プルリクエスト時はドライランで実行するよにし、この結果をコメントとして貼り付けるようにしています。
デプロイに関するサンプルコードはこちらです。
(必要な箇所のみ抜粋しています)
jobs:
migration:
name: Migration
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# ここまでで Checkout, AWS Credentialの設定, Docker build, ECRへのpush が完了している前提です
- name: Install AWS CLI
id: install-aws-cli
uses: unfor19/install-aws-cli-action@v1
- name: ECSタスクの起動に必要な設定
run: |
echo "TASK_DEFINITION_ARN=$(aws ecs list-task-definitions --family-prefix "${{ vars.FAMILY_NAME }}" --query "reverse(taskDefinitionArns)[0]" --output text)" >> $GITHUB_ENV
echo "NETWORK_CONFIGURATION=awsvpcConfiguration={subnets=['${{ vars.SUBNET_ID }}'],securityGroups=['${{ vars.SECURITY_GROUP_ID }}'],assignPublicIp=ENABLED}" >> $GITHUB_ENV
- name: ECSタスクの実行
run: |
result=$(aws ecs run-task \
--region "ap-northeast-1" \
--launch-type FARGATE \
--cluster ${{ vars.CLUSTER_NAME }} \
--count 1 \
--network-configuration "${NETWORK_CONFIGURATION}" \
--task-definition "${TASK_DEFINITION_ARN}" \
--overrides '{"containerOverrides": [{"name": "db_migration", "command": ["--db=log", "--dry-run"] }]}' \
--query "tasks[0].taskArn" \
--output text)
echo "TASK_ARN=${result}" >> $GITHUB_ENV
- name: 完了するまで待機
run: |
aws ecs wait tasks-stopped --cluster ${{ vars.CLUSTER_NAME }} --tasks ${TASK_ARN}
- name: 終了ステータスの取得
id: check-exit-code
run: |
code=$(aws ecs describe-tasks --cluster ${{ vars.CLUSTER_NAME }} --tasks ${TASK_ARN} --query 'tasks[0].containers[0].exitCode' --output text)
echo "EXIT_CODE=${code}" >> $GITHUB_ENV
# このあとに EXIT_CODE によって成功 ・失敗のslack通知を行う
成功したらこんな通知が飛んできます。
プルリクエスト時はドライランの結果がコメントされるため、エンジニアが想定している差分かどうか確認を行うことが可能です。
上記は1つのデータベースに対してのCI/CDになりますが、実際には複数のデータベースが存在しているため、matrixを使って各DB並列実行を行うようにしています。
まとめ
DDLを管理することでマイグレーションファイルを作らずに済むようになり、スキーマの構成がわかりやすくなりました。
また、データベースごとに異なる方式でマイグレーションしていたものが統一され、CI/CDを整備したことでエンジニアの手間削減へつなげることが可能となりました。
すでに進行中の案件でスキーマ変更プルリクが別ブランチにあるため新方式への切り替えは少し先となりますが、こういった改善活動を一緒にやってあげるよという方はぜひ! 🙋 👇👇👇
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion