0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS CDK】ECS Blue/Green デプロイを段階的に育てた話 〜GitHub Actions から CodePipeline + CodeDeploy へ〜

0
Posted at

はじめに

ECS Fargate への CI/CD パイプラインを AWS CDK (TypeScript) で構築しました。

v1.0 の「GitHub Actions で force-new-deployment」から始め、v2.0 の「CodePipeline + CodeDeploy Blue/Green」まで段階的に育てています。

この記事では各バージョンで 何を選択し、なぜそう設計したか をまとめます。

最終構成(v2.0):

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],
});

デプロイの流れ:

  1. CodeDeploy が Green TG に新バージョンのタスクを起動
  2. :8080 でテストトラフィックを流して動作確認
  3. 問題なければ :80 のトラフィックを Blue → Green に切り替え
  4. 旧タスク(Blue)を終了

CodeBuild で appspec.yaml / taskdef.json を動的生成する理由

CodeDeploy Blue/Green には appspec.yamltaskdef.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 の deploymentControllerCODE_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 のインシデント自動通知ボットについて書きます。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?