はじめに
前回、ECRリポジトリを作成し、Next.jsアプリのDockerイメージをpushしました。
第2回となる本記事では、ECSのタスク定義(Task Definition)を作成します。
本シリーズの構成
1. ECR編
2. ECS Task Definition編(本記事)
3. ALB編
4. ECS Service編
5. セキュリティ編
構成全体と今回の作業範囲
AWS全体構成の中で本記事がどの範囲を扱うのかを確認します。
今回のゴール
- ECS Clusterを作成する
- Fargate用Task Definitionを作成する
- ECRのDockerイメージを指定する
- CloudWatch Logsへのログ出力設定を行う
- ポート設定を行う
ECS Task Definitionとは?
ECSでコンテナを動かすための「設計図」です。
Task Definitionには、
- 使用するコンテナイメージ
- CPUやメモリの割り当て
- ポート番号
- 環境変数
- ログ出力先
などを定義します。
この定義は、ECS Serviceがタスクを起動する際に参照されます。
2-1. ECS Clusterを作成する
まずは、ECSを動かすための「入れ物」となるClusterを作成します。
CDKプロジェクト内に"lib/ecs-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';
export class EcsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC(デフォルトVPCを使用)
const vpc = ec2.Vpc.fromLookup(this, 'DefaultVpc', {
isDefault: true,
});
// ECS Cluster
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
});
}
}
ECS Clusterは「入れ物」で、この段階ではまだ使いません。ECS Serviceを作成して初めて、Cluster上でタスク(コンテナ)が起動します(本記事の時点ではコンテナは起動しません)。
また今回は検証のためデフォルトVPCを使用します。デフォルトVPCが存在しない環境ではエラーになります。本番運用では専用VPC(Public/Private Subnetを分ける等)を用意する構成が一般的です。
2-2. Fargate用のTask Definitionを作成する
次に、Fargate用のTask Definitionを作成します。
以下のコードを、lib/ecs-stack.ts内のEcsStack constructorの中に追加します。
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
cpu: 256,
memoryLimitMiB: 512,
});
FargateTaskDefinitionでは、ECRからのイメージ取得やCloudWatch Logsへのログ出力に必要な実行ロール(Execution Role)はCDKが自動作成します。
そのため、明示的にIAMロールを定義しなくても動作します。
この定義は、ECS Serviceがタスクを起動する際に参照されます。
また、Task Definitionは変更するたびに新しいリビジョンが作成されます。
CPUとメモリについて
cpu: 256 → 0.25 vCPU
memoryLimitMiB: 512 → 512MB
今回は検証用の最小構成です。
FargateではCPUとメモリの組み合わせに制約があります。値を変更する場合は公式ドキュメントを確認してください。
Fargateとは?
Fargateは、サーバー(EC2インスタンス)を自分で用意・管理しなくてもコンテナを実行できるAWSの仕組みです。
通常のECSでは、コンテナを動かすためのEC2インスタンスを用意する必要がありますが、Fargateを使うとその管理をAWSが代わりに行ってくれます。
そのため、インフラ管理の負担を減らし、コンテナの実行に集中できます。
2-3. ECRイメージを指定する
lib/ecs-stack.ts先頭にimportを追加します。
import * as ecr from 'aws-cdk-lib/aws-ecr';
以降の手順では、基本的にEcsStackのconstructor内にコードを追加してください。
リポジトリの定義を追加します。
const repository = ecr.Repository.fromRepositoryName(
this,
'ImportedRepository',
'my-nextjs-app'
);
'my-nextjs-app' は ECRリポジトリ名です。
前回作った名前と一致してないと後続のタスク起動時にイメージ取得で失敗します。
2-4. コンテナ定義とログ出力設定を追加する
lib/ecs-stack.ts先頭にimportを追加します。
import * as logs from 'aws-cdk-lib/aws-logs';
Task Definitionにコンテナを追加します。
const container = taskDefinition.addContainer('Container', {
// ECRリポジトリからイメージを指定
// ※本番は固定タグ推奨
image: ecs.ContainerImage.fromEcrRepository(repository),
// CloudWatch Logsへログを出力する設定
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nextjs',
logRetention: logs.RetentionDays.ONE_WEEK,
}),
});
fromEcrRepository(repository) のようにタグを省略した場合、デフォルトでlatestタグが参照されます。
latestは上書き可能なタグのため、本番環境では再現性が担保できません。
本番では固定バージョンタグを明示的に指定することを推奨します。
CloudWatch Logs設定について
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nextjs',
logRetention: logs.RetentionDays.ONE_WEEK,
}),
awsLogsを指定すると、CloudWatch Logs用のLogGroupはCDKによって自動生成されます。
- streamPrefixはログストリーム名の接頭辞になります。
- logRetentionはログの保持期間を指定する設定です。
この設定により、ECSコンテナの標準出力(console.logなど)および標準エラー出力がCloudWatch Logsへ自動送信されます。
この段階ではまだタスクは起動していないため、CloudWatch Logsにはログは出力されません。実際のログ確認はECS Service作成後に行います。
2-5. ポート設定を行う
コンテナが待ち受けるポートをECSに伝える設定です。
※これはコンテナ内部ポートの指定であり、外部公開は次回のALBおよびService設定で行います。
container.addPortMappings({
containerPort: 3000,
});
これで、Task Definitionの設定が完了しました。
2-6. bin側でStackを追加する
bin 配下のファイルにStackを追加します。
import { EcsStack } from '../lib/ecs-stack';
//...
new EcsStack(app, 'NextjsInfraEcsStack', { env });
2-7. デプロイする
cdk deployを実行すると、CloudFormationを通じてECS ClusterとTask Definitionが作成されます。
cdk deploy NextjsInfraEcsStack --profile <プロファイル名>
デプロイが正常に完了すると、以下のログが表示されます。
✅ NextjsInfraEcsStack
✨ Deployment time: xx.xx s
本記事で作成したスタックに含まれるAWSリソースは、削除するまで料金が発生します。
検証が不要になった場合は、以下のコマンドでスタックを削除してください。
cdk destroy <スタック名> --profile <プロファイル名>
AWSコンソールで確認
① ECS → クラスター
- クラスターが作成されているか
クラスター名(参考): NextjsInfraEcsStack-NextjsClusterXXXXXXXX-XXXXXXXX
② ECS → タスク定義
- タスク定義が登録されているか
- ステータスがACTIVEになっているか
タスク定義名(参考):NextjsInfraEcsStackNextjsTaskDefXXXX
③ CloudFormation → スタック
- NextjsInfraEcsStackのステータスが
CREATE_COMPLETEまたはUPDATE_COMPLETEになっているか
ここまで確認できれば、基盤構築は完了です。
なお、まだECS Serviceを作成していないため、コンテナは起動していません。
最終コード(今回追加・修正したファイル)
lib/ecs-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 ecr from 'aws-cdk-lib/aws-ecr';
import * as logs from 'aws-cdk-lib/aws-logs';
export class EcsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC(デフォルトVPCを使用)
const vpc = ec2.Vpc.fromLookup(this, 'DefaultVpc', {
isDefault: true,
});
// ECS Cluster
const cluster = new ecs.Cluster(this, 'Cluster', {
vpc,
});
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
cpu: 256,
memoryLimitMiB: 512,
});
const repository = ecr.Repository.fromRepositoryName(
this,
'ImportedRepository',
'my-nextjs-app'
);
const container = taskDefinition.addContainer('Container', {
// ECRリポジトリからイメージを指定
// ※本番は固定タグ推奨
image: ecs.ContainerImage.fromEcrRepository(repository),
// CloudWatch Logsへログを出力する設定
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'nextjs',
logRetention: logs.RetentionDays.ONE_WEEK,
}),
});
container.addPortMappings({
containerPort: 3000,
});
}
}
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';
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
new EcrStack(app, 'NextjsInfraEcrStack', { env });
new EcsStack(app, 'NextjsInfraEcsStack', { env });
ここまでの成果
ECSでコンテナを起動するためのTask Definitionが完成しました。
次回
次回は、ALB(Application Load Balancer)を作成し、ECSへ外部からアクセスできるように設定します。