1. はじめに
ECSのCICD構成について、エンタープライズレベルでの構築方法をご紹介します。
単にコンテナをデプロイするだけでなく、セキュリティ、運用保守性などエンタープライズレベルの要件を満たすための構成を目指します。
本記事は3回目/全3回です。
初回、及び2回目記事はこちらです。
【AWS】エンタープライズレベル ECS CI/CD の作り方①(GitHub Actions)
【AWS】エンタープライズレベル ECS CI/CD の作り方②(GitHub Actions)
Github Actionsワークフローファイルのコードは、初回記事に記載しています。
2. 設計ポイント
3回目の記事で紹介する設計ポイントは以下の(★)の項目です。
- セキュリティ対策
- ウィルススキャンの実施
- 脆弱性スキャンの実施
- S3上の環境変数ファイルからECSタスクへの機微情報の注入
- Github上に機微情報を配置しない
- アクセスキーではなく、IAMロールでのAssumeRoleを利用
- IAMポリシーの最小限の権限付与
- 運用保守性
- (★)単一のワークフローのみで複数環境へ選択的にデプロイ
- (★)単一のワークフローのみで複数のイメージをまとめてビルドしてデプロイ
- (★)デプロイ実行後のCodeDeploy URL等を表示し、デプロイ結果を迅速に確認
- (★)コンテナイメージのタグにコミットハッシュを使用、アプリケーションのデプロイバージョンを一意に識別
3. 運用保守性
3-1. 単一のワークフローのみで複数環境へ選択的にデプロイ
GithubActionsのワークフローファイルの改修やメンテナンスが発生した際に、複数のワークフローファイルを管理していると手間がかかります。
このため、単一のワークフローファイルのみで複数環境へ選択的にデプロイするするようにしています。
ワークフローを実行する際に、プルダウンメニューから環境を選択し、その環境に対してデプロイを実行できるようにします。
実装にあたってのポイントは以下になります。
- AWS環境のECS Task, ECS Cluster, ECS Serviceなどの関連AWSリソースの名称に環境略称
dev
,prod
などを付与 - appspecファイル名にも同様に環境略称
dev
,prod
などを付与- 例:
appspec_dev.yml
,appspec_prod.yml
,
- 例:
-
inputsコンテキストで
choice
型を利用し、環境略称などの変数をワークフローの実行時に選択可能に - AWSリソースの名称をenvコンテキストで指定
- このとき、inputsで指定された変数の値を利用し、envコンテキストの変数の値に代入
- 例:
ECS_TASKNAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-task
コードはコチラ!
name: Deploy ECS Service
on:
workflow_dispatch:
inputs:
Environment:
description: '対象の環境名称を選択'
required: true
type: choice
options: # ここに記載した値がプルダウンメニューに出てきます。
- dev
- prod
PJ_NAME:
description: '案件略称(固定)'
required: true
type: choice
default: sample
options:
- sample
~省略~
env:
AWS_DEFAULT_REGION: ap-northeast-1
PJ_NAME: ${{ inputs.PJ_NAME }}
ENV: ${{ inputs.Environment }}
~省略~
CD_APPSPEC_FILE: .github/workflows/aws/appspec_${{ inputs.Environment }}.yml
~省略~
# AWSリソースの名称
ECS_CLUSTER: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-cluster
ECS_SERVICE: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-service
ECS_TASKNAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-task
また、ワークフローを実行した後にどの環境に対してデプロイしたのかがわからなくなるためGithub Actionsのサマリーにどの環境に対してデプロイしたのかを表示してあげるとなおよいです。
該当のコードは以下です。
deploymentenv:
runs-on: ubuntu-latest
steps:
- name: Set environment summary
run: echo "## このワークフローは${{ inputs.Environment }}環境に対して実行されました。" >> "${GITHUB_STEP_SUMMARY}"
3-2. 単一のワークフローのみで複数のイメージをまとめてビルドしてデプロイ
ECSタスク内にApache, NginxなどのWebサーバー、tomcatなどのアプリケーションサーバーなど複数のコンテナイメージを含む場合があります。
このような場合、複数のコンテナイメージをまとめてビルドしてデプロイするようにします。
デプロイにあたっては、まず最新のtask-definition.jsonを取得し、そのtask-definition.json内のコンテナイメージタグを書き換えます。
具体的には、aws ecs describe-task-definition
コマンドで最新のtask-definition.jsonを取得した後、aws-actions/amazon-ecs-render-task-definition
アクションを利用して、task-definition.json内のコンテナイメージタグを書き換えます。
実装にあたってのポイントは以下になります。
- まずwebサーバーのコンテナイメージをビルドした後、
aws-actions/amazon-ecs-render-task-definition
アクションを利用して、task-definition.json内のwebサーバーのコンテナイメージタグを書き換えます。 - さらに、アプリケーションサーバーのコンテナイメージをビルドした後、直前のstepで書き換えたtask-definition.jsonを、再度
aws-actions/amazon-ecs-render-task-definition
アクションを利用して、task-definition.json内のアプリケーションサーバーのコンテナイメージタグを書き換えます。
ここでは、以下のコードのtask-definition: ${{ steps.render-web-container.outputs.task-definition }}
の部分が肝になります。
steprender-web-container
で書き換えたtask-definition.jsonを、次のsteprender-app-container
で再度書き換えるために${{ steps.render-web-container.outputs.task-definition }}
で直前のstepの出力を参照しています。
コードはコチラ!
# deploy
- name: Download task definition
# 最新(revision番号値が最大)のtask-definition.jsonを取得
# warningが出ないようにjqでtaskDefinitionArn等の不要な要素を削除
run: |
aws ecs describe-task-definition --task-definition $ECS_TASKNAME --query taskDefinition --output json | jq -r 'del(
.taskDefinitionArn,
.requiresAttributes,
.compatibilities,
.revision,
.status,
.registeredAt,
.registeredBy
)' > task-definition.json
- name: Web Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.APACHE_CONTAINER_NAME }}
image: ${{ steps.build-web.outputs.image }}
- name: App Render Amazon ECS task definition
id: render-app-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }} # 直前のstepで書換たtask-definition
container-name: ${{ env.TOMCAT_CONTAINER_NAME }}
image: ${{ steps.build-app.outputs.image }}
以下のコードではsteprender-app-container
で書き換えたtask-definition.jsonを、次のstepdeploy
で``${{ steps.render-app-container.outputs.task-definition }}`で参照しています。
コードはコチラ!
- name: Deploy to Amazon ECS service
id: deploy
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render-app-container.outputs.task-definition }} # 直前のstepで書換たtask-definition
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
codedeploy-appspec: ${{ env.CD_APPSPEC_FILE }}
codedeploy-application: ${{ env.CD_APPNAME }}
codedeploy-deployment-group: ${{ env.CD_DEPLOYNAME }}
wait-for-service-stability: false # true:ecs servce上でtaskのヘルスチェック完了まで待つ。5分程度かかる。B/G deployを使うならfalse。
3-3. デプロイ実行後のCodeDeploy URL等を表示し、デプロイ結果を迅速に確認
Blue/Greenデプロイメントを行う場合、CodeDeployのデプロイのステータスのURLを表示し、デプロイ結果を迅速に確認できるようにします。
具体的には以下の3つを表示します。
- CodeDeployのデプロイのステータス確認のURL
- CloudShellのURL、及びコンテナログインコマンド(デプロイされたECSタスクIDを引数に指定済み)
- コンテナのログを確認するためのロググループのURL
コードはコチラ!
# Summary
- name: ECS Task Deploy Control url summary
env:
CODEDEPLOY_URL: https://ap-northeast-1.console.aws.amazon.com/codesuite/codedeploy/deployments/
DEPLOYMENT_ID: ${{ steps.deploy.outputs.codedeploy-deployment-id }}
run: |
echo "## AWS Console URL for B/G Deploy Control" >> "${GITHUB_STEP_SUMMARY}"
echo "- ${CODEDEPLOY_URL}${DEPLOYMENT_ID}?region=ap-northeast-1" >> "${GITHUB_STEP_SUMMARY}"
containerlogin: # アプリTがコンテナログインやログ参照可能なようにURLとコマンドを出力
needs: ecsdeploy # ecsdeploy jobの正常完了後、本ジョブスタート
runs-on: ubuntu-latest
steps:
- name: Configure AWS Credentials # AWSアクセス権限設定,github secretsから取得
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ env.AWS_ROLE_ARN }}
- name: ECS Container login cmd, Logs url summary
env:
CLOUDSHELL_URL: https://ap-northeast-1.console.aws.amazon.com/cloudshell/home
CLOUDWATCHLOGS_URL: https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home#logsV2:log-groups
run: |
echo "## AWS CloudShell and Login ECS container cmd" >> "${GITHUB_STEP_SUMMARY}"
echo "### コンテナログイン用terminal (AWS CloudShell URL)" >> "${GITHUB_STEP_SUMMARY}"
echo "- ${CLOUDSHELL_URL}?region=ap-northeast-1" >> "${GITHUB_STEP_SUMMARY}"
echo "### タスク一覧と各コンテナログインコマンド" >> "${GITHUB_STEP_SUMMARY}"
LATEST_REVISION=$(aws ecs describe-task-definition --task-definition "$ECS_TASKNAME" --query 'taskDefinition.revision' --output text)
echo "LATEST_REVISION=$LATEST_REVISION" >> "${GITHUB_STEP_SUMMARY}"
echo "please wait for 60 sec... tasks starting up " >> "${GITHUB_STEP_SUMMARY}"
sleep 60
# タスク定義の ARN を取得
TASK_DEFINITION_ARN=$(aws ecs list-task-definitions --family-prefix "$ECS_TASKNAME" --query "taskDefinitionArns[?ends_with(@,':$LATEST_REVISION')]" --output text)
# タスク定義を使用して実行されているタスクのリストを取得
TASK_ARNS=$(aws ecs list-tasks --cluster "$ECS_CLUSTER" --query "taskArns[]" --output text)
i=0
for task_arn in $TASK_ARNS; do
task_definition=$(aws ecs describe-tasks --cluster "$ECS_CLUSTER" --tasks "$task_arn" --query "tasks[0].taskDefinitionArn" --output text)
if [ "$task_definition" == "$TASK_DEFINITION_ARN" ]; then
task_id=$(echo "$task_arn" | cut -d/ -f3)
echo "- タスクID${i+1}: $task_id" >> "${GITHUB_STEP_SUMMARY}"
echo " - apacheログインコマンド: " >> "${GITHUB_STEP_SUMMARY}"
echo " - aws ecs execute-command --cluster ${ECS_CLUSTER} --task $task_id --container ${APACHE_CONTAINER_NAME} --interactive --command bash" >> "${GITHUB_STEP_SUMMARY}"
echo " - tomcatログインコマンド: " >> "${GITHUB_STEP_SUMMARY}"
echo " - aws ecs execute-command --cluster ${ECS_CLUSTER} --task $task_id --container ${TOMCAT_CONTAINER_NAME} --interactive --command bash" >> "${GITHUB_STEP_SUMMARY}"
i=$((i+1))
fi
done
echo "## コンテナの標準出力ログとファイル出力ログ" >> "${GITHUB_STEP_SUMMARY}"
echo "- ${CLOUDWATCHLOGS_URL}\$3FlogGroupNameFilter\$3D${ECS_TASKNAME}" >> "${GITHUB_STEP_SUMMARY}"
3-4. コンテナイメージのタグにコミットハッシュを使用、アプリケーションのデプロイバージョンを一意に識別
コンテナイメージのタグにコミットハッシュを使用することで、アプリケーションのデプロイバージョンを一意に識別します。
デプロイした後に不具合が見つかった場合などに、どのコミットのコードによって不具合が発生したのかを特定しやすくなります。
具体的なコードは以下になります。
コードはコチラ!
- name: Get short commit hash
id: commit
run: echo "IMAGE_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV
# build web image
- name: Web Build, tag, and push image to Amazon ECR
id: build-web
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ${{ env.APACHE_ECR_REPOSITORY }}
CONTEXT: ${{ env.APACHE_DOCKERFILE_PATH }}
DOCKERFILE_NAME: Dockerfile
PATH: ${{ steps.download-tmas-cli.outputs.PATH }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f $CONTEXT/$DOCKERFILE_NAME $CONTEXT
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
~省略~
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
おわりに
以上で、エンタープライズレベルのECS CI/CD構成の作り方について、3回にわたりご紹介いたしました。
ここまでお付き合いいただきありがとうございました。
セキュリティ対策、運用保守性の観点から、エンタープライズレベルの要件を満たすための設計ポイントをご紹介しました。
本記事を参考にエンタープライズレベルのECS CI/CD構成を構築し、より安全で運用保守性の高いシステムを構築していただければ幸いです。