1. はじめに
ECSのCICD構成について、エンタープライズレベルでの構築方法をご紹介します。
単にコンテナをデプロイするだけでなく、セキュリティ、運用保守性などエンタープライズレベルの要件を満たすための構成を目指します。
全3回に分けてご紹介する予定です。
第2回はコチラ
【AWS】エンタープライズレベル ECS CI/CD の作り方②(GitHub Actions)
2. 設計ポイント
セキュリティ対策やアプリケーションチームの求める運用保守性を考慮し、以下の設計ポイントを採用しています。
本記事では以下のそれぞれのポイントについて順次ご紹介していきます。
- セキュリティ対策
- ウィルススキャンの実施
- 脆弱性スキャンの実施
- S3上の環境変数ファイルからECSタスクへの機微情報の注入
- Github上に機微情報を配置しない
- アクセスキーではなく、IAMロール用いてGHAを実行
- IAMポリシーでの最小限の権限付与
- 運用保守性
- 単一のワークフローのみで複数環境へ選択的にデプロイ
- 単一のワークフローのみで複数のイメージをまとめてビルドしてデプロイ
- デプロイ実行後のCodeDeploy URL等を表示し、デプロイ結果を迅速に確認
- コンテナイメージのタグにコミットハッシュを使用し、アプリケーションのデプロイバージョンを一意に識別
3. 構成図
- 全体構成
ポイントは以下です。
- 開発者はGitHubにコードを、環境変数ファイルをS3に配置するだけでデプロイ準備完了
- GithubActionsに処理を集約し、単一のワークフローで複数環境へ選択的にデプロイ可能
- 脆弱性管理だけでなく、ウィルススキャンも実施し一般的なセキュリティ要件に対応
4. ワークフローファイル
- 動作条件
- ApacheコンテナとTomcatコンテナの2つをビルドし、ECSにデプロイします。
- それぞれのDockerfileは
deploy/apache
とdeploy/tomcat
に配置します。(envコンテキストで適時変更可) - appspecファイルは
.github/workflows/aws
に配置します。(envコンテキストで適時変更可) - ECSタスク定義等のAWSリソース名称は、envコンテキストに記述していますので、適宜変更してください。
GithubActionsワークフロー全体はコチラ!
name: Deploy ECS Service
on:
workflow_dispatch:
inputs:
Environment:
description: '対象の環境名称を選択'
required: true
type: choice
options:
- dev
- pre
- stg
- mnt
- trn
- prod
PJ_NAME:
description: '案件略称(固定)'
required: true
type: choice
default: sample
options:
- sample
permissions:
id-token: write
contents: read
env: #workflowファイルのjobs内で利用する環境変数
AWS_DEFAULT_REGION: ap-northeast-1
PJ_NAME: ${{ inputs.PJ_NAME }}
ENV: ${{ inputs.Environment }}
# githubリポジトリ内のファイルパス
APACHE_DOCKERFILE_PATH: deploy/apache
TOMCAT_DOCKERFILE_PATH: deploy/tomcat
CD_APPSPEC_FILE: .github/workflows/aws/appspec_${{ inputs.Environment }}.yml
# AWSリソースの名称
APACHE_CONTAINER_NAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-apache
APACHE_ECR_REPOSITORY: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-apache
CD_APPNAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-cluster-service-CodeDeployAppName
CD_DEPLOYNAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-cluster-service-CodeDeployDeploymentGroupName
ECS_CLUSTER: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-cluster
ECS_SERVICE: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-service
ECS_TASKNAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-task
TOMCAT_CONTAINER_NAME: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-tomcat
TOMCAT_ECR_REPOSITORY: ${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-tomcat
AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxxx:role/${{ inputs.PJ_NAME }}-${{ inputs.Environment }}-ghhostedrunner
# TMAS API キー
TMAS_API_KEY: ${{ secrets.TMAS_API_KEY }}
jobs:
deploymentenv:
runs-on: ubuntu-latest
steps:
- name: Set environment summary
run: echo "## このワークフローは${{ inputs.Environment }}環境に対して実行されました。" >> "${GITHUB_STEP_SUMMARY}"
ecsdeploy:
runs-on: ubuntu-latest
# checkout
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
clean: false
fetch-depth: 1 # <= https://github.com/actions/checkout/issues/265
- name: Checkout LFS objects
run: git lfs checkout # https://stackoverflow.com/questions/61463578/github-actions-actions-checkoutv2-lfs-true-flag-not-converting-pointers-to-act
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ env.AWS_ROLE_ARN }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: 'true'
- name: Get short commit hash
id: commit
run: echo "IMAGE_TAG=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV
# build
- name: Download tmas-cli # tmas-cliをダウンロード
id: download-tmas-cli
run: |
mkdir tmas && cd tmas
wget https://cli.artifactscan.cloudone.trendmicro.com/tmas-cli/latest/tmas-cli_Linux_x86_64.tar.gz
tar -zxvf tmas-cli_Linux_x86_64.tar.gz
TMAS_PATH=$(pwd)
echo "PATH=$TMAS_PATH:$PATH" >> $GITHUB_OUTPUT
# build web image
- name: Web Build, tag, and push image to Amazon ECR # ECRイメージPush, Dockerfileの配置パスは[CONTEXTに記載]
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
SCAN_REPORT=$(tmas scan docker:$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -M -r ap-northeast-1 | jq -r '.malware')
SCAN_RESULT=$(echo $SCAN_REPORT | jq -r '.scanResult')
if [ $SCAN_RESULT -ne 0 ]; then
echo "malware detected"
echo "## イメージスキャン時のマルウェア検出結果(Web)" >> "${GITHUB_STEP_SUMMARY}"
echo "$SCAN_REPORT" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
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
# build app image
- name: App Build, tag, and push image to Amazon ECR # ECRイメージPush, Dockerfileの配置パスは[CONTEXTに記載]
id: build-app
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ${{ env.TOMCAT_ECR_REPOSITORY }}
CONTEXT: ${{ env.TOMCAT_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
SCAN_REPORT=$(tmas scan docker:$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -M -r ap-northeast-1 | jq -r '.malware')
SCAN_RESULT=$(echo $SCAN_REPORT | jq -r '.scanResult')
if [ $SCAN_RESULT -ne 0 ]; then
echo "malware detected"
echo "## イメージスキャン時のマルウェア検出結果(App)" >> "${GITHUB_STEP_SUMMARY}"
echo "$SCAN_REPORT" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
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
# deploy
- name: Download task definition # 最新(revision番号値が最大)のtask-definition.jsonを取得
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 # ECSタスク定義ファイルを修正
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 }} # task-definition.json内のコンテナ名
image: ${{ steps.build-web.outputs.image }}
- name: App Render Amazon ECS task definition # ECSタスク定義ファイルを修正
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 }} # task-definition.json内のコンテナ名
image: ${{ steps.build-app.outputs.image }}
- 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 }} # task-definition.jsonを利用して起動するタスクの配置先サービス
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。
- name: Logout of Amazon ECR
if: always()
run: docker logout ${{ steps.login-ecr.outputs.registry }}
# 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}"
5. セキュリティ対策
5.1. ウィルススキャン
-
Trend Micro Artifact Scannerを利用し、コンテナイメージにウィルススキャンを実施します。
-
Trend Micro Artifact Scanner (以降TMAS)はCLIツールを任意の環境に実装して実行します。仕組みとしては、スキャン対象のコンテナイメージのSBOMを作成し、SaaSとして存在するバックエンドにSBOMを送信してスキャンをするようです。CLIツールとして実装するので、CI/CDパイプラインや開発時の任意タイミングでのスキャンが可能です。
-
事前準備としてTMAS用のAPIキーを発行し、GithubSecretsに登録します。
- ここでは、
TMAS_API_KEY
という名前で登録しています。 - APIキーの発行方法は、クラスメソッド様の記事がわかりやすいです。
- ここでは、
-
GithubHostedランナー上にTMAS-CLIをダウンロードするstepを記述します。
- name: Download tmas-cli # tmas-cliをダウンロード
id: download-tmas-cli
run: |
mkdir tmas && cd tmas
wget https://cli.artifactscan.cloudone.trendmicro.com/tmas-cli/latest/tmas-cli_Linux_x86_64.tar.gz
tar -zxvf tmas-cli_Linux_x86_64.tar.gz
TMAS_PATH=$(pwd)
echo "PATH=$TMAS_PATH:$PATH" >> $GITHUB_OUTPUT
- コンテナイメージのビルド後、TMASを実行してスキャン結果を取得します。
- マルウェアが検出された場合は、スキャン結果を出力し、ステップを失敗させます。
- name: Web Build, tag, and push image to Amazon ECR
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
SCAN_REPORT=$(tmas scan docker:$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -M -r ap-northeast-1 | jq -r '.malware')
SCAN_RESULT=$(echo $SCAN_REPORT | jq -r '.scanResult')
if [ $SCAN_RESULT -ne 0 ]; then
echo "malware detected"
echo "## イメージスキャン時のマルウェア検出結果(Web)" >> "${GITHUB_STEP_SUMMARY}"
echo "$SCAN_REPORT" >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
5.2. 脆弱性スキャン
- 文字量が多くなってしまいましたので、次回記事にて記載予定です。
おわりに
今回の記事では、エンタープライズレベルでのECS CI/CDの構築における基本的な方針と、セキュリティや運用保守性を高めるための設計ポイントについて紹介しました。
特に、GitHub Actionsを活用したセキュアで効率的なパイプライン設計や、ウィルススキャンを含むセキュリティ対策の重要性に焦点を当てています。
エンタープライズレベルのシステムでは、単なるデプロイ機能以上に、信頼性や保守性、セキュリティを担保することが求められます。
本記事の内容が、そのような要件を満たすための一助となれば幸いです。
次回の記事では、[2. 設計ポイント]で紹介したポイントの内、脆弱性スキャン以降の内容について詳しく解説します。
引き続き、実務での活用や改善提案などがあれば、ぜひコメントやフィードバックをお寄せください。一緒により良いCI/CDを作り上げていきましょう!