はじめに
AWS CDKを使ってALB+ECSの構成を実装して、デプロイまで試してみました。
CDKは普段さわっておらず、超絶初心者なので、内容はかなり基本的なものなんですが、備忘録としてまとめておきます。
実務で使うには考慮不足なところが多いシンプル構成ですが、まずは触ってみることが目的です。今後はMCPサーバーを活用してアーキテクチャ拡張にも挑戦していくつもりです。
作成した構成
今回は、VPC内にALBをパブリックサブネットに置き、そこからプライベートサブネットのECS(Fargate)上で動くSpring Bootアプリケーションにリクエストを流す、という流れを作りました。
プロジェクト構成
ecs-mcp-server-demo/
├── app/ # Spring Bootアプリケーション
│ ├── src/main/java/com/example/demo/
│ │ ├── DemoApplication.java
│ │ └── HelloController.java
│ ├── src/main/resources/
│ │ ├── application.properties
│ │ └── templates/index.html
│ └── pom.xml
├── lib/ # CDKスタック定義
│ ├── vpc-stack.ts # VPC・セキュリティグループ
│ ├── alb-stack.ts # Application Load Balancer
│ └── ecs-stack.ts # ECS Fargate
├── bin/
│ └── ecs-mcp-server-demo.ts # CDKエントリーポイント
├── docker/
│ └── Dockerfile # マルチステージビルド
└── package.json
使用技術
- IaC: AWS CDK (TypeScript)
- Runtime: Spring Boot 3.5.3 (Java 17)
- Container: Amazon Corretto 17 Alpine
- Orchestration: Amazon ECS Fargate
- Load Balancer: Application Load Balancer
- Build Tool: Maven 3.9.7
- Container Registry: Amazon ECR (CDKで自動作成)
CDKスタックの構成と実装
プロジェクトは3つのスタックに分けました。
- VPCスタック → ネットワークとセキュリティグループを定義
- ALBスタック → ロードバランサーとターゲットグループを定義
- ECSスタック → クラスター、タスク定義、Fargateサービスを定義(詳細説明あり)
1. VPCとセキュリティグループ
VPCと、ALB⇔ECS間の通信を制御するセキュリティグループを作成します。
ALBは特定の固定IPからのみアクセスを許可し、ECSはALB経由の通信のみ許可することで、最低限のセキュリティを確保しています。
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
export interface VpcStackProps extends cdk.StackProps {
readonly environment: string;
}
/**
* VPC + セキュリティグループを作成するスタック
*/
export class VpcStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
public readonly albSecurityGroup: ec2.SecurityGroup;
public readonly ecsSecurityGroup: ec2.SecurityGroup;
constructor(scope: Construct, id: string, props: VpcStackProps) {
super(scope, id, props);
// VPC(Public: ALB、Private: ECS/Fargate)
this.vpc = new ec2.Vpc(this, 'EcsVpc', {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{ cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
{ cidrMask: 24, name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
],
});
// ALB用SG(固定IPからのHTTP/HTTPSのみ許可)
this.albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
vpc: this.vpc,
description: 'Security group for ALB',
allowAllOutbound: true,
});
this.albSecurityGroup.addIngressRule(ec2.Peer.ipv4('##.##.##.##/32'), ec2.Port.tcp(80));
this.albSecurityGroup.addIngressRule(ec2.Peer.ipv4('##.##.##.##/32'), ec2.Port.tcp(443));
// ECS用SG(ALBからのHTTPのみ許可)
this.ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
vpc: this.vpc,
description: 'Security group for ECS',
allowAllOutbound: true,
});
this.ecsSecurityGroup.addIngressRule(this.albSecurityGroup, ec2.Port.tcp(80));
// タグ付け
cdk.Tags.of(this).add('Environment', props.environment);
cdk.Tags.of(this).add('Purpose', 'ECS VPC');
}
}
2. ALBとターゲットグループ
ALBを作成し、Spring Bootのヘルスチェック(/actuator/health)を設定しました。
外部からの入り口となるため、適切な設定を行わないとタスクが正常に登録されないことがあります。
import * as cdk from 'aws-cdk-lib';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
export interface AlbStackProps extends cdk.StackProps {
readonly vpc: ec2.IVpc;
readonly environment: string;
readonly appName: string;
readonly albSecurityGroup: ec2.ISecurityGroup;
}
export class AlbStack extends cdk.Stack {
public readonly loadBalancer: elbv2.ApplicationLoadBalancer;
public readonly targetGroup: elbv2.ApplicationTargetGroup;
constructor(scope: Construct, id: string, props: AlbStackProps) {
super(scope, id, props);
// ALB
this.loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'Alb', {
vpc: props.vpc,
internetFacing: true,
loadBalancerName: `${props.appName}-${props.environment}-alb`,
vpcSubnets: {
subnetType: ec2.SubnetType.PUBLIC,
},
securityGroup: props.albSecurityGroup,
});
// ターゲットグループ
this.targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
vpc: props.vpc,
port: 8080, // Spring Bootのデフォルトポート
protocol: elbv2.ApplicationProtocol.HTTP,
targetType: elbv2.TargetType.IP,
healthCheck: {
path: '/actuator/health', // Spring Boot Actuatorのヘルスチェックパス
healthyHttpCodes: '200',
interval: cdk.Duration.seconds(30),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 3,
},
});
// HTTPリスナー
this.loadBalancer.addListener('HttpListener', {
port: 80,
protocol: elbv2.ApplicationProtocol.HTTP,
defaultTargetGroups: [this.targetGroup],
});
cdk.Tags.of(this).add('Environment', props.environment);
cdk.Tags.of(this).add('Purpose', 'Application Load Balancer');
}
}
3. ECSスタック
ECSスタックでは、以下を自動化しています。
- Dockerイメージのビルド→
docker/Dockerfileを使用 - ECRレジストリ作成&Push→ CDK
DockerImageAssetで自動作成 - タスク定義の作成→ リソース・環境変数・ログ設定を指定
- サービス作成とALBへの登録→ トラフィックを受け付けられるよう設定
- オートスケーリング設定→ CPU使用率70%を閾値にスケーリング
M1/M2 Macではarm64ビルドがデフォルトになるため、LINUX_AMD64指定を入れてFargate上での起動失敗を防いでいます。
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { Construct } from 'constructs';
export interface EcsStackProps extends cdk.StackProps {
readonly vpc: ec2.IVpc;
readonly environment: string;
readonly appName: string;
readonly targetGroup: elbv2.IApplicationTargetGroup;
}
export class EcsStack extends cdk.Stack {
public readonly cluster: ecs.Cluster;
public readonly service: ecs.FargateService;
constructor(scope: Construct, id: string, props: EcsStackProps) {
super(scope, id, props);
// ECSクラスター
this.cluster = new ecs.Cluster(this, 'EcsCluster', {
vpc: props.vpc,
clusterName: `${props.appName}-${props.environment}-cluster`,
containerInsightsV2: ecs.ContainerInsights.ENABLED, // containerInsightsV2の正しい型を使用
});
// タスク実行ロール
const taskExecutionRole = new iam.Role(this, 'TaskExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
taskExecutionRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')
);
// タスクロール
const taskRole = new iam.Role(this, 'TaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
// Dockerイメージアセットを作成(ビルド&ECRレポジトリを作成&push)
const imageAsset = new DockerImageAsset(this, 'EcsMcpServerDemoImageAsset', {
directory: '.', // プロジェクトルートをビルドコンテキストとして使用
file: 'docker/Dockerfile', // Dockerfileの場所を明示的に指定
assetName: 'ecs-mcp-server-demo', // ECRリポジトリ名を明示的に指定
platform: Platform.LINUX_AMD64, // x86_64プラットフォームを明示的に指定
});
// Fargateタスク定義
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
memoryLimitMiB: 512,
cpu: 256,
executionRole: taskExecutionRole,
taskRole: taskRole,
family: `${props.appName}-${props.environment}-task`,
});
// 3. ECSタスク定義でイメージURIを指定
taskDefinition.addContainer('AppContainer', {
image: ecs.ContainerImage.fromRegistry(imageAsset.imageUri),
containerName: 'app',
logging: ecs.LogDrivers.awsLogs({
streamPrefix: `${props.appName}-${props.environment}`,
logRetention: logs.RetentionDays.ONE_WEEK,
}),
portMappings: [
{
containerPort: 8080, // Spring Bootのデフォルトポート
protocol: ecs.Protocol.TCP,
},
],
environment: {
NODE_ENV: props.environment,
},
});
// Fargateサービス
this.service = new ecs.FargateService(this, 'FargateService', {
cluster: this.cluster,
taskDefinition: taskDefinition,
serviceName: `${props.appName}-${props.environment}-service`,
desiredCount: props.environment === 'prod' ? 2 : 1,
assignPublicIp: false,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [
new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
vpc: props.vpc,
description: 'Security group for ECS service',
allowAllOutbound: true,
}),
],
minHealthyPercent: 100, // デプロイ中も100%のタスクを維持
maxHealthyPercent: 200, // デプロイ中は最大200%までスケールアップ可能
});
// ALBのターゲットグループにサービスを追加
props.targetGroup.addTarget(this.service);
// オートスケーリング
const scaling = this.service.autoScaleTaskCount({
maxCapacity: props.environment === 'prod' ? 4 : 2,
minCapacity: props.environment === 'prod' ? 2 : 1,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
cdk.Tags.of(this).add('Environment', props.environment);
cdk.Tags.of(this).add('Purpose', 'ECS Service');
}
}
![]()
検証時に地味にハマったポイントをご紹介します。
あくまで今回の検証段階での話なので、可能な方法があるかもしれません。今後もう少し調べたいと思っています。
-
ECRリポジトリ名を指定できない?
→ DockerImageAsset を使うと自動生成のリポジトリ名になり、固定リポジトリを使えませんでした。
→ 事前に ECR を作ってタグ付きイメージを Push する方が確実かな?と感じました。 -
ECRタグを指定できない?
→ ハッシュタグしか付かず、指定のタグがつけられませんでした。
→ CI/CD で別途タグを付ける運用が必要そうです。
4. binからスタック呼び出し
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { EcsStack } from '../lib/ecs-stack';
import { AlbStack } from '../lib/alb-stack';
const app = new cdk.App();
// 環境変数の取得
const environment = process.env.ENVIRONMENT || 'dev';
const appName = 'ecs-mcp-server-demo';
// VPCスタック
const vpcStack = new VpcStack(app, `${appName}-${environment}-vpc-stack`, {
environment,
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'ap-northeast-1',
},
});
// ALBスタック
const albStack = new AlbStack(app, `${appName}-${environment}-alb-stack`, {
vpc: vpcStack.vpc,
environment,
appName,
albSecurityGroup: vpcStack.albSecurityGroup,
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'ap-northeast-1',
},
});
// ECSスタック
const ecsStack = new EcsStack(app, `${appName}-${environment}-ecs-stack`, {
vpc: vpcStack.vpc,
environment,
appName,
targetGroup: albStack.targetGroup, // ALBのターゲットグループを渡す
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || 'ap-northeast-1',
},
});
// スタック間の依存関係を設定
ecsStack.addDependency(vpcStack);
ecsStack.addDependency(albStack); // ECSはALBに依存
app.synth();
Spring Bootアプリケーション
シンプルに "Hello, Spring Boot!" を返すだけのアプリケーションです。
今回は Spring Boot のキャッチアップを目的に選択したため、特に深い理由はありません・・!
@Controller
public class HelloController {
@GetMapping("/")
public String hello(Model model) {
model.addAttribute("message", "Hello, Spring Boot!");
return "index";
}
}
※index.htmlのViewを用意して返す。
Dockerfile(M1/M2対応版)
今回のDockerfileはクロスビルド対応を入れています。
結構忘れがちなのですが、クロスビルドを入れないとexec format errorで落ちます。DockerfileとCDK側のLINUX_AMD64指定で回避しました。
# --- ビルドステージ ---
FROM --platform=$BUILDPLATFORM maven:3.9.7-eclipse-temurin-17 AS build
WORKDIR /build
COPY app/pom.xml .
COPY app/src ./src
RUN mvn clean package -DskipTests
# --- 実行ステージ ---
FROM --platform=$TARGETPLATFORM amazoncorretto:17-alpine
WORKDIR /app
COPY --from=build /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
デプロイ
ではデプロイしてみましょう。
npx を使うのは、プロジェクトの CDK バージョンをそのまま利用するためです。
なおAWS CLIはインストール済みでprofileはhossoを指定します。
- 初回セットアップ
初回のみ必要。
npx cdk bootstrap --profile hosso
- 差分確認
npx cdk diff --profile hosso
- デプロイ
VPC、ALB、Docker ビルド → ECR → ECS まで一括で作成。
npx cdk deploy --profile hosso --all
![]()
-
npxはローカル開発環境のバージョン固定用 - bootstrap は最初の 1 回だけ
- 不要になったら
npx cdk destroy --profile hosso --allで削除
リソース確認・動作確認
デプロイ後にリソースの状態を確認してみます。
まず、ECS サービス内でタスクが正常に起動しており、ALB のターゲットグループのヘルスチェックにも成功していることが分かります。IP アドレスも問題なく登録されています!
続いて、ALB の DNS にアクセスすると、ブラウザ上でアプリケーションが正しく表示されることを確認できました!
まとめ
一先ず、以上です!
今回は AWS CDK を使ってシンプルな ECS(Fargate) 環境を構築し、デプロイまでの流れを一通り試しました。
スタックを分けて構成することで責務が明確になり、再利用性が高まることを実感できました。
また、Dockerfile を工夫することで、M1/M2 Mac からでも安全に Fargate へデプロイできることを確認できました。
今後はこの環境をベースに、MCP サーバー (CDK×ECS) と組み合わせ、AI から ECS の状態確認やトラブルシューティングを支援できる仕組みを試してみたいと思います。
CDK 構成の改善やリソース調整を対話的に行えるようにし、より使いやすい開発体験を目指したいです。
本記事が、同じように試してみたい方の参考になれば幸いです。



