Let’s assume that you have a Node.js application deployed on AWS ECS. In this post, we will guide you through the process of adding a continuous integration workflow to that app using AWS Code Pipeline- with zero downtime.
Adding Health-Check API for your application
Since we are talking about ECS, the Node.js application will be containerised and running inside a docker environment fronted by the Elastic Load Balancer. Typical load balancer configuration would be to point to the TCP port on which the application is running and check if the port is open. This is not a fool-proof way of checking if your application is alive as it is possible that the port is open but the application is not booting due to a recent code change. First, we will add a /status api to the application that returns an affirmative HTTP response whenever called as seen below.
const express = require('express')
const app = express()
app.get('/status', (req, res) => res.status(200).json({status: 'ok'}))
app.listen(3000, () => console.log('Example app listening on port 3000!'))
We have a simple /status api end-point that just returns 200 OK when called. But, you can add logic to check if the dependent components are healthy and send back a consolidated health signal using the above api.
Configuring ELB to use the health monitor API
Navigate to the load balancer console for the Node.js application from ECS and click on the Health Check tab. The default port based configuration would be displayed there. Let’s override that by clicking on the Edit Health Check button. Add the following configuration as shown in this screenshot:
Couple of things to note:
the port should be the one that the application is listening to inside the container. In the above example, the app is running on port 3000
the interval implies that the health check api is invoked every 30 seconds (YMMV)
it is recommended to have a higher healthy threshold than unhealthy threshold as is 4 and 2 here. More on that in the next point.
Close the ELB configuration and open the application’s task definition. Under deployment, update Minimum Healthy Percent to 100 and Maximum Percent to 200. So, when a new version of code is deployed, ELB will maintain old and new instances of the application for sometime and destroy the old instance after healthy threshold is achieved with newly deployed version.
Setup a deployment pipeline using Code Pipeline
A typical deployment pipeline in the context of a Node.js application will have the following steps:
We will walkthrough the steps to create the above CI/CD pipeline for our application to ensure that whenever new code is checked in, all the tests are run, the code is linted and certified, packaged and deployed without causing any outage.
1. First navigate to Code Pipeline console and click on Create Pipeline button.
2. Point to the repository where the application code is available.
3. On the next screen, select build provider as AWS CodeBuild and add a project name by selecting create a new build option.
Pay attention to the next section to choose the runtime as Node.js and appropriate version as shown below.
4. To speed up builds, turn on cache and select a S3 bucket name. Now, we will add a build_spec.yml file that will be present in the root directory of the code repository.
version: 0.2
phases:
pre_build:
commands:
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- npm ci
- $(aws ecr get-login --region $AWS_DEFAULT_REGION)
build:
commands:
- echo Running tests
- npm run test
- echo Packaging application
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing image definitions file...
- printf '[{"name":"expresso-staging","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
cache:
paths:
- ~/.npm
- /var/lib/docker/**/*
5. We will quickly walk through the configuration present in buildspec.yml.
The current git commit is captured as IMAGE_TAG to be suffixed to docker image name.
We use npm ci instead of npm install to install the dependencies as a pre-build step. Refer to npm documentation to understand why this command is better performant and clean.
The aws login command is to initialise the variables to later push the docker image to AWS ECR
After the pre-build phase, we run the test suite and if that succeeds, we build the docker image (this post assumes you have a Dockerfile in the root of the repo relevant to booting a Node.js app)
In the post-build phase, we push the docker image to ECR and create a imagedefinitions.json file that will be used in the deploy step.
8. Now that we have run the test suite and packaged the application, it is ready to be deployed.
9. Click on next to go to step 4. Select deployment Provider as Amazon ECS and select the respective values for Cluster and Service Name where your application is running.
10. As the last step, select the image filename as imagedefinitions.json that we created after packaging the application
11. Clicking on next, you would be prompted to create a role for code pipeline or select an existing role if you have configured other projects. Do as needed.
12. As next step, review the configuration and submit the changes to create your pipeline.
Blue-Green Deployment
The final setup would look like below:
ELB will keep pinging each task instance of the current running version of the application using the /status url to confirm that all tasks are healthy (let us assume the task count is 3).
When there is a new commit checked into the source code repository, Code Pipeline will detect the change and execute the steps in build_spec.yml When the new version is deployed, ECS will create a new task definition with latest image and for brief period of time, there will be 6 instances of the application running (remember we set Maximum Percent to 200 earlier) . 3 with the old version of code and 3 with the latest version.
After ELB confirms that the three new task instances are healthy, it will kill off the 3 old instances and restore the task count to three. Voila!
Comments