pt-online-schema-change の実行が必要かどうか判断するタイミングをより早くした話

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: いわゆる本番環境

開発の流れは以下のようになっています。

  1. develop ブランチから作業用のブランチを作成してコードを変更する
  2. 作業用ブランチを member 環境や dev_staging 環境へデプロイして動作確認する
  3. PR を作成し、レビュー依頼を出す
  4. レビューが OK だったら develop ブランチにマージする
  5. develop ブランチと master ブランチの差分から release PR (release ブランチ) を作成する
  6. release ブランチを staging 環境へデプロイして動作確認する
  7. 動作確認して OK だったら release ブランチを master ブランチにマージする
  8. 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 が必要なことに気付くことができるようになりました。


  1. 以下 pt-osc とします。
  2. 数時間から丸一日以上かかることもある
  3. イメージです。実際に使用している設定はもう少し複雑です。