👋

GitHub ActionsでECSのPR環境を作る

に公開

はじめに

久しぶりにAWSを触る機会があったので、キャッチアップも兼ねて、ECS/Fargateを使ったPR環境(PRごとに独立した環境)を構築するワークフローを試しに作ってみました。
CloudRunやVercelのようにPR環境を簡単に作れるサービスもありますが、ECSではそのような機能がないため、自前で実装する必要があります。
この記事では、GitHub ActionsとTerraformを使ってPR環境を構築する流れを雑にまとめます。

前提

今回はとりあえずミニマムに検証したかったので、以下のようなシンプルなインフラ構成を作って検証してみました(面倒なのでデータストアは省略しています)。

  • Route53
    • PR環境ごとにサブドメインを発行
  • CloudFront
    • 代替ドメインにワイルドカードを指定して、PR環境ごとのサブドメインを利用可能にしている
  • ALB
    • VPC Originとしてプライベートサブネットに配置
  • ECS
    • Nginxをデプロイ

構成図

上記の構成に対して、PRごとにGitHub ActionsとTerraformを使って以下のリソースを作成します。

  • Route53のAレコード
    • PR環境のドメインのAliasレコードを作りCloudFrontに紐付け
  • ALBのターゲットグループ
    • PR環境のECSサービスに紐付けるやつ
  • ALBのリスナールール
    • ホストヘッダベースのルールで、PR環境のドメイン宛のリクエストをPR環境のターゲットにルーティングする
  • ECSサービス
    • PRブランチのコードをデプロイして動かすECSサービス

PR環境構成図

PR環境作成用のTerraformディレクトリ構成

PR環境用のリソースを定義するTerraformコードを以下のように配置しています。

terraform/pr-env/
├── main.tf          # PR環境作成のためのリソースを定義
├── pr-env.tfbackend # ステートファイルの保存先を指定
├── pr-env.tfvars
├── providers.tf
└── variables.tf

各PR環境のリソースを単一のステートファイルで管理するのは面倒なので、PR環境ごとにステートファイルを分けるためにステートファイルの保存先を動的に切り替えられるようにしています。

具体的には、以下のようにpr-env.tfbackendファイル内のkeyの値にプレースホルダー(PR_NUMBER)を設定し、GitHub Actionsの処理内でTerraformで各PR環境を構築する際にプレースホルダーをPR番号に置換するようにします。

bucket         = "xxxx"
key            = "PR_NUMBER/terraform.tfstate"
encrypt        = true
region         = "ap-northeast-1"
use_lockfile   = true

GitHub Actionsのワークフロー

PR環境作成用ワークフロー (pr-env-create.yml)

PRにpr-deployラベルを付与、またはラベル付与後にコードをpushすると以下の処理を実行するようにしています。

  1. コンテナイメージのビルドとプッシュ
    PRのコードを元にDockerイメージをビルドし、Amazon ECRにプッシュします。

  2. ECSタスク定義の登録
    ビルドしたDockerイメージを使用してECSタスク定義を登録します。

  3. TerraformでPR環境用リソースを作成
    Terraformを使ってPR環境用のリソースを作成します。

  4. ECSサービスにデプロイ
    ecspressoを使ってECSサービスにデプロイします。

上記の処理のうち、ポイントについて以下で解説します。

ワークフローのトリガー

pr-deployラベル付与とラベル付与後のpushをトリガーにするために、トリガーは以下のように設定しています。

pr-env-create.yml
on:
  pull_request:
    types: [opened, synchronize, labeled]

また、各ジョブの実行条件を以下のように設定することで、ラベル付与時とラベル付与後のpush時ににワークフローが実行されるようにしています。

pr-env-create.yml
if: contains(github.event.pull_request.labels.*.name, 'pr-deploy') && (github.event.action == 'labeled' || github.event.action == 'synchronize')

TerraformでPR環境用リソースを作成

pr-env.tfbackend内のPR_NUMBERをPR番号に置換します。

pr-env-create.yml
      - name: Replace PR_NUMBER
        working-directory: terraform/pr-env
        run: |
          sed -i -e "s/PR_NUMBER/${{ github.event.pull_request.number }}/g" pr-env.tfbackend

その後、Terraformのinit実行時に-backend-configオプションでpr-env.tfbackendを指定してapplyすることでPR環境ごとにステートファイルを独立させています。

pr-env-create.yml
      - name: Terraform Init
        working-directory: terraform/pr-env
        run: terraform init -backend-config=pr-env.tfbackend

      - name: Terraform Apply
        working-directory: terraform/pr-env
        run: |
          terraform apply \
            -auto-approve \
            -var-file=pr-env.tfvars \
            -var "pr_number=${{ github.event.pull_request.number }}" \
            -var "ecs_cluster_name=${{ env.ECS_CLUSTER_NAME }}" \
            -var "ecs_task_definition_arn=${{ needs.build.outputs.task_definition_arn }}" \
            -var "ecs_service_name=${{ env.ECS_SERVICE_NAME }}" \
            -var "ecs_container_name=${{ env.ECS_CONTAINER_NAME }}"

PR環境削除用ワークフロー (pr-env-destroy.yml)

PRのpr-deployラベルを外す、またはPRをクローズすると以下の処理が実行されます。

  1. TerraformでPR環境用リソースを削除
    Terraformを使ってPR環境用リソースを削除します。

ポイントはPR環境作成の時とほぼ同じですが、以下で解説します。

ワークフローのトリガー

以下の条件でワークフローが実行されます。

pr-env-destroy.yml
on:
  pull_request:
    types: [unlabeled, closed]

また、各ジョブの実行条件を以下のように設定することで、ラベル除外時とPRクローズ時にワークフローが実行されるようにしています。

pr-env-destroy.yml
if: >
  github.event.label.name == 'pr-deploy' && github.event.action == 'unlabeled' ||
  github.event.action == 'closed'

TerraformでPR環境用リソースを削除

PR環境用リソース作成時と同じように、pr-env.tfbackend内のPR_NUMBERをPR番号に置換し、PR環境ごとにbackendを切り替えるようにしています。

pr-env-destroy.yml
      - name: Replace PR_NUMBER
        working-directory: terraform/pr-env
        run: |
          sed -i -e "s/PR_NUMBER/${{ github.event.pull_request.number }}/g" pr-env.tfbackend

上記のbackendを指定して、Terraformのinitdestroyを行うことでPRに紐づくPR環境を削除するようにしています。

pr-env-destroy.yml
      - name: Terraform Init
        working-directory: terraform/pr-env
        run: terraform init -backend-config=pr-env.tfbackend

      - name: Terraform Destroy
        working-directory: terraform/pr-env
        run: |
          terraform destroy \
            -auto-approve \
            -var-file=pr-env.tfvars \
            -var "pr_number=${{ github.event.pull_request.number }}" \
            -var "ecs_cluster_name=" \
            -var "ecs_task_definition_arn=" \
            -var "ecs_service_name=" \
            -var "ecs_container_name="

所感

とりあえずミニマムにPR環境をザクッと作るワークフローを試してみて、割と楽に作れそうなイメージが掴めました。
また、時間がある時に以下なども実装してみようと想います。

  • PR環境ごとにDBも作る
  • 通知周り
GitHubで編集を提案

Discussion