LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

DB移行を見据えたRidgepoleによる宣言的なスキーマ管理の導入

こんにちは、かたいなかです。

マッハバイトではシステムのオンプレからAWSへの移行を絶賛爆速で進めています。 そのなかでDB移行の下準備としてRidgepoleを導入することにしました。

今回はそのRidgepoleの導入について記事にまとめます。

Ridgepoleとは

github.com

Ridgepoleは、DBのスキーマをRubyのDSLで宣言的に管理するためのツールです。

今までマッハバイトで使用していた、Active Recordのマイグレーションでは「どのような変更を行いたいか」の差分をDSLで書いていました。

Ridgepoleでは、ほしいテーブル構造をDSLで宣言的に定義すると、現在のスキーマ構造から定義した状態にするために必要なALTER TABLE文をRidgepoleが計算して発行してくれます。

具体的には以下のようなDSLのテーブル定義を並べていくことでスキーマを記述します。

create_table "foo", force: :cascade do |t|
  t.string   "bar"
  t.text     "hoge"
  t.text     "fuga"
  t.datetime "created_at"
  t.datetime "updated_at"
end

なぜRidgepoleを導入したのか

マッハバイトではシステムのAWS移行が絶賛進行中です。

Ridgepoleを導入した理由としては宣言的なスキーマ管理の恩恵を受けたいというのももちろんありました。

しかし、このタイミングでの採用に至った一番の理由は、AWS Database Migration Service (AWS DMS)を使った、オンプレDBからAuroraへの移行がスムーズに行えるようにすることでした。

オンプレDBをDMSを使ってAurora移行する上で、大きく以下の2点が問題になっていました。

  • Aurora移行時にテーブル構造を変更する必要がある箇所がある
  • DMSのためにインデックスや外部キーをあとから別々に適用できる必要がある

これらをRidgepoleで解決しようとしました。

AWS移行時にテーブル構造を変更する必要がある箇所がある: Ridgepoleで比較を容易に

DBをAuroraに移す上で、Mroongaのインデックスなどで、そのままの移行が難しい箇所やDMSの都合でスキーマを一時的に変更する必要がある箇所があることがわかってきました。

様々な修正箇所があるなかで、最終的にどのような差分があるかがわかりにくい状態だと、移行手順についてのレビューも難しくなってきます。

これについては、Ridgepoleで宣言的スキーマ管理ができることで、最終的に作りたいスキーマとの差分をGitHubなどで簡単に確認することができます。これにより、DB移行の手順のレビュー可能性を確保することができました。

DMSのためにインデックスや外部キーをあとから別々に適用できる必要がある:Ridgepoleで不要な箇所をコメントアウトする

AWS DMSで移行するにあたり、移行の各フェーズで外部キー、インデックス、トリガーなどが定義されていない状態を経由するのがベストプラクティスとされています。

repost.aws

外部キーやインデックスだけを適用するための長大なDDLを準備して適用する手順では、本当に出来上がったテーブルが正しいスキーマ構造をしているのかに不安が残ります。また、もしスキーマ構造が正しいことの検証ができたとしても、DB移行の検証が進む間もDBの変更を伴うような開発は続いている状態のため、ALTER TABLE文のメンテナンスをしたり、検証を何度も行うのが苦しいです。

Ridgepoleを使うと、最終的にDB移行が終わった状態のスキーマをPRで用意し、そこからさらにインデックスや外部キーなどの定義の部分だけコメントアウトしたPRを準備することで、このような問題をスッキリと解決することができます。

移行の各フェーズで必要な部分以外がコメントアウトされた定義ファイルを適用していくことで、DMSのベストプラクティスに従うことができます。また、コメントアウトを調整するだけなので、開発に伴ってテーブル定義が変化したとしても調整も容易です。

これにより、DMSの都合に合わせてインデックスや外部キーを順番に追加していく手順に関しても、作業手順のレビュー可能性やメンテナンス性を確保することができました。

GitHub ActionsでRidgepoleを動かす

さて、上記のような理由でRidgepoleを採用することにしたのですが、チームで便利に使えるようにするにはCIから動かせるようにしておく必要があります。

今回は、GitHub ActionsのSelf Hosted Runnerを使って動かすことにしました。

  • Pull Requestにスキーマの差分をコメント
  • マージ/タグプッシュ時の実行
  • テーブル削除時の手動実行

Pull Requestにスキーマの差分をコメント

スキーマをコード管理する上で、コードの変更で実際にどのようなALTER TABLE文が発行されるかがPull Request上で見られると内容の確認がしやすく、レビュワーに優しいです。

そこで、Pull Requestのオープンや新しいコミットのプッシュを契機に差分を計算させるようにしました。

差分があると、以下のようなコメントがPull Requestに追加されます。

マージ/タグプッシュ時の実行

実際の実行は、ステージング環境についてはマージ時に、本番環境についてはタグのプッシュを行った際に行うようにしました。

この際、カジュアルにテーブル削除が実行されると危険なため、後述のようにテーブル削除については手動実行が必要となるようにしています。

テーブル削除時の手動実行

テーブルの削除をカジュアルに実行させないようにするため、テーブルの削除については手動で削除フラグを有効にしてジョブを実行した場合のみ行うようにします。

Viewの再作成についても、後述のRidgepoleの制約によりテーブルの変更と同じように行うことができないため、手動実行で行うようにしました。

Ridgepoleを使ってみてよかったこと

エクスポートが楽なため導入しやすい

Ridgepoleはエクスポート機能が優れているため、既存のテーブル構造をすぐにRubyのDSLに変換することができました。

これにより、導入にかかる工数が大きく削減できました。

DBスキーマがGitHub上で確認できるようになった

マイグレーションごとの差分のリストではなく、現在のスキーマの状態がGitHubで管理されるようになったことで、現在のDBスキーマがどうなっているのかが比較的DBを触る機会の少ないメンバーも理解しやすくなりました。

また、技術的負債となってしまっているテーブルのスキーマの定義にコメントを付けたりもしやすくなりました。将来的なDB構造のリファクタリングの際にも大きく役立ちそうです。

RubyのDSLのため、ツールの機能不足を運用でカバーしやすい

Ridgepoleでテーブル構造を定義するために使うSchemafileは、RubyのDSLとして書かれているため、手が届かない箇所をコードを書いて補うことができます。

これにより、Triggerがたくさん使われていたりするDBにもうまく対応することができました。詳しくは「工夫が必要だった点」の「View、Trigger、Function」のところで述べます。

工夫が必要だった点

View、Trigger、Function

Ridgepoleでは、View、Trigger、Functionは管理の対象から外れているため、そのままでは管理することができません。

そこで、以下のようなコードで管理することにしました。

execute(<<-FUNCTION_DEF) do |c|
  CREATE FUNCTION `関数名`(fuga INT)
  略  
FUNCTION_DEF
  c.raw_connection.query(<<-SQL).count.zero?
    SHOW FUNCTION STATUS WHERE `NAME` = '関数名' AND `DB` = 'DB名
  SQL
end

対象のFunction、View、Triggerがなければ作成するというものです。

TriggerやFunctionはめったに変更することがないのですが、Viewに関しては変更の機会が数ヶ月に一回あるため、手動変更でViewを再作成できるようにもしています。

なお、対抗馬でsqldefというツールもあり、ViewやTriggerにも対応しているのですが、こちらは使用している複雑なTriggerのパースに失敗したため、泣く泣く検討から外しました。

pt-online-schema-changeとの相性

DB移行の準備で大きなテーブルの定義を修正する必要があり、pt-online-schema-changeでスキーマを修正する必要がある場面がありました。

外部キーに関して --alter-foreign-keys-method=rebuild_constraints を指定すると、既存の外部キーの名前に _ がプレフィックスとして追加されます。

そうすると、Ridgepoleで外部キーの定義の部分で差分がでてしまいます。外部キーが多いテーブルだと差分がなくなるように調整するのが少し面倒に感じました。

このあたりは良い方法があれば教えてほしいです・・・

対応していないカラムの型

Ridgepoleが geometry 型を含むテーブルに対応していないことも、導入検討時に課題となりました。

今回は、きちんと調べてみると、使われていないテーブルであることがわかったため、大きな問題にはなりませんでした。

まとめ

Ridgepoleを利用することで、現在のスキーマの情報がコード上で宣言的に管理されるようになり、修正を行う際にも見通しよく行うことができるようになりました。

また、日々の開発で便利なだけではなく、DB移行やDBマイグレーションなどの局面でスキーマ管理が宣言的に行われていることが大いに役立つことを感じました。

We are hiring!

マッハバイトではAWS移行に向けて今回紹介したRidgepole以外にも様々な改善がものすごいスピードで進んでいます。

爆速で改善していきたいエンジニアの方はぜひ以下のリンクから!

hrmos.co