はじめに
今回はAIエージェントで自作したローカルアプリをAWS上で稼働させてみたので、備忘録として残します。AWS上のインフラへ移行し、開発からデプロイまでの工程が自動化された、簡易的なGitOpsパイプラインを構築する手順を説明します。なお、記載しているコード類はサンプルであり、実際に作成するアプリの内容によって多少の修正が必要です。また、動かすアプリ自体は本記事に含んでいません。
開発フロー
本構成は、2つのGitリポジトリで運用します。
- インフラリポジトリ・・・インフラ担当者が管理する想定。CDKの導入により構成が大規模化した場合にコードの保守性を維持し、IaCスキャンによりクラウド設定不備をデプロイする前に検知します。
- 開発用リポジトリ・・・開発チームが管理する想定。AIエージェントが開発し、人間の管理者が承認する開発フローで、Human-in-the-Loopです。PR時に自動的にソースコードのSAST検査を行います。承認されたコードは自動的にAWSへデプロイされます。
ブランチ保護
開発用リポジトリは2つのブランチで運用します。AIエージェントはコーディングやローカルテストが完了したら、gitを利用してdevelopブランチへ最新コードをPushします。AIエージェントにはPRをmainブランチにマージする権限がないため、ここで人間の承認なしに勝手にコードが更新されることからアプリを保護します。
CI/CDパイプラインフロー
一度パイプラインが構成されると、アプリの修正からデプロイを繰り返すCI/CDの状態になります。パイプラインを図示すると下記のようなフローになります。
アプリの構成
私が実際にAIエージェントに作成させたアプリの構成です。
全体アーキテクチャ
まずはシステム全体の構成です。CodeCommitで、アプリとインフラをコードで管理します。CodeCommitへのPushをトリガーとして、インフラリポジトリであればCDKを利用してインフラを自動デプロイします。アプリリポジトリであれば、コンテナイメージをビルドしてECRを更新し、ECSで実行されるタスクを自動的に更新します。アプリのデータは、EFSに保管します。
本構成では個人利用を想定し、極力コストを抑えています。およそ毎月のランニングコストは1,000円〜2,000円になります。
- ECS Fargateでスポットインスタンス、スケジューリングで利用するときだけ起動
- NATは利用せず、ECSはパブリックセグメントに配置
- PCやスマートフォンからのアクセスにngrok(無料版)を利用
以下構築手順です。
フェーズ1 CI/CD基盤とガードレールの構築
①リポジトリの準備
CodeCommitに2つのリポジトリを作成します。(例:autotrade / autotrade-infra )
CloudShellで操作するのが簡単です。
# 1. アプリケーション用リポジトリの作成
aws codecommit create-repository --repository-name autotrade --repository-description "AutoTrade Application Repository"
# 2. インフラ(CDK)用リポジトリの作成
aws codecommit create-repository --repository-name autotrade-infra --repository-description "AutoTrade Infrastructure CDK Repository"
②認証とブランチ保護
CodeCommit用のIAMユーザーを作成し、SSHキーをAIエージェントを利用しているローカルに設定します。このIAMユーザーに対し、mainブランチへの直接Pushを禁止するインラインポリシーをアタッチします。
SSHキーはローカル側で作成して、上記画面でキーをアップロードして登録してください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowRepositoryCommonActionsAndPullPush",
"Effect": "Allow",
"Action": [
"codecommit:GitPull",
"codecommit:GitPush",
"codecommit:Get*",
"codecommit:BatchGet*",
"codecommit:List*",
"codecommit:Describe*"
],
"Resource": "arn:aws:codecommit:ap-northeast-1:your-AWSAccount:autotrade"
},
{
"Sid": "DenyDirectPushToMainBranch",
"Effect": "Deny",
"Action": [
"codecommit:GitPush"
],
"Resource": "arn:aws:codecommit:ap-northeast-1:your-AWSAccount:autotrade",
"Condition": {
"StringEqualsIfExists": {
"codecommit:References": [
"refs/heads/main"
]
},
"Null": {
"codecommit:References": "false"
}
}
},
{
"Sid": "AllowPullRequestManagement",
"Effect": "Allow",
"Action": [
"codecommit:CreatePullRequest",
"codecommit:UpdatePullRequestDescription",
"codecommit:UpdatePullRequestStatus",
"codecommit:UpdatePullRequestTitle",
"codecommit:PostCommentForPullRequest",
"codecommit:UpdateComment"
],
"Resource": "arn:aws:codecommit:ap-northeast-1:your-AWSAccount:autotrade"
}
]
}
③パイプライン基盤のデプロイ
パイプライン構築用のCloudFormationテンプレートを実行し、CodePipelineとEventBrdige(PR検知用)のフレームワークを作成します。
AWSTemplateFormatVersion: '2010-09-09'
Description: CI/CD Pipeline for AutoTrade System (Infra & App)
Parameters:
InfraRepositoryName:
Type: String
Default: autotrade-infra # 自身の環境のリポジトリ名称に変更してください。
Description: Name of the CodeCommit repository for Infrastructure (CDK)
AppRepositoryName:
Type: String
Default: autotrade # 自身の環境のリポジトリ名称に変更してください。
Description: Name of the CodeCommit repository for Application
BranchName:
Type: String
Default: main
Description: Branch name to trigger the pipeline
ClusterName:
Type: String
Default: autotrade-cluster # 自身のアプリ名称などに変更してください。
Description: ECS Cluster name
ServiceName:
Type: String
Default: autotrade-prod # 自身のアプリ名称などに変更してください。
Description: ECS Service name
Resources:
# --- Artifact Storage ---
PipelineArtifactBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
# --- CodeBuild Projects ---
BuildInfraProject:
Type: AWS::CodeBuild::Project
Properties:
Name: Build-Infra-Project
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: "buildspec-infra.yml"
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:8.0
Type: LINUX_CONTAINER
PrivilegedMode: false
BuildAppProject:
Type: AWS::CodeBuild::Project
Properties:
Name: Build-App-Project
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: "buildspec-app.yml"
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:8.0
Type: LINUX_CONTAINER
PrivilegedMode: true # Dockerビルドに必須
# --- 1. インフラ用パイプライン ---
AutoTradeInfraPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: AutoTrade-Infra-Pipeline
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore: {Type: S3, Location: !Ref PipelineArtifactBucket}
Stages:
- Name: Source
Actions:
- Name: SourceInfra
ActionTypeId: {Category: Source, Owner: AWS, Provider: CodeCommit, Version: '1'}
Configuration: {RepositoryName: !Ref InfraRepositoryName, BranchName: !Ref BranchName}
OutputArtifacts: [{Name: InfraSource}]
- Name: InfraDeployment
Actions:
- Name: DeployInfra
ActionTypeId: {Category: Build, Owner: AWS, Provider: CodeBuild, Version: '1'}
InputArtifacts: [{Name: InfraSource}]
Configuration: {ProjectName: !Ref BuildInfraProject}
# --- 2. アプリ用パイプライン ---
AutoTradeAppPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: AutoTrade-App-Pipeline
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore: {Type: S3, Location: !Ref PipelineArtifactBucket}
Stages:
- Name: Source
Actions:
- Name: SourceApp
ActionTypeId: {Category: Source, Owner: AWS, Provider: CodeCommit, Version: '1'}
Configuration: {RepositoryName: !Ref AppRepositoryName, BranchName: !Ref BranchName}
OutputArtifacts: [{Name: AppSource}]
- Name: AppBuild
Actions:
- Name: BuildDocker
ActionTypeId: {Category: Build, Owner: AWS, Provider: CodeBuild, Version: '1'}
InputArtifacts: [{Name: AppSource}]
OutputArtifacts: [{Name: AppBuildArtifact}]
Configuration: {ProjectName: !Ref BuildAppProject}
- Name: AppDeploy
Actions:
- Name: DeployECS
ActionTypeId: {Category: Deploy, Owner: AWS, Provider: ECS, Version: '1'}
InputArtifacts: [{Name: AppBuildArtifact}]
Configuration: {ClusterName: !Ref ClusterName, ServiceName: !Ref ServiceName}
# --- IAM Roles ---
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement: [{Effect: Allow, Principal: {Service: codebuild.amazonaws.com}, Action: sts:AssumeRole}]
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess # インフラ構築のため権限強め推奨
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement: [{Effect: Allow, Principal: {Service: codepipeline.amazonaws.com}, Action: sts:AssumeRole}]
Policies:
- PolicyName: PipelineAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: [s3:GetObject, s3:PutObject, s3:GetBucketLocation]
Resource: ["arn:aws:s3:::*", "arn:aws:s3:::*/*"]
- Effect: Allow
Action:
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:UploadArchive
- codecommit:GetUploadArchiveStatus
- codecommit:GetRepository
Resource: "*"
- Effect: Allow
Action: [codebuild:StartBuild, codebuild:BatchGetBuilds]
Resource: "*"
# ECSデプロイに必要な権限を網羅
- Effect: Allow
Action:
- ecs:UpdateService
- ecs:DescribeServices
- ecs:DescribeTaskDefinition
- ecs:RegisterTaskDefinition
Resource: "*"
# 新しいECSタスクに実行ロール等を渡すための権限
- Effect: Allow
Action: iam:PassRole
Resource: "*"
# --- アプリPR検証用 CodeBuild プロジェクト ---
BuildAppPRProject:
Type: AWS::CodeBuild::Project
Properties:
Name: Test-App-PR-Project
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
Type: CODECOMMIT
Location: !Sub "https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${AppRepositoryName}"
BuildSpec: "buildspec-app-pr.yml"
Artifacts:
Type: NO_ARTIFACTS
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:8.0
Type: LINUX_CONTAINER
PrivilegedMode: false
# --- 【追加】PR検証用 EventBridge が CodeBuild を起動するための IAM ロール ---
EventBridgeInvokeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CloudWatchEventsCodeBuildExecutionPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- codebuild:StartBuild
Resource: !GetAtt BuildAppPRProject.Arn
# --- PRの作成・更新を検知してCodeBuildを起動するEventBridgeルール ---
AppPRTriggerRule:
Type: AWS::Events::Rule
Properties:
Description: Trigger CodeBuild when a PR is created or updated for autotrade repo
EventPattern:
source:
- aws.codecommit
detail-type:
- 'CodeCommit Pull Request State Change'
resources:
- !Sub "arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${AppRepositoryName}"
detail:
event:
- pullRequestCreated
- pullRequestSourceBranchUpdated
destinationReference:
- refs/heads/main
Targets:
- Arn: !GetAtt BuildAppPRProject.Arn
RoleArn: !GetAtt EventBridgeInvokeBuildRole.Arn
Id: TargetCodeBuildPRCheck
# PRのメタデータを環境変数としてCodeBuildに注入する
InputTransformer:
InputPathsMap:
sourceBranch: "$.detail.sourceReference"
prId: "$.detail.pullRequestId"
sourceCommit: "$.detail.sourceCommit"
destCommit: "$.detail.destinationCommit"
InputTemplate: |
{
"sourceVersion": <sourceBranch>,
"environmentVariablesOverride": [
{ "name": "PR_ID", "value": <prId>, "type": "PLAINTEXT" },
{ "name": "SOURCE_COMMIT", "value": <sourceCommit>, "type": "PLAINTEXT" },
{ "name": "DEST_COMMIT", "value": <destCommit>, "type": "PLAINTEXT" }
]
}
④インフラのベース初期化
CloudShellでCDK initを実施し、buildspec-infra.ymlを含めた初期のディレクトリツリーをインフラリポジトリにPushします。
# .nvm を有効化して最新のLTS(Node 20など)を隔離インストール
mkdir autotrade-infra && cd autotrade-infra
nvm install 20
nvm use 20
# この隔離環境のnpmを使ってCDKを初期化
npx cdk init app --language typescript
version: 0.2
phases:
install:
runtime-versions:
nodejs: 22
python: 3.11
commands:
- echo "[INSTALL] 依存関係とスキャンツールのインストール..."
- pip install checkov
- npm install -g aws-cdk
- npm ci
build:
commands:
- echo "[BUILD] CDKの合成(synth)..."
- npx cdk synth
- echo "[BUILD] Checkov による IaC セキュリティスキャンを実行..."
- checkov -d cdk.out/
# IaCスキャンで問題がみつかったら、デプロイに進まずにここで終了する。
- echo "[BUILD] インフラのデプロイ開始..."
- npx cdk deploy --all --require-approval never
CDKを初期化すると下記のようなフォルダ構成になりますので、buildspec-infra.ymlをcdk-infraの直下にアップロードします。
autotrade-infra/
├── .git/ # Git管理ディレクトリ (隠しフォルダ)
├── .gitignore # node_modules などを除外する設定 (自動生成)
├── README.md
├── bin/
│ └── infra.ts # CDKアプリのエントリポイント
├── buildspec-infra.yml # ★追加してください:Checkov等の検証・デプロイ仕様書
├── cdk.json
├── jest.config.js
├── lib/
│ ├── app-stack.ts # (後で作成・修正するECS等のスタック)
│ └── infra-stack.ts # (後で作成・修正するVPC等のスタック)
├── package-lock.json
├── package.json
├── test/
│ └── infra.test.ts
└── tsconfig.json
そのままCodeCommitへPushします。
git config user.name "init"
git config user.email "init@example.com"
git add .
git commit -m "feat: initial CDK setup with buildspec-infra.yml"
git branch -M main
# CodeCommit のリモートリポジトリを登録(リポジトリ名は自身の環境に合わせて変更)
git remote add origin codecommit://autotrade-infra
git push -u origin main
フェーズ2 アプリの初期ビルド(イメージの用意)
ここからは、実際に動くアプリがないと先に進むことができません。
⑤アプリ側のCI/CDファイル配置
ローカルで、Dockerfile、SAST検証用の buildspec-app-pr.yml、ビルド用の buildspec-app.yml の3点をアプリのルートディレクトリに作成します。
(※この時、最低限の実行ができるアプリコードも配置してください。)
# これはサンプルです。作成したアプリにあわせたものをAIエージェントに依頼して作成してもらってください。
FROM public.ecr.aws/docker/library/python:3.13-slim
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
version: 0.2
phases:
install:
runtime-versions:
python: 3.11
commands:
- echo "[INSTALL] テスト・SAST・シークレットスキャンツールのインストール..."
- pip install pytest pytest-cov bandit flake8 detect-secrets
- |
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
pre_build:
commands:
- echo "[PRE-BUILD] 静的解析 (SAST) およびシークレットスキャンの実行..."
# エラーが起きても最後まで完走させるためのフラグと、結果記録用のファイルを用意
- ERROR_FLAG=0
- touch report.md
# ----------------------------------------------------------------------
# 1. detect-secrets によるパスワード・秘密鍵の直書きスキャン
# ----------------------------------------------------------------------
# .secrets.baseline は既知の誤検知(is_verified=True)を記録したファイル (git でコミット)
- echo "-> シークレットチェック実行中..."
- |
# baseline がない場合は作成
if [ ! -f .secrets.baseline ]; then
detect-secrets scan \
--exclude-files '^\.secrets\.baseline$' \
> .secrets.baseline 2>/dev/null || true
fi
# 新規スキャン
detect-secrets scan \
--exclude-files '^\.secrets\.baseline$' \
> .secrets.new 2>/dev/null || true
# baseline と比較して未確認(is_verified=False)かつ新規の検出のみエラー
# hashed_secret で比較(同じシークレットなら同じ hash)
NEW_SECRETS=$(python3 scripts/check_secrets_diff.py 2>&1)
if echo "$NEW_SECRETS" | grep -q "FOUND"; then
echo "- ❌ **Secrets**: 新規シークレットが検出されました" >> report.md
echo "**baseline に未登録の検出:**" >> report.md
echo '```' >> report.md
echo "$NEW_SECRETS" | grep -v "^$" | head -10 >> report.md
echo '```' >> report.md
echo "" >> report.md
echo "修正方法: baseline を更新するか、コードから秘密情報を削除" >> report.md
ERROR_FLAG=1
else
echo "- ✅ **Secrets**: 異常なし (既知の偽陽性は baseline で除外)" >> report.md
fi
rm -f .secrets.new
# ----------------------------------------------------------------------
# 2. Flake8 によるコードスタイル・構文チェック
# ----------------------------------------------------------------------
- echo "-> 構文チェック実行中..."
- cd $CODEBUILD_SRC_DIR
- flake8 ui/ scripts/ --select=E9,F63,F7,F82 --show-source --statistics | tee flake8_critical.log
- flake8 ui/ scripts/ --max-line-length=130 --ignore=E1,E2,E3,E5,W5 --exit-zero | tee flake8_warnings.log
- |
# クリティカルエラーがあればフラグを立てる
if [ -s flake8_critical.log ]; then
echo "❌ **Flake8**: 致命的な構文エラーが検出されました" >> report.md
ERROR_FLAG=1
fi
if [ -s flake8_warnings.log ]; then
WARN_COUNT=$(wc -l < flake8_warnings.log)
echo "- ⚠️ **Flake8**: ${WARN_COUNT} 件のスタイル警告(純粋なスタイルは除外済み)" >> report.md
echo "" >> report.md
echo "<details><summary>Flake8 詳細 (最初の 10 件)</summary>" >> report.md
echo "" >> report.md
echo '```' >> report.md
head -10 flake8_warnings.log >> report.md
echo '```' >> report.md
echo "" >> report.md
echo "</details>" >> report.md
else
echo "- ✅ **Flake8**: 異常なし" >> report.md
fi
rm -f flake8_critical.log flake8_warnings.log
# ----------------------------------------------------------------------
# 3. Bandit による Python 脆弱性スキャン
# ----------------------------------------------------------------------
- echo "-> セキュリティスキャン実行中..."
- bandit -r ui/ scripts/ -ll > bandit.log 2>&1 || true
- |
if grep -q "No issues identified" bandit.log; then
echo "- ✅ **SAST (Bandit)**: 異常なし" >> report.md
else
HIGH=$(grep "Severity: High" bandit.log | wc -l | tr -d ' ')
MED=$(grep "Severity: Medium" bandit.log | wc -l | tr -d ' ')
LOW=$(grep "Severity: Low" bandit.log | wc -l | tr -d ' ')
echo "- ⚠️ **SAST (Bandit)**: High=${HIGH} Medium=${MED} Low=${LOW} 件の検出" >> report.md
echo "" >> report.md
echo "<details><summary>Bandit 詳細 (最初の 10 件)</summary>" >> report.md
echo "" >> report.md
echo '```' >> report.md
head -50 bandit.log >> report.md
echo '```' >> report.md
echo "" >> report.md
echo "</details>" >> report.md
# High があれば失敗、Medium/Low のみは警告
if [ "$HIGH" -gt 0 ]; then
ERROR_FLAG=1
fi
fi
rm -f bandit.log
build:
commands:
- echo "[BUILD] pytest による単体/結合テストの実行..."
- cd $CODEBUILD_SRC_DIR
- pytest tests/ -q --tb=short --cov=scripts --cov=ui > pytest.log 2>&1 || true
- |
# テスト結果を集計
PASSED=$(grep -oE "[0-9]+ passed" pytest.log | tail -1 | grep -oE "^[0-9]+" || echo 0)
FAILED=$(grep -oE "[0-9]+ failed" pytest.log | tail -1 | grep -oE "^[0-9]+" || echo 0)
ERRORS=$(grep -oE "[0-9]+ error" pytest.log | tail -1 | grep -oE "^[0-9]+" || echo 0)
SKIPPED=$(grep -oE "[0-9]+ skipped" pytest.log | tail -1 | grep -oE "^[0-9]+" || echo 0)
# 最終行に 'XXX passed in Y.YYs' または 'no tests ran' が来る
COVERAGE=$(grep -oE "TOTAL [0-9]+ +[0-9]+ +[0-9]+%?" pytest.log | tail -1 | grep -oE "[0-9]+%" || echo "N/A")
TOTAL=$((PASSED + FAILED + ERRORS))
if [ "$PASSED" -eq 0 ] && [ "$FAILED" -eq 0 ] && [ "$ERRORS" -eq 0 ]; then
# テストが実行されなかった(カバレッジ測定の失敗など)
if grep -q "no tests ran" pytest.log; then
echo "- ❌ **Pytest**: テストが実行されていません (テストファイルが認識されない)" >> report.md
ERROR_FLAG=1
else
echo "- ⚠️ **Pytest**: テスト結果を取得できません(ログを確認)" >> report.md
ERROR_FLAG=1
fi
elif [ "$FAILED" -gt 0 ] || [ "$ERRORS" -gt 0 ]; then
echo "- ❌ **Pytest**: ${PASSED}/${TOTAL} 通過, ${FAILED} 失敗, ${ERRORS} エラー" >> report.md
ERROR_FLAG=1
else
echo "- ✅ **Pytest**: ${PASSED}/${TOTAL} 通過 (カバレッジ: ${COVERAGE})" >> report.md
fi
# 失敗時の詳細
if [ "$FAILED" -gt 0 ] || [ "$ERRORS" -gt 0 ] || [ "$PASSED" -eq 0 ]; then
echo "" >> report.md
echo "<details><summary>Pytest ログ (最後の 30 行)</summary>" >> report.md
echo "" >> report.md
echo '```' >> report.md
tail -30 pytest.log >> report.md
echo '```' >> report.md
echo "" >> report.md
echo "</details>" >> report.md
fi
rm -f pytest.log
post_build:
commands:
- echo "[POST-BUILD] PR コメント投稿処理"
- |
if [ -n "$PR_ID" ]; then
if [ "$ERROR_FLAG" -eq 0 ]; then
OVERALL="✅ **SUCCESS**: 全ての検証をクリアしました。マージ可能です。"
else
OVERALL="❌ **FAILED**: 検証エラーがあります。ログを確認して修正してください。"
fi
# AWS CodeBuildコンソールの詳細ログへの直リンクを組み立て
LOG_URL="https://${AWS_DEFAULT_REGION}.console.aws.amazon.com/codesuite/codebuild/projects/${CODEBUILD_BUILD_ID%:*}/build/${CODEBUILD_BUILD_ID}/?region=${AWS_DEFAULT_REGION}"
# コメント本文の組み立て
{
echo "### 🔍 CI/CD 自動検証レポート"
echo ""
echo "$OVERALL"
echo ""
echo "| 検証項目 | 結果 |"
echo "|---------|------|"
cat report.md
echo ""
echo "---"
echo "🔍 **[CodeBuildの詳細ログを確認する]($LOG_URL)**"
echo ""
echo "<!-- buildspec-app-pr-auto-comment -->"
} > comment.md
aws codecommit post-comment-for-pull-request \
--repository-name autotrade \
--pull-request-id "$PR_ID" \
--before-commit-id "$DEST_COMMIT" \
--after-commit-id "$SOURCE_COMMIT" \
--content "$(cat comment.md)"
if [ $? -eq 0 ]; then
echo "[OK] PR コメント投稿完了"
else
echo "[WARN] PR コメント投稿に失敗 (CodeCommit API の権限を確認)"
fi
rm -f comment.md report.md
else
echo "[INFO] PR トリガーではないため、コメント投稿をスキップ"
fi
# 最後にビルドの成否をAWSへ伝達(1つでもエラーがあればFAILさせる)
- |
if [ "$ERROR_FLAG" -ne 0 ]; then
exit 1
fi
version: 0.2
phases:
pre_build:
commands:
- echo "[PRE-BUILD] ECRへのログイン..."
- AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
- REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/autotrade
- IMAGE_TAG=latest
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $REPOSITORY_URI
build:
commands:
- echo "[BUILD] Dockerイメージのビルド開始..."
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
post_build:
commands:
- echo "[POST-BUILD] DockerイメージをECRへプッシュ..."
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo "[POST-BUILD] ECSデプロイ用の定義ファイルを生成..."
# ※ "autotrade-container" の部分は、CDKの AppStack で定義するコンテナ名と完全に一致させる必要があります
- printf '[{"name":"autotrade-container","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
⑥PR作成と自動検証の実行
AIエージェント(または手動)でアプリコードを develop ブランチにPushし、PRを作成します。EventBridgeが検知してCodeBuildが走り、SAST(シークレットスキャン、脆弱性診断、Pytest)とPRへの結果コメント自動投稿が行われるようになります。
⑦マージとコンテナイメージの保存
PR検証成功後、main にマージします。マージを実行すると、アプリ側の CodePipeline が自動的に開始され、Dockerビルド → ECRへ最初のコンテナイメージがPushされます。
(※インフラが未構築のため、パイプラインの最後の「ECSへのデプロイ」フェーズが失敗して止まります)
フェーズ3 インフラの実装とデプロイ
⑧ インフラコード (CDK) の実装とパラメータ登録
CloudShellで、ngrokの認証トークン等をSSMパラメータストアに登録します。その後、autotrade-infra リポジトリに、VPC、ECR(参照)、EFS(アクセスポイント付き)、ECS(Fargate)、ngrokサイドカー、スケジュール起動ロジックを含めた本格的なCDKコードを実装します。
aws ssm put-parameter \
--name "/autotrade/prod/ngrok-auth-token" \
--value "ここにngrokの実際のトークンを貼り付けます" \
--type "SecureString" \
--overwrite
# 必要なパラメータを登録してください。
CDKデフォルトのものを下記のように変更します。
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { InfraStack } from '../lib/infra-stack';
import { AppStack } from '../lib/app-stack';
const app = new cdk.App();
// 1. 変数として InfraStack をインスタンス化する
const infraStack = new InfraStack(app, 'InfraStack', {});
// 2. AppStack をインスタンス化し、props として infraStack を渡す
new AppStack(app, 'AppStack', {
infra: infraStack,
});
インフラのリソース定義を作成します。
下記はサンプルです。作成するリソースや、リソース名は自身のプロジェクトに応じて適宜変更してください。AIエージェントに依頼すればだいたい作成してくれます。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as efs from 'aws-cdk-lib/aws-efs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
export class InfraStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
public readonly efsFileSystem: efs.FileSystem;
public readonly appSecurityGroup: ec2.SecurityGroup;
public readonly repository: ecr.Repository;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 1. VPCの作成
this.vpc = new ec2.Vpc(this, 'AutoTradeVpc', {
maxAzs: 2,
natGateways: 0,
});
// CDKの裏側の処理に対するCheckovの指摘(LambdaのDLQ、同時実行数、VPC配置)をスキップする設定
// CDKが裏側で自動作成するVPCクリーンアップ用のLambda(CustomResourceProvider)を取得
const cfnVpcRestrictDefaultSG = this.node.tryFindChild('Custom::VpcRestrictDefaultSGCustomResourceProvider') as cdk.CustomResourceProvider;
if (cfnVpcRestrictDefaultSG) {
// 内部のLambdaカスタムリソースのメタデータにCheckovスキップを注入
const lambdaFunction = cfnVpcRestrictDefaultSG.node.findChild('Handler') as cdk.CfnResource;
lambdaFunction.cfnOptions.metadata = {
checkov: {
skip: [
{ id: 'CKV_AWS_116', comment: 'CDK internal VPC custom resource Lambda does not need DLQ' },
{ id: 'CKV_AWS_115', comment: 'CDK internal VPC custom resource Lambda does not need concurrency limit' },
{ id: 'CKV_AWS_117', comment: 'CDK internal VPC custom resource Lambda is safe outside VPC' }
]
}
};
}
// 2. セキュリティグループの作成 (インバウンドはデフォルト全拒否)
this.appSecurityGroup = new ec2.SecurityGroup(this, 'AppSecurityGroup', {
vpc: this.vpc,
allowAllOutbound: true,
securityGroupName: 'autotrade-sg',
});
// 3. EFSの作成
this.efsFileSystem = new efs.FileSystem(this, 'AutoTradeEfs', {
vpc: this.vpc,
encrypted: true,
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
this.efsFileSystem.connections.allowDefaultPortFrom(this.appSecurityGroup);
// 4. ECRリポジトリの作成
this.repository = new ecr.Repository(this, 'AutoTradeRepo', {
repositoryName: 'autotrade',
removalPolicy: cdk.RemovalPolicy.DESTROY,
lifecycleRules: [{ maxImageCount: 3 }],
// プッシュ時のイメージスキャンを有効化
imageScanOnPush: true,
});
// タグの不変性、KMS暗号化への対応:
// パイプライン(latest上書き運用)の維持と、CMKの使用を回避するため、意図的にスキップを設定
const cfnRepository = this.repository.node.defaultChild as ecr.CfnRepository;
cfnRepository.cfnOptions.metadata = {
checkov: {
skip: [
{ id: 'CKV_AWS_51', comment: 'Allow tag overwriting for latest tag deployment strategy' },
{ id: 'CKV_AWS_136', comment: 'Use AWS managed key instead of KMS CMK to minimize costs' }
]
}
};
}
}
次にアプリで利用するリソースの定義を作成します。
こちらもサンプルです。自身のアプリにあわせて作成するリソースや名称、設定を変更してください。AIエージェントにお願いすれば概ね作成してもらえます。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as efs from 'aws-cdk-lib/aws-efs';
import * as appscaling from 'aws-cdk-lib/aws-applicationautoscaling';
import { InfraStack } from './infra-stack';
interface AppStackProps extends cdk.StackProps {
infra: InfraStack;
}
export class AppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: AppStackProps) {
super(scope, id, props);
const { vpc, efsFileSystem, appSecurityGroup, repository } = props.infra;
// 1. ECS クラスターの作成
const cluster = new ecs.Cluster(this, 'AutoTradeCluster', {
vpc,
clusterName: 'autotrade-cluster'
});
const cfnCluster = cluster.node.defaultChild as ecs.CfnCluster;
cfnCluster.cfnOptions.metadata = {
checkov: { skip: [{ id: 'CKV_AWS_65', comment: 'Container Insights incurs additional custom metric costs. Not required for this environment.' }] }
};
// 2. SSMパラメータの参照取得
const googleKey = ecs.Secret.fromSsmParameter(ssm.StringParameter.fromSecureStringParameterAttributes(this, 'GoogleKey', { parameterName: '/autotrade/prod/google-api-key' }));
const appLogGroup = new logs.LogGroup(this, 'AppLogGroup', {
logGroupName: '/ecs/autotrade-prod',
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const cfnLogGroup = appLogGroup.node.defaultChild as logs.CfnLogGroup;
cfnLogGroup.cfnOptions.metadata = {
checkov: { skip: [{ id: 'CKV_AWS_158', comment: 'Using AWS default encryption to avoid $1/month CMK cost.' }] }
};
// 3. Fargate タスク定義
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
cpu: 1024,
memoryLimitMiB: 2048,
});
const efsPolicy = new iam.PolicyStatement({
actions: [
'elasticfilesystem:ClientMount',
'elasticfilesystem:ClientWrite',
'elasticfilesystem:ClientRootAccess'
],
resources: [efsFileSystem.fileSystemArn],
});
taskDefinition.addToExecutionRolePolicy(efsPolicy);
taskDefinition.addToTaskRolePolicy(efsPolicy);
// =========================================================
// EFS アクセスポイントの定義(パーミッションエラー解消)
// =========================================================
const accessPoint = new efs.AccessPoint(this, 'AutoTradeAccessPoint', {
fileSystem: efsFileSystem,
path: '/autotrade',
posixUser: { uid: '1000', gid: '1000' },
createAcl: { ownerUid: '1000', ownerGid: '1000', permissions: '755' },
});
// EFSボリュームをタスク定義に追加
taskDefinition.addVolume({
name: 'autotrade-efs-volume',
efsVolumeConfiguration: {
fileSystemId: efsFileSystem.fileSystemId,
transitEncryption: 'ENABLED',
authorizationConfig: {
iam: 'ENABLED',
accessPointId: accessPoint.accessPointId, // アクセスポイントをバインド
}
},
});
// コンテナ定義
const container = taskDefinition.addContainer('autotrade-container', {
image: ecs.ContainerImage.fromEcrRepository(repository, 'latest'),
logging: ecs.LogDrivers.awsLogs({
logGroup: appLogGroup,
streamPrefix: 'app'
}),
command: ['./entrypoint.sh'],
environment: {
TZ: 'Asia/Tokyo',
// アプリが利用する環境変数を定義
},
secrets: {
GOOGLE_API_KEY: googleKey,
// アプリが利用するシークレットキーを定義
},
});
container.addPortMappings({ containerPort: 5000 });
container.addMountPoints({
containerPath: '/mnt/efs',
sourceVolume: 'autotrade-efs-volume',
readOnly: false,
});
// 本来はALBを利用すべきです。あくまで個人利用のアプリなので無料で利用できる下記を代用します。
// ngrok サイドカーコンテナの定義
const ngrokToken = ecs.Secret.fromSsmParameter(ssm.StringParameter.fromSecureStringParameterAttributes(this, 'NgrokToken', { parameterName: '/autotrade/prod/ngrok-auth-token' }));
const ngrokContainer = taskDefinition.addContainer('ngrok-sidecar', {
image: ecs.ContainerImage.fromRegistry('ngrok/ngrok:latest'),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'ngrok',
logGroup: appLogGroup
}),
command: [
'http',
'localhost:5000',
'--log', 'stdout'
],
secrets: {
NGROK_AUTHTOKEN: ngrokToken,
},
});
ngrokContainer.addContainerDependencies({
container: container,
condition: ecs.ContainerDependencyCondition.START,
});
// 4. Fargate サービスの作成
const appService = new ecs.FargateService(this, 'AutoTradeService', {
cluster,
serviceName: 'autotrade-prod',
taskDefinition,
desiredCount: 1,
capacityProviderStrategies: [{ capacityProvider: 'FARGATE_SPOT', weight: 1 }],
securityGroups: [appSecurityGroup],
assignPublicIp: true,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
enableExecuteCommand: true,
minHealthyPercent: 0,
maxHealthyPercent: 100,
availabilityZoneRebalancing: ecs.AvailabilityZoneRebalancing.DISABLED,
});
// =========================================================
// ECS タスクのスケジュール起動・停止設定
// =========================================================
const scalableTarget = appService.autoScaleTaskCount({
minCapacity: 0,
maxCapacity: 1,
});
// ① 平日 7:00 起動
scalableTarget.scaleOnSchedule('StartMorning', {
schedule: appscaling.Schedule.cron({ minute: '0', hour: '7', weekDay: 'MON-FRI' }),
timeZone: cdk.TimeZone.ASIA_TOKYO,
minCapacity: 1,
maxCapacity: 1,
});
// ② 平日 16:00 停止
scalableTarget.scaleOnSchedule('StopAfternoon', {
schedule: appscaling.Schedule.cron({ minute: '0', hour: '16', weekDay: 'MON-FRI' }),
timeZone: cdk.TimeZone.ASIA_TOKYO,
minCapacity: 0,
maxCapacity: 0,
});
}
}
⑨ インフラのデプロイ実行
インフラコードを main にPushします。インフラ側の CodePipeline が走り、Checkovのスキャンを通過した後、AWS環境が構築されます。
※autotrade-infraリポジトリの3つのファイルをCodeCommit上で編集した場合、Pipelineは3回実行されますが、気にせず進めてください。最初の2回は定義が完成しないため、かならず失敗します。
CodeBuildのログで、Checkovスキャンが実行されていることが確認できます。
※ここまで到達していない場合は、CDKコードのSyntax Errorになっている場合があります。
Checkovのセキュリティチェックに引っかかった例です。対応する必要があるかどうかを確認し、CDKコード修正を行ってください。下記ではCloudWatch Logsのロググループに保持期間を設定していないことを指摘されています。対応不要であればCDKコードにSkipのタグを追加します。
成功するとCloudFormationスタックが実行され、インフラリソースが作成されます。
⑩アプリパイプラインの最終同期
フェーズ2の最後で止まっていたアプリ側の CodePipeline を開き、エラーになっていたECSデプロイフェーズを「再試行」します。最新のタスクが稼働し、構築完了します。
おわりに
今回は個々の仕組みの解説はせず、パイプライン構築の最低限の内容を一式詰め込み、すぐに利用できるものをコンセプトにしました。内容をアレンジすれば他の環境にも適用できるのではないかと思います。誰かのお役に立てれば幸いです。
