Introduction#
GitHub this project shows how to build a chatbot and deploy it on amazon ecs. Here are main components
- video demo
- vercel ai sdk and hugging face model
- deploy the web app using cdk
- build a ci/cd pipeline using cdk
[!IMPORTANT] Please check this video for detail how to deploy the app on amazon ecs and codepipeline
Setup Project#
Let create a new folder for the project
mkdir ecs-chatbot-app
Then init a new CDK project in typescript
cdk init --l typescript
Next, create a new nextjs project in chatbot-app folder
npx create-next-app@latest chatbot-app
Go into the chatbot-app folder and install some dependencies
npm install ai
Finally the project structure looks like this
|--bin|--lib|--chatbot-app|--test|--cdk.out|--node_modules|--cdk.context.json|--cdk.json|--jest.config.js|--package-lock.json|--package.json|--README.md|--tsconfig.json
Build Chatbot#
Prerequisites: you have to create a Hugging Face token here
Let go int the directory chatbot-app and create a new nextjs project
npx create-next-app@latest
Then install dependencies
npm install ai openai @huggingface/inference clsx lucide-react
Store your Hugging Face token in .env
OPENAI_API_KEY=xxxxxxxxx
The nextjs project has a project structure as below
|--chatbot-app|--app|--api|--route.ts|--global.css|--icons.tsx|--layout.tsx|--page.tsx|--public|--.env|--.eslintrc.json|--Dockerfile|--next.config.js|--package-lock.json|--package.json|--postcss.config.js|--tailwind.config.ts|--tsconfig.json
Build ECS Stack#
Create an Amazon ECS Cluster and a service for the chatbot app in lib/ecs-stack.ts as the following
import {aws_ec2,aws_ecr,aws_ecs,aws_elasticloadbalancingv2,aws_iam,Duration,Stack,StackProps,} from "aws-cdk-lib";import { Effect } from "aws-cdk-lib/aws-iam";import { Construct } from "constructs";interface EcsProps extends StackProps {vpcId: string;vpcName: string;}export class EcsStack extends Stack {public readonly service: aws_ecs.FargateService;constructor(scope: Construct, id: string, props: EcsProps) {super(scope, id, props);// lookup an existed vpcconst vpc = aws_ec2.Vpc.fromLookup(this, "LookUpVpc", {vpcId: props.vpcId,vpcName: props.vpcName,});// ecs clusterconst cluster = new aws_ecs.Cluster(this, "EcsClusterForWebServer", {vpc: vpc,clusterName: "EcsClusterForWebServer",containerInsights: true,enableFargateCapacityProviders: true,});// 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,});// taask add containertask.addContainer("NextChatbotContainer", {containerName: "entest-chatbot-app",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/entest-chatbot-app:latest"// ),image: aws_ecs.ContainerImage.fromEcrRepository(aws_ecr.Repository.fromRepositoryName(this,"entest-chatbot-app","entest-chatbot-app")),portMappings: [{ containerPort: 3000 }],});// serviceconst service = new aws_ecs.FargateService(this, "ChatbotService", {vpcSubnets: {subnetType: aws_ec2.SubnetType.PUBLIC,},assignPublicIp: true,cluster: 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: 1,});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: "entest-chatbot-app",containerPort: 3000,protocol: aws_ecs.Protocol.TCP,}),],healthCheck: {timeout: Duration.seconds(10),},});// exportedthis.service = service;}}
Build CI/CD Pipeline#
Let create a CI/CD pipeline for deploying the chatbot app continuously as the following
import {Duration,RemovalPolicy,Stack,StackProps,aws_codebuild,aws_codecommit,aws_codepipeline,aws_codepipeline_actions,aws_ecr,aws_ecs,aws_iam,} from "aws-cdk-lib";import { Construct } from "constructs";import * as path from "path";interface CodePipelineProps extends StackProps {readonly connectArn?: string;readonly repoName: string;readonly repoBranch: string;readonly repoOwner: string;readonly ecrRepoName: string;readonly service: aws_ecs.FargateService;}export class CodePipelineStack extends Stack {constructor(scope: Construct, id: string, props: CodePipelineProps) {super(scope, id, props);// code commitconst codecommitRepository = new aws_codecommit.Repository(this,"CodeCommitChatbot",{repositoryName: props.repoName,});// ecr repositoryconst ecrRepository = new aws_ecr.Repository(this,"EcrRepositoryForChatbot",{removalPolicy: RemovalPolicy.DESTROY,repositoryName: props.ecrRepoName,autoDeleteImages: true,});// artifact - source codeconst sourceOutput = new aws_codepipeline.Artifact("SourceOutput");// artifact - codebuild outputconst codeBuildOutput = new aws_codepipeline.Artifact("CodeBuildOutput");// codebuild role push ecr imageconst codebuildRole = new aws_iam.Role(this, "RoleForCodeBuildChatbotApp", {roleName: "RoleForCodeBuildChatbotApp",assumedBy: new aws_iam.ServicePrincipal("codebuild.amazonaws.com"),});ecrRepository.grantPullPush(codebuildRole);// codebuild - build ecr imageconst ecrBuild = new aws_codebuild.PipelineProject(this,"BuildChatbotEcrImage",{projectName: "BuildChatbotEcrImage",role: codebuildRole,environment: {privileged: true,buildImage: aws_codebuild.LinuxBuildImage.STANDARD_5_0,computeType: aws_codebuild.ComputeType.MEDIUM,environmentVariables: {ACCOUNT_ID: {value: this.account,type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,},REGION: {value: this.region,type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,},REPO_NAME: {value: props.ecrRepoName,type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,},TAG: {value: "demo",type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,},},},// cdk upload build_spec.yaml to s3buildSpec: aws_codebuild.BuildSpec.fromAsset(path.join(__dirname, "./build_spec.yaml")),});// code pipelinenew aws_codepipeline.Pipeline(this, "CodePipelineChatbot", {pipelineName: "CodePipelineChatbot",// cdk automatically creates role for codepipeline// role: pipelineRole,stages: [// source{stageName: "SourceCode",actions: [// new aws_codepipeline_actions.CodeStarConnectionsSourceAction({// actionName: "GitHub",// owner: props.repoOwner,// repo: props.repoName,// branch: props.repoBranch,// connectionArn: props.connectArn,// output: sourceOutput,// }),new aws_codepipeline_actions.CodeCommitSourceAction({actionName: "CodeCommitChatbot",repository: codecommitRepository,branch: "main",output: sourceOutput,}),],},// build docker image and push to ecr{stageName: "BuildChatbotEcrImageStage",actions: [new aws_codepipeline_actions.CodeBuildAction({actionName: "BuildChatbotEcrImage",project: ecrBuild,input: sourceOutput,outputs: [codeBuildOutput],}),],},// deploy new tag image to ecs service{stageName: "EcsCodeDeploy",actions: [new aws_codepipeline_actions.EcsDeployAction({// role: pipelineRole,actionName: "Deploy",service: props.service,input: codeBuildOutput,// imageFile: codeBuildOutput.atPath(""),deploymentTimeout: Duration.minutes(10),}),],},],});}}
[!IMPORTANT]
CDK automatically create role for codebuild, codedeploy, and codepipeline. Below is the content of the iam policy generated for codepipeline role. The codepline role will assume on of three different role for codebuild action, ecsdeploy action, and source action.
ROLE
{"Version": "2012-10-17","Statement": [{"Action": ["s3:Abort*","s3:DeleteObject*","s3:GetBucket*","s3:GetObject*","s3:List*","s3:PutObject","s3:PutObjectLegalHold","s3:PutObjectRetention","s3:PutObjectTagging","s3:PutObjectVersionTagging"],"Resource": ["arn:aws:s3:::artifact-bucket-name","arn:aws:s3:::artifact-bucket-name/*"],"Effect": "Allow"},{"Action": ["kms:Decrypt","kms:DescribeKey","kms:Encrypt","kms:GenerateDataKey*","kms:ReEncrypt*"],"Resource": "arn:aws:kms:ap-southeast-1:$ACCOUNT_ID:key/$KEY_ID","Effect": "Allow"},{"Action": "sts:AssumeRole","Resource": ["arn:aws:iam::$ACCOUNT_ID:role/CodePipelineChatbotBuildC-9DSS5JG1VE7T","arn:aws:iam::$ACCOUNT_ID:role/CodePipelineChatbotEcsCod-AO6ZDE82ELPC","arn:aws:iam::$ACCOUNT_ID:role/CodePipelineChatbotSource-1SZLHE9CFAAXO"],"Effect": "Allow"}]}
CDK Deploy#
- Step 1. Deploy EcrStack
- Step 2. Build and push an ECR image manulaly
- Step 3. Deploy the EcsStack
- Step 4. Deploy the CodePipelineChatbotStack
[!IMPORTANT] Please provide parameters in /bin/aws-ecs-demo.ts Please provide Hugging Face API Key in /chatbot-app/.env Due to rate limite of free API, sometimes you might experience no response from the bot.
Step 1. Deploy EcrStack which create a ECR repository
cdk deploy EcrStack
Step 2. Build and push an ECR image manulaly
There is a python script in /chatbot-app/build.py will
python3 build.py
You can test this iamge locally
sudo docker run -p 3000:3000 $IMAGE_NAME
Step 3. Deploy the EcsStack
Goto the bin directory and deploy the ecs cluster using cdk
cdk deploy EcsStack
Step 4. Deploy the CodePipeline
cdk deploy CodePipelineChatbotStack
Delete Resource#
[!WARNING]
Please be aware cdk issue when destroying ecs cluster
First destroy the codepipeline stack
cdk destroy CodePipelineChatbotStack
Then destroy the EcrStack
cdk destroy EcrStack
Finally destroy the EcsStack
cdk destroy EcsStack
It is possible to automate all in one big application, but for demo purpose, just make it simple.CodePipelineChatbotStack