Containerized Microservices with Docker & NodeJS

- By David Grimes on May 06, 2015

 

Your application has been chugging along nicely when suddenly it grinds to a halt, again! After debugging thousands of lines of code you finally found the one tiny piece that caused it, but you have to reinitialize your whole service to fix it. How can you avoid this painful process every time your application inevitably breaks? By smashing it into pieces, and then letting each piece do its own thing! Thus your monolithic application becomes a series of interchangeable and easily manageable microservices.

MicroService

Shippable is evolving constantly to help developers ship code faster by building the best continuous integration and delivery platform. The newest improvement to our infrastructure is the introduction of microservices. Our API not only handled http calls, it processed the tasks itself, so we're moving the processing of tasks into a microservices. Microservices are self-managed, self-contained units that monitor dependencies, listen to changes, and complete tasks delegated to it. In this way we can take out the parts and put in new ones without disturbing other services.

Let's take a look on how we implemented microservices with Docker. Each microservice is a Nodejs express app that runs in its own Docker container. Inside, there is a Server class and a MicroService class. The Server sets up routes that can be used to talk to the service, such as polling for its health or updating variables. The MicroService instance runs through a dependency check, such as confirming environment vars are present and it has access to external resources it depends upon like APIs. After the check successfully completes it will initiate the service itself and you can add listeners or tasks for it to complete when triggered. If any errors occur that can't be ignored such as a bad request, it will stop and throw the error to the Microservice and perform a health check to see what is wrong. If there is anything wrong, it will perform a backoff and retry until the problem is fixed.

The Server and MicroService are separate so that routes can be accessed that won't throw MicroService errors, such as a health check or changing environment variables. Each has a different flow so each can perform different tasks without interfering with each other. The Server acts like its own API, while the MicroService initiates tasks and tracks dependencies.

Microservices Architecture Workflow:

1. Container is started.
2. An express app is started.
3a. An instance of Server is started.
3b. Server creates an http server, listens to a port, handles server errors, and sets up routes.
4a. An instance of MicroService is started.
4b. It performs a health check to confirm dependencies are met or are available, if they fail, retry after some time.
4c. Once checks are passed, it initiates the service and adds any listeners if needed.
4d. Tasks complete (if possible) and throw error to MicroService if there is an unrecoverable error to perform health check.
HTTP Request: Server catches HTTP requests and routes it to tasks if needed and sends a response.
Trigger listener: Listeners perform tasks when triggered.

Flow representation

Switching to microservices allows a new level of flexibility when it comes to software development. Each piece operates independently allowing them to be changed without making a mess or bringing the whole system down. All the data is sent through one microservice that acts as a manager which will route the data to other microservices. When those microservices are done, they can push it back to the microservice manager and the cycle continues until the flow is complete. It also allows scalability, so if one process takes a lot of a time you can run more of that microservice allowing multiples of the same task to be processed asynchronously instead of waiting for the resource to become free again.

The best way to get started is take a part of your app and route it into a microservice. Need to build a file, fetch data from multiple sources, or make complex calculations? Send the needed data to microservices and have them complete the tasks. You can set up a microservice to act as your API with server routes, another can delegate where tasks and data need to be sent, and still more for each task that needs completion.

Eventually, every task will have its own microservice that will become self-sufficient and interchangeable allowing swift repairs, rapid adaptability, and maximum flexibility. You'll be able to start multiple services to handle high traffic or usage based on the difficulty or popularity of the task. This will allow the API to be lean and fast for accessing other elements, and could become a microservice in itself.

Docker: https://docs.docker.com/
Nodejs: https://nodejs.org/api/
Express: http://expressjs.com/api.html
Sample Tutorial: http://docs.shippable.com/tutorials/pipelines/samplePipeline/


Topics: Docker, development process, containers, how-to, nodejs