Microservice architectures are quite popular nowadays.
A typical application may consist of 10, 50, or even more than 100 microservices. Although it is relatively easy to run five microservices locally on your laptop, it’s not as easy to run 20 or 50 of them.
And it’s not because of lack of computing power, but because it is just very tedious to set up. Services tend to use databases, cloud services, some 3rd party APIs, etc.
You need to set all of this up in order to use all of them locally.
Because of that, I often find myself running just one or two services locally and using the rest of them from the staging environment. It is also somewhat annoying to set up because we need to rewire everything manually.
Let's explain the idea better using this diagram:
Here we have a media streaming app example, which consists of a few services, two of which we want to run and debug locally (recommendation and notifications services, marked with red arrows).
To do that, we’d need to change two URLs in the frontend configuration, to point to our local services, and one URL in the notifications service. So three changes in total.
But what if we wanted to run 5 of them locally? What if we needed to work on another feature and forgot to undo some of our changes?
Depending on the communication patterns between those services, it may become very messy soon enough.
So we came up with the idea of the local router.
At Aspecto, we help teams that build distributed services troubleshoot their microservices. And since our daily work with microservices became way easier once we started using the local router, we wanted to share with you how it’s done.
The plan is to route all the local traffic through a reverse-proxy server and rewire everything in its configuration file.
We call it “the local router”.
Here’s another simple diagram to illustrate the idea:
Instead of making changes in the configuration of each service (and most likely restarting it afterward), we just update the router’s config file and it immediately switches the target for us.
No service or router restart is needed.
Here's how you can actually do that.
We are going to use traefik reverse proxy because it supports dynamic configuration reload.
We could also use nginx or HAProxy, however, we decided on traefik because of its simplicity. Also, it is worth mentioning that we’re going to use version 2.2.
There’re two types of configuration in traefik: static and dynamic. The difference is that dynamic configuration can be changed in the runtime, whilst static can’t.
Let’s create a static configuration file and call it static.config.yml:
entryPoints:
web:
address: :80
providers:
file:
filename: /config/dynamic.config.yml
api:
dashboard: true
insecure: true
log:
level: INFO
As you can see, we specify an entrypoint, HTTP port 80, and a path to the dynamic config file. Nothing too complicated.
Let’s now create a dynamic configuration file (dynamic.config.yml):
http:
routers:
recommendation:
rule: Host(`recommendation.some-domain.localhost`)
service: recommendation
auth:
rule: Host(`auth.some-domain.localhost`)
service: auth
email:
rule: Host(`email.some-domain.localhost`)
service: email
notifications:
rule: Host(`notifications.some-domain.localhost`)
service: notifications
dashboard:
rule: >-
Host(`traefik.some-domain.localhost`) && (PathPrefix(`/api`)
||
PathPrefix(`/dashboard`))
service: api@internal
services:
auth:
loadBalancer:
passHostHeader: false
servers:
- url: "https://stg-auth.some-domain.io/"
email:
loadBalancer:
passHostHeader: false
servers:
- url: "https://stg-email.some-domain.io/"
notifications:
loadBalancer:
passHostHeader: false
servers:
- url: "http://localhost:8089/"
recommendation:
loadBalancer:
passHostHeader: false
servers:
- url: "http://localhost:8088/"
The file consists of a single HTTP section that contains definitions of routers and services.
If you’re not familiar with traefik documentation, make sure to check https://doc.traefik.io/traefik/routing/overview/.
The configuration specifies that for each request to localhost port 80, traefik inspects the Host header and, depending on its value, passes the traffic to a corresponding URL.
For example, a request to http://auth.some-domain.localhost:80/ will be proxied to https://stg-auth.some-domain.io/. TLS termination will be handled by traefik automatically (which is also pretty cool).
Because we’re using .localhost as a root domain, it should work even without modifying the
/etc/hosts
file, but in practice, some services may not work. We had such issues with socket.io, for example, so to make it more reliable, let’s also add the following records to /etc/hosts
: 127.0.0.1 recommendations.some-domain.localhost
127.0.0.1 auth.some-domain.localhost
127.0.0.1 notifications.some-domain.localhost
127.0.0.1 email.some-domain.localhost
Also, in our services, we need to update the local config to use some-domain.localhost instead of staging URLs everywhere.
We can run traefik on a host machine, but usually, it’s easier to spin a docker container. We’ll create a dockerfile:
FROM traefik:v2.2
RUN mkdir -p /config
CMD traefik --configFile=/config/static.config.yml
This is how the folder structure should look like to make it work:
router
├─ Dockerfile
└─ config
├─ dynamic.config.yml
└─ static.config.yml
Now, in the router folder run:
docker build -t local-router .
And then
docker run -p 80:80 local-router
Now we can use two local services and two staging services, without changing anything in the configuration of the services themselves.
When we want to switch to the staging version of, let’s say recommendation service, all we need to do is to change a corresponding record in traefik’s dynamic config, specifically the URL.
From this:
recommendation:
loadBalancer:
passHostHeader: false
servers:
- url: "http://localhost:8088/"
to this:
recommendation:
loadBalancer:
passHostHeader: false
servers:
- url: "https://stg-recommendation.some-domain.io"
And save the config file.
From this point, all calls to http://recommendations.some-domain.localhost will be proxied to staging.
It simplifies things a lot because now we have a single file with all the configuration and we don’t need to restart our services.
There are many ways to improve this setup.
For example, we can create a config generator, that will allow us to change the configuration by flipping a boolean value. And then run it along with traefik in docker compose (that’s what we actually did).
Or maybe you want to run your local services with docker instead of host machine. It all depends on your use case. Our hopes are that local router will be useful for some of you, or maybe inspire you to do something even better.
Give this one a go and if you find it helpful, share this blog post with your team.
We are always happy to receive feedback – let us know.
Developed by Aspecto with ❤️
Previously published at https://www.aspecto.io/blog/easy-way-to-route-traffic-between-microservices-during-development/