Deploy Node.js to AWS: Build an Automated CI/CD Pipeline
Last updated on 22 November 2022
Deploying a Node.js application to AWS is not straightforward. One needs to follow many steps, and only when done in the correct order, can one deploy a Node.js application.
That is why I've decided to write a series on how to deploy your Node.js application. There are five areas this series will cover:
- 👉 Deploying Node.js to AWS with CI/CD
- Automate with CloudFormation and IAAS
- Monitoring and alerting using AWS CloudWatch
- Securing your Node.js Application with AWS
- Achieve high availability on your Node.js application with AWS
There's a lot more you can do with AWS. Let me know what else you want me to cover in this series in the comments.
This first article will go through all the steps required to deploy your Node.js application to AWS and have an automated Continuous Integration & Continuous Deployment (CI/CD) setup.
Here are the ideas we will discuss:
- 🎯 Overview
- 🏮 Our sample Node.js application
- 🏗 Understanding CodeBuild
- 🧱 Provide env variables to CodeBuild
- 🕋 Storing build artifacts
- Introduction to CodeDeploy
- 🌠 Setting up CodeDeploy in an EC2 instance
- 🐢 Create a CodeDeploy application
- 💎 How to create a deployment (manual & automated way)
- 🎛 Customizing in-place deployments
- ➫ Rollback
- 🦄 Conclusion
We will build an automated CI/CD pipeline for our Node.js application completely on AWS. We go through a series of steps to deploy our changes to AWS. Once your code changes are approved and merged into the repository's base branch, they go through the following stages:
Software development lifecycle
1Merge to production > Build code > Run tests > Start the deployment
Once the build stage completes, a deployment is triggered to push the latest code revision to the servers.
Continuous Integration (CI) makes sure your changes are continuously integrated into the main stable branch. Continuous Deployment (CD) automates the deployment process completely. It does not require any manual intervention. This allows faster and frequent releases.
Our sample Node.js application
We will be using a sample Node.js application to deploy to AWS. You can find the code here.
If you want to follow along, follow these steps:
Clone the repo1git clone firstname.lastname@example.org:Rishabh570/deploy-nodejs-aws-cicd.git
Install the packages1npm install
Run the server1npm start
You should have a running Node.js application on your local machine. Let's get to the interesting stuff.
On to CodeBuild!
Once we have our application running, the next step is building our code. It can involve many things. AWS CodeBuild is a developer tool and it helps in building our code.
Some of the common use cases for CodeBuild are:
- Compiling the code (eg. compiling TypeScript)
- Running linters and formatter (eg. eslint, prettier).
- Testing the code (eg. running unit tests)
- Build the image and push it to the docker registry (eg. AWS ECR).
To start using CodeBuild, we first need to create a CodeBuild project. There are a few things required to create one:
- Code source
- Batch configuration
- Meta information about the project
- Build specification file
Let's understand each one in better detail.
CodeBuild requires access to your code repository to run a build. You can use any one of CodeCommit, GitHub, BitBucket, GitHub Enterprise, or Amazon S3. Since our code is hosted on GitHub, we'll use that as the source of code.
We will set the source version to point to the "main" branch. You can choose your production branch. In our case, the "main" branch is the production branch.
Lastly, we will allow CodeBuild to modify the service role (tick the checkbox) so that it can get the necessary permissions.
This specifies the environment where the build will run. You can choose "Managed Image" for starters. Additionally, follow these steps:
- Operating system = "Ubuntu".
- Runtime = "standard"
- Set the Image to the latest one available. At the time of writing, this is
This setting tells AWS to batch a certain predefined number of builds and run them at once. This is not required but can be considered based on your use case.
You might want to store the files generated by the build process to use or refer to later. You can do it by generating artifacts. You can instruct CodeBuild to store a specific set of files or all the files in Amazon S3 after the build finishes.
We will talk about adding artifacts in the later section.
If you want access to your build logs, you can choose to store them on Amazon S3. The build logs are encrypted by default, but you can choose to disable encryption and store them as plain text.
If the CodeBuild project is a folder then all the builds are files in that folder. A CodeBuild project contains all the builds for a particular repository. You need one CodeBuild project for each of your repositories.
Meta information includes the name of the CodeBuild project and an optional description.
Additionally, you can set the max limit on the number of concurrent builds for a given build project.
Build specification file
This is the most important part of your build configuration. A buildspec.yml (or, buildspec.json) file declares how CodeBuild will build the code. You can check out the AWS CodeBuild reference for detailed information on the syntax and examples.
You can choose to have a buildspec file in your codebase or specify the build commands directly in the editor.
This is what our buildspec.yml file looks like:
buildspec.yml file containing the build instructions
1version: 0.223phases:4 install:5 runtime-versions:6 nodejs: 187 commands:8 - echo "📦 installing packages..."9 - echo "✅ Packages installed successfully."10 pre_build:11 commands:12 - echo "⚙️ Testing..."13 - echo "✅ Tests passed successfully."14 build:15 commands:16 - echo "🚧 Starting compiling packages..."17 - echo "✅ Build passed successfully."18 post_build:19 commands:20 - echo "🚚 Performing post-build packing and operations..."21 - echo "✅ Post build successful"
Providing env vars to CodeBuild
If your build uses values that are sensitive, you should not put them as plaintext in the buildspec.yml file. There are two options:
- Use ENV variables and provide them directly from CodeBuild editor. Or,
- Use AWS parameter store for your secrets and use them directly in your builspec.yml file.
If you observe closely, first approach does not help much. Providing ENV variables right from the CodeBuild dashboard is not only manual effort but it also exposes your ENV variables in the build logs as plaintext. Not good.
This leaves us with only one option – using the AWS parameter store.
Let's head over to the parameter store and create a new parameter.
We'll store one sample application password and then use it directly in our buildspec.yml file.
Parameter store offers three types of values:
Make sure to use the SecureString type when creating any sensitive parameter. Once it is done, you can refer to the parameter in your buildspec.yml file like this:
Using secret from parameter store in buildspec.yml
1version: 0.223env:4 parameter-store:5 PASSWORD: /Production/AppPassword67phases:8 install:9 runtime-versions:10 nodejs: 1811 commands:12 - echo "📦 installing packages..."13 - echo "✅ Packages installed successfully."14 pre_build:15 commands:16 - echo "⚙️ Testing..."17 - echo "✅ Tests passed successfully."18 - echo $PASSWORD19 build:20 commands:21 - echo "🚧 Starting compiling packages..."22 - echo "✅ Build passed successfully."23 post_build:24 commands:25 - echo "🚚 Performing post-build packing and operations..."26 - echo "✅ Post build successful"
With this, our builds have access to our application password. It is encrypted and does not need any manual effort to inject during the build phase.
Store artifacts for future reference
We have successfully set up CodeBuild and the builds are passing. Now we will look at how to add artifact support to your builds.
Following the buildspec official reference, we can add the support for artifact by adding the highlighted section to our buildspec.yml file:
adding artifact support in your buildspec.yml
1version: 0.223env:4 parameter-store:5 PASSWORD: /Production/AppPassword67phases:8 install:9 runtime-versions:10 nodejs: 1811 commands:12 - echo "📦 installing packages..."13 - echo "✅ Packages installed successfully."14 pre_build:15 commands:16 - echo "⚙️ Testing..."17 - echo "✅ Tests passed successfully."18 - echo $PASSWORD19 build:20 commands:21 - echo "🚧 Starting compiling packages..."22 - echo "✅ Build passed successfully."23 post_build:24 commands:25 - echo "🚚 Performing post-build packing and operations..."26 - echo "✅ Post build successful"2728artifacts:29 files:30 - '**/*'31 name: deploy-nodejs-build-artifacts
And we're done, running a fresh build will store the artifacts in Amazon S3 ✨. Here's a sample build artifact stored on S3:
First steps with CodeDeploy - understanding appspec.yml
Before we start our deployment journey with CodeDeploy, we need to understand a very crucial piece of the puzzle – Application Specification File (or, appspec.yml). The appspec.yml instructs CodeDeploy how to go about the deployment process.
Here's a simple appspec.yml file:
appspec.yml file breaks down deployment stages into steps
1version: 0.02os: linux3files:4 - source: /5 destination: /deploy-nodejs-aws-cicd6hooks:7 BeforeInstall:8 - location: scripts/install_dependencies.sh9 timeout: 30010 runas: root1112 ApplicationStart:13 - location: scripts/start_server.sh14 timeout: 30015 runas: root1617 ValidateService:18 - location: scripts/validate_service.sh19 timeout: 300
Let's break it down.
This represents the version of the Application Specification (or, appspec) file.
This is for explicitly specifying the operating system of the machine.
It instructs CodeDeploy which files from your source code should be copied over to the EC2 instance(s) during the deployment's install event.
For each event in a deployment lifecycle, there is a dedicated hook provided by CodeDeploy. Hooks can be used to run scripts against each lifecycle event. You can choose to attach one or more scripts to a single hook. It is not mandatory to include all the hooks in your appspec file.
Here are the following hooks that are available in an appspec file, in the exact run order:
The list of environment variables that are accessible to hook scripts is available here. On the same page, you can learn about available hooks in case you're using a load balancer. Additionally, the documentation also states which hooks are available for blue-green deployment.
To give a broad overview, here are the steps to deploy your code using CodeDeploy:
- Create an EC2 instance.
- Install CodeDeploy agent on EC2.
- Manually create a CodeDeploy application and a deployment group.
- Finally, create a deployment (manually and in an automated way).
Let's take a deeper look at each of these steps.
Create an EC2 instance
We will need one running EC2 instance where we can deploy our changes. You can select a t2.micro Amazon Linux 2 AMI instance to follow along.
Make sure to attach an IAM role that can list and fetch items from Amazon S3. The EC2 instance needs this permission to fetch the revision during the deployment.
Before installing CodeDeploy, add the following tags to your EC2 instance:
- name = webserver
- environment = development
This is required as CodeDeploy uses tags to target eligible instances.
To install CodeDeploy in your newly created EC2 instance, follow these steps:
sudo yum update
sudo yum install -y ruby wget
chmod +x ./install
sudo ./install auto
sudo service codedeploy-agent status
You can find more information in the installation guide for Linux here.
Once done, you should be able to see the CodeDeploy status with a process ID like below:
output for command #6
1AWS CodeDeploy agent is running as PID 27393
Create a CodeDeploy application and deployment group
We have our EC2 instance up and running. And we have installed the CodeDeploy agent. The next step, create a CodeDeploy application and deployment group.
Each application requires a corresponding CodeDeploy application for deployments. A deployment group is required to target a set of EC2 instances (using name and environment tags).
To create a CodeDeploy application:
- Visit CodeDeploy.
- There are only two things required – application name and compute platform. You can name the application anything you like. We will be choosing "EC2/On-premises" for the computing platform.
Next up, let's create a deployment group. To do this, we will need to create an IAM role for CodeDeploy for it to access the EC2 instance(s).
- Head over to IAM > create role.
- Search for "CodeDeploy" in AWS services (shown below).
- Select and name your role "CodeDeployRoleForEC2".
- Review and click "Create Role".
Finally, let's get to the last step of creating a deployment group before we can trigger a deployment.
Here are the steps:
- Enter the deployment group name (aka DGN) = development.
- Select the service role you just created for CodeDeploy.
- For the deployment type, we will use "In-place".
- Select "Amazon EC2 instances" for the environment configuration. Additionally, add the tags to help CodeDeploy select the eligible EC2 instances.
- Key = environment, Value = development
- Choose deployment settings = CodeDeployDefault.AllAtOnce (explained later).
- Disable the load balancer.
- Finally, click "Create deployment group".
Create a deployment
Finally, time to create a deployment. There are two ways to do this:
- Manually creating the deployment.
- Automating the deployment by updating our build stage (buildspec.yml).
Regardless of the method you choose, you'll need to upload the code revision on Amazon S3 (or GitHub) first. CodeDeploy picks up the revision file (.zip) and creates a deployment using that.
Let's create a separate S3 bucket to host our code revisions.
Using profile flag (--profile) to select AWS region and credentials for make-bucket command
1aws s3 mb s3://deploy-nodejs-deployment-revisions --profile personal-mumbai
You should see the following success output:
It is a good idea to have bucket versioning enabled, you can do that by running this command (can be done from AWS console as well):
Enabling Amazon S3 bucket versioning
1aws s3api put-bucket-versioning --bucket deploy-nodejs-deployment-revisions --versioning-configuration Status=Enabled --profile personal-mumbai
Upon checking the properties of the bucket from the AWS console, we can confirm that the versioning has been enabled:
Manually creating a deployment
We will upload our code revision to the bucket by following this command:
Uploading revision zip file to Amazon S3
1aws deploy push --application-name deploy-nodejs-codeDeploy --s3-location s3://deploy-nodejs-deployment-revisions/development/source.zip --ignore-hidden-files --profile personal-mumbai
AWS CLI conveniently tells us to deploy with the revision just uploaded. This command will help us automate the deployment soon. But first, let's look at how we can do this from the AWS console.
Our revision is now available on the S3 bucket:
Let's create a deployment now.
- Click "Create deployment" from the deployment group details page (shown below).
- Select the deployment group. In our case, it is "development".
- Make sure the revision type is set to S3.
- Select the revision location.
- Finally, click "Create deployment".
After following the above steps, you should have your latest revision deployed on your EC2 instance(s).
Automating the deployment
To automate the deployment, we can inject the steps shown above (in the manual deployment section) into the buildspec.yml file. CI/CD workflow:
Here's our updated builspec.yml file:
Using builspec.yml > post_build hook to deploy our changes once the build succeeds
1version: 0.223env:4 parameter-store:5 PASSWORD: /Production/AppPassword67phases:8 install:9 runtime-versions:10 nodejs: 1811 commands:12 - echo "📦 installing packages..."13 - echo "✅ Packages installed successfully."14 pre_build:15 commands:16 - echo "⚙️ Testing..."17 - echo "✅ Tests passed successfully."18 - echo $PASSWORD19 build:20 commands:21 - echo "🚧 Starting compiling packages..."22 - echo "✅ Build passed successfully."23 post_build:24 commands:25 - echo "🚚 Performing post-build packing and operations..."26 - aws deploy push --application-name deploy-nodejs-codeDeploy --s3-location s3://deploy-nodejs-deployment-revisions/development/source.zip --ignore-hidden-files --profile personal-mumbai27 - aws deploy create-deployment --application-name deploy-nodejs-codeDeploy --s3-location bucket=deploy-nodejs-deployment-revisions,key=development/source.zip,bundleType=zip,eTag=80e501480a8545019660e87ca42a6f00,version=67ZE9Q8CZeo9XszaZ4F.eKmrlW7mnDrm --deployment-group-name development --deployment-config-name CodeDeployDefault.AllAtOnce --description "This deployment aims to deploy our code to the eligible EC2 instance(s)."28 - echo "✅ Post build successful"2930artifacts:31 files:32 - '**/*'33 name: deploy-nodejs-build-artifacts
If you are following along, you can try this out by pushing or merging your changes to the main branch (your production branch). And this successfully automates the whole CI/CD workflow.
PS: This is one of the possible ways to achieve CI/CD on AWS. There are other ways to achieve the same outcome (eg. using AWS CodePipeline).
Customizing In-place deployments
Deploying to all the instances at once works fine on the development environment. But when it comes to production, it is risky to deploy the changes to all the instances at once. AWS, by default, provides the option of deploying changes one instance at a time.
You can even choose to have a custom deployment configuration. For example, you might want at least 50% of your instances to be healthy at any point. You can create a custom deployment configuration for that.
There are times when the deployment introduces some unexpected behaviors. We need a way to cut the impact area of the bug as quickly as possible.
Rollbacks help in such scenarios.
Rollbacks allow you to move to a safe & stable previous build in face of a production bug. But, previous deployments are not overridden during the process. A new deployment with the previous stable code revision is deployed instead.
There are two ways of performing rollback in CodeDeploy:
- Manual rollback
- Automatic rollback
For manual rollbacks, we need to perform the rollback manually every time. To do this, create a deployment with any previous stable code revision.
To set up automatic rollbacks, we will edit our "development" deployment group. In the advanced options, you can find Alarms and Rollbacks.
There are two scenarios where automatic rollback is triggered:
- When a deployment fails.
- When some alarm threshold is met.
The first one is straightforward. And AWS gives a lot of flexibility for setting up alarm thresholds. You can set up CloudWatch Alarms and use those to determine if a rollback is necessary.
For example, I've set up a CloudWatch Alarm on the EC2 CPU utilization. If it exceeds 60%, there will be an automatic rollback to a previous stable code revision.
You can read more about various kinds of CloudWatch Alarms here.
This wraps up our journey on Node.js CI/CD on AWS. On every Pull Request (PR) merge to the production branch, the changes will be built, tested, and deployed automatically.
In the next article, I'll write about how we can automate some manual tasks using AWS CloudFormation. Why? Because it becomes harder to maintain and operate the AWS infrastructure manually when your applications starts to scale.
I hope this article helped you get an initial understanding on the capabilities of AWS. And what you can expect next.
If you were not following along, this is the best opportunity for you to go ahead and learn by doing. It was a lot, make sure you understand. Final code is available here.
Happy to answer any questions and take feedback in the comments or on my Twitter.