8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

本番環境のデータを別環境に差分コピーするRuby製ツールを作りました

Posted at

本番環境のデータを別環境に差分コピーする、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環境等に手軽にコピーできています。

8
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?