From Code to Deployment: Building a Free CI/CD Workflow for Node.js Application
Improving software development lifecycle with CI/CD using Node.js, CircleCI and Render.
CI/CD, which stands for Continuous Integration and Continuous Delivery/Deployment, is a set of practices and principles in software development aimed at automating the process of building, testing, and deploying software applications.
It involves a combination of tools, techniques, and workflows that enable developers to deliver high-quality software more efficiently and frequently.
In this article, we will build a simple CI/CD workflow that will allow us to automatically run tests, build an application, and deploy it to a remote server.
Prerequisites:
- Having existing code in the GitHub repository (ideally with unit tests)
TL;DR
Setting up Continuous Integration (CI)
Continuous Integration (CI) focuses on integrating code changes from multiple developers into a shared repository regularly. It involves automating the build process and running automated tests to identify any issues or conflicts that may arise due to code changes.
By integrating code frequently, CI
helps catch and resolve conflicts early, reducing the chances of introducing bugs and improving overall code stability.
There are several CI
services, some of the most popular are: GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Azure Pipelines, Travis CI, Jenkins, CircleCI.
I personally prefer CircleCI for its simplicity and customizability. The main features available in CirlceCI are:
- Free tier: CircleCI offers a free tier that provides generous limits for build minutes and active users per month. The free tier allows you to leverage CircleCI’s CI/CD platform without incurring any costs, making it an attractive option for open-source projects or small teams.
- Multiple execution environments: CircleCI provides several runtime environments, including cloud virtual machines (VMs) and private infrastructure support. These runtime environments provide flexibility and support for different programming languages and project requirements.
- Parallelism and caching: CircleCI allows the parallelization of jobs and provides caching mechanisms to speed up builds by storing and reusing dependencies between builds.
- Extensive integrations: CircleCI integrates with a wide range of tools, services, and deployment platforms, enabling you to seamlessly integrate with your existing development ecosystem
Creating a CirceCI pipeline
To begin utilizing CircleCI, the first step is to create an account on their website. Once registered, you can select the appropriate project from the available options to establish the CI
setup.
Next, you will be prompted to create config.yml. In this file, you will describe all the actions you need to perform. Select the last option and click “Set Up Project”.
You will be redirected to a new page with the online editor:
Now, let’s write a simple manifest that runs unit tests and builds the application locally.
I use TS
for development, and have written some scripts to start, test, and build an application in package.json
.
You can change the configuration according to your own preferences and requirements.
version: 2.1
orbs:
node: circleci/node@5.1.0
jobs:
build_and_test:
executor:
name: node/default
tag: '18.16'
steps:
- checkout
- run: node --version
- node/install-packages:
pkg-manager: npm
- run:
command: npm run test:unit
name: Run unit tests
- run:
command: npm run build
name: Build server
- run:
command: npm run start
name: Start server
background: true
- run:
command: sleep 5 && curl -I localhost:5000
name: Verify server is running
workflows:
build_and_test_app:
jobs:
- build_and_test
Let’s analyze the written manifest:
1. orbs section:
- The
node
orb from the CircleCI ecosystem is being included. Thecircleci/node@5.1.0
orb provides predefined commands and configurations for working with Node.js projects.
2. jobs section:
- A job named
build_and_test
is defined. It represents a unit of work in the pipeline. - The job uses the
node/default
executor, which is a predefined executor configuration for Node.js projects. It specifies the Node.js version to use (tag: '18.16'). - The steps of the job are defined using the
-
notation. checkout
step: Check out the source code from the version control system. This will locate your repository and retrieve the corresponding branch of git origin. This way, you guarantee that the next steps of the pipeline will work with the most up-to-date version of the code base.run: node --version
step: Print the version of Node.js being used.node/install-packages
step: Installs project dependencies using the specified package manager (npm).run: npm run test:unit
step: Run the unit tests of the project.run: npm run build
step: Build the server application.run: npm run start
step: Start the server application in the background.run: sleep 5 && curl -I localhost:5000
step: Waits for 5 seconds and then sends an HTTP request to the server running onlocalhost:5000
to verify that the server is running.
3. workflows section:
- A workflow named
build_and_test_app
is defined, which represents the sequence of jobs to execute. - The
build_and_test
job is included in the workflow.
Next, you can click the “Commit and Run” button, which will create a new circleci-project-setup
branch in your GitHub repository. This will start this pipeline, and you can observe it in the “Dashboad” section.
Additional configuration
If you need some environment variables to build your application, you can add them manually in the project settings.
In the same settings under “Advanced”, I recommend enabling “Only built pull requests” to avoid unnecessary running workflow on every push to a remote repository.
GitHub status check
To protect your branch from skipping tests, you need to enable “Require status checks to pass before merging” and select your created workflow as a status check in the corresponding GitHub branch.
That’s it, we’ve set up a simple CI configuration to run unit tests and build the app at each pull request and new commit in the default branch (the master branch in my case).
Setting up Continuous Deployment (CD)
As I mentioned before, CD
stands for Continuous Deployment or Continuous Delivery.
- Continuous Deployment refers to an automated process where changes to the codebase are automatically deployed to production or a production-like environment as soon as they pass the necessary tests and quality checks. With continuous deployment, every successful change is immediately released to end-users without manual intervention.
- Continuous Delivery, on the other hand, is an approach where changes to the codebase are continuously prepared and made ready for deployment. While the process is automated, the decision to actually deploy the changes to production is typically done manually. Continuous Delivery ensures that software can be reliably and consistently delivered to production, but the deployment itself is triggered by a human decision.
Thus, the only difference is the final deployment decision, which in the case of Continuous Deployment is automatic, while in the case of Continuous Delivery you have to make the decision manually.
Choosing a cloud platform account
There are many cloud service providers that can be easily integrated into CD
workflow, such as AWS (Amazon Web Services), Google Cloud, Microsoft Azure, Heroku.
Until recently, Heroku was the best option because it offered a free level for hosting projects. Now, overwhelmingly, there are no platforms that offer a permanent free level.
But I recently found a cloud service called Render (not a sponsor) that offers different solutions as well as a free level for your own personal projects.
Render has support for various environments for different programming languages as well as Docker support. I’ll choose the last one.
Using a Docker environment for the deployment process offers several advantages over using native support like a Node.js environment directly.
Docker provides a consistent and isolated runtime environment, ensuring that the application behaves consistently across different systems regardless of the underlying infrastructure or operating system.
Render cloud can automatically detect a Dockerfile.
You can use my sample to create your own Dockerfile
if you have not already done so.
FROM node:18
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 5000
CMD [ "node", "./dist/src/index.js" ]
Creating a CD pipeline using Render
All you have to do to get started is create a new account and then select “Create a new Web Service”. Then you can connect via GitHub or simply paste in a link to your repository.
Render will automatically start the deployment process, and you can see the result in the “Logs” tab.
To set up a continuous deployment, we will do the following:
- After merging the pull request into the “master” branch, the
CD
workflow will be started. - We will run tests and build the application again to make sure the new changes are integrated correctly.
- Only after that, we will run the Render Webhook to start the new deployment.
Updated config.yml
version: 2.1
orbs:
node: circleci/node@5.1.0
jobs:
build_and_test:
executor:
name: node/default
tag: '18.16'
steps:
- checkout
- run: node --version
- node/install-packages:
pkg-manager: npm
- run:
command: npm run test:unit
name: Run unit tests
- run:
command: npm run build
name: Build server
- run:
command: npm run start
name: Start server
background: true
- run:
command: sleep 5 && curl -I localhost:5000
name: Verify server is running
deploy:
machine:
image: ubuntu-2004:current
resource_class: medium
steps:
- run:
name: Deploy API to Render
command: |
response=$(curl -s -w "%{http_code}" -o response.txt $DEPLOY_URI)
response_code=${response:(-3)}
if [ $response_code -eq 200 ]; then
echo "Deployment successful!"
cat response.txt # Print the response body
else
echo "Deployment failed with response code: $response_code"
cat response.txt # Print the response body
exit 1
fi
workflows:
build_and_test_app:
jobs:
- build_and_test
- deploy:
requires:
- build_and_test
filters:
branches:
only: master
We have added a new job called deploy
.
In this job, we spin up a new virtual machine to create an HTTP
request for deployment. $DEPLOY_URI
is an environment variable that you need to register in the current CircleCI project.
You can grab it from the Render cloud under “Setting” in the “Deploy hook” section.
- If the request status code is
200
, we will output “Deployment successful!” and the corresponding body. - Otherwise, we will output “Deployment failed with response code: $response_code” with the response body, where $response_code is the
HTTP
status code.
We have also added to the build_and_test_app
workflow a deploy
job call only if the current branch is master and if the previous job succeeded.
Conclusion
CI/CD has become a cornerstone of efficient and reliable software development. Its ability to automate key processes, ensure code quality, and enable frequent and reliable deployments makes it an indispensable approach for modern development teams.
In this article, we went through the entire process of setting up a basic CI/CD
pipeline using CirlceCI and Render to test and deploy a Node.js web server completely free.
Definitely, this configuration is not complete, and there is a lot that could be added and improved.
For example, testing the application in a pre-production environment with end-to-end tests and a database. Also, logging, monitoring, automatic code security checks, easy rollback and many other useful things. I plan to write a second part in which I will try to implement all of these ideas.
If you have any questions or suggestions, feel free to write a comment. I would also appreciate it if you clap for this article and follow me.
Stay tuned!