Deploying Microservices using Now v2

How It Went From 1 Monolith to 6 Microservices & 4 Different Languages

  • Published at 
  • 5 mins reading time

When Now v2 was released, I didn't immediately jump ship and refactor my blog to leverage the new infra. Sure, the monorepo integration looks cool and easy, but it requires an entirely different mindset. Even now, there still things that I don't like, but I've grown accustomed to it.

Last April, I finally decided to do the big refactor. 5 months after the official announcement.

Originally my blog is a single express monolith (I know, calling it monolith seems a bit of a stretch). This service handles the traffic and forwards them to its respective handler. For my blog frontend, I use next.js. For API, RSS, and sitemap, it's an express middleware. It's like typical next.js with a custom server.

Based on this setup, I was thinking of refactoring this into 4 different services.

Repository Setup

Considering that all services will be written in javascript inside single repo, this is a perfect time to use yarn workspace. There are few shared modules across those services, and yarn workspace is exactly built for this purpose.

I ended up with 5 top-level directories: api, rss, shared, sitemap, and www. Each of them is an isolated package with their own package.json. It may not be the perfect separation, but it worked well enough for me. Even though there are 5 packages, only 4 of them are deployable. shared is only used to store reusable modules across different packages.

To deploy these services, Now introduced a concept of builders to help with deployment. It's a set of instructions that will transform your entry point into lambda function or static assets depending on what the builders do.

It took me a while to learn how to use the appropriate builders (especially @now/node vs @now/node-server). Eventually, I settled on using @now/node and @now/next.

API server

The first thing to do was refactoring the API server. It's the backbone of all services and it's vital to get this up and running first. Fortunately, because I used apollo-server-express, the refactoring should be as easy as moving the modules to its own package and use apollo-server-micro instead.

Except it wasn't.

The way the API works is I have markdown files, that will be parsed at start time to generate post data to be served through a GraphQL schema. This means that the markdown files need to be deployed alongside the API code.

After searching through the builder repository, I finally found config.includeFiles option that I can pass to the builder to include those markdown files. I needed to use @now/node@canary because this was a new feature (even I couldn't find it in the docs).

Crisis averted.

RSS and Sitemap

This was a relatively easy one because micro has a similar signature with an express middleware.

module.exports = async (req, res) => {
const rss = await generateRss(apiURL);
res.setHeader('Content-Type', 'application/rss+xml');
res.end(rss);
};

The only problem was this service need to call API server, which means it needs to know which host to talk to (because Now creates a different hostname for each deployment). Thankfully, we can use a proper HTTP header to know which hostname this request belongs to by using host header.

module.exports = async (req, res) => {
const apiURL = 'https://' + req.headers.host + '/api';
};

Blog Frontend (www)

This was another easy refactor, especially after Next released serverless target option. Yes, we still need to use the same method as before to determine API url, but we've done that.

The complications arose when I started to write the config for routes. As you might see from this post URL, the slug for every post is appended directly to the domain name. So /example rather than /post/example.

This means that I need to catch all slug route to be handled by www/post lambda, but not all paths so that static assets still served correctly by next. After reading a few examples in zeit/now-examples repository I finally found the correct route config for this scenario:

{
"routes": [
{
"src": "/(?<slug>[^/]+)$",
"dest": "/www/post?slug=$slug"
},
{
"src": "/(.*)",
"dest": "/www/$1"
}
]
}

This means any route that doesn't contain more / at the end will be handled by www/post lambda, and the rest will be handled by next static assets.

Development Environment

At the time of refactoring, now already included now dev command in CLI which means I got to play a little with it. Unfortunately, it was so unstable and buggy I decided to create a temporary hack to create my own dev environment. I'm glad that I use GitHub-Now integration, and it helps a lot to test around with the actual deployment.

Now, now dev is pretty stable and useful in most scenarios that you can rely on it to test your microservices locally. There're still some issues that you might run into, so keep an eye on the builders' repository for any updates.

Don't be afraid to use the canary version as it's the latest version that automatically published for every change in the repo.

Improvements and Future Plans

After the first commit in April, I've done a bunch of changes and improvements to the initial setup.

I've rewritten RSS and Sitemap handler to Golang using @now/go builder, converted the API server to typescript now that @now/node builder supports typescript with no additional config, added @now/static builder to deal with robots.txt, favicon and any other static assets, and finally experimented using @now/rust to handle 3rd party embed. I might write some of this in a future blog post.

The great thing about these changes was the change to support the new platform is pretty minimal. Change the builder and point them to your new entry point and you're done. You rarely need to change route definition. I think that's pretty cool.

In the future, I'd like to try deploying ReasonML app but it seems there's an issue with build time & size and I haven't had a chance to experiment with it.

Conclusion

Generally, I was impressed with the direction that Zeit is heading. I agree that microservices using lambda is the way to go in the future. Using now v2 means you can use monorepo with various programming languages without worrying about slow build step because deployment happens in parallel. With GitHub integration, you get a staging service that mimics production environment for free without additional setup. That's big.

Still, I can't help but feel like using builders is kind of lock-in.

Using a builder instead of docker also means your knowledge of custom build step is useless. You're limited to what the builder currently offers. If you want to extend or create a new builder, you have to learn an entirely different stack. Compared to docker, I'd say it's not better.

I wish now v2 was released when more builders were ready and now dev already reliable enough. But now that it's closer to ideal, there's no reason for you not to try it.

Happy building!

Categorized under

Webmentions

If you think this article is helpful
© 2023 Fatih Kalifa. All rights reserved.