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 vpcconst vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpc", {vpcId: props.vpcId,vpcName: props.vpcName,});// ecs clusterthis.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 vpcconst vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpc", {vpcId: props.vpcId,vpcName: props.vpcName,});// task role pull ecr imageconst 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 definitionconst 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 containertask.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 }],});// serviceconst 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 utilizationconst scaling = service.autoScaleTaskCount({maxCapacity: 4,minCapacity: 2,});scaling.scaleOnMemoryUtilization("CpuUtilization", {targetUtilizationPercent: 50,});// application load balancerconst alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,"AlbForEcs",{loadBalancerName: "AlbForEcsDemo",vpc: vpc,internetFacing: true,});// add listenerconst listener = alb.addListener("Listener", {port: 80,open: true,protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,});// add targetlistener.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 httpsconst listenerHttps = alb.addListener("ListenerHttps", {port: 443,open: true,protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTPS,certificates: [ListenerCertificate.fromArn(props.certificate)],});// listner add targetlistenerHttps.addTargets("EcsServiceHttps", {port: 80,targets: [service.loadBalancerTarget({containerName: "chat-bot-ecr",containerPort: 3000,protocol: aws_ecs.Protocol.TCP,}),],healthCheck: {timeout: Duration.seconds(10),},});// exportedthis.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 vpcconst vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpcBlogService", {vpcId: props.vpcId,vpcName: props.vpcName,});// task role pull ecr imageconst 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 definitionconst 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 containertask.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 }],});// serviceconst 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 utilizationconst scaling = service.autoScaleTaskCount({maxCapacity: 4,minCapacity: 2,});scaling.scaleOnMemoryUtilization("CpuUtilization", {targetUtilizationPercent: 50,});// application load balancerconst alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(this,"AlbForEcsBlogService",{loadBalancerName: "AlbForEcsDemoBlogService",vpc: vpc,internetFacing: true,});// add listenerconst listener = alb.addListener("ListenerBlogService", {port: 80,open: true,protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,});// add targetlistener.addTargets("EcsServiceBlogService", {port: 80,targets: [service.loadBalancerTarget({containerName: "blog-ecr",containerPort: 3000,protocol: aws_ecs.Protocol.TCP,}),],healthCheck: {timeout: Duration.seconds(10),},});// add listener httpsconst listenerHttps = alb.addListener("ListenerHttpsBlogService", {port: 443,open: true,protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTPS,certificates: [ListenerCertificate.fromArn(props.certificate)],});// listner add targetlistenerHttps.addTargets("EcsServiceHttpsBlogService", {port: 80,targets: [service.loadBalancerTarget({containerName: "blog-ecr",containerPort: 3000,protocol: aws_ecs.Protocol.TCP,}),],healthCheck: {timeout: Duration.seconds(10),},});// exportedthis.service = service;}}