Bun JavaScript container that uses AWS SDK to connect to DynamoDB
Build a Bun JavaScript container that runs in AWS Fargate via Amazon ECS, and uses AWS SDK to query a DynamoDB table
Nathan Peck
Senior Developer Advocate at AWS
About
Bun is a fast, lightweight server side JavaScript implementation that
is based on Apple’s JavaScriptCore instead of Google’s V8.
In this pattern you will learn how to create a container that hosts a Bun app,
and deploy the container to AWS Fargate using Amazon ECS.
The containerized application will use the AWS SDK to interact with DynamoDB table.
The sample application is a basic hit counter that will count the number of requests
that it receives, and return the grand total.
Architecture
The following architecture will be deployed to your AWS account:
The application is packaged up as a container image that has the Bun runtime,
application code, and the JavaScript AWS SDK.
Amazon ECS orchestrates a scalable replica set of containers running on AWS Fargate
Traffic ingress from the public arrives at the container via an Application Load Balancer
The container running inside of AWS Fargate uses the JavaScript AWS SDK, and an
automatically vended AWS Identity and Access Management (IAM) Role.
Each time a request arrives at the container, it makes a database query to increment
the hit counter, and return the result.
Dependencies
This pattern requires the following local dependencies:
import{DynamoDBClient}from"@aws-sdk/client-dynamodb";import{DynamoDBDocumentClient,UpdateCommand,UpdateCommandOutput}from"@aws-sdk/lib-dynamodb";// Bare-bones DynamoDB Client
constdynamodb=newDynamoDBClient();// Bare-bones document client
constdb=DynamoDBDocumentClient.from(dynamodb);constTABLE_NAME=process.env.TABLE_NAME;if(!TABLE_NAME){thrownewError('Expected environment variable `TABLE_NAME` with name of DynamoDB table to store counter in');}constserver=Bun.serve({port: 3000,asyncfetch(req){constcommand=newUpdateCommand({TableName: TABLE_NAME,UpdateExpression:"ADD hitCount :increment",ExpressionAttributeValues:{':increment':1},Key:{counter:'global'},ReturnValues:"ALL_NEW"});constresp=awaitdb.send(command)asUpdateCommandOutput;if(resp.Attributes){returnnewResponse(resp.Attributes.hitCount);}else{returnnewResponse(JSON.stringify(resp));}},});console.log(`Listening on http://localhost:${server.port} ...`);
package.json - Defines some third party open source packages to install. In specific,
this installs AWS SDK dependencies for connecting to DynamoDB
index.ts - A small JavaScript application that implements a hit counter that increments
in DynamoDB each time a request arrives.
Now we can use the following Dockerfile to describe how to package this application up
and run it with the Bun JavaScript runtime:
File: DockerfileLanguage: Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
FROMoven/bunWORKDIR/srv# Add the package manifest and install packagesADD package.json .RUN bun install# Add the application codeADD index.ts .# Specify the command to run when launching the containerCMD bun index.ts
Build and push the application container image to a private Amazon ECR container registry
using the following commands:
Which one you choose depends on your goals for this deployment. You can choose the low cost VPC to start with and upgrade to the large sized VPC later on if you have additional private services, or private database servers you wish to deploy in the VPC.
If you have any doubts as to which VPC to choose, then go with the “Low cost VPC” option.
Download the vpc.yml file from your chosen pattern, but do not deploy it yet. Deployment will be done later in the process
Define the Amazon ECS cluster
The following AWS CloudFormation template creates a simple Amazon ECS cluster that is setup for serverless usage with AWS Fargate.
AWSTemplateFormatVersion:'2010-09-09'Description:Empty ECS cluster that has no EC2 instances. It is designedto be used with AWS Fargate serverless capacityResources:## Cluster that keeps track of container deploymentsECSCluster:Type:AWS::ECS::ClusterProperties:ClusterSettings:- Name:containerInsightsValue:enabled# This is a role which is used within Fargate to allow the Fargate agent# to download images, and upload logs.ECSTaskExecutionRole:Type:AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Statement:- Effect:AllowPrincipal:Service:[ecs-tasks.amazonaws.com]Action:['sts:AssumeRole']Condition:ArnLike:aws:SourceArn:!Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*StringEquals:aws:SourceAccount:!Ref AWS::AccountIdPath:/# This role enables basic features of ECS. See reference:# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonECSTaskExecutionRolePolicyManagedPolicyArns:- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicyOutputs:ClusterName:Description:The ECS cluster into which to launch resourcesValue:!Ref ECSClusterECSTaskExecutionRole:Description:The role used to start up a taskValue:!Ref ECSTaskExecutionRole
Define the service
Now we will define the service itself and it’s dependencies.
AWSTemplateFormatVersion:'2010-09-09'Description:An example service that deploys in AWS VPC networking modeonAWS Fargate. Service runs with networking in publicsubnets and public IP addressesParameters:VpcId:Type:StringDescription:The VPC that the service is running inside ofPublicSubnetIds:Type:List<AWS::EC2::Subnet::Id>Description:List of public subnet ID's to put the load balancer and tasks inClusterName:Type:StringDescription:The name of the ECS cluster into which to launch capacity.ECSTaskExecutionRole:Type:StringDescription:The role used to start up an ECS taskServiceName:Type:StringDefault:webDescription:A name for the serviceImageUrl:Type:StringDefault:public.ecr.aws/docker/library/nginx:latestDescription:The url of a docker image that contains the application process thatwill handle the traffic for this serviceContainerCpu:Type:NumberDefault:256Description:How much CPU to give the container. 1024 is 1 CPUContainerMemory:Type:NumberDefault:512Description:How much memory in megabytes to give the containerContainerPort:Type:NumberDefault:3000Description:What port that the application expects traffic onDesiredCount:Type:NumberDefault:2Description:How many copies of the service task to runResources:# The task definition. This is a simple metadata description of what# container to run, and what resource requirements it has.TaskDefinition:Type:AWS::ECS::TaskDefinitionProperties:Family:!Ref ServiceNameCpu:!Ref ContainerCpuMemory:!Ref ContainerMemoryNetworkMode:awsvpcRequiresCompatibilities:- FARGATEExecutionRoleArn:!Ref ECSTaskExecutionRoleTaskRoleArn:!Ref TaskRoleContainerDefinitions:- Name:!Ref ServiceNameCpu:!Ref ContainerCpuMemory:!Ref ContainerMemoryImage:!Ref ImageUrlEnvironment:- Name:TABLE_NAMEValue:!Ref HitCountersPortMappings:- ContainerPort:!Ref ContainerPortHostPort:!Ref ContainerPortLogConfiguration:LogDriver:'awslogs'Options:mode:non-blockingmax-buffer-size:25mawslogs-group:!Ref LogGroupawslogs-region:!Ref AWS::Regionawslogs-stream-prefix:!Ref ServiceName# The service. The service is a resource which allows you to run multiple# copies of a type of task, and gather up their logs and metrics, as well# as monitor the number of running tasks and replace any that have crashedService:Type:AWS::ECS::Service# Avoid race condition between ECS service creation and associating# the target group with the LBDependsOn:PublicLoadBalancerListenerProperties:ServiceName:!Ref ServiceNameCluster:!Ref ClusterNameLaunchType:FARGATENetworkConfiguration:AwsvpcConfiguration:AssignPublicIp:ENABLEDSecurityGroups:- !Ref ServiceSecurityGroupSubnets:!Ref PublicSubnetIdsDeploymentConfiguration:MaximumPercent:200MinimumHealthyPercent:75DesiredCount:!Ref DesiredCountTaskDefinition:!Ref TaskDefinitionLoadBalancers:- ContainerName:!Ref ServiceNameContainerPort:!Ref ContainerPortTargetGroupArn:!Ref ServiceTargetGroup# Security group that limits network access# to the taskServiceSecurityGroup:Type:AWS::EC2::SecurityGroupProperties:GroupDescription:Security group for serviceVpcId:!Ref VpcId# Keeps track of the list of tasks for the serviceServiceTargetGroup:Type:AWS::ElasticLoadBalancingV2::TargetGroupProperties:HealthCheckIntervalSeconds:6HealthCheckPath:/HealthCheckProtocol:HTTPHealthCheckTimeoutSeconds:5HealthyThresholdCount:2TargetType:ipPort:!Ref ContainerPortProtocol:HTTPUnhealthyThresholdCount:10VpcId:!Ref VpcIdTargetGroupAttributes:- Key:deregistration_delay.timeout_secondsValue:0# A public facing load balancer, this is used as ingress for# public facing internet traffic.PublicLoadBalancerSG:Type:AWS::EC2::SecurityGroupProperties:GroupDescription:Access to the public facing load balancerVpcId:!Ref VpcIdSecurityGroupIngress:# Allow access to public facing ALB from any IP address- CidrIp:0.0.0.0/0IpProtocol:-1PublicLoadBalancer:Type:AWS::ElasticLoadBalancingV2::LoadBalancerProperties:Scheme:internet-facingLoadBalancerAttributes:- Key:idle_timeout.timeout_secondsValue:'30'Subnets:!Ref PublicSubnetIdsSecurityGroups:- !Ref PublicLoadBalancerSGPublicLoadBalancerListener:Type:AWS::ElasticLoadBalancingV2::ListenerProperties:DefaultActions:- Type:'forward'ForwardConfig:TargetGroups:- TargetGroupArn:!Ref ServiceTargetGroupWeight:100LoadBalancerArn:!Ref 'PublicLoadBalancer'Port:80Protocol:HTTP# Open up the service's security group to traffic originating# from the security group of the load balancer.ServiceIngressfromLoadBalancer:Type:AWS::EC2::SecurityGroupIngressProperties:Description:Ingress from the public ALBGroupId:!Ref ServiceSecurityGroupIpProtocol:-1SourceSecurityGroupId:!Ref 'PublicLoadBalancerSG'# The table that will store the hitsHitCounters:Type:AWS::DynamoDB::TableProperties:KeySchema:- AttributeName:counterKeyType:HASHAttributeDefinitions:- AttributeName:counterAttributeType:"S"BillingMode:PAY_PER_REQUEST# The IAM role that will be used by the running task.# This grants the task permission to use the hit counter tableTaskRole:Type:AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Statement:- Effect:AllowPrincipal:Service:[ecs-tasks.amazonaws.com]Action:['sts:AssumeRole']Condition:ArnLike:aws:SourceArn:!Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*StringEquals:aws:SourceAccount:!Ref AWS::AccountIdPolicies:- PolicyName:AccessToHitCounterTablePolicyDocument:Version:'2012-10-17'Statement:- Effect:AllowAction:- dynamodb:Get*- dynamodb:UpdateItemResource:!GetAtt HitCounters.Arn# This log group stores the stdout logs from this service's containersLogGroup:Type:AWS::Logs::LogGroup
Some things to note in this template:
The template creates a DynamoDB table resource called HitCounter.
AWSTemplateFormatVersion:"2010-09-09"Transform:AWS::Serverless-2016-10-31Description:Parent stack that deploys VPC, Amazon ECS cluster for AWS Fargate,and a serverless Amazon ECS service deployment that hoststhe task containers on AWS FargateParameters:ImageUrl:Type:StringDescription:The url of the container image that you builtResources:# The networking configuration. This creates an isolated# network specific to this particular environmentVpcStack:Type:AWS::Serverless::ApplicationProperties:Location:vpc.yml# This stack contains the Amazon ECS cluster itselfClusterStack:Type:AWS::Serverless::ApplicationProperties:Location:cluster.yml# This stack contains the container deploymentServiceStack:Type:AWS::Serverless::ApplicationProperties:Location:service.ymlParameters:VpcId:!GetAtt VpcStack.Outputs.VpcIdPublicSubnetIds:!GetAtt VpcStack.Outputs.PublicSubnetIdsClusterName:!GetAtt ClusterStack.Outputs.ClusterNameECSTaskExecutionRole:!GetAtt ClusterStack.Outputs.ECSTaskExecutionRoleImageUrl:!Ref ImageUrl
You should now have the following four YAML files:
vpc.yml - Defines the core networking setup for the application
cluster.yml - Defines the Amazon ECS cluster that will launch AWS Fargate tasks
service.yml - Defines the settings for the Bun application, and the DynamoDB table it will connect to
parent.yml - Orchestrates launching the previous three stacks on your AWS account.
Open up the Amazon ECS console and locate the deployed service. The cluster and service name will be autogenerated, but will look something like this:
Cluster: bun-fargate-hitcounter-ClusterStack-*
Service: web
Click into the service details and then select the “Network” tab. Under the section “DNS Names” you can see the public facing load balancer ingress URL. Click on “open address” or copy and paste the URL to your browser window.
You will see a number, which is the number of web requests that have hit this endpoint so far. You can reload the URL to see the number count up.
Tear it down
You can use the following commands to clean up when you are done:
# Delete the Amazon ECS deploymentsam delete --stack-name bun-fargate-hitcounter --no-prompts
# Empty and delete the Amazon ECR container registry we createdaws ecr delete-repository --repository-name sample-app-repo --force
Next steps
Try increasing the CPU and memory size of the Fargate task in the AWS::ECS::TaskDefinition in service.yml, or increasing the desired count of deployed tasks in the AWS::ECS::Service from two to ten.
Try running a load test with ab or hey to see how Bun in AWS Fargate performs under load.
🎓
New Workshop Series!
Join our upcoming container workshop series and learn best practices for Amazon ECS, AWS Fargate, and more.