pull requestを利用したいい感じのECS feature環境管理方法を考えた

はじめに

SREチームの大木です。スノボの季節がもう終わりかけており、さみしい限りです。

feature staging環境*( 以下 feature環境 )自体のライフサイクルや管理をどうするか問題、なかなかどこも苦労していると思いますが、その中で今回それなりにいい感じの回答を出せたと思うので共有したいと思います。

*呼び方はpre-staging環境、pull request環境、テスト環境などいろいろありそうですが、私たちはfeature環境と呼んでいます。

どこが「いい感じ」なのかというと、PRのラベル付与によって環境の生成/削除を制御できる点です。PR画面上で楽々とfeature環境の管理ができたり、PR一覧からどのブランチでfeature環境が立っているかが分かりやすくなります。

feature環境について

feature環境を当社のプロダクトであるPark Directの開発にも導入しました。各開発者の作業ブランチのコードを、そのままアプリケーションとしてインフラにデプロイすることができるアレです。

Park DirectのアプリケーションはECS serviceで動作しており、Dockerfileやタスク定義ファイルもアプリケーションのリポジトリに格納されていたので、それらをfeature環境でも使用することにしました。

作成はGitHub ActionsをトリガーとしてCloudFormationを用いて行い、作成されるリソースとしてはECS task定義、ECS serviceのほか、ALB target group ( ALB 自体は共有 ) 、その他リソース(後述)があります。

独自ドメインも払い出されるため、そのドメインを使用して他の開発者や非エンジニアもスムーズに確認と検証ができるようになります。

また、通常ではdevelopのDBにそのまま繋いでいるのですが、ECSタスク定義をよしなに書き換えることにより、別のDBに接続することも可能です。

環境の作成方法

この辺が工夫したポイントです。

feature環境はpull request単位で立ち上げる必要がありますが、全てのpull requestで等しく環境が欲しいわけでもなく、またpr自体は長期間オープンにされ続けることもあるので、pull requestのライフサイクルとfeature環境を一緒にするのは難しいものがありました。

そこで、pull requestのラベル付与/剥奪をトリガーにfeature環境作成/削除を行うことにしました。冒頭でも触れたとおり、PR画面上で管理ができるのが嬉しいポイントです。

また、feature環境はFargate Spotを採用することによりコスト圧縮も行っています。

しかし、長時間処理の検証で使いたいなど、サービスの停止が許容できない環境もあるので、付与するラベルによりFargate or Fargate Spotどちらを使用するのか選べるようになっています。

ラベルを付与することで環境が立ち上がる。-ha を付与すると通常のFargateで起動

GitHub Actionsを使用している場合に限りますが、feature環境をpull request のラベルで管理するのは操作もしやすくカスタマイズ性も高いのでオススメです。

また、21時に定期的にfeature環境用ラベルを削除する処理も実装し、feature環境の立ち上げっぱなしを防止しています。

技術的なお話

feature環境の1単位としては以下を作成する必要があります。

  • 独自ドメイン用のRoute53 record
  • ALB target group
  • ECS各種 ( ECS service, ECS task definition)
  • CloudWatch Event rule

ALB自体はfeature環境用を1台用意し、そのALBのリスナールール上でhost名を確認することによってどのfeature環境へアクセスするかを振り分けています。 これにより、1本のALBで複数のfeature環境を管理することができます。

(優先度の重複が認められないので、pull request 番号を使用しています。pull request単位で立ち上がるので重複の心配がなく幸せです)

実際の構築の流れは以下の感じです。

  1. pull requestにラベルが付与される
  2. ラベルの追加 ( or ラベルが追加されたprにpushがあった時) をトリガーでGitHub Actions workflowが発火
  3. ALB listener ruleのpriorityなどの動的な値をパラメータとして、CloudFormation stack作成

作成時のworkflow job発火条件はこんな感じ

    if: >

      (github.event.label.name == '[feature] backend' && github.event.action == 'labeled')

      ||

      (github.event.label.name == '[feature] backend-ha' && github.event.action == 'labeled')

      ||

      (

        (

          contains(github.event.pull_request.labels.*.name, '[feature] backend') 

          || contains(github.event.pull_request.labels.*.name, '[feature] backend-ha')

        )

        && github.event.action == 'synchronize'

      )

CloudFormation stack名をブランチ名とすることで、どのブランチの環境なのかが明確だったり、更新や削除の際にもブランチ名からCloudFormation stackを引っ張ってくれば良いので幸せです。

  delete_resources:

    name: Delete resources

    runs-on: ubuntu-22.04

    if: >

      (

        (github.event.label.name == '[feature] backend' || github.event.label.name == '[feature] backend-ha' )

        && github.event.action == 'unlabeled'

      )

      || (

        github.event.action == 'closed' 

          && (

            contains(github.event.pull_request.labels.*.name, '[feature] backend') 

            || contains(github.event.pull_request.labels.*.name, '[feature] backend-ha')

          )

      )

    permissions:

      contents: read

      id-token: write

    timeout-minutes: 5

    steps:

      - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 commit hash

      - name: Configure AWS Credentials

        uses: aws-actions/configure-aws-credentials@8c3f20df09ac63af7b3ae3d7c91f105f857d8497 # v4.0.0 commit hash

        with:

          role-to-assume: ${{ env.AWS_ASSUME_ROLE }}

          aws-region: ${{ env.AWS_REGION }}

      - uses: ./.github/actions/set-env-variables

      - name: delete stacks

        run:

          aws cloudformation delete-stack --stack-name "pd-backend-feat-${{env.task_id}}"

このように、『各環境とCloudFormation stackが1:1で紐づいて管理ができる』ところがGitHub Actions上で動的に環境を作成/削除する際にとても親和性がよかったです。

また、CloudFormationを使用することによりECS関連以外のリソースも作成することができます。 アプリケーションの都合上本来であれば1 feature環境につき1 Lambda を用意しなければいけない仕様でした。 各環境固有の値はEventで渡せるようにコードを直した feature環境用 lambda を用意し、CloudFormation でEventBridge ruleを作成することで実現しました。 このように ECS 以外のリソースも柔軟に作成することができるのは amazon-ecs-deploy-task-definition や ecspresso などにはない利点なのかなと思います。

終わりに

当時は弊社の他の場所でCloudFormationを使用したことがなかったので採用は迷ったのですが、feature環境の構築というごく一部で、且つ管理対象リソースも少ないことから得られるメリットの方が大きいと判断し採用しました。結果としては、CloudFormation template自体はほとんど変更されることもなく ( = CloudFormationのメンテナンスコストを払うことなく ) 、恩恵だけを得ることができているので採用してよかったです。 また、ラベルでの管理も「このブランチの環境は立っているのかどうか」がすぐ分かり、また簡単に管理ができるのでとても使用感が良いです。

想像以上に使用されたのでfeature環境のコスト削減に取り組む必要があり、夜間停止を導入することになったのですが、その際も「n時以降自動でラベルを剥奪」という workflow を組むことで簡単に対応することができました。 feature環境を管理するにあたって「pull requestのラベルをトリガーとした環境の操作」だったり「CloudFormationを用いた環境の管理」を選択したことでだいぶ幸せになれました。おすすめです。

そして、9月ぐらいからfeature環境を導入したのですが、9月から12月までの数ヶ月間で実に200近くのfeature環境が使用されていました。確実な需要を元に環境を整備しましたが、実際にこうやって使用されているのは嬉しい限りです。