Introduction

Moving to a Cloud has became a non-neglected approach for any company that anticipates growth, service continuity, security while improving its operation experience, therefore reducing certain costs at a specific extend.

In this article, we are covering a flexible approach that makes it possible to move your NodeJS projects and/or services to the Cloud with minimal efforts. Started from a single code monolithic App therefore broken into independent and scalable modules/services deployable in the Cloud.

The Simplicity of NodeJS

NodeJS is a JavaScript backend runtime environment that has been mostly adopted by developers and companies because it enables developers to start writing, within the same project, almost the same code not only on the frontend but also on the backend in order to fully enjoy a Full-Stack JS experience.

Nowadays, it’s amongst the top backend platform used for small, medium and large infrastructures for software development and so a proper Cloud Migration strategy should be kept in mind regardless of what we are building.

Cloud Migration Strategies

Approaches put in place to safely move to the Cloud should be part of a well defined and thoughtful plan that fits the needs of the project or company, this might differ from one team/company to another depending on the problems they are solving (in their own priority list) and the third party involved. It’s possible at the same time to adopt multiple strategies, or to keep using one while having the gate opened to jump to another with the least amount of efforts. Let’s focus on the Rehosting aka lift-and-shift and Replatforming aka lift-tinker-and-shift, more of these are described in this article by Stephen Orban that first appeared in AWS Cloud Enterprise Strategy Blog.

Initially, it started with the easiest lift-and-shift strategy where we simply moved the monolith App to an EC2 with its API being served via an Nginx server. Next, the team started some reorganization, mastering clouds concepts, started breaking the App to have independent modules that do very specific task/features by making the main App even more stateless. By doing so, we ended up with a NodeJS microservice template that seems convenient for cloud migrations still with the idea in mind to perform on-premise deployments whenever we want to revert from the cloud migration.

Sample NodeJS Architecture

Not only for illustrations purposes, but this approach has been used while accompanying many companies quickly migrate their production NodeJS services to the cloud prior to developing their expertise on it for further improvements.

High Level Architecture

AWS has been chosen for this example, but the idea behind is replicable to many other Cloud providers and even for On-Premise infrastructures, that’s why we consider this to be a multi-purpose migration strategy for NodeJS Apps, here’s what the high level architecture diagram looks like:

High level architecture diagram
High level architecture diagram

In this architecture, a set of services are involved to materialize a micro-services environment (not at its full capacity) in which NodeJS Apps are deployed either as Lambda functions or as normal server Apps.

The code has been improved just a bit to be able to support Lambda deployment to the already existing server one, which is related to run different starter files to start the App, respectively lambda.js and server.js. The full source code is available over on GitHub.

Configurations

Regardless of the way each service is deployed, a kind of custom service discovery/configurations is present in order to know more about services at runtime. When deploying, the service specifies the mode in which it got deployed by setting a value in the cache (ElasticCache or Custom Redis).

Very soon, the need to centralize the configurations arises, Knife was the top option but we finally opted to use a simple private (soon to be encrypted) AWS S3 bucket for simplicity even though it’s an intermediate choice.

Services Communication

As illustrated up here, the services communication is handled by two main protocoles; the popular and trivial Rest API way and by using asynchronous notification channels (Redis/SNS pub and sub). The project is built to listen to specific channels depending on its relations with other deployed services/modules or third parties.

If deployed as a Lambda function (replatforming), AWS SNS is used to send asynchronous notifications to other deployed services and scheduled events (sometimes AWS Batch with pre-built Docker images) is the cron jobs runner.

In contrary, in a server (rehosting) mode, Redis Pub/Sub is at the command for services notifications while the package node-cron handles cron jobs at a small scale.

Cache

Redis is used as the services’ caching server regardless of the deployment strategies, started with a single node ElastiCache cluster till we notice more is needed.

Database

In this particular setup, we first moved out the database from the same machine as the server code, so the App will be more independent and stateless. We were able to run the databases either on MongoDB Atlas service (recommended) within a VPC or by deploying and managing custom ones within AWS EC2 instances, the On-premise approach could also work since we are thriving to build an hybrid architecture that solves our own problems, but that comes with other maintenance/management costs.

File Storage

Still in the path of making the server App lightweight enough and stateless, AWS S3 is being used for images/videos storage, not via a direct integration but by using another small service built for the purpose of the migration with everything put in place to work either on premises, or runnable no matter the cloud provider since it’s flexible enough and has an interface to change the storage service whenever needed.

Emailing

Breaking the initial code base into re-usable modules was one of the first decisions we made to accomplish the desired result, it resulted in having a separate service that handles email sending by using any providers as the implementation details. AWS SES is used for our migration purpose.

Security

It’s important to adopt a decentralized security mechanism for a decentralized architecture, or at least having proper integration patterns to make it work flawlessly amongst your components. JWT was present already and so we kept it, once we reached that level where all our components were deployed in the Cloud, we opted to use an isolated network (AWS VPC) to deploy these and made the necessary changes in our CI/CD pipelines, without putting in place other security best practices gradually.

Monitoring

Once you deploy your Lambdas, some default metrics are being saved on CloudWatch to help you to know how your functions perform overtime, it includes and are not limited to: invocations, duration, error count, thotties, concurrent executions … etc.

Another important factor is the ability to programmatically publish custom metrics via the SDK to AWS CloudWatch at runtime depending on what we desire to track or by using a simple console log that respects the embedded metric format specification.

Technical Considerations

The project started as a basic NodeJS service in which was added more features to facilitate cloud migrations, it includes the following present in the README file:

  • TypeScript is being used for development within the src folder, later compiled to vanilla JavaScript (into bin folder) prior to deployments.
  • Running it locally or within a custom server is done as usual with npm start.
  • ClaudiaJS, the tool at the center of the deployments procedures makes it easy to perform new Lambda deployments of the App/service with a single command for each of our environment: npm run deploy:dev or npm run deploy:prod, prior to that we should create it for the first time by using npm run create.
  • During each deployment, a copy of the App is saved in ni-deployments bucket defined as “use-s3-bucket” parameter, this could be used for compliance and other verification purposes.
  • If deployed as a Lambda function, some warmers should be put in place to keep our functions hot, we do so by running the script npm run job:warm:env (where env is either dev or prod in our context) by updating the warm.json file to have multiple distinct warmers (we felt covered with 5). Another solution is to take advantage of Provisioned Concurrency which looks more accurate but expensive.
  • Similar to the precedent point, enabling the function to subscribe to some specific SNS topics is possible first by defining the targeted topics in the policy.json file, it should match with the variable Constant.Events in used by Redis still for pub/sub pattern and then running npm run sns:env.
  • At the deployment stage, the environment variables are fetched from a specific bucket accessible via the script npm run get-vars:env and saved in env.json which will then be used to deploy the function. The command npm run set-vars:env does the inverse, takes env.json and push it to configuration bucket that you can change in the package.json file.
  • Express-gateway is used in local development and serves as API Gateway to the other services while the production deployment is ran on top of AWS API Gateway. Meaning the developers have to start their services by using the suggested template that we built, then updating their gateway configurations for the API endpoint redirections to work as expected.
  • The CI/CD pipeline is implemented using Gitlab CI, the corresponding configuration file is presents in the root folder.

By following the server deployment approach while scaling the services horizontally using ASG, we should make sure that there’s no duplicates of Redis pub/sub subscriptions' handlers by adding custom logics or simply covering that need differently.

To cover more technical aspects, the CI/CD pipelines and some sample NodeJS microservices coming from this initial architecture breakdown will be built and documented for more clarity over its usages.

Also we strongly recommend AWS Skill Builder as a go to resources in order to improve your AWS cloud skills, it's well-structured and practical.

———————

We have just started our journey to build a network of professionals to grow even more our free knowledge-sharing community that’ll give you a chance to learn interesting things about topics like cloud computing, software development, and software architectures while keeping the door open to more opportunities.

Does this speak to you? If YES, feel free to Join our Discord Server to stay in touch with the community and be part of independently organized events.

———————

Conclusion

In this article, we highlighted the decisions put in place to migrate a big NodeJS App in multiple small components gradually migrated to AWS public Cloud but let’s take into consideration that there is not a “one decision fits all needs approach” in regards to Cloud migrations, your infrastructure could be different, your actual architecture/components could be special so you may need a dedicated plan to move the right way.

The project source that now serves as a base for us is hosted on GitHub in case you are interested in this NodeJS migration implementation.

Thanks for reading this article, recommend and share if you enjoyed it. Follow us on Facebook, Twitter, LinkedIn for more content.