はじめに
RubyのアプリケーションをPR単位でテストできるようにしてみた記事です。
仕事でRubyを使っていて「PR単位で気軽にテスト環境を作りたいなぁ」という人向けの内容です。
背景など
元々仕事で
- 複数のプロジェクトが同時並行で進んでいる
- 単一のステージング環境を使い、テストをしている
- そのためプロジェクトがリリースされるまで、別のプロジェクトの機能がステージング環境でテストできない
という状況が発生しており、新機能リリース時のリードタイムが発生しやすいという課題を抱えていました。
また僕自身も直近ではリリースマネージャ的な動きをしていたこともあり、この状況をどうにかしたいという思いがありました。
そんな時、メルカリから公開されたKubetempuraの「PR単位でテスト環境を構築する」という考えを知り、「この方法で何とかできないだろうか?」と対応方法を考え始めた感じですね
ただ、
- 僕のk8sなどへのまだ理解が浅いこと
- 現状の環境に追加するにはKubetempuraは規模が大きすぎるかもしれない
- 実際に運用が回るのかスモールスタートで検証したい
というところもあり、Kubetempura導入までは至っていませんでした。
そんな中、個人的な興味から試していたAppRunnerがテスト環境として使えるのではないかと思い始め
色々と試行錯誤してみたところ、以下のようにPR単位でテスト環境を構築できるものができました。
作ったもの
作ったものとしては以下です。
シンプルなSinatraのアプリケーションをTerraform/GitHub Actionsを使うことでAWS上にテスト環境を自動で構築できるようにしています。
まだREADMEなどはまだ整備できていませんが、ForkしてAWS周りの認証キーなどをセットすれば動かせると思います。
やったこと
先日公開した以下の記事で作成したTerraformをもとに最低限(AppRunner/IAM/ECRまわり)の環境を構築できるコードを書き、
以下のようなシンプルなSinatraのWebアプリケーションを作成しています。
(RubyとSinatraを使っているのは一番使い慣れた言語であることと、テストのための環境構築が楽だったためです)
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側をどう呼び出すのかなどが課題になりそうです。