開発環境のデータベースでも本番環境相当のデータを使う

こんにちは。レシピ事業部バックエンド基盤グループの石川です。

2014 年、このブログに『開発環境のデータをできるだけ本番に近づける』というタイトルの記事が投稿されました。

クックパッドでは、ユーザーさんが実際に体験している状況と近い状況を再現しながら開発することに価値があると考えています。技術的には、最初からレコードがたくさんあることによってパフォーマンス問題に気付きやすくなるなどの長所がありますし、サービス開発としても、実際のユーザーさんの体験を最速でなぞって素早くフィードバックループを回せるようになるという長所があります。

この慣習は 2014 年の記事から 10 年経った今でも続いています。一方でその実現手法については変化を続けてきました。現在のクックパッドでは状況に応じていくつかの手段を使い分けています。それらの手段については今まであまり公開されていなかったような気がするため、この記事ではそのうちのひとつをご紹介いたします。

なおこの記事で紹介する手法は社内で数年使っているもので、以前弊社に在籍していた菅原 (@sgwr_dts) が作成し、その後奥村 (@hfm)、鈴木 (id:eagletmt)、小川 (@coord_e)、および自分を含めたチームとして開発してきました。ご承知おきください。

前提

クックパッドでアプリケーションがデプロイされる環境としては本番環境の他に、本番環境へ出す前の検証作業で使う環境(検証環境)があります。これらふたつは AWS 上にデプロイされています。それとは別に開発者ひとりひとりの手元のパソコンでアプリケーションを動かす環境を開発環境と呼ぶことにしましょう。このあたりは文献によって微妙に呼び方が異なるので、この記事ではこれらの呼び方を使うことにします。

クックパッドでは RDBMS として Amazon Aurora MySQL を多用しており、特に cookpad.com を提供するアプリケーション群が使う RDBMS の多くは Aurora MySQL です。Aurora のリリースが 2014 年 11 月らしい ので、早速 10 年前にはなかったものが出てきましたね。

それではここから、Aurora を対象として、定期的に本番環境のデータベースの複製を作って検証環境や開発環境のデータベースとして使うやり方についてご説明します。

Aurora クラスターのリストア

さて、Aurora ではリストアという操作を行うことで既存の DB クラスターを元に同じデータを持った別の DB クラスターを作成できます。Aurora のリストアはそれなりに速く、クラスターの VolumeBytesUsed が 1 テラバイトを超えていても 5 分程度で完了します。そのあと出来上がったクラスターに DB インスタンスをひとつ作る作業に 10 分程度かかるので、そちらの方が長いです。

こうして出来上がったクラスターをそのまま検証環境として使えるとラクなのですが、そのままだとセキュリティやプライバシー上の懸念があります。たとえばユーザーさんのメールアドレスを開発者全員が読めてしまうと問題なわけです。これを避けるため、リストアによって出来上がったクラスターに入っているデータのマスキングを行います。

どのデータをマスクしてどのデータをそのまま残すかは、DB クラスターごとに設定ファイルを作って管理しています。たとえば以下の Jsonnet ファイルのような感じです。この設定では、どのテーブルを残してよいか許可する一覧を持っておいて、それ以外のテーブルは truncate で空っぽにしてしまいます。

local allowed_tables = [
    'recipes',
    'ingredients',
    'users',
    // などなど……
];

{
    database: 'global_main',
    truncate: true,
    only: allowed_tables,
    pre_queries: [],
    rules: [],
    post_queries: [],
}

上の例には書きませんでしたが、特定のテーブルの特定のカラムだけ処理するクエリを流すこともできます。たとえば credentials テーブルの email カラムの値をすべて ${user_id}@example.com のようなダミー文字列に書き換えてしまう、などです。

ただし特定のテーブルのカラムを書き換える処理は、テーブルが巨大な場合は長い時間がかかります。実際にあった例として、すぐ上に書いた credentials テーブルの書き換えはデータ量が小さい場合はうまくいくのですが、本番環境の巨大なデータで試すと一晩経ってもクエリ実行が終わりませんでした。もちろん DB インスタンスのタイプやどういう書き換えをするかで実行時間は変わるので、このあたりは兼ね合いになります。credentials テーブルについては要件の方を変え、全レコードを残すことは諦め、一度レコードをすべて消したあと特定のスタッフユーザーだけ新しくレコードを作る処理を入れて回避しました。

なお、このマスキングの仕組みは社内的には新しいものではなく、2020 年に公開した『Amazon RDS/Auroraをクローンするシステムを作った話』でも同じ仕組みを使っています。設定ファイルも共用しています。こちらは検証環境や開発環境と直接は関係なく、単に複製を作ってクエリパフォーマンスを計測するなどの用途で使うものです。

定期的な実行

この記事の最初の方で定期的に複製を作っていると書いたように、検証環境のデータベースについて、データベースを新しくリストアし古い方と入れ替える作業を、日次のバッチジョブとして実行しています。

この際、Aurora のリストア操作はパラメーターグループなどの設定値を指定しないとデフォルトのものが使われるため、古い方に設定されているものを引き継ぐようにしています。DB ユーザーも改めて作る必要があり、古い方のデータベースのユーザー情報と権限を調べ新しい方に再作成します。

また、検証環境にしかないレコードについても移行処理を行っています。実はリストアした後のデータベースでは各テーブルの主キーの AUTO_INCREMENT を大きな値に設定しています。これによりそれぞれのレコードについて主キーがしきい値より大きい場合は検証環境のみにあるデータだと判断し、古い方のデータベースから新しい方のデータベースへレコードを流し込んでいます。これにより開発者による検証環境での作業をなるべく壊さないようにしています。

外部キー制約がある場合は少しやっかいで、検証環境にのみ存在するレコードの参照している先が本番環境のレコードの場合、参照先のレコードが入れ替えのタイミングで削除されている可能性があります。このような場合どうするかは各チームに任せていて、たとえばとあるチームではこのような外部キーの不整合を見つけて検証環境側のレコードを消してしまう処理を入れています。

古い方と新しい方のデータベースを入れ替えると書きましたが、これは対象のデータベースを参照するための DNS レコードを作り、DNS レコードの参照先を書き換えることで実現しています。これにより接続元が持っている情報は書き換えずにデータベースを入れ替えられます。

クロスリージョン・リードレプリカ

今回のツールで作られたデータベースに対しては、手元の開発環境からでも接続できるように設定しています。弊社では Twingate を利用して開発者が手元からクラウドへ安全に接続できるようにしており、その一環で接続を許可しています。

ここで問題になるのは通信速度です。AWS にあるデータベースに対して、検証環境では同じく AWS にあるアプリケーションとの通信になりますが、開発環境では手元のパソコンで動くアプリケーションとの通信になります。この通信に時間がかかると、ちょっと手元でサーバーアプリを動かしてレスポンスを見ようとしても、複数のクエリを実行するために何回も通信が往復してレスポンスまでの時間が遅くなり、ユーザー体験に乖離が生まれてしまいます。他の環境と比べてクエリ 1 回あたり 100 ミリ秒遅くなったとしても、直列で 10 クエリしていると合計 1 秒遅くなってしまいます。正直まともに確認できません。

特に、目的のデータベースが海を越えた向こう側に位置している場合は厄介です。実際、データベースはアメリカにありつつ開発者は日本に居るといった状況が起こっています。クエリが海の上を行ったり来たりして、しっかり時間がかかります。

そこでそのような場合は、Aurora が備えているリージョンを跨いでリードレプリカを作成できる機能を使っています(クロスリージョン・リードレプリカ)。つまりライターインスタンスはアメリカに置きつつ、リーダーインスタンスは東京にあるものを参照できるようにします。クックパッドのサービスは多くの部分で read heavy なので、開発環境でもそれなりの応答時間で確認できるようになります。

ただし、クロスリージョン・リードレプリカの作成には多少時間がかかります。いま実際に動かしている例では 2 時間弱を要します。1 日は超えていないので許容範囲ではあるのですが、実行失敗時のリトライまで考えると少し面倒な点ではあります。

長所と短所

以上、検証環境や開発環境で本番環境相当のデータを使えるようにするために使っている仕組みのひとつを解説しました。このように本番環境相当の検証ができるようにすることで、サービス開発を高速に進められるようにしています。またここでは説明しませんでしたが、本番環境の DB バージョンを上げたいとき先に検証環境で試せるように、リストアした後に DB バージョンを変更できる機能を入れていたりもしています。

今回の仕組みはマスキングがそれなりに柔軟にできるという意味で扱いやすく、社内では複数のデータベースで利用しています。一方で本番環境に起こった変更の反映は翌日となるため、本番環境でのデータ変更の勢いも再現したいような場合には不向きです。イベントドリブンな部分の検証を受動的には行いづらい、などですね。

リアルタイムに近い状態で本番環境に近いデータを用意したい場合は、MySQL の binlog を直接使った仕組みや、AWS Database Migration Service (DMS) を使った仕組みが考えられます。これらも状況によっては便利なのですが、AUTO_INCREMENT の調整の簡易さ、マスキングの柔軟性、壊れたときのメンテナンスの容易さなどに差異があります。特に何かしらが壊れたときに直すのがやや面倒なため、日次入れ替えで充分な場合はそうするようにしています。

ところで……。途中でデータベースがアメリカにある事例の話をしましたが、一体それはどんな場合なのでしょうか? 詳細はこの記事ではまだ秘密! 後日ご紹介予定ですので、弊社の X アカウントをフォローして更新をお待ちください。

x.com