はじめに
ECS Fargate への CI/CD パイプラインを AWS CDK (TypeScript) で構築しました。
v1.0 の「GitHub Actions で force-new-deployment」から始め、v2.0 の「CodePipeline + CodeDeploy Blue/Green」まで段階的に育てています。
この記事では各バージョンで 何を選択し、なぜそう設計したか をまとめます。
最終構成(v2.0):
ソースコード: GitHub
CDK の構成
1スタック + Construct 分割の構成です。
スタック分割を採用しなかった理由は、リソース間の参照が多く(SG → ECS → ALB → CodeDeploy など)、マルチスタックにすると CfnOutput / Fn::ImportValue が増えて可読性が下がるためです。
単一スタックなら props 渡しだけで完結し、循環依存も発生しません。
lib/
├── pipeline-stack.ts # ルートスタック
└── constructs/
├── network.ts # VPC / SG
├── ecr.ts # ECR リポジトリ
├── compute.ts # ALB / ECS(Blue/Green コントローラー)
└── pipeline.ts # CodePipeline / CodeBuild / CodeDeploy
v1.0 — GitHub Actions + force-new-deployment
構成
GitHub(main push)
│
└── GitHub Actions
├── OIDC 認証(IAM ロール AssumeRole)
├── Docker ビルド
├── ECR push(:latest)
└── ECS force-new-deployment
AWS構成図(イメージ)は以下記事のGithub→ECR部分を参照してください。
OIDC 認証を選んだ理由
GitHub Actions から AWS を操作する方法は2つあります。
| 方法 | 判定 | 理由 |
|---|---|---|
| OIDC 認証 | ◎ 採用 | アクセスキーを発行しない。一時トークンで認証するため漏洩リスクがない |
| アクセスキー | × | Secrets に保存しても漏洩リスクがある。ローテーション運用も必要 |
OIDC の仕組みは「GitHub Actions が実行されるたびに GitHub が一時トークンを発行 → そのトークンで AWS に証明 → IAM ロールを一時的に貸し出す」という流れです。
CDK での実装:
const provider = new iam.OpenIdConnectProvider(this, "GitHubOidcProvider", {
url: "https://token.actions.githubusercontent.com",
clientIds: ["sts.amazonaws.com"],
});
const role = new iam.Role(this, "GitHubActionsRole", {
assumedBy: new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
StringEquals: {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub":
"repo:<GITHUB_OWNER>/<GITHUB_REPO>:ref:refs/heads/main",
},
}),
});
sub クレームで ref:refs/heads/main に絞るのが重要です。
絞らないと他ブランチからも AssumeRole できてしまいます。
IAM 最小権限設計
ecr:GetAuthorizationToken だけはリソース指定ができない(* が必須)ため、別の PolicyStatement に分離しています。
// GetAuthorizationToken はリソース指定不可
role.addToPolicy(new iam.PolicyStatement({
actions: ["ecr:GetAuthorizationToken"],
resources: ["*"],
}));
// ECR push は対象リポジトリに限定
role.addToPolicy(new iam.PolicyStatement({
actions: [
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
],
resources: [props.repository.repositoryArn],
}));
// ECS 更新は対象サービスに限定
role.addToPolicy(new iam.PolicyStatement({
actions: ["ecs:UpdateService"],
resources: [props.service.serviceArn],
}));
v1.0 の限界
force-new-deployment にはロールバック機能がありません。
デプロイ失敗時は手動で前のイメージを push し直す必要がありますので、本番運用には不向きです。
v2.0 — CodePipeline + CodeDeploy Blue/Green
構成
GitHub(main push)
│
└── GitHub Actions
├── OIDC 認証
├── Docker ビルド
└── ECR push(:latest)
↓ ECR イメージ変更を EventBridge で検知
CodePipeline
├── Source: ECR(imageDetail.json)
├── Build: CodeBuild(appspec.yaml / taskdef.json 動的生成)
└── Deploy: CodeDeploy Blue/Green
└── ECS(ALB :80 Blue ↔ Green 切り替え、テスト :8080)
GitHub Actions の役割を「ECR push のみ」に絞った理由
v2.0 では GitHub Actions は ECR push だけを担当し、それ以降の処理は AWS 側(CodePipeline)が管理します。
| 役割 | v1.0 | v2.0 |
|---|---|---|
| ECR push | GitHub Actions | GitHub Actions |
| デプロイ実行 | GitHub Actions(force-new-deployment) | CodePipeline + CodeDeploy |
| ロールバック | 手動 | CodeDeploy が自動 |
| デプロイ履歴 | GitHub Actions のログのみ | CodePipeline コンソールで可視化 |
Blue/Green デプロイの仕組み
ALB にターゲットグループを2つ用意します。
// Blue TG(本番トラフィック)
this.blueTargetGroup = new elbv2.ApplicationTargetGroup(this, "BlueTg", {
targetGroupName: `${props.envName}-blue-tg`,
// ...
});
// Green TG(テストトラフィック)
this.greenTargetGroup = new elbv2.ApplicationTargetGroup(this, "GreenTg", {
targetGroupName: `${props.envName}-green-tg`,
// ...
});
// 本番リスナー(80)→ Blue TG
this.productionListener = this.alb.addListener("ProductionListener", {
port: 80,
defaultTargetGroups: [this.blueTargetGroup],
});
// テストリスナー(8080)→ Green TG(デプロイ中の動作確認用)
this.testListener = this.alb.addListener("TestListener", {
port: 8080,
defaultTargetGroups: [this.greenTargetGroup],
});
デプロイの流れ:
- CodeDeploy が Green TG に新バージョンのタスクを起動
-
:8080でテストトラフィックを流して動作確認 - 問題なければ
:80のトラフィックを Blue → Green に切り替え - 旧タスク(Blue)を終了
CodeBuild で appspec.yaml / taskdef.json を動的生成する理由
CodeDeploy Blue/Green には appspec.yaml と taskdef.json が必要です。
これらをリポジトリに静的に置くと、タスク定義の ARN やイメージ URI が変わるたびに手動更新が必要になります。
CodeBuild で動的生成することで、常に最新のタスク定義を参照できます。
buildSpec: codebuild.BuildSpec.fromObject({
version: "0.2",
phases: {
build: {
commands: [
// 現在のタスク定義を取得してイメージを <IMAGE1_NAME> プレースホルダーに差し替え
"aws ecs describe-task-definition --task-definition $TASK_DEFINITION_FAMILY --query taskDefinition > taskdef.json",
"python3 -c \"import json; d=json.load(open('taskdef.json')); [c.update({'image': '<IMAGE1_NAME>'}) for c in d['containerDefinitions'] if c['name']=='$CONTAINER_NAME']; json.dump(d, open('taskdef.json','w'))\"",
// appspec.yaml を生成
"echo 'version: 0.0' > appspec.yaml",
"echo 'Resources:' >> appspec.yaml",
"echo ' - TargetService:' >> appspec.yaml",
"echo ' Type: AWS::ECS::Service' >> appspec.yaml",
"echo ' Properties:' >> appspec.yaml",
"echo ' TaskDefinition: <TASK_DEFINITION>' >> appspec.yaml",
"echo ' LoadBalancerInfo:' >> appspec.yaml",
"echo ' ContainerName: app' >> appspec.yaml",
"echo ' ContainerPort: 80' >> appspec.yaml",
],
},
},
artifacts: {
files: ["appspec.yaml", "taskdef.json"],
},
}),
ECS Service の deploymentController を CODE_DEPLOY に設定する
Blue/Green デプロイを使うには、ECS Service の deploymentController を CODE_DEPLOY に設定する必要があります。
this.service = new ecs.FargateService(this, "Service", {
desiredCount: 0, // 初期は 0。初回イメージ push 後にパイプラインが起動する
deploymentController: { type: ecs.DeploymentControllerType.CODE_DEPLOY },
});
desiredCount: 0 にしているのは、CDK デプロイ時点では ECR にイメージがまだないためです。
初回イメージ push 後にパイプラインが自動起動し、タスクが立ち上がります。
ハマりポイント
cdk destroy 前にリスナーを Blue TG に戻す必要がある
Blue/Green デプロイ後はリスナーの向き先が CodeDeploy によって切り替わっています。この状態で cdk destroy すると、CDK が管理するリソースと実際の状態が乖離しているため削除に失敗します。
destroy 手順:
1. EC2 コンソール → ロードバランサー → リスナー(ポート 80)
→ デフォルトアクションを編集 → 転送先を Blue TG に変更
2. リスナー(ポート 8080)→ デフォルトアクションを編集 → 転送先を Green TG に変更
※ デプロイ後は CodeDeploy がポート 8080 のリスナーも書き換えている場合があるため、
CDK 初期定義(Green TG)に戻してから destroy する
3. cdk destroy
ECR Source は EventBridge 経由でパイプラインを起動する
CodePipeline の ECR Source アクションは、ECR へのイメージ push を EventBridge で検知してパイプラインを起動します。ECR push 直後にパイプラインが起動するため、GitHub Actions 側で明示的にパイプラインを起動する処理は不要です。
まとめ
| 観点 | v1.0 | v2.0 |
|---|---|---|
| ロールバック | 手動 | CodeDeploy が自動 |
| デプロイ可視化 | GitHub Actions のみ | CodePipeline コンソール |
| テストトラフィック | なし |
:8080 で確認可能 |
| 実装コスト | 低 | 中 |
v1.0 は手軽に始められますが、本番運用を意識するなら v2.0 の Blue/Green が現実的です。CodeDeploy の自動ロールバックと、デプロイ中のテストトラフィック確認は実務で特に価値があります。
ソースコードはGitHubで公開してるので良ければ参考にしてください。
次回は CloudWatch Alarm → Bedrock → Slack のインシデント自動通知ボットについて書きます。
