この記事は Retty Part2 Advent Calendar 2021 の 22 日目の記事です。
はじめまして、Retty 技術部 インフラチームの中西と申します。
今回は Pull Request毎の検証環境を自動構築した お話となります。
要約
- Pull Request をトリガとして ECS Fargate の環境を CI/CD で構築するようにした
というお話です。
構築の背景について
フロントエンドの開発チームから以下の相談を頂きました。
- 各自が作成した Pull Request に応じた検証環境がほしい
CI/CDの設定で自動構築できそうと判断して設計や構築、検証を行いました。
技術スタックについて
使用した技術スタックは以下の通りとなります
課題について
構築にあたっての疑問や課題を洗い出した所、以下の3点が課題となりました。
- Pull Request 毎の判定をどう行うか
- ECS関連のAWSリソースをどうやって作成してくか
- CI/CD のフローをどうしていくか
解決について
1. Pull Request 毎の判定をどう行うか
こちらは簡単で Pull Request 作成時の番号 (PR Number)を使うことにしました。
(例)PR Number -> 1817
https://github.com/pull/1817
2. ECS関連のAWSリソースをどうやって作成してくか
Pull Request の環境毎に都度リソースをすべて構築するのはデリバリーの速度や.
コストの面からも宜しくありません。
なるべく共通のAWSリソースを使用できるか確認、検証を行いました。
結果、以下の仕様としました.
※本記事で紹介している内容は架空の物なります
DNS について
ALB について
- PR Number 毎の Target group を作成
- 1つのALBで複数の Listener rule を登録して Host header -> Target group の振り分けを設定する
- Prirority 重複による作成エラーを防ぐ為、現在の Priority の 最大値 + 1 に設定して Listner rule を登録するようにする

ECS について
- PR 作成時に Docker build -> PR Number に基づいた Docker tag を設定して ECR push を行う
- PR Number 毎の ECS service を作成し、同じ PR Number の Target group を設定する

3. CI/CDフローをどうしていくか
状況に応じて CircleCI or GitHub Actions で行うようにしました。
環境の構築 or update 時 -> CircleCI を使用
- Docker コンテナbuild , PR Number で tag を付与
- ECR Push
- ALB Target Group を作成
- ELB Listener Rule 作成
- Task Definition 更新
- ECS Service の作成 or Update
フローとしては以下の通りとなります

環境不要時 -> GitHub Actions を使用
- Pull Request close 時の PR Number を取得
- PR Number に基づいた ECS関連のリソースを削除する.
※ 環境復活が必要な場合は再度Pull Request を作成してもらう運用としています
構成図について
上記課題を解決して作成した構成図はこちらになります

設定内容
CircleCI
CircleCI での設定の一例です。
※ 各 step で実行している shell script は割愛します
commands:
deploy_frontend_pre_stg:
steps:
- run:
name: Create Target Group
command: |
if [[ -n ${CIRCLE_PULL_REQUEST} ]]; then
# When pull request is created/updated
PR_NUMBER=`echo ${CIRCLE_PULL_REQUEST} | grep -oP '\d+$'`
echo "export PR_NUMBER=$PR_NUMBER" >> $BASH_ENV
else
echo "Please make Pull Request on GitHub."
exit 1
fi
HOST_HEADER="pr-${PR_NUMBER}.$HOST_DOMAIN"
TARGET_GROUP_NAME="${SERVICE_NAME}-${APPLICATION_ENV_SHORT}-pr-${PR_NUMBER}"
echo "export HOST_HEADER=$HOST_HEADER" >> $BASH_ENV
echo "export TARGET_GROUP_NAME=$TARGET_GROUP_NAME" >> $BASH_ENV
### check Target Group exits
set +e
CHECK_EXITS=`aws elbv2 describe-target-groups --name ${TARGET_GROUP_NAME} | jq -r '.TargetGroups[] | .TargetGroupArn'`
if [ $? -eq 0 ]; then
echo "Taget Group: ${TARGET_GROUP_NAME} is already exits."
echo "This step is skkiped."
else
echo -e "Target Group: ${TARGET_GROUP_NAME} is not found\nCreate Target Group: ${TARGET_GROUP_NAME}"
.circleci/scripts/aws/create_target_group.sh
echo "Create Target Group: ${TARGET_GROUP_NAME} done"
CHECK_EXITS=`aws elbv2 describe-target-groups --name ${TARGET_GROUP_NAME} | jq -r '.TargetGroups[] | .TargetGroupArn'`
fi
echo "Target Group ARN: $CHECK_EXITS"
echo "export TARGET_GROUP_ARN=$CHECK_EXITS" >> $BASH_ENV
- run:
name: Create ELB Listener rule
command: |
set +e
CHECK_EXITS=`aws elbv2 describe-rules --listener-arn $LISTENER_ARN | grep -w ${HOST_HEADER}`
if [ $? -eq 0 ]; then
echo -e "ELB Listener rule for ${HOST_HEADER} is already exits.\nThis step is skkiped."
else
PRIORITY=`.circleci/scripts/aws/set_elb_listener_rule_priority.sh $LISTENER_ARN`
echo "export PRIORITY=$PRIORITY" >> $BASH_ENV
echo "Priority Number: $PRIORITY"
.circleci/scripts/aws/create_elb_listener_rule.sh
fi
- run:
name: create task definition for frontend-pre-stg
command: |
sudo apt update -y && sudo apt install -y gettext jq
export COMMIT_HASH=$CIRCLE_SHA1
envsubst < .circleci/ecs/taskdefinition-frontend-pre-stg.json.template> .circleci/ecs/taskdefinition.json
aws ecs register-task-definition --cli-input-json file://$PWD/.circleci/ecs/taskdefinition.json > newtask.json
- run:
name: create service for frontend-pre-stg
command: |
TASK_DEFINITION=$(cat newtask.json | jq -r '.taskDefinition.taskDefinitionArn')
ECS_SERVICE_NAME="pr-${PR_NUMBER}"
echo "export ECS_SERVICE_NAME=$ECS_SERVICE_NAME" >> $BASH_ENV
echo "export TASK_DEFINITION=$TASK_DEFINITION" >> $BASH_ENV
CHECK_EXITS=`aws ecs describe-services --cluster $CLUSTER_NAME --service $ECS_SERVICE_NAME | jq -r '.services[].status'`
if [ $CHECK_EXITS == "ACTIVE" ]; then
echo "ECS Sevice Name: $ECS_SERVICE_NAME is already exist."
.circleci/scripts/aws/update_or_create_ecs_service.sh update
echo "export PRE_STG_RESOURCE_EXIST='true'" >> $BASH_ENV
else
echo "ECS Sevice Name: $ECS_ERVICE_NAME is not exits."
.circleci/scripts/aws/update_or_create_ecs_service.sh create
fi
- run:
name: describe frontend-pre-stg resources
command: |
## ELB target group Listenr rule
aws elbv2 describe-target-groups --name ${TARGET_GROUP_NAME} | jq -r '.TargetGroups[] | .TargetGroupArn'
aws elbv2 describe-rules --listener-arn $LISTENER_ARN | jq --arg host_value $HOST_HEADER '.Rules[] | select (.Conditions[].Values[] == $host_value )' | jq -r .RuleArn
### ecs service task
aws ecs describe-services --cluster $CLUSTER_NAME --service $ECS_SERVICE_NAME
aws ecs describe-task-definition --task-definition --frontend-pre-stg | jq -r '.taskDefinition | .taskDefinitionArn , .containerDefinitions[].dockerLabels'
jobs:
deploy-frontend-pre-stg:
executor: python
environment:
AWS_PAGER: ""
APPLICATION_ENV: pre-staging
APPLICATION_ENV_SHORT: pre-stg
VPC_ID: "vpc-xxxx"
SUBNET_GROUPS: "subnet-xxxx,subnet-xxxx"
SECURITY_GROUPS: "sg-xxx"
LISTENER_ARN: "arn:aws:elasticloadbalancing:xxxx"
HEALTH_CHECK_PATH: "/healthz"
CLUSTER_NAME: "frontend-pre-stg"
CONTAINER_NAME: "-frontend"
CONTAINER_PORT: 80
DESIRED_COUNT: 1
PLATFORM_VERSION: "LATEST"
TASK_CPU: 512
TASK_MEMORY: 1024
CPU_UNIT: 502
MEMORY_RESERVE: 768
steps:
- checkout
- aws_assume_role
- deploy_frontend_pre_stg
GitHub Actions
Pull Request をCloseした時のみの処理となります。
CircleCI 内で設定しなかった理由はGitHub Actions のcontext である.
github.event.number で PR Numberの取得が簡単にできたからです。
name: Remove unnecessary pre-stg environment
on:
pull_request:
types: [ closed ]
jobs:
deploy:
if: startsWith(github.head_ref, 'renovate-') == false
name: Remove unnecessary pre-stg environment
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{ github.event.number }}
LISTENER_ARN: "arn:aws:elasticloadbalancing:xxxx"
CLUSTER_NAME: "frontend-pre-stg"
SERVICE_NAME: "frontend"
APPLICATION_ENV: pre-staging
APPLICATION_ENV_SHORT: pre-stg
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS Credentials with assume role
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: ap-northeast-1
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
role-duration-seconds: 900
- name: remove ECS service
run: |
ECS_SERVICE="pr-${PR_NUMBER}"
echo ${ECS_SERVICE}
aws ecs delete-service --cluster $CLUSTER_NAME --service $ECS_SERVICE --force
- name: remove ELB Lister rule
run: |
HOST_HEADER="pr-${PR_NUMBER}.${HOST_DOMAIN}"
REMOVE_RULE_ARN=`aws elbv2 describe-rules --listener-arn $LISTENER_ARN | \
jq --arg host_value ${HOST_HEADER} '.Rules[] | select (.Conditions[].Values[] == $host_value )' | jq -r .RuleArn`
echo $REMOVE_RULE_ARN
aws elbv2 delete-rule --rule-arn ${REMOVE_RULE_ARN}
- name: remove ELB Target group
run: |
TARGET_GROUP_NAME="${SERVICE_NAME}-${APPLICATION_ENV_SHORT}-pr-${PR_NUMBER}"
REMOVE_TARGET_GROUP_ARN=`aws elbv2 describe-target-groups --name ${TARGET_GROUP_NAME} --query 'TargetGroups[].TargetGroupArn' --output text`
echo ${TARGET_GROUP_NAME}
echo ${REMOVE_TARGET_GROUP_ARN}
aws elbv2 delete-target-group --target-group-arn ${REMOVE_TARGET_GROUP_ARN}
最後に
こうしてAWS上で気軽に使い捨てできる環境を用意することができ、開発効率を上げることができました。
今後は他のプロダクトでも同様の環境が EC2で稼働しているので、今回の知見を活かして今回のような
Pull Request毎の検証環境に移行していく予定です
似たような要件で悩んでいた方のご参考になれば幸いです。
お読みくださいましてありがとうございました。
今年の Advent Calendar は Part 1 と Part2 がございます。
引き続きお楽しみ下さい。