Repro では Aurora MySQL を使用しています。いくつか数千万行を越えるデータを持つ大規模なテーブルもあります。 大規模なテーブルのスキーマを変更するときは pt-online-schema-change1 を使用していますが、今回はその必要性を判断するタイミングを早めた話です。
pt-osc が必要になる理由等は次の記事が詳しいです。 - pt-online-schema-changeの導入時に検討したこと、およびRailsアプリとの併用について - freee Developers Hub
解決したい課題
Repro では Rails アプリケーションが管理画面や API を提供しています。これらについて、目的別に複数の環境を用意しています。
- member: 主に管理画面の動作確認目的で開発者が自由に使ってよい環境
- いくつかのミドルウェアは dev_staging と共用のためある程度制約がある
- dev_staging: 開発者が自由に使ってよい環境
- 使う際はロックを取得するので、一人しか使えない
- staging: production 環境へデプロイする前に最終的な動作確認をする環境
- production: いわゆる本番環境
開発の流れは以下のようになっています。
- develop ブランチから作業用のブランチを作成してコードを変更する
- 作業用ブランチを member 環境や dev_staging 環境へデプロイして動作確認する
- PR を作成し、レビュー依頼を出す
- レビューが OK だったら develop ブランチにマージする
- develop ブランチと master ブランチの差分から release PR (release ブランチ) を作成する
- release ブランチを staging 環境へデプロイして動作確認する
- 動作確認して OK だったら release ブランチを master ブランチにマージする
- master ブランチを production 環境にデプロイする
初期は、チェックリストを人間が確認して pt-osc に必要性を判断する運用でした。しかし、判断ミスや確認漏れがありました。 その後、判断ミスや確認漏れをなくすため、 staging 環境にデプロイしたタイミングで pt-osc の必要性を判断する専用の Rake タスクを自動的に実行するようにしました。
先にも書いたとおり staging 環境は最終的な動作確認をする環境なので、ここにデプロイしてから production 環境にデプロイするまでの時間は多くの場合、一時間以内です。pt-osc の実行は頻繁に発生するものでもないので、作業経験があり、作業手順がまとまっていても準備や実行には時間がかかります2。
このように staging 環境にデプロイしたタイミングで pt-osc が必要だと通知されても短時間での実行は難しいです。
そこで、より早いタイミングで pt-osc の必要性を判断して通知するようにしたいと考えました。
実現方法
pt-osc の必要性を判断するタイミングの候補はいくつか方法を考えられます。
- dev_staging 環境にデプロイしたタイミング
- CI を実行するタイミング
- PR を作成した後 CI を実行するタイミング
dev_staging 環境にデプロイしたタイミングだと production 環境にアクセスするための権限を既存のロールに付与する必要があって面倒でした。今回は CI の一部で実行することにし、 PR にコメントを残したかったので「PR を作成した後 CI を実行するタイミング」で pt-osc の必要性を判断することにしました。
Repro では CI の実行に CircleCI を利用しているので workflow は以下のようになっています3。
workflows: version: 2 commit-workflow: jobs: - build # CI 用の docker image をビルドして CI を実行する - build-arm64 # arm64 用の docker image をビルドする。CI を実行していないので build より実行時間が短い - check-pt-osc # pt-osc の必要性をチェックする requires: - build-arm64 filters: branches: ignore: - master - develop - release/*
各ジョブの定義は以下のようになっています。
jobs: build: steps: - checkout - run: bundle install -j3 --retry=3 - run: bundle exec rake ci:docker_push_and_spec build-arm64: steps: - checkout - run: bundle install -j3 --retry=3 - run: bundle exec cap test docker:push check-pt-osc: steps: - checkout - run: bundle install -j3 --retry=3 - run: name: check pt-osc necessity command: | export GIT_SHA1=${CIRCLE_SHA1} if [ $(git diff origin/develop...HEAD --name-only | grep -E "\.schema$" | wc -l) -gt 0 ]; then bundle exec wrapbox ecs run_cmd -f ./config/wrapbox.yml -n check_pt_osc_necessity --timeout 600 --execution-retry 3 else echo "No changes to check pt-osc necessity" fi
ポイントは以下の2つです。
- workflows の filters で開発用のブランチでのみ check-pt-osc ジョブを実行している
- スキーマに変更があった場合のみ Rake タスクを実行する
ちなみに Rake タスクは wrapbox 経由で実行しています。wrapbox は Repro で開発している YAML で書いた設定に従って ECS タスクを簡単に起動するためのツールです。
wrapbox の設定は ERB を通してから解釈されるので環境変数を埋め込んだり、環境変数の値によって一部の値を動的に変更したりできます。
check_pt_osc_necessity: cluster: repro-ci-worker region: ap-northeast-1 execution_role_arn: "arn:aws:iam::123456789012:role/CheckPtOscNecessityTaskExecutionRole" task_role_arn: "arn:aws:iam::123456789012:role/CheckPtOscNecessityTaskRole" network_mode: "awsvpc" network_configuration: awsvpc_configuration: subnets: ["subnet-1a", "subnet-1b", "subnet-1c"] security_groups: ["sg-xxx"] cpu: 512 memory: 1024 capacity_provider_strategy: - capacity_provider: FARGATE weight: 1 base: 1 container_definitions: - image: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/repro/main:<%= ENV["GIT_SHA1"]&.chomp %>-test-amd64 cpu: 512 memory: 1024 essential: true environment: - {name: "TZ", value: "UTC"} - {name: "RAILS_ENV", value: "production"} - {name: "PULL_REQUEST_URL", value: "<%= ENV["CIRCLE_PULL_REQUEST"] %>"} secrets: - {name: "GITHUB_APPS_APP_ID", value_from: "/path/to/github/app_id"} - {name: "GITHUB_APPS_PRIVATE_KEY", value_from: "/path/to/github/private_key"} - {name: "DATABASE_HOST", value_from: "/path/to/database/host"} - {name: "DATABASE_USERNAME", value_from: "/path/to/database/username"} - {name: "DATABASE_PASSWORD", value_from: "/path/to/database/password"} entry_point: ["prehook", "bundle exec ruby docker/setup.rb", "--", "bundle", "exec", "rake"] command: ["db:check_pt_osc_necessity"] log_configuration: log_driver: awslogs options: "awslogs-group": "/ecs/rails-ci" "awslogs-region": "ap-northeast-1" "awslogs-stream-prefix": "check-pt-osc-necessity"
専用の IAM ロールで ECS タスクを起動します。CircleCI から呼ぶので CircleCI で使用している IAM ロールから iam:PassRole できるようにしておきます。 専用の IAM ロールを使うことで、最小の権限でタスクを実行するようにしています。
Rake タスク db:check_pt_osc_necessity
は以前から使用していたものがあったので、ほぼそのまま使いました。
pt-osc の必要性を判断する部分では以下のようなことをしています。
db:ridgepole:diff
の結果をパースして DDL 実行対象のテーブル名を抽出する- INFORMATION_SCHEMA.TABLES から↑で取得したテーブルの行数を取得する
- テーブルの行数がしきい値以上であれば pt-osc が必要と判断する
- PR にコメントを書き込む
- Slack に通知する
細かい調整として PR のコメントは最新の1つだけ残すようにし pt-osc が必要だとコメント済みの場合は Slack には通知しないようにしました。
まとめ
pt-osc の必要性を判断するタイミングが開発用 PR を作成するタイミングになり、これまでよりもかなり早いタイミングで pt-osc が必要なことに気付くことができるようになりました。