Publish Node.js applications with Docker on AWS App Runner
AWS offers multiple options to publish and host Docker containers. Depending on your experience with Docker and with Kubernetes, you can opt for different solutions.
- AWS App Runner
AWS App Runner is a fully managed container application service that lets you build, deploy, and run containerised web applications and API services without prior infrastructure or container experience. - Amazon Elastic Container Services
Amazon ECS is a fully managed container orchestration service that makes it easy for you to deploy, manage, and scale containerised applications. - Amazon Elastic Kubernetes Services
Amazon EKS is a managed Kubernetes service to run Kubernetes in the AWS cloud and on-premises data centers. - AWS Elastic Beanstalk
Elastic Beanstalk is a service for deploying and scaling web applications and services.
In this article I will show you how to deploy a two tiers application, composed by a Front-end (React.js 18) and a Back-end (Node.js with Express). The application will provide two different Docker containers that will be deployed into two different AWS App Runner instances. I choose AWS App Runner because it is one of the easiest way to deploy a Docker container on AWS, so it is the best fit for a “getting started” scenario.
Note: I use heavily AWS Developer Tools, so in my example you’ll see usage of tools such as: CodeBuild, CodePipeline, Cloud9. That doesn’t mean that you are forced to use the same. You can opt for other tools: GitHub, Gitlab, BitBucket, you name it …
The Solution
The Diagram below is a representation of the final solution.
- A Developer will Push some code inside the GIT repository
- CodeBuild will trigger a CI/CD Pipeline
- The Pipeline will Publish a new Docker image inside ECR
- The Attached AppRunner instance will start a new Blue/Green deployment
The Setup
Before starting the project, you will need few services inside your AWS Account. The following services need to be created:
- A CodeCommit GIT Repository
- A Cloud9 development Environment
- Two ECR Registries (one for Front-end, one for Back-end)
- Two AppRunner instances
There are many different ways to do so. To make it simple, I use AWS CLI. I will use a TAG on all my resources so that I can easily monitor the cost of the solution.
#01 — CodeCommit
aws codecommit create-repository --repository-name two-tiers-nodejs /
--repository-description "Two tiers App
Runner example using Node.js" /
--tags "project=medium-apprunner"
#02 — Cloud9
aws cloud9 create-environment-ec2 --name two-tiers-nodejs-ide /
--description "Two tiers App Runner Development environment" /
--instance-type t2.micro --automatic-stop-time-minutes 60 /
--connection-type CONNECT_SSM --tags "Key=project,Value=medium-apprunner"
#03 — ECR (x2)
aws ecr create-repository --repository-name two-tiers-nodejs/frontend /
--tags "Key=project,Value=medium-apprunner"
aws ecr create-repository --repository-name two-tiers-nodejs/backend /
--tags "Key=project,Value=medium-apprunner"
You will create the two app runners later, after you have published at least one Docker image for each tier.
Create the Applications
In order to create the two applications, you need to open Cloud9 and configure the environment. Head to AWS Console, open Cloud9 and start the terminal. Then Click on the tab “Source Control” and clone the GIT CodeCommit URL which you have previously created. You can use HTTPS for that or SSH.
You should have something like this at the end:
#01 — React Application
# create a React application
$: npx create-react-app front-end
# verify
$: cd front-end
$: npm start
With Preview window you should be able to see the React application running on port 8080 or on port 3000. It depends on the version of React/NPX you have on your Cloud9.
Remember: Cloud9 can preview 8080, 8081, 8082 only.
#02 — Node Express
# ensure you are in root folder ;-)
$: mkdir back-end
$: npx degit kklee998/express-rest-api-scaffold back-end
$: cd back-end
$: npm install
# verify
$: npm run dev
You should be able to see it on preview on port 8080 again. If you do not, it means your back-end is running on a different port and you need to tell “Cloud9 preview”.
Docker”ize” everything
Time to “dockerize” everything. In my example, I am using port 80 for front-end and port 8080 for back-end. Pay attention to this detail, because that’s how you will tell AppRunner, where the application is running. I shield the application(s) with NGINX so 80 is the default HTTP port.
The idea here is to:
- build the application
- package a Docker image
- push it into the ECR repository.
Of course you want to leverage CI/CD, so first step would be to create the Docker image.
#01 — Dockerize Front-end
$: cd front-end
$: touch Dockerfile
$: touch nginx.conf
Open the newly created Dockerfile and enter the following:
# Stage 0, "build-stage"
FROM public.ecr.aws/docker/library/node:lts-slim as build-stage
WORKDIR /app
COPY package*.json /app/
RUN npm install
COPY ./ /app/
RUN npm run build
# Stage 1, based on Nginx
FROM public.ecr.aws/nginx/nginx:1.19.5
COPY --from=build-stage /app/build/ /usr/share/nginx/html
COPY --from=build-stage /app/nginx.conf /etc/nginx/conf.d/default.conf
Open the newly created nginx.conf and enter the following:
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
Build it:
$: cd front-end
$: docker build . --tag two-tiers/frontend
# ensure it's built
$: docker image list
# how to test? inside preview as following
$: docker run -p 8080:80 two-tiers/frontend
#02 Dockerize Back-end
$: cd back-end
$: touch Dockerfile
Open the newly created Dockerfile and enter the following:
FROM public.ecr.aws/docker/library/node:lts-slim as build-stage
WORKDIR /app
COPY package*.json /app/
RUN npm install
COPY ./ /app/
EXPOSE 8080
CMD ["npm", "start"]
Build it:
$: cd back-end
$: docker build . --tag two-tiers/backend
# ensure it's built
$: docker image list
# how to test? inside preview as following
$: docker run -p 8080:8080 two-tiers/backend
If you have followed all steps, you should have now everything ready for CodeBuild and CodeCommit. Front-end hosted on port :80 by NGNIX and Back-end hosted on port :8080 by Node.js.
CI/CD using CodeCommit and CodeBuild
CodeCommit and CodeBuild work together, as far as you provide some build-specs. To do so, we will go back to the root folder of our project and create a buildspec file as following:
$: CD ..
$: touch buildspec.yml
In the buildspec I will execute the following steps:
- Build the Front-end Docker image
- Build the Back-end Docker image
- Publish the Front-end Docker image to ECR
- Publish the Back-end Docker image to ECR
# configure settings
version: 0.2
env:
variables:
PROJECT: "two-tiers"
phases:
install:
commands:
- echo "Entered the install phase..."
pre_build:
commands:
- echo "Entered the pre_build phase..."
- aws --version
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [your account].dkr.ecr.us-east-1.amazonaws.com
build:
commands:
- echo "Build Front-end..."
- cd front-end
- docker build -t two-tiers-nodejs/frontend .
- docker tag two-tiers-nodejs/frontend:latest [your account].dkr.ecr.us-east-1.amazonaws.com/two-tiers-nodejs/frontend:latest
- cd ../back-end
- docker build -t two-tiers-nodejs/backend .
- docker tag two-tiers-nodejs/backend:latest [your account].dkr.ecr.us-east-1.amazonaws.com/two-tiers-nodejs/backend:latest
post_build:
commands:
- echo "Publish images to ECR..."
- docker push [your account].dkr.ecr.us-east-1.amazonaws.com/two-tiers-nodejs/frontend:latest
- docker push [your account].dkr.ecr.us-east-1.amazongitaws.com/two-tiers-nodejs/backend:latest
Commit your code to CodeCommit and head to the AWS Console, CodeBuild. From CodeBuild you can create a new Build project:
Project name:
two-tiers-nodejs
Tags:
project : medium-apprunner
Source:
Source provider: AWS CodeCommit
Repository: two-tiers-nodejs
Reference type: Branch
Branch: master
Environment:
Managed Image
Operating system: Amazon Linux 2
Runtime: standard
Image: [latest AWS image]
Environment type: Linux
Privileged: true
New service role: [custom name for your role]
Click on Create Project and then you will be re-directed to the CodeBuild project page. From here choose “Start build” and wait until the first dry run is completed:
warning: If you get an error that CodeBuild cannot login into ECR, it is because your IAM Role on CodeBuild, does not have those permissions. To solve the problem, head to IAM Role, choose the role you created for CodeBuild and grant ECR policy permissions. The ECR policies are: AmazonEC2ContainerRegistryFullAccess or AmazonElasticContainerRegistryPublicFullAccess depending if you are using Private or Public ECR.
Final Step, App Runner
At this point you have completed your CI/CD setup.
- You can commit code to CodeCommit
- You can trigger a CI/CD build with CodeBuild
- A new Docker image is published on ECR for each of your tiers
If you want, you can query ECR to ensure your images are getting published by CodeBuild:
raffaeu:~/environment (master) $ aws ecr list-images --repository-name two-tiers-nodejs/frontend
{
"imageIds": [
{
"imageDigest": "sha256:f4e25a33a85c6217f9e7d39362af38cb1aa58ad5894cba466876473ddf55ecc7",
"imageTag": "latest"
},
{
"imageDigest": "sha256:d6bfbe78e3c2ea076230fd157530d7a7b043ccbaa334c7abbd7b688411782703"
}
]
}
The next steps are required to create an AppRunner instance for each Docker image and inform AppRunner that, every time a new ECR image is available, a new blue/green deployment needs to be triggered.
#01 - AppRunner
On the Network tab, remember to set the port of Docker to :80 for Front-end and to :8080 for Back-end
Wait around 5 minutes and you should be able to see your Docker images hosted by the two AppRunner services.
Note: For convenience I have published one image, but you will need to create two app runner, one for the Front-end and one for the Back-end.