この記事は 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 がございます。
引き続きお楽しみ下さい。