When I first started writing healthcheck’s for Node.js, they were pretty naive. They also followed the commonly suggested wisdom of the internet. Just install curl and hit your API!

Great!

But, turns out it’s not so great.

For one, many of my services do not have API endpoints over HTTP. Most of my internal services use AMQP. I ended up adding on lightweight APIs and installing curl in my production containers just for healthchecks!

Adding more complexity to a service is a never a great solution.

Additionally, just because curl can hit your API, doesn’t mean it’s working correctly.

Maybe your API is responding but your service can’t connect to your database, or queue, or whatever other dependency it needs to be fully operational.

I really wanted to be able to TRUST my healthcheck. If it says healthy, I want to know that the ability to connect to it’s dependencies was part of that state’s computation.

In celebration of Node.js 10’s LTS release, I’ll show you how to create a custom healthcheck for your service, and it’s dependencies, by writing a simple ES6 module with .mjs!

Here we go!

Let’s imagine a service.

We still want our healthcheck to be relatively lightweight, and for my purposes, just checking that all of the dependencies are working and that I’m able to connect with them would be loads better than a curl to a endpoint that just says ‘ok’.

In my example, I’m using two libraries servicebus and sourced . These libraries are dependent on rabbitmq , mongodb , and redis .

If our service is unable to connect to any of these three, it is definitely not healthy.

Seems how these libraries can be passed a configuration with their connection options, and that’s how our application is using them internally, no need to do anything extra fancy or reinvent the wheel.

The healthcheck

Here’s the healthcheck example. I store it in ./bin/healthcheck.mjs .

#!/bin/sh

':' //# https://medium.com/@patrickleet ; exec /usr/bin/env node --experimental-modules "$0" "$@"



import servicebus from 'servicebus-bus-common'

import { config } from '../config.mjs'

import mongoClient from 'sourced-repo-mongo/mongo'



export const exit = ({ healthy = true } = {}) => {

return healthy ? process.exit(0) : process.exit(1)

}



export const check = () => {

return Promise.all([

mongoClient.connect(config.sourced.mongo.url),

servicebus.makeBus(config.servicebus)

])

}



export const handleSuccessfulConnection = (healthcheck) => {

return () => {

healthcheck({ healthy: true })

}

}



export const handleUnsuccessfulConnection = (healthcheck) => {

return (e) => {

healthcheck({ healthy: false })

}

}



check()

.then(handleSuccessfulConnection(exit))

.catch(handleUnsuccessfulConnection(exit))

And that’s it!

Let’s break it down, and then explore how we can use it in production.

#!/bin/sh

':' //# https://medium.com/@patrickleet ; exec /usr/bin/env node --experimental-modules "$0" "$@"

This first line executes /bin/sh to call node with the experimental modules flag.

This way, we can just run the file without trying to figure out how to modify our calling code to know or expect to need to use the --experimental-modules flag.

Next, we have our imports, and then define the exit function.

This is so we can call exit and say what we mean, instead of trying to remember whether we want exit code 0 or 1.

export const exit = ({ healthy = true } = {}) => {

return healthy ? process.exit(0) : process.exit(1)

}

Our actual healthcheck function then just returns an array of promises that need to be resolved for the service to be considered healthy:

export const check = () => {

return Promise.all([

mongoClient.connect(config.sourced.mongo.url),

servicebus.makeBus(config.servicebus)

])

}

Notice that I’m able to reuse the same config that my application uses.

Lastly, we call check, and provide a success and a failure function.

export const handleSuccessfulConnection = (healthcheck) => {

return () => {

healthcheck({ healthy: true })

}

}



export const handleUnsuccessfulConnection = (healthcheck) => {

return (e) => {

healthcheck({ healthy: false })

}

}



check()

.then(handleSuccessfulConnection(exit))

.catch(handleUnsuccessfulConnection(exit))

Using the healthcheck

Now, to use the healthcheck easily, we are going to do two things.

Set up the bin section of package.json to expose our healthcheck command. npm link to be able to call the healthcheck

package.json

{

"bin": {

"healthcheck": "./bin/healthcheck.mjs"

},

// rest of package.json

}

Now when we npm link our project, the commands defined in bin will be available via our CLI.

This doesn’t make a ton of sense to do in our local machine, but it does inside of our container.

Here’s an example multi-stage Dockerfile which does so:

FROM node:10-alpine as build



# install gyp tools

RUN apk add --update --no-cache \

python \

make \

g++



ADD . /src

WORKDIR /src

RUN npm ci

RUN npm run lint

RUN npm run test

RUN npm prune --production FROM node:10-alpine



ENV PORT=3010

EXPOSE 3010



COPY --from=build /src/package.json package.json

COPY --from=build /src/package-lock.json package-lock.json

COPY --from=build /src/node_modules node_modules

COPY --from=build /src/bin bin

COPY --from=build /src/handlers handlers

COPY --from=build /src/lib lib

COPY --from=build /src/config.mjs config.mjs



RUN npm link

HEALTHCHECK CMD healthcheck



CMD node --experimental-modules ./bin/start.mjs

Notice the command RUN npm link in the second stage (second FROM defines second stage).

RUN npm link

HEALTHCHECK CMD healthcheck

This makes the command healthcheck in that container, execute the healthcheck we’ve defined.

This can also be used by Kubernetes livenessProbes and readinessProbes!

Check out the probes above, and then check out the /bin folder for the healthcheck example!

Happy Hacking!

Interested in hearing my DevOps story? Read it on HackerNoon now!