RubyのアプリケーションをPR単位でテストできるようにしてみた

はじめに

RubyのアプリケーションをPR単位でテストできるようにしてみた記事です。
仕事でRubyを使っていて「PR単位で気軽にテスト環境を作りたいなぁ」という人向けの内容です。

背景など

元々仕事で

  • 複数のプロジェクトが同時並行で進んでいる
  • 単一のステージング環境を使い、テストをしている
  • そのためプロジェクトがリリースされるまで、別のプロジェクトの機能がステージング環境でテストできない

という状況が発生しており、新機能リリース時のリードタイムが発生しやすいという課題を抱えていました。
また僕自身も直近ではリリースマネージャ的な動きをしていたこともあり、この状況をどうにかしたいという思いがありました。

そんな時、メルカリから公開されたKubetempuraの「PR単位でテスト環境を構築する」という考えを知り、「この方法で何とかできないだろうか?」と対応方法を考え始めた感じですね

engineering.mercari.com

ただ、

  • 僕のk8sなどへのまだ理解が浅いこと
  • 現状の環境に追加するにはKubetempuraは規模が大きすぎるかもしれない
  • 実際に運用が回るのかスモールスタートで検証したい

というところもあり、Kubetempura導入までは至っていませんでした。

そんな中、個人的な興味から試していたAppRunnerがテスト環境として使えるのではないかと思い始め

gamelinks007.hatenablog.com

色々と試行錯誤してみたところ、以下のようにPR単位でテスト環境を構築できるものができました。

github.com

作ったもの

作ったものとしては以下です。

github.com

シンプルなSinatraのアプリケーションをTerraform/GitHub Actionsを使うことでAWS上にテスト環境を自動で構築できるようにしています。
まだREADMEなどはまだ整備できていませんが、ForkしてAWS周りの認証キーなどをセットすれば動かせると思います。

やったこと

先日公開した以下の記事で作成したTerraformをもとに最低限(AppRunner/IAM/ECRまわり)の環境を構築できるコードを書き、

github.com

以下のようなシンプルなSinatraのWebアプリケーションを作成しています。
(RubySinatraを使っているのは一番使い慣れた言語であることと、テストのための環境構築が楽だったためです)

require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello world!'
end

get '/halo_infinite' do
  'HALO Infinite'
end

get '/halo_2' do
  'HALO 2'
end

それを以下のようにPRが作成されたタイミングでテスト環境を構築するようにしました。

name: 'Create staging with pull request'

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure aws credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.1.9

      - name: Init
        run: terraform init
        working-directory: infra
      
      - name: Get
        run: terraform get
        working-directory: infra

      - name: Apply
        id: apply
        run: terraform apply -auto-approve -var 'pr_number=${{ github.event.number }}' -var 'commit_hash=${{ github.sha }}' -var 'region=${{ secrets.AWS_REGION }}'
        working-directory: infra
        continue-on-error: true

      - name: Login to Amazon ECR
        if: ${{ steps.apply.outcome == 'failure' }}
        uses: aws-actions/amazon-ecr-login@v1

      - name: Push Container(Only synchronize pull request)
        if: ${{ steps.apply.outcome == 'failure' }}
        run: |
          docker build -t ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest .
          docker push ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest

初回のみterraform applyが成功し、二回目以降はすでに同じ名前のリソースが生成されているので失敗するようになっています。
またPRの情報(PRやIssueの番号)などを使い、PRごとに環境を作れるようにしています。

二回目以降(つまり追加でPRにコミットがpushされた場合は以下のように失敗した結果をもとにECRへ新しいコンテナをビルドしてプッシュするようにしています。

      - name: Apply
        id: apply
        run: terraform apply -auto-approve -var 'pr_number=${{ github.event.number }}' -var 'commit_hash=${{ github.sha }}' -var 'region=${{ secrets.AWS_REGION }}'
        working-directory: infra
        continue-on-error: true

      - name: Login to Amazon ECR
        if: ${{ steps.apply.outcome == 'failure' }}
        uses: aws-actions/amazon-ecr-login@v1

      - name: Push Container(Only synchronize pull request)
        if: ${{ steps.apply.outcome == 'failure' }}
        run: |
          docker build -t ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest .
          docker push ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest

正直、この辺はもう少しやりようがあるとは思いますね……。
とりあえず、動くものを作りたかったので一旦はこれでOKとしてます(何かいい知見ご存じの方いればご教授いただければ幸いです)

またPRがマージされた(またはクローズされた)場合は以下のように作成されたリソースをAWS CLIで削除するようにしています。

name: 'Delete staging with pull request'

on:
  pull_request:
    types: [closed]

jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Configure aws credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Get AppRunner Service ARN
        id: apprunner-service-arn
        run: |
          service_arn=$(aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='pullrequest-${{ github.event.number }}'].ServiceArn | [0]")
          echo "::set-output name=stdout::$service_arn"
      - name: Delete AppRunner Service
        run: aws apprunner delete-service --service-arn ${{ steps.apprunner-service-arn.outputs.stdout }}

      - name: Delete ECR repository
        run: aws ecr delete-repository --repository-name pullrequest-${{ github.event.number }} --force

      - name: Get Role attachment policy
        id: role-attachment-policy
        run: |
          role_attachment_policy=$(aws iam list-attached-role-policies --role-name pullrequest-4-service-role --query "AttachedPolicies[0].PolicyArn")
          echo "::set-output name=stdout::$role_attachment_policy"
      - name: Dettach policy
        run: aws iam detach-role-policy --role-name pullrequest-${{ github.event.number }}-service-role --policy-arn ${{ steps.role-attachment-policy.outputs.stdout }}

      - name: Delete role
        run: aws iam delete-role --role-name pullrequest-${{ github.event.number }}-service-role

これのおかげでテスト用に作成された環境を削除し忘れることが回避できています。
この辺ももう少しきれいにしたい感じはありますね……。

今後

今回検証用としてPR単位でのテスト環境自動構築をやってみましたが、大分いい感じではありますね。
なので、一旦仕事のほうでつかえないか導入してみて検証を進めていきたいと思います。

今後課題になりそうなのは実用的なWebアプリケーションなどではDBとの接続(RDSやVPCの設定)が必要になりそうなので、そのあたりをどうするかですね。
また、PRにコミットが積まれるたびにtarraform applyされているのは非常に微妙なのでそのあたりもどうにかしたいですね。

さらに、フロントエンドとバックエンドが分かれているような場合ではAPI側をどう呼び出すのかなどが課題になりそうです。