🐈‍⬛

Github ActionsでDBマイグレーションを自動化する part3

に公開

こんにちは、株式会社tacoms SREの はぶちん(@modokkin) です。
前回は手動トリガーでDry runを実行できるようにする方法について解説しました。よければ前回の記事もあわせてご覧ください。
Part2の投稿から期間が空いてしまったのですが、その後の運用状況なども含めてお伝えできればと思います。

Part3では、PRへのDry run結果投稿とマイグレーション適用する方法について解説します。

前回までの振り返りと今回の内容

前回までの記事では以下のような内容について触れました。

  • 既存のマイグレーションプロセスで抱えていた課題
  • GitHub Actionsでどのように承認プロセスを実現するか
  • 手動トリガーでGitHub Actionsのワークフローを実行しsql-migrateのDry runを実行できるようにする

今回の記事では次のような対応を行います。

  1. PRが作成・更新された際に自動でDry runを実行し、実行結果をPRにコメントとして投稿します。
  2. Dry runの結果を確認した後、承認されたユーザーが安全にマイグレーションを適用できる仕組みを構築します。

PRを作成したら自動的にDry runを実行し、実行結果をコメントする

前回までにDry runのワークフロー自体は作成できたので、次にPR作成時に自動実行し、実行結果をPRにコメントするようにします。
もし参考にしていただく場合は {} の部分は適宜置き換えてご利用ください。

name: execute-sql-migrate-dryrun
run-name: execute sql-migrate dryrun
on: 
  workflow_dispatch:
  pull_request_target:
    types: # トリガー条件
      - opened 
      - synchronize
    branches: # 対象とするPRマージ先ブランチを指定
      - develop
    paths: # 対象パス、ファイル
      - scripts/migrations/*.sql
concurrency: # 同じPRをトリガーとして複数のワークフローが実行された場合、後勝ちで1つのみ実行する設定
  group: ${{ github.workflow }}-${{ github.event.number }}
  cancel-in-progress: true
env:
  ENV: {ENVIRONMENT}
  ROLE_ARN: 'arn:aws:iam::{AWS_ACCOUNT_ID}:role/{ROLE_NAME}'
  CONTAINER_NAME: {CONTAINER_NAME}
  ECSPRESSO_CONFIG: {ECSPRESSO_CONFIG}

jobs:
  dryrun:

      #--------------中略(Part2の記事と同じ)--------------#

      # sql-migrateのステータス確認とdryrunを実施し、
      # 結果の一部を抽出してGitHub Actionsの出力変数にセットする
      - name: Execute sql-migrate status and dryrun
        id: dryrun
        working-directory: ./deploy/sqlmigrate
        run: |
          set -eou pipefail
          ecspresso run --config=${{ env.ECSPRESSO_CONFIG }} --overrides-file=overrides.json | tee result.txt
          {
            echo 'RESULT<<EOF'
            cut -d ' ' -f 3- result.txt | sed -n '/+------------/,$p' | (head -7 && echo '...' && tail -20)
            echo EOF
          } >> "${GITHUB_OUTPUT}"
        env:
          IMAGE_URI: ${{ steps.meta.outputs.tags }}
          CONTAINER_NAME: ${{ env.CONTAINER_NAME }}
          COMMAND: "./mysql-dryrun.sh"
          ENV: ${{ env.ENV }}
          ARG: ""

  pr_comment:
    if: github.event_name == 'pull_request_target' && always()
    name: Output dryrun result to PR
    runs-on: ubuntu-latest
    needs: dryrun
    permissions:
      pull-requests: write
    steps:
      # PRに対してdryrunの結果をコメントとして出力する
      - name: Post result to PR comment
        uses: mshick/add-pr-comment@v2
        with:
          message: |
            ## Dry run result of sql-migrate
            For the full log, please refer to the GitHub Actions workflow: [${{ env.RUN_ID }}](${{ env.WORKFLOW_URL }})
            
            <details>
              <summary>Result (Click me)</summary>

              ```
              ${{ env.RESULT }}
              ```
            </details>
          message-failure: |
            ## Dry run result of sql-migrate
            Failed to execute sql-migrate. Please check the GitHub Actions workflow: [${{ env.RUN_ID }}](${{ env.WORKFLOW_URL }})
          refresh-message-position: true
          status: ${{ needs.dryrun.result }}
        env:
          RUN_ID: ${{ github.run_id }}
          WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
          RESULT: ${{ needs.dryrun.outputs.RESULT }}

PRにコメントする処理の補足

Dry runを実行する dryrun ジョブと、PRにコメントする pr_comment ジョブ間で複数行の文字列を渡す際に、変数で渡そうとすると文字化けしたり改行コードが消えてしまう問題に遭遇しました。
当初は base64 に変換したりして渡していたのですが、行数が多い文字列を扱う場合はシンプルに一度txtファイルに出力するのが良さそうです。
また、1つのPR内でマイグレーションファイルの修正を繰り返した場合に、複数のDry Run結果コメントが残ってしまいノイズになっていました。
そのため mshick/add-pr-comment@v2 を利用することで一番最後の実行結果だけがPRコメントとして残るようにしました。

PRに投稿されるコメントはこんな感じです。
PRのDry run結果コメント

マイグレーション適用ワークフローについて

マイグレーションを適用するワークフローは、運用プロセスと組み合わせることで安全にマイグレーションを実行できるようにしています。
マイグレーションは次の流れで実行します。

  1. マイグレーションファイルの修正を含むPRを作成し、DB管理者にレビューを依頼(GithubのCode Ownersを利用することで承認者を限定)
  2. DB管理者のレビューが完了したら、DBへのマイグレーションタイミングを調整する
  3. DB管理者がGitHub Actionsのマイグレーション適用ワークフローを手動で実行する
    ※本来はリリース時に常に最新のマイグレーションが自動実行できることが理想なのですが、現状はリスクを考慮してトリガー自体は手動運用としています。

GitHub Actionsのマイグレーション適用ワークフローは以下の通りです。

name: execute-sql-migrate-up
run-name: execute sql-migrate up
on: 
  workflow_dispatch: # 手動実行用の設定。自動実行する場合は条件を追加する。
concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false
env:
  ENV: {ENVIRONMENT}
  ROLE_ARN: 'arn:aws:iam::{AWS_ACCOUNT_ID}:role/{ROLE_NAME}'
  CONTAINER_NAME: {CONTAINER_NAME}
  ECSPRESSO_CONFIG: {ECSPRESSO_CONFIG}

jobs:
  dryrun:
    name: Build and dryrun
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      actions: read
    outputs:
      IMAGE_URI: ${{ steps.meta.outputs.tags }}
      AUTHRIZED_USERS: ${{ steps.get-authrized-users.outputs.AUTHORIZED_USERS }}
      REVISION: ${{ steps.dryrun.outputs.REVISION }}
    env:
      APP_ID: ${{ vars.{GITHUB_APP_ID} }}
      APP_KEY: ${{ secrets.{GITHUB_APP_PRIVATE_KEY} }}
    steps:
      # GitHubアプリの認証トークンを生成する
      - name: Generate a token
        id: generate-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ env.APP_ID }}
          private-key: ${{ env.APP_KEY }}

      # 指定のGitHub組織/チームから許可されたユーザ一覧を取得する
      - name: Get authorized users
        id: get-authrized-users
        run: |
          AUTHORIZED_USERS=$(gh api orgs/{YOUR_ORG}/teams/{YOUR_TEAM}/members --jq '.[].login' | tr '\n' ' ' | sed 's/ $//')
          echo "AUTHORIZED_USERS=$AUTHORIZED_USERS" >> $GITHUB_OUTPUT
        env:
          GH_TOKEN: ${{ steps.generate-token.outputs.token }}

      # ワークフロー実行者が許可リストに含まれているかをチェックする
      - name: Check unauthorized users
        run: |
          for user in ${{ steps.get-authrized-users.outputs.AUTHORIZED_USERS }}; do
            if [ "$user" = "${{ github.triggering_actor }}" ]; then
              exit 0
            fi
          done
          exit 1

      #--------------dryrunワークフローのdryrunジョブと同様なので省略--------------#

      # sql-migrateのステータス確認とdryrun(テスト実行)を行い、最新リビジョン番号を取得する
      - name: Execute sql-migrate status and dryrun
        id: dryrun
        working-directory: ./deploy/sqlmigrate
        run: |
          set -eou pipefail
          ecspresso run --config=${{ env.ECSPRESSO_CONFIG }} --overrides-file=overrides.json
          REVISION=`ecspresso revisions --config=${{ env.ECSPRESSO_CONFIG }} --output="tsv" | tail -1 | awk -F':' '{print $2}'`
          echo "REVISION=$REVISION" >> $GITHUB_OUTPUT
        env:
          IMAGE_URI: ${{ steps.meta.outputs.tags }}
          CONTAINER_NAME: ${{ env.CONTAINER_NAME }}
          COMMAND: "./mysql-dryrun.sh"
          ENV: ${{ env.ENV }}
          ARG: ""

  apply:
    name: Apply sql-migrate
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      actions: read
    needs: dryrun
    steps:
      # ワークフロー実行者が許可リストに含まれているかをチェックする
      - name: Check unauthorized users
        run: |
          for user in ${{ needs.dryrun.outputs.AUTHRIZED_USERS }}; do
            if [ "$user" = "${{ github.triggering_actor }}" ]; then
              exit 0
            fi
          done
          exit 1

      # リポジトリのコードをチェックアウトする
      - name: Checkout
        uses: actions/checkout@v4

      # AWSの認証情報を再設定し、指定ロールを利用する
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ROLE_ARN }}
          aws-region: {AWS_REGION}

      # ecspressoをインストールする
      - name: Install ecspresso
        uses: kayac/ecspresso@v2
        with:
          version-file: ./deploy/sqlmigrate/.ecspresso-version

      # dryrunの結果を確認するために一定時間待機する
      - name: Dryrun confirmation wait time
        run: sleep 120

      # sql-migrateを実行し、マイグレーションを適用する
      - name: Execute sql-migrate up
        working-directory: ./deploy/sqlmigrate
        run: |
          ecspresso run \
          --config={ECSPRESSO_CONFIG} \
          --overrides-file=overrides.json \
          --skip-task-definition \
          --revision=${{ needs.dryrun.outputs.REVISION }}
        env:
          IMAGE_URI: ${{ needs.dryrun.outputs.IMAGE_URI }}
          CONTAINER_NAME: ${{ env.CONTAINER_NAME }}
          COMMAND: "./mysql-env.sh"
          ENV: ${{ env.ENV }}
          ARG: ""

Applyジョブのポイント

Applyジョブの重要なポイントをいくつかご紹介します。

  • 実行者チェック
     Githubの特定のチームに所属しているメンバーだけがこのワークフローを実行できるようにするため、チームメンバー一覧と実行者が含まれているかをチェックしています。dryrun ジョブと apply ジョブでそれぞれ実行しているのは、ワークフローを途中から実行する際にもチェックを行うためです。
  • 待機処理
     Dry run結果の確認用に一定時間(例:120秒)待機することで、万が一問題が見つかった場合はワークフローをキャンセルします。
  • マイグレーション適用
     Dry runと同様にecspresso を用いて、マイグレーション適用を実行します。Dry run実行時との違いは処理を一部上書きすることで -dryrun オプションが外れるようにしているだけです。

まとめ

GitHub Actions と ecspresso を活用して、マイグレーションのレビューフローと適用フローを実現しました。
運用開始にあたっては、この他にもマイグレーション運用フローのドキュメント整備や周知を行い、開発環境から徐々に活用を始め、本番運用へと適用していきました。
最初こそ多少の混乱はありましたが、現在では組織に浸透し安全かつスムーズにマイグレーションを適用できるようになりました。
DBを運用する以上は、マイグレーション運用は切っても切れないプロセスなので、もし同じような課題を抱えている方の参考になれば幸いです。

最後までご覧いただきありがとうございました!

Discussion