🍙

GitHub Actionsを使ったRDSへのDBマイグレーションについて(ECS構成の場合)

2022/07/24に公開

GitHub Actionsを使ったRDSへのDBマイグレーションについて、これまで手探りで実装していましたが、社内のエンジニア(ratmieさん)から情報をもらって改善してみたので、いくつかのポイントをメモとして残しておきます。
前提として、インフラはECSで構築、アプリケーションはTypeScriptでTypeORMを使用しています。

背景

RDSをVPC内のプライベートサブネット内に構築した場合、インターネット経由でのアクセスはできません。そのため、CI/CDの中でマイグレーションを行いたい場合、VPC内でマイグレーションコマンドを実行する、何かしらの手段が必要となります。
これまで以下を試してみていましたが、それぞれデメリットもあり、いまいちしっくりきていませんでした。

  • ECSのアプリ起動コマンドの中でマイグレーションを行う
    → ECSを複数台立ち上げるようにスケールすると、複数回マイグレーションが実行されてしまう
  • CodePipelineを使い、DBマイグレーションはVPC内のCodeBuildから行う
    → GitHub Actionsと比べて構築が複雑&CodeBuildはプライベートサブネット内で実行する必要があるようで、NATゲートウェイの料金がかかる

どうしようかなーと思っていたところ、GitHub Actionsに以下のライブラリがあることを教えてもらいました。

https://github.com/marketplace/actions/run-one-off-task-on-ecs

これを使うと、ECSで設定した構成を再利用して、起動コマンドだけ変更した上で、一度きり(継続して起動しないという意味)のタスクを実行できるようです。そして、この起動コマンドで、マイグレーションを実行すれば解決、というわけです。

ECSの以下の機能を利用していると思われます。確かに、DockerのCMDって上書き可能ですもんね。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs_run_task.html

ということで、この記事では、以下の3点について解説します。

  1. ライブラリ(run-one-off-task-on-ecs)を使用した、マイグレーションコマンドの実行
  2. アプリのデプロイが完了したかどうかを判定するstepの実装
    → マイグレーションコマンドはアプリのデプロイ完了後に実行したいので
  3. 特定のディレクトリに差分があった時のみstepを実行する方法
    → マイグレーションファイルに変更があった時だけマイグレーションコマンドを実行したいため

2つ目の項目は、今回のアプリケーションがamplifyを使ってバックエンドを構築しているために必要な処理なのですが、多くのケースでは不要かもしれません。

1. ライブラリ(run-one-off-task-on-ecs)を使用した、マイグレーションコマンドの実行

ライブラリの存在さえ知ってしまえば、使い方は簡単です。以下の通り記載しました。

- name: Run Migrations
  uses: noelzubin/aws-ecs-run-task@v1.0
  with:
    cluster: ${{ env.CLUSTER_NAME }}
    task-definition: ${{ env.TASK_DEFINITION_NAME }}
    subnets: ${{ env.SUBNET_NAME }}
    security-groups: ${{ env.SECURITY_GROUP_NAME }}
    assign-public-ip: ENABLED
    override-container: express // コンテナ名
    override-container-command: |
      sh
      -c
      yarn migrate

クラスター、タスク定義、サブネット、セキュリティグループ、コンテナ名などは各自設定してください。これらの要素は環境(dev、stgなど)によって変わると思うので、以下を参考に、各環境ごとに環境変数にセットし、参照するようにしました。

https://dev.classmethod.jp/articles/github-actions-workflow-for-each-branch-into-one/

そして、override-container-commandの箇所に、コンテナの起動コマンドを上書きしたい内容で記載します。

また、ここでは詳細記載しませんが、当然、ECSヘのアクセス権限が必要なため、configure-aws-credentialsを使って事前に権限を付与しておく必要があります。

https://github.com/aws-actions/configure-aws-credentials

2. ECSヘのデプロイが完了したかどうかを判定するstepの実装

上記でマイグレーションコマンドを実行する際には、前提として、最新のマイグレーションファイルがECSにデプロイされている必要があります。
今回のアプリケーションでは、上記マイグレーションを行う前のstepで、amplifyを使ってECSへデプロイしているのですが、こちらの処理が非同期であるため、GitHub Action上のデプロイのstepが終わったタイミングでは、ECSへのデプロイは完了していない、という問題がありました。

(参考)以下、ECSへのデプロイのstepです。

- name: Amplify Push // ※このstepが完了した時点ではECSへのデプロイが完了していない
  run: |
    // 略)amplifyコマンドを使ったデプロイ

amplifyでは内部的にCodePipelineを使っており、デプロイのstepが終わったタイミングで、少なくともCodePipelineの処理は開始しているようだったので、CodePipelineのステータスを監視して、ECSへのデプロイが完了しているかを判定するstepを作成しました。

- name: Get CodePipeline status
  run: |
    deploy_sec=0
    INTERVAL=10
    while true
    do
      DEPLOY_STATUS=$(aws codepipeline list-pipeline-executions --pipeline-name ${PIPELINE_NAME} --query 'pipelineExecutionSummaries[0].status')
      echo ${DEPLOY_STATUS}
      if [ "${DEPLOY_STATUS}" = "\"Succeeded\"" ]; then
        exit 0
      fi
      if [ "${DEPLOY_STATUS}" = "\"Failed\"" ]; then
        exit 1
      fi
      if [ ${deploy_sec} -ge 1200 ]; then
        echo Time Out
        exit 1
      fi
      sleep ${INTERVAL}
      deploy_sec=$(( ${deploy_sec}+${INTERVAL} ))
    done

10秒おきに、CodePipelineの実行履歴のうち、最新のもののステータスを取得し、成功or失敗するまでwaitしています。

3. 特定のディレクトリに差分があった時のみstepを実行する方法

マイグレーションコマンドの実行は、マイグレーションファイルに差分があった時のみ実行すればよく、それ以外のタイミングでは実行すると、不必要にGitHub Actionsでの実行時間を増やしてしまいます。

以下ライブラリで、ファイルに差分があるかどうかのチェックができるようです。

https://github.com/marketplace/actions/changed-files

こちらを使用するには、まずチェックアウトの段階でfetch-depthを0か2に設定する必要があります。0は全履歴の取得で、2は直前のコミットも取得できるオプションのようです。ここでは、一旦2を指定します。

- name: Checkout
  uses: actions/checkout@v2
  with:
    fetch-depth: 2 // 追加

そして、ライブラリを使って、差分をチェックします。
filesというオプションを指定することで、特定のディレクトリに差分があったかどうかを判定できます。

- name: Check Changed Files
  id: changed-files
  uses: tj-actions/changed-files@v10.1
  with:
    files: |
      migrations-dir/*.ts

あとは、マイグレーションコマンドを実行するstepで、上記の判定結果を、実行条件に追加します。

- name: Run Migrations
  uses: noelzubin/aws-ecs-run-task@v1.0
  if: steps.changed-files.outputs.any_changed == 'true' // 追加
  (以下省略)

Discussion