本番環境のデータを別環境に差分コピーする、Gamma というRuby製のツールを作りました。
背景
- 本番と同等のデータで別環境(Staging/Dev環境) でもテストを行いたい
- 同等のデータで動作確認することで、バグやパフォーマンス問題の早期発見ができる
- 開発環境のデータをできるだけ本番に近づける と同じ認識を持ってます
- 本番データはS3 と RDS(mysql) に存在している
- 本番にはセンシティブな情報が含まれているため、データマスクを施した上で別環境にコピーする必要がある
- S3は本番とStaging/Dev環境で別バケットを利用している
- S3のデータは単純に別バケットに全コピーするだけでは動かない
特にS3のデータの扱いが厄介でした。S3のデータは当初バケットのデータを全コピーして開発環境のS3に移すつもりでした。
しかし、私が今関わっているプロジェクトだとS3のデータの扱いが厄介でした。
具体的にはファイルアップロードに [Paperclip] (https://github.com/thoughtbot/paperclip) の hash を使っている(かつ、本番と開発環境ではhash secretが異なる)関係で、本番環境と開発環境では全く同じデータを使ってもデータのpathが変わってしまうという、とてもめんどくさい問題に直面してました。
よって、データを本番環境から開発環境にコピーするタイミングで、S3上のデータを本番S3から開発S3にパスを書き換えてコピーする必要がありました。
既存ツールの調査
mysqldump
手軽に本番データを開発環境に入れる方法としては、本番データをmysqldumpして開発環境にimportする方法があります。
しかし、
- データマスクは別スクリプトで実施する必要がある
- S3のデータは別スクリプトでコピーする方法を考えないといけない
という問題があり、どうやってマスクしたらいいのかとか、S3のデータは毎回全コピーし直すわけにはいかないので差分コピーをどうやってやろうとか、色々考えることが多すぎてこの手は諦めました。
S3のバケットは本番を見る かつ Paperclip Hash Secret も本番のものを使うということも考えました。
しかし、開発環境で常に本番に接続するのはうっかりミスで本番データを壊す可能性もあり危険なので、この方針も諦めました。
Embulk
Embulk を使うと、本番環境から別環境に手軽にデータをコピーできそうです。
embulk-filter-mask 等のプラグインを利用すれば、データのマスクも実施できます。
しかし、
- S3のデータを差分コピーする方法を別途考えないといけない
- embulkのプラグインを別途作る?
など、やはりS3のデータコピーの方法は考えなければいけませんでした。
また、マスクスクリプトやコピースクリプトはRubyで柔軟に書きたいという思いもありました。
ツールに求める要件
本番データをコピーする際にやりたいことは以下のとおりです。
- 差分コピーできる
- レコードをマスクしてコピーできる
- レコードコピーのタイミングでフックを掛けて別スクリプトを呼び出せる
- (マスク用のスクリプトはRubyで書きたい)
Gammaの紹介
DBのレコードを差分コピーしつつ、行コピーのタイミングでフックを掛けられる、Gamma
というツールを作りました。
フックスクリプト内で値をマスクしたり、ファイルコピーなどの処理ができます。
今はまだmysqlにしか対応してません。また、同期するテーブルにはidというカラムが無いと動かないです。
(※ これらの点は今後改修が必要と認識しています)
基本的な使い方
まずはテーブルを単純に全コピーする方法を紹介します。
gammaのインストールは以下コマンドで行えます。
gem install gamma
次にmysqlに以下テーブルを用意します。DBはコピー元とコピー先の両方を用意しておきます。
CREATE TABLE `ar_internal_metadata` (
`key` varchar(255) NOT NULL,
`value` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `products` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`image_path` varchar(512) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`email` varchar(255) DEFAULT NULL,
`encrypted_password` varchar(255) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
次にgammaの設定ファイル settings.yml を用意し、コピー元DBとコピー先DBの設定をします。
# settings.yml
in_database_config:
adapter: mysql2
encoding: utf8
database: gamma_production
pool: 5
host: localhost
username: root
password:
out_database_config:
adapter: mysql2
encoding: utf8
database: gamma_development
pool: 5
host: localhost
username: root
password:
次に、どのテーブルをどのようにコピーするかを設定したファイル data.yml を用意します。
# data.yml
- data:
table:
- "*"
table_without:
- "ar_internal_metadata"
mode: "replace"
delta_column: "updated_at"
table に * 指定をすると、DBにある全てのテーブルが対象となります。
その場合、table_without にコピーしたくないテーブル名を記載することが可能です。
今回は products と users テーブルが同期されることとなります。
mode は 今のところ replace しか対応していません。これはいわゆるupsert(レコードがなければ新規作成、存在すれば更新)となります。
delta_column は差分コピーが必要かを判定するカラムを指定します。updated_at の値がコピー元先で異なる場合に差分コピーが走ります。
まずは gamma dryrun を走らせてみます。Dryrun することで、実際にどのように同期されるかのSQLが出力されます。
gamma dryrun --settings settings.yml --data data.yml
実行結果は以下のようになります。
I, [2018-03-11T19:02:54.482910 #81194] INFO -- : [replace] Sync Start products
I, [2018-03-11T19:02:54.612671 #81194] INFO -- : DRYRUN: INSERT INTO products(`id`,`name`,`image_path`,`created_at`,`updated_at`) VALUES ("9","production_product_8","/test/image/8.png","2018-03-11 06:13:26","2018-03-11 06:13:26")
I, [2018-03-11T19:02:55.518998 #81194] INFO -- : [replace] Sync Start users
I, [2018-03-11T19:02:55.633015 #81194] INFO -- : DRYRUN: UPDATE `users` SET `id` = "12",`email` = "test_email_11@example.com",`encrypted_password` = "6512bd43d9caa6e02c990b0a82652dca",`created_at` = "2018-03-11 06:13:23",`updated_at` = "2018-03-11 06:13:23" WHERE id = 12
I, [2018-03-11T19:02:55.633129 #81194] INFO -- : DRYRUN: INSERT INTO users(`id`,`email`,`encrypted_password`,`created_at`,`updated_at`) VALUES ("14","test_email_13@example.com","c51ce410c124a10e0db5e4b97fc2af39","2018-03-11 06:13:23","2018-03-11 06:13:23")
実際にデータをコピーするには gamma apply を利用します。
gamma apply --settings settings.yml --data data.yml
I, [2018-03-11T18:42:44.713544 #72090] INFO -- : [replace] Sync Start products
I, [2018-03-11T18:42:45.722601 #72090] INFO -- : [replace] Sync Start users
レコードコピーの際にフックをかける
レコードをコピーする際に、特定のカラムの値を書き換えたり、別スクリプトを走らせたりするhookをかけることができます。
以下のような data.yml を用意します。
- data:
table:
- "products"
mode: "replace"
delta_column: "updated_at"
hooks:
- row:
scripts:
- "hooks/copy_image.rb"
- data:
table:
- "users"
mode: "replace"
delta_column: "updated_at"
hooks:
- column:
name:
- "email"
scripts:
- "hooks/mask_email.rb"
hook用のスクリプトを以下2つ用意します。
# hooks/copy_image.rb
# hooks の row指定をすると、特定行全てが含まれた値がhookスクリプトに渡されます
# apply は dryrunのときには false となります
# record には DBのレコードが入っています
class CopyImage
def execute(apply, record)
if apply
# ここに画像コピーする処理を記載する
puts "Copy Image. path: #{record["image_path"]}"
else
puts "[DRYRUN] Copy Image: #{record}"
end
record
end
end
# hooks/mask_email.rb
# hooks の column 指定をすると、対象行の column名 と value がhookスクリプトに渡されます
# apply は dryrunのときには false となります
class MaskEmail
def execute(apply, column, value)
result = value.split("@")[0]
result = "#{result}@example.com"
unless apply
puts "[DRYRUN] #{column} #{value}"
end
result
end
end
この状態で dryrun してみます。
gamma dryrun --settings settings.yml --data data.yml
実行結果は以下のとおりです。
I, [2018-03-11T18:51:35.611121 #76155] INFO -- : [replace] Sync Start products
[DRYRUN] Copy Image: {"id"=>686, "name"=>"production_product_685", "image_path"=>"/test/image/685.png", "created_at"=>2018-03-11 06:13:27 +0900, "updated_at"=>2018-03-11 02:13:27 +0900}
I, [2018-03-11T18:51:35.726459 #76155] INFO -- : DRYRUN: UPDATE `products` SET `id` = "686",`name` = "production_product_685",`image_path` = "/test/image/685.png",`created_at` = "2018-03-11 06:13:27",`updated_at` = "2018-03-11 02:13:27" WHERE id = 686
I, [2018-03-11T18:51:36.618468 #76155] INFO -- : [replace] Sync Start users
[DRYRUN] email test_email_0@example.com
I, [2018-03-11T18:51:36.739384 #76155] INFO -- : DRYRUN: UPDATE `users` SET `id` = "1",`email` = "test_email_0@example.com",`encrypted_password` = "cfcd208495d565ef66e7dff9f98764da",`created_at` = "2018-03-11 06:13:23",`updated_at` = "2018-03-11 13:13:23" WHERE id = 1
[DRYRUN] email test_email_1@example.com
I, [2018-03-11T18:51:36.739654 #76155] INFO -- : DRYRUN: UPDATE `users` SET `id` = "2",`email` = "test_email_1@example.com",`encrypted_password` = "c4ca4238a0b923820dcc509a6f75849b",`created_at` = "2018-03-11 06:13:23",`updated_at` = "2018-03-11 13:13:23" WHERE id = 2
productsのレコード同期の際に、hooks/copy_image.rb が実行されています。
また、users.email のレコードは hooks/mask_email.rb にてマスクされた値となっています。
apply すると以下のような実行結果となります。
gamma apply --settings settings.yml --data data.yml
I, [2018-03-11T18:52:32.250442 #76616] INFO -- : [replace] Sync Start products
Copy Image. path: /test/image/685.png
I, [2018-03-11T18:52:33.268832 #76616] INFO -- : [replace] Sync Start users
サンプルスクリプト
Gamma-example に動作確認できるサンプルスクリプトを置いています。
まとめ
本番環境のデータを手軽に差分コピーできるツールを作成しました。
コピーのタイミングでフックスクリプトを走らせたり、フック内で値を書き換えられるようなツールを作りました。
まだまだ改善する余地の多いツールですが、ひとまずこのツールで本番データをStaging環境等に手軽にコピーできています。