はじめに
第3回では、ALB(Application Load Balancer)を構築しました。
ここまでで、以下のリソースが作成されています。
- ECR:コンテナイメージの保存場所
- Task Definition:コンテナの実行設定
- ALB:外部からアクセスする入口
しかし、この状態ではまだコンテナは起動していません。
ECSでは、Task Definitionを作成しただけではコンテナは実行されず、
タスクを起動・維持する仕組み(Service) が必要になります。
そこで今回はECS Serviceを作成し、Next.jsアプリをAWS上で実際に起動させます。
本シリーズの構成
1. ECR編
2. ECS Task Definition編
3. ALB編
4. ECS Service編(本記事)
5. セキュリティ編
構成全体と今回の作業範囲
AWS全体構成の中で、本記事がどの範囲を扱うのかを確認します。
今回の作業はECS Serviceの作成です。
ServiceがTask Definitionをもとにタスクを起動します。
そして、指定された数(desiredCount)のタスクが常に動作するように管理します。
起動したタスクにはALB経由でアクセスできるようになります。
ECS Serviceとは?
ECS Serviceは、タスクを指定した数(desiredCount)だけ起動し、その状態を維持する仕組みです。
タスクが停止した場合、Serviceが自動的に新しいタスクを起動します。
また、ALB(Target Group)と連携し、外部からのリクエストを起動中のタスクへ分散する役割も担います。
Task Definitionとの違い
ECSでは、Task DefinitionとServiceで役割が分かれています。
Task Definition
- コンテナの実行設定(イメージ・ポート・環境変数など)
- いわば「コンテナの設計図」
Service
- Task Definitionを使ってタスクを起動する
- 指定した数のタスクを常に維持する
4-1. EcsStackのリソースを外部参照できるようにする
ServiceStackでは、EcsStackで作成したclusterとtaskDefinitionを参照してServiceを作成します。
第3回では、AlbStackから利用できるようにvpcをクラスのプロパティとして公開しました。
今回もそれと同様に、ServiceStackから参照できるようclusterとtaskDefinitionをプロパティとして公開します。
Stack内で作成したリソースを他のStackから利用する場合、そのリソースをクラスのプロパティとして公開する必要があります。
コンストラクタ内のconst変数のままだと、Stack外から参照できません。
1. EcsStackクラスにプロパティを追加
public readonly cluster: ecs.Cluster;
public readonly taskDefinition: ecs.FargateTaskDefinition;
2. コンストラクタ内で this.cluster / this.taskDefinition に代入する
修正後のコード
export class EcsStack extends cdk.Stack {
public readonly vpc: ec2.IVpc;
public readonly cluster: ecs.Cluster;
public readonly taskDefinition: ecs.FargateTaskDefinition;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = ec2.Vpc.fromLookup(this, 'DefaultVpc', {
isDefault: true,
});
this.cluster = new ecs.Cluster(this, 'Cluster', {
vpc: this.vpc,
});
this.taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
cpu: 256,
memoryLimitMiB: 512,
});
const repository = ecr.Repository.fromRepositoryName(
this,
'ImportedRepository',
'my-nextjs-app'
);
const container = this.taskDefinition.addContainer('Container', {
image: ecs.ContainerImage.fromEcrRepository(repository),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nextjs',
logRetention: logs.RetentionDays.ONE_WEEK,
}),
});
container.addPortMappings({ containerPort: 3000 });
}
}
4-2. Service用Stackを作成する
lib/service-stack.tsを作成します。
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 elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
interface ServiceStackProps extends cdk.StackProps {
cluster: ecs.Cluster;
taskDefinition: ecs.FargateTaskDefinition;
targetGroup: elbv2.ApplicationTargetGroup;
vpc: ec2.IVpc;
}
export class ServiceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: ServiceStackProps) {
super(scope, id, props);
const serviceSg = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
vpc: props.vpc,
allowAllOutbound: true,
});
const service = new ecs.FargateService(this, 'Service', {
cluster: props.cluster,
taskDefinition: props.taskDefinition,
desiredCount: 1, // 常時稼働させるタスク(コンテナ)の数
assignPublicIp: true, // 検証用
securityGroups: [serviceSg],
});
service.attachToApplicationTargetGroup(props.targetGroup);
}
}
ServiceStackでは、既存のStackから以下のリソースを受け取ります。
- ECS Cluster
- Task Definition
- ALB Target Group
- VPC
ポイント解説
desiredCount
desiredCount: 1
常に1つのタスクを起動する設定です。
タスクが停止した場合、自動的に再起動されます。
assignPublicIp
assignPublicIp: true
Public Subnetに配置する設定です。(検証用)
attachToApplicationTargetGroup
service.attachToApplicationTargetGroup(props.targetGroup);
ECS Serviceが起動したタスクをALBのTargetGroupに登録します。
これにより、ALBからのリクエストがECSタスクへ転送されます。
4-3. binにStackを追加
ServiceStackから他Stackのリソースを参照できるように、Stackを変数として保持します。
(ecsStack, albStack)
import { ServiceStack } from '../lib/service-stack';
const ecsStack = new EcsStack(app, 'NextjsInfraEcsStack', { ... });
const albStack = new AlbStack(app, 'NextjsInfraAlbStack', { ... });
new ServiceStack(app, 'NextjsInfraServiceStack', {
env,
cluster: ecsStack.cluster,
taskDefinition: ecsStack.taskDefinition,
targetGroup: albStack.targetGroup,
vpc: ecsStack.vpc,
});
4-4. デプロイ
cdk deploy NextjsServiceStack --profile <プロファイル名>
本記事で作成したスタックに含まれるAWSリソースは、削除するまで料金が発生します。
検証が不要になった場合は、以下のコマンドでスタックを削除してください。
cdk destroy <スタック名> --profile <プロファイル名>
AWSコンソールで確認
①ECS → クラスター
作成したクラスターを開き、以下を確認します。
- サービスが作成されているか
- タスクが1つ実行中になっているか
②EC2 → ターゲットグループ
作成したターゲットグループを開き、ターゲットタブで以下を確認します。
- ターゲットが1つ登録されているか
- ヘルスステータスが Healthy になっているか
動作確認
EC2 → ロードバランサー
対象ALBの 「DNS名」を確認し、ブラウザからアクセス
http://<DNS名>
- Next.js画面が表示されるか
最終コード(今回追加・修正したファイル)
lib/ecs-stack.ts
コード全体
export class EcsStack extends cdk.Stack {
public readonly vpc: ec2.IVpc;
public readonly cluster: ecs.Cluster;
public readonly taskDefinition: ecs.FargateTaskDefinition;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.vpc = ec2.Vpc.fromLookup(this, 'DefaultVpc', {
isDefault: true,
});
this.cluster = new ecs.Cluster(this, 'Cluster', {
vpc: this.vpc,
});
this.taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
cpu: 256,
memoryLimitMiB: 512,
});
const repository = ecr.Repository.fromRepositoryName(
this,
'ImportedRepository',
'my-nextjs-app'
);
const container = this.taskDefinition.addContainer('Container', {
image: ecs.ContainerImage.fromEcrRepository(repository),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nextjs',
logRetention: logs.RetentionDays.ONE_WEEK,
}),
});
container.addPortMappings({ containerPort: 3000 });
}
}
lib/service-stack.ts
コード全体
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 elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
interface ServiceStackProps extends cdk.StackProps {
cluster: ecs.Cluster;
taskDefinition: ecs.FargateTaskDefinition;
targetGroup: elbv2.ApplicationTargetGroup;
vpc: ec2.IVpc;
}
export class ServiceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: ServiceStackProps) {
super(scope, id, props);
const serviceSg = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
vpc: props.vpc,
allowAllOutbound: true,
});
const service = new ecs.FargateService(this, 'Service', {
cluster: props.cluster,
taskDefinition: props.taskDefinition,
desiredCount: 1, // 常時稼働させるタスク(コンテナ)の数
assignPublicIp: true, // 検証用
securityGroups: [serviceSg],
});
service.attachToApplicationTargetGroup(props.targetGroup);
}
}
bin/cdk-nextjs-infra.ts
コード全体
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { EcrStack } from '../lib/ecr-stack';
import { EcsStack } from '../lib/ecs-stack';
import { AlbStack } from '../lib/alb-stack';
import { ServiceStack } from '../lib/service-stack';
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
new EcrStack(app, 'NextjsInfraEcrStack', { env });
const ecsStack = new EcsStack(app, 'NextjsInfraEcsStack', { env });
const albStack = new AlbStack(app, 'NextjsInfraAlbStack', {
env,
vpc: ecsStack.vpc,
});
new ServiceStack(app, 'NextjsInfraServiceStack', {
env,
cluster: ecsStack.cluster,
taskDefinition: ecsStack.taskDefinition,
targetGroup: albStack.targetGroup,
vpc: ecsStack.vpc,
});
ここまでの成果
成果
- ECS Serviceを作成した
- タスクが常時起動するようになった
- ALBと接続された
- 外部アクセスが可能になった
ALBのDNSからアプリにアクセスできる状態になりました。
次回
次回はセキュリティ編として、以下を実施していきます。
- Security Groupの最適化
- 不要なPublic設定の削除
- 疎通確認
- 本番構成への修正