この記事はBASE Advent Calendar 2021の22日目の記事です。
はじめに
はじめまして、Owners Success Frontend Shop Frontチームの坂口です。
普段はフロントエンドエンジニアとしてVue.jsを使った開発をメインに行なっているのですが、チームでプロジェクトマネージャーやデザイナーが手軽に動作を確認できるレビュー環境がほしいという話があり、AWS App RunnerとGitHub Actionsを連携して構築をしたのでその話をしたいと思います。
レビュー環境とは
レビュー環境というのはGitHubのプルリクエストやブランチごとに動作確認ができる環境で以下のような動きをするものを今回は構築することにしました。
- プルリクエストが作成されるとレビュー環境立ち上げ (作成)
- プルリクエストに紐付いたブランチが更新されるとレビュー環境更新 (更新)
- プルリクエストがclose、またはマージされるとレビュー環境削除 (削除)
イメージとしてはHerokuのReview AppsやNetlifyのDeploy Previewsなどに近いかもしれません。
使用技術
AWS App RunnerとGitHub Actionsを利用して以下のような構成にしました。
AWS App Runner
AWS App Runnerは今年5月に発表されたコンテナベースのフルマネージドサービスで、インフラの知識があまりなくても簡単にアプリケーションをデプロイすることができます。
今回構築するにあたってAWS LambdaやAWS ECSなど選択肢はありましたが、簡単にデプロイできるということで採用しました。
準備
事前にGitHub Actionsで利用するAWSのIAMユーザーを作成して、リポジトリのsecretsにアクセスキーとシークレットアクセスキーを用意しておきます。
必要な権限はこちらの公式記事で書いてあるものになります。
ワークフロー内容
ワークフローは先ほど上げた作成、更新、削除に加えて、一時停止、復帰を追加した以下5パターンを用意しました。
- プルリクエストにラベルがつくとレビュー環境立ち上げ (作成)
- ラベルが付いたプルリクエストへ新たにpushされると更新 (更新)
- プルリクエストがclose、またはマージされるとレビュー環境削除 (削除)
- 毎日21時になると稼働中のレビュー環境を一時停止(一時停止)
- プルリクエストの
/resume
とコメントすると復帰(復帰)
プルリクエストにラベルがつくとレビュー環境立ち上げ
name: review-app-deploy on: pull_request: types: - labeled jobs: deploy-review-app: name: Deploy Review App runs-on: ubuntu-latest environment: development if: ${{ github.event.label.name == 'ラベル名' }} env: BRANCH_NAME: ${{ github.head_ref }} steps: - name: Checkout uses: actions/checkout@v2 - 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: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR id: build-image env: ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} run: | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g"` SERVICE_NAME=`echo review-app_$IMAGE_TAG | cut -c 1-40` docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=service::$SERVICE_NAME" echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - name: Deploy to App Runner id: deploy-service uses: awslabs/amazon-app-runner-deploy@main with: service: ${{ steps.build-image.outputs.service }} image: ${{ steps.build-image.outputs.image }} access-role-arn: ${{ secrets.APPRUNNER_ROLE_ARN }} runtime: NODEJS_16 region: ${{ secrets.AWS_REGION }} cpu : 1 memory : 2 port: 80 wait-for-service-stability: false - name: Comment Review App URL run: | SERVICE_URL=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='${{ steps.build-image.outputs.service }}']" | jq -r '.[]["ServiceUrl"]'` gh pr comment $PR_NUMBER --body "Review App URL: https://$SERVICE_URL"
大まかな流れは以下になります。
- Dockerイメージビルド
- Amazon ECRへDockerイメージプッシュ
- AWS App Runnerのサービス作成
今回、プルリクエストのブランチ名をDockerイメージのタグとAWS App Runnerのサービス名に利用しているのですが、AWS App Runnerのサービス名の制限に合わせて文字列を置換しています。
またあとから識別しやすいようにサービス名にreview-app_
プレフィックスを付与しています。
IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g"` SERVICE_NAME=`echo review-app_$IMAGE_TAG | cut -c 1-40`
サービス作成では、AWS公式GitHub Actionsであるaws-actions/configure-aws-credentialsを利用していて、CPU、メモリは最低限にしています。
wait-for-service-stability
をtrue
にするとサービス作成完了まで待ってくれるのですが、完了まで5分ほどかかってしまいGitHub Actionsの枠を使い切ってしまうことを懸念してfalse
にしています。
さらにAWS App RunnerのURLをプルリクエストのコメントに通知するようにしています。(※サービス作成完了後ではないので、利用可能までに5分程度待機が必要です。)
SERVICE_URL=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='${{ steps.build-image.outputs.service }}']" | jq -r '.[]["ServiceUrl"]'` gh pr comment $PR_NUMBER --body "Review App URL: https://$SERVICE_URL"
以下のように特定のラベル(今回はdeploy review app
)を付与するとワークフローが走り、サービスURLをコメントしてくれるようになります。
ラベルが付いたプルリクエストへ新たにpushされると更新
name: review-app-update on: pull_request: types: - synchronize jobs: update-review-app: name: Update Review App runs-on: ubuntu-latest environment: development if: ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名') }} env: BRANCH_NAME: ${{ github.head_ref }} steps: - name: Checkout uses: actions/checkout@v2 - 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: Check Service Exist id: check-service-exist run: | IMAGE_TAG=`echo $BRANCH_NAME | sed -e "s/[^a-zA-Z0-9_-]/-/g"` SERVICE_NAME=`echo "review-app_$IMAGE_TAG" | cut -c 1-40` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='RUNNING']" | jq -r '.[]["ServiceArn"]'` echo "::set-output name=image-tag::$IMAGE_TAG" echo "::set-output name=service-arn::$SERVICE_ARN" - name: Login to Amazon ECR id: login-ecr if: ${{ steps.check-service-exist.outputs.service-arn }} uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR id: build-image if: ${{ steps.check-service-exist.outputs.service-arn }} env: ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} . docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ steps.check-service-exist.outputs.image-tag }} - name: Update App Runner Service id: update-service if: ${{ steps.check-service-exist.outputs.service-arn }} run: | aws apprunner start-deployment --service-arn ${{ steps.check-service-exist.outputs.service-arn }}
大まかな流れは作成とほぼ同じになりますが、注意点としてaws-actions/configure-aws-credentials
内部でサービスが存在する場合は更新コマンドを実行しているのですが、更新コマンドはサービスの設定を更新してくれるのみでソースを更新してはくれないため、そのまま利用せず、start-deployment
を実行する必要があります。
aws apprunner start-deployment --service-arn ${{ steps.check-service-exist.outputs.service-arn }}
また細かいですが、jq
では-r
オプションをつけて結果の文字列からダブルクウォートを削除して扱いやすくしています。
SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='RUNNING']" | jq -r '.[]["ServiceArn"]'`
毎日21時になると稼働中のレビュー環境を一時停止
name: review-apps-pause on: schedule: - cron: '0 12 * * MON-FRI' jobs: pause-review-app: name: Pause Review App runs-on: ubuntu-latest environment: development steps: - name: Checkout uses: actions/checkout@v2 - 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: Pause App Runner Services id: pause-services run: | SERVICE_ARN_LIST=`aws apprunner list-services --query "ServiceSummaryList[?Status=='RUNNING']" | jq -r '.[] | select(.ServiceName | test("^review-app_")) | .ServiceArn'` if [ -n "$SERVICE_ARN_LIST" ] ; then echo $SERVICE_ARN_LIST | while read SERVICE_ARN ; do aws apprunner pause-service --service-arn $SERVICE_ARN done fi
AWS App Runnerはサービスが起動している間課金されてしまうため、深夜の誰も利用しない時間では一時停止するようにしています。タイムゾーンはUTCなので注意してください。
on: schedule: - cron: '0 12 * * MON-FRI'
一時停止するサービスはAWS CLIの--query
オプションで稼働中のものを抽出した後、jqでサービス名がreview-app_
プレフィックスのものを絞り込んでいます。
SERVICE_ARN_LIST=`aws apprunner list-services --query "ServiceSummaryList[?Status=='RUNNING']" | jq -r '.[] | select(.ServiceName | test("^review-app_")) | .ServiceArn'`
プルリクエストに/resume
とコメントすると復帰
name: review-app-resume on: issue_comment: types: [created, edited] jobs: resume-review-app: name: Resume Review App runs-on: ubuntu-latest environment: development if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/resume') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v2 - 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: Check Service Exist id: check-service-exist run: | BRANCH_NAME=`gh pr view ${{ github.event.issue.number }} --json headRefName --jq '.headRefName'` SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g"` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME'&&Status=='PAUSED']" | jq -r '.[]["ServiceArn"]'` echo "::set-output name=service-arn::$SERVICE_ARN" - name: Resume App Runner Service id: resume-service if: ${{ steps.check-service-exist.outputs.service-arn }} run: | resume-service --service-arn ${{ steps.check-service-exist.outputs.service-arn }}
issue_comment
をトリガーにしており、作成や更新のようにgithub.head_ref
でブランチ名が取得できないため、コメントされたプルリクエストからブランチを特定するための処理を挟んでいます。
BRANCH_NAME=`gh pr view ${{ github.event.issue.number }} --json headRefName --jq '.headRefName'`
以下のようにコメントすることでワークフローが走り、一時停止中のAWS App Runnerサービスが復帰してくれます。
プルリクエストがclose、またはマージされるとレビュー環境削除
name: review-app-delete on: pull_request: types: - closed jobs: delete-review-app: name: Delete Review App runs-on: ubuntu-latest environment: development if: ${{ contains(github.event.pull_request.labels.*.name, 'ラベル名') }} env: BRANCH_NAME: ${{ github.head_ref }} steps: - name: Checkout uses: actions/checkout@v2 - 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: Delete App Runner Service id: delete-service run: | SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g"` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME']" | jq -r '.[]["ServiceArn"]'` if [ -n "$SERVICE_ARN" ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo "::set-output name=tag::$SERVICE_NAME" fi - name: Delete ECR Image id: delete-ecr-image if: success() env: ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} run: | TAG=`echo "$BRANCH_NAME" | sed -e "s/[^a-zA-Z0-9_-]/-/g"` aws ecr batch-delete-image --repository-name $ECR_REPOSITORY --image-ids imageTag=$TAG
削除したいサービスの存在確認をして、サービスとDockerイメージを削除します。
SERVICE_NAME=`echo "review-app_$BRANCH_NAME" | cut -c 1-40 | sed -e "s/[^a-zA-Z0-9_-]/-/g"` SERVICE_ARN=`aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='$SERVICE_NAME']" | jq -r '.[]["ServiceArn"]'` if [ -n "$SERVICE_ARN" ] ; then aws apprunner delete-service --service-arn $SERVICE_ARN echo "::set-output name=tag::$SERVICE_NAME" fi
おわりに
今回は公式でGitHub Actionsが用意されているのもあって比較的簡単に連携・レビュー環境の構築ができました。まだサービス作成完了時の通知など改善の余地はたくさんありますが、手軽に構築ができるので、ぜひ一度検討してみてはいかがでしょうか。