Introduction#

GitHub this note shows how to deploy multiple NextJS on Amazon ECS using CDK.

  • Create an Amazon ECS Cluster
  • Create a service
  • Create ALB with HTTPS listener
  • Update Route53 CNAME Record

ECR Image#

Let create two ecr repositories

import { RemovalPolicy, Stack, StackProps, aws_ecr } from "aws-cdk-lib";
import { Construct } from "constructs";
interface EcrProps extends StackProps {
repoName: string;
}
export class EcrStack extends Stack {
public readonly repoName: string;
constructor(scope: Construct, id: string, props: EcrProps) {
super(scope, id, props);
const ecr = new aws_ecr.Repository(this, `${props.repoName}`, {
removalPolicy: RemovalPolicy.DESTROY,
repositoryName: props.repoName,
autoDeleteImages: true,
});
this.repoName = ecr.repositoryName;
}
}

Then we can create two ecr repositories

const blogEcr = new EcrStack(app, "BlogEcr", {
repoName: "blog-ecr",
env: {
region: process.env.CDK_DEFAULT_REGION,
account: process.env.CDK_DEFAULT_ACCOUNT,
},
});
const cluster = new EcsStack(app, "EcsCluster", {
vpcId: vpcId,
vpcName: vpcName,
env: {
region: process.env.CDK_DEFAULT_REGION,
account: process.env.CDK_DEFAULT_ACCOUNT,
},
});

ECS Cluster#

Let create a ECS Cluster

  • Interface with vpcId and vpcName which already existing
  • Create an application load balancer
  • Create HTTP and HTTPS listener
interface EcsProps extends StackProps {
vpcId: string;
vpcName: string;
}
export class EcsStack extends Stack {
public readonly cluster: aws_ecs.Cluster;
constructor(scope: Construct, id: string, props: EcsProps) {
super(scope, id, props);
Aspects.of(this).add(new CapacityProviderDependencyAspect());
// lookup an existed vpc
const vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpc", {
vpcId: props.vpcId,
vpcName: props.vpcName,
});
// ecs cluster
this.cluster = new aws_ecs.Cluster(this, "EcsClusterForWebServer", {
vpc: vpc,
clusterName: "EcsClusterForWebServer",
containerInsights: true,
enableFargateCapacityProviders: true,
});
}
}

We need to add a IASpect to fix error when destroying the ecs stack

/**
* Add a dependency from capacity provider association to the cluster
* and from each service to the capacity provider association.
*/
class CapacityProviderDependencyAspect implements IAspect {
public visit(node: IConstruct): void {
if (node instanceof aws_ecs.CfnClusterCapacityProviderAssociations) {
// IMPORTANT: The id supplied here must be the same as the id of your cluster. Don't worry, you won't remove the cluster.
node.node.scope?.node.tryRemoveChild("EcsClusterForWebServer");
}
if (node instanceof aws_ecs.Ec2Service) {
const children = node.cluster.node.findAll();
for (const child of children) {
if (child instanceof aws_ecs.CfnClusterCapacityProviderAssociations) {
child.node.addDependency(node.cluster);
node.node.addDependency(child);
}
}
}
}
}

Chat Service#

Let create a chat service

interface ChatBotProps extends StackProps {
cluster: aws_ecs.Cluster;
ecrRepoName: string;
certificate: string;
vpcId: string;
vpcName: string;
}

Create a chat service

export class ChatBotService extends Stack {
public readonly service: aws_ecs.FargateService;
constructor(scope: Construct, id: string, props: ChatBotProps) {
super(scope, id, props);
// lookup an existed vpc
const vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpc", {
vpcId: props.vpcId,
vpcName: props.vpcName,
});
// task role pull ecr image
const executionRole = new aws_iam.Role(
this,
"RoleForEcsTaskToPullEcrChatbotImage",
{
assumedBy: new aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
roleName: "RoleForEcsTaskToPullEcrChatbotImage",
}
);
executionRole.addToPolicy(
new aws_iam.PolicyStatement({
effect: Effect.ALLOW,
actions: ["ecr:*"],
resources: ["*"],
})
);
// ecs task definition
const task = new aws_ecs.FargateTaskDefinition(
this,
"TaskDefinitionForWeb",
{
family: "latest",
cpu: 2048,
memoryLimitMiB: 4096,
runtimePlatform: {
operatingSystemFamily: aws_ecs.OperatingSystemFamily.LINUX,
cpuArchitecture: aws_ecs.CpuArchitecture.X86_64,
},
// taskRole: "",
// retrieve container images from ECR
// executionRole: executionRole,
}
);
// task add container
task.addContainer("NextChatbotContainer", {
containerName: "chat-bot-ecr",
memoryLimitMiB: 4096,
memoryReservationMiB: 4096,
stopTimeout: Duration.seconds(120),
startTimeout: Duration.seconds(120),
environment: {
FHR_ENV: "DEPLOY",
},
// image: aws_ecs.ContainerImage.fromRegistry(
// "public.ecr.aws/b5v7e4v7/chat-bot-ecr:latest"
// ),
image: aws_ecs.ContainerImage.fromEcrRepository(
aws_ecr.Repository.fromRepositoryName(
this,
"chat-bot-ecr",
props.ecrRepoName
)
),
portMappings: [{ containerPort: 3000 }],
});
// service
const service = new aws_ecs.FargateService(this, "ChatbotService", {
vpcSubnets: {
subnetType: aws_ec2.SubnetType.PUBLIC,
},
assignPublicIp: true,
cluster: props.cluster,
taskDefinition: task,
desiredCount: 2,
// deploymentController: {
// default rolling update
// type: aws_ecs.DeploymentControllerType.ECS,
// type: aws_ecs.DeploymentControllerType.CODE_DEPLOY,
// },
capacityProviderStrategies: [
{
capacityProvider: "FARGATE",
weight: 1,
},
{
capacityProvider: "FARGATE_SPOT",
weight: 0,
},
],
});
// scaling on cpu utilization
const scaling = service.autoScaleTaskCount({
maxCapacity: 4,
minCapacity: 2,
});
scaling.scaleOnMemoryUtilization("CpuUtilization", {
targetUtilizationPercent: 50,
});
// application load balancer
const alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(
this,
"AlbForEcs",
{
loadBalancerName: "AlbForEcsDemo",
vpc: vpc,
internetFacing: true,
}
);
// add listener
const listener = alb.addListener("Listener", {
port: 80,
open: true,
protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
});
// add target
listener.addTargets("EcsService", {
port: 80,
targets: [
service.loadBalancerTarget({
containerName: "chat-bot-ecr",
containerPort: 3000,
protocol: aws_ecs.Protocol.TCP,
}),
],
healthCheck: {
timeout: Duration.seconds(10),
},
});
// add listener https
const listenerHttps = alb.addListener("ListenerHttps", {
port: 443,
open: true,
protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTPS,
certificates: [ListenerCertificate.fromArn(props.certificate)],
});
// listner add target
listenerHttps.addTargets("EcsServiceHttps", {
port: 80,
targets: [
service.loadBalancerTarget({
containerName: "chat-bot-ecr",
containerPort: 3000,
protocol: aws_ecs.Protocol.TCP,
}),
],
healthCheck: {
timeout: Duration.seconds(10),
},
});
// exported
this.service = service;
}
}

Blog Service#

Similar as the chat service, we also can create a blog service

BlogServiceStack
export class BlogService extends Stack {
public readonly service: aws_ecs.FargateService;
constructor(scope: Construct, id: string, props: BlogProps) {
super(scope, id, props);
// lookup an existed vpc
const vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpcBlogService", {
vpcId: props.vpcId,
vpcName: props.vpcName,
});
// task role pull ecr image
const executionRole = new aws_iam.Role(
this,
"RoleForEcsTaskToPullEcrChatbotImageBlogService",
{
assumedBy: new aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
roleName: "RoleForEcsTaskToPullEcrChatbotImageBlogService",
}
);
executionRole.addToPolicy(
new aws_iam.PolicyStatement({
effect: Effect.ALLOW,
actions: ["ecr:*"],
resources: ["*"],
})
);
// ecs task definition
const task = new aws_ecs.FargateTaskDefinition(
this,
"TaskDefinitionForWebBlogService",
{
family: "latest",
cpu: 2048,
memoryLimitMiB: 4096,
runtimePlatform: {
operatingSystemFamily: aws_ecs.OperatingSystemFamily.LINUX,
cpuArchitecture: aws_ecs.CpuArchitecture.X86_64,
},
// taskRole: "",
// retrieve container images from ECR
// executionRole: executionRole,
}
);
// task add container
task.addContainer("NextChatbotContainerBlogService", {
containerName: "blog-ecr",
memoryLimitMiB: 4096,
memoryReservationMiB: 4096,
stopTimeout: Duration.seconds(120),
startTimeout: Duration.seconds(120),
environment: {
FHR_ENV: "DEPLOY",
},
// image: aws_ecs.ContainerImage.fromRegistry(
// "public.ecr.aws/b5v7e4v7/blog-ecr:latest"
// ),
image: aws_ecs.ContainerImage.fromEcrRepository(
aws_ecr.Repository.fromRepositoryName(
this,
"blog-ecr",
props.ecrRepoName
)
),
portMappings: [{ containerPort: 3000 }],
});
// service
const service = new aws_ecs.FargateService(this, "BlogService", {
vpcSubnets: {
subnetType: aws_ec2.SubnetType.PUBLIC,
},
assignPublicIp: true,
cluster: props.cluster,
taskDefinition: task,
desiredCount: 2,
// deploymentController: {
// default rolling update
// type: aws_ecs.DeploymentControllerType.ECS,
// type: aws_ecs.DeploymentControllerType.CODE_DEPLOY,
// },
capacityProviderStrategies: [
{
capacityProvider: "FARGATE",
weight: 1,
},
{
capacityProvider: "FARGATE_SPOT",
weight: 0,
},
],
});
// scaling on cpu utilization
const scaling = service.autoScaleTaskCount({
maxCapacity: 4,
minCapacity: 2,
});
scaling.scaleOnMemoryUtilization("CpuUtilization", {
targetUtilizationPercent: 50,
});
// application load balancer
const alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(
this,
"AlbForEcsBlogService",
{
loadBalancerName: "AlbForEcsDemoBlogService",
vpc: vpc,
internetFacing: true,
}
);
// add listener
const listener = alb.addListener("ListenerBlogService", {
port: 80,
open: true,
protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
});
// add target
listener.addTargets("EcsServiceBlogService", {
port: 80,
targets: [
service.loadBalancerTarget({
containerName: "blog-ecr",
containerPort: 3000,
protocol: aws_ecs.Protocol.TCP,
}),
],
healthCheck: {
timeout: Duration.seconds(10),
},
});
// add listener https
const listenerHttps = alb.addListener("ListenerHttpsBlogService", {
port: 443,
open: true,
protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTPS,
certificates: [ListenerCertificate.fromArn(props.certificate)],
});
// listner add target
listenerHttps.addTargets("EcsServiceHttpsBlogService", {
port: 80,
targets: [
service.loadBalancerTarget({
containerName: "blog-ecr",
containerPort: 3000,
protocol: aws_ecs.Protocol.TCP,
}),
],
healthCheck: {
timeout: Duration.seconds(10),
},
});
// exported
this.service = service;
}
}