Data Migration on Rails ツクリンクの場合
ごきげんよう🙋♀️
ツクリンクでエンジニアリングマネージャーをしているあっきー(@kuronekopunk)です。
Kaigi on Rails 2024に参加してきました。その中で、@ohbaryeさんが話された『Data Migration on Rails』を聞き、自社でのデータ変更のユースケースを整理して考え直しています。
ツクリンクのデータ変更について、以前は少人数で迅速にスクリプトを書き、実行していました。しかし、現在もその運用が残っている部分が多くあります。会社や事業が成長し、責任やエンジニアの数が増えたことで、複数人で安全に運用する重要性が一層高まっています。
※こちらはツクリンク主催の勉強会つくてくトーク #4で話した内容を記事化したものです。
月1回のペースで昼時にオンラインにて定期的に開催しているのでお気軽にご参加ください。
Data Migrationとは
まず、『Data Migration on Rails』を参考に、Data Migrationの前提を確認します。
- データベース全体の移行や変更
- 特定のデータの変更や移行
本記事でもData Migrationは『特定のデータ変更』を対象とします。
出典:https://speakerdeck.com/ohbarye/data-migration-on-rails?slide=3
代表的なアプローチ
ツクリンクでのユースケースでも同じアプローチになるのでこちらも前提を確認しておきます。
- SQLによる直接データ操作
- rails console, rails runner
- db:migrateと同時に実行
- rake task, ruby scriptを実行
- 専用gemの活用
出典:https://speakerdeck.com/ohbarye/data-migration-on-rails?slide=20
ツクリンクでのData Migration
ここからはツクリンクにおけるData Migration(特定のデータ変更)のユースケースを確認し、どのようなアプローチを行っているか紹介します。
代表的なユースケースとして以下の3つがあります。
- DB migrationに伴うデータ変更
- 不具合による不整合データの修正
- CSからの依頼でデータ変更
DB migrationに伴うデータ変更
新しいカラムを作り、既存データから算出できるデータを埋めるケースです。
過去migrationファイル内でデータ変更をしていた
migrationファイルを探してみると、migration内でデータ変更しているファイルを見つけました。
class AddContractOnToContract < ActiveRecord::Migration[6.1]
def change
add_column :contracts, :contracted_on, :date, null: false, default: -> { '(CURRENT_DATE)' }, comment: '契約日'
reversible do |dir|
change_table :contracts do
dir.up { up_process }
dir.down { down_process }
end
end
end
private
def up_process
# 既存のsubscribed_atをcontracted_onに移行する
ActiveRecord::Base.connection.execute('update contracts set contracted_on = cast(subscribed_at as DATE)')
end
def down_process
# ~~ 例外なデータがある場合、通知する処理 ~~
end
end
down
時に想定外のデータがある場合に検知できるように配慮がありますね。
そもそもRailsガイドではmigration内でのデータ操作は非推奨となっています。
データをマイグレーションすると、データベース内でデータが変換されたり移動したりします。Railsでは一般的に、マイグレーションファイルでデータを操作することは推奨されません。
出典:Active Record マイグレーション - Railsガイド
migrationファイル内でデータ変更していたのは、最新が2022年6月のこのファイルだったので、これ以降はmigrationファイル内での変更をしていないようでした。
ステップ分割をしたDB migrationに伴うデータ変更
直近のデータ変更ではFeatureトグルを利用した機能制御と合わせて以下のステップでデータ変更が行われています。
- DB変更のみのdb:migrateを実行
- rake taskでデータ変更
- データに問題がなければfeatureトグルを利用し機能を有効化
前項でmigrationファイル内で行っていたデータ変更をrake taskに切り出し別ステップとして行っています。
また、該当のカラムを利用する機能はFeatureトグルで制御し、安全にデータ変更ができたことを確認後に有効化しています。
不具合による不整合データの修正
不具合や過去データと整合性が取れなくなり想定外のデータが見つかり修正するケースです。
ユーザー影響のある場合は即時対応
データ不整合により500エラーが出るなどユーザーさんに影響がある場合は即座に対応する必要があります。この場合、rake taskの実装からデプロイの時間も惜しいためrails consoleで即時対応をしています。
rails consoleでの実行プロセス
@ohbaryeさんの資料にもあるようにrails consoleでの実行はミスなどの危険があります。ここを最小化するためPRと同じくエンジニア同士のレビューを必須にしています。
また、実行ログを詳細に残せるようにスクリプト内にLoggerを入れ該当データを何から何に変更したのかを出力するようにしています。
rails consoleでのデータ変更フロー
- ログ出力を考慮しスクリプト作成
- エンジニアレビュー
- スクリプト実行
直接のユーザー影響のないデータ不整合の場合はrake taskで対応
直接の不具合は出ないもののデータ不整合の場合はrake taskで安全にデータ変更をしています。
こういった不整合が発見されるケースとしては、データ分析でイレギュラーデータを見つけたり、エンジニアが他の調査をしているときに偶然見つけることもあります。
CS(カスタマーサポート)からの依頼でデータ変更
ツクリンクではユーザーさんのサポートで情報変更をするための管理画面があります。ですが管理画面も完璧ではなく変更できないデータも存在します。そういったデータ変更をCSから依頼され変更するケースではエンジニアがrails consoleでデータ変更を行っています。
依頼されるユースケースを整理し、管理画面への機能化などは進めていますが、まだできていないものの対応がメインになります。
rails consoleで実行
機能として存在しないものはエンジニアがスクリプトを作りconsoleで実行することがあります。
その場限りで終わらせないため対応のドキュメントを作り再利用可能にしています。
※ドキュメントの概要
- 依頼内容、概要
- 再利用可能な対応スクリプト
- 機能化するためのチケット
ドキュメントを作成したら、ドキュメントとスクリプトを他のエンジニアにレビューしてもらいスクリプトを実行しています。
※「再利用可能な形で対応スクリプトを作る」のイメージ
def cs_irai(id)
# 対象IDのデータに対して何かしらする処理
end
cs_irai(1)
ドキュメントとスクリプトのレビューが終わっていることで機能化されるまでの2回目以降の変更依頼は cs_irai(1)
のIDが適切であることだけで担保されています。
rake taskで実行
管理画面への追加が難しい場合、一旦対応スクリプトをrake task化しています。
rake taskでテストも用意されているためrails consoleより安全に実行できるようになっています。
ですがrails consoleと同じく対象IDの間違いなどは起こり得る可能性もあるため注意が必要です
namespace :cs_irai_task do
desc "CSから依頼された何かのタスク"
task :run, [:id] => :environment do |_task, args|
# 対象IDのデータに対して何かしらする処理
maintenance_tasksを試してみた
@ohbaryeさんのスライドやRailsガイドでも紹介されているmaintenance_tasksを軽く試してみました。
maintenance_tasksを試してみたところ、インストールが簡単で即座に使える点が魅力的でした。また、app/tasksにタスクが入るため、lib/tasksよりも直感的で扱いやすいと感じました。
管理画面でスクリプトの内容や実行結果を確認できるのも便利です。
タスクのフォーマットが決まっているので、チームメンバーが増えても統一した書き方ができ、引数やバリデーションも簡単に実装できました。
詳細はこちらのスクラップにあります。
maintenance_tasksで気になるところ
細かいところは未検証ですが気になっているところがあります。
権限管理を別の場所でしなければいけない
管理画面をmountしていますがこのままでは誰でもアクセス可能になるため制御が必要そうでした。
mountする際に認証を挟む感じでしょうか。
Jobをキックするのには使いやすそう
ビジネスロジックがtaskに入るのは極力避けたいと考えたのでJobをキックするだけのtaskなどは利用しやすい気がしました。
module Maintenance
class NoCollectionTask < MaintenanceTasks::Task
no_collection
def process
SomeAsyncJob.perform_later
end
end
end
さいごに
ここ2年ほど開発から離れていたのでmaintenance_tasksの検証で実際にコードを触れることができて嬉しかったです。maintenance_tasksは実際に小さく試してみたいと考えています。
今後も新たな知見を積極的に取り入れ、検証を重ねて、安全で最適な方法へと改善していきたいと考えています。
こちらはツクリンク株式会社のアドベントカレンダーの記事でした。
ぜひ他の記事も読んでいただけると嬉しいです🎉
Discussion