Welcome to our five-part series about microservices and a legolized software development. We’d like to share our lessons learned about architecture, development environment and security considerations with you. We will also explain some issues we stumbled over and what solutions we chose to solve them.
I) In the first part, we present an example microservice structure, with multiple services, a foreign API interface and a reverse proxy that also allows load balancing.
II) Part two will take a closer look on how caching improves the heavy and frequent communication within our setup. [read]
III) Security is a topic that always occurs with microservices. We’ll present our solution for managing both, authentication and authorization at one single point. [read]
IV) An automated development environment will save you. We explain how we set up Jenkins, Docker and Git to work seamlessly together. [read]
V) We finish with a concluding review about the use of microservices in small projects and give an overview about our top stumbling blocks. [read]
Let’s start with an example generic microservices architecture, containing a user website and a monitoring page for developers. The authentication and authorization is controlled by the Auth service. Furthermore, we have the four services A-D and one for monitoring. The red External Service could be a foreign API, which is connected by the External Service Mapper. The connection lines are purple, because all requests will be send over the Reverse Proxy, e.g. a Nginx server, which maps IPs and ports to static URLs.
The questions we want to answer in this section:
- How do the services communicate with each other?
- How do we integrate external services into our system?
- Can we protect our services from unauthorized requests?
- How to handle all the monitoring?
Reverse Proxy and Load Balancer
By letting the services communicate via the reverse proxy we gain helpful advantages.
- All information about servers, IP addresses and ports are only held by the reverse proxy. We only need to store static URLs in each service to their dependents.
- The services can easily be spread over various servers, because they aren’t referring to the localhost or a fixed IP address.
- The reverse proxy can also load balance the traffic, if multiple instances are running (e.g. service A v1).
- It’s easy to run different versions of a service in parallel (e.g. service A v1 and v2). In some scenarios, it might be useful to initially route only a part of the traffic to the newer version, before migrating to it completely.
Now we have a scalable and easy controllable setup for the routing. Managing all IP addresses and ports within each service would rapidly create a mess of configuration and handling flexible or automated changes would seem to be nearly impossible. However, the downside of this configuration is that the reverse proxy is the single point of failure in the system. We think this is a fair trade off, because the Nginx server doesn’t contain a lot of application logic and at some point, you need an entry point for the URL of your services. It would take a lot more time to update reconfiguration in the DNS service.
Down the road, we plan to evaluate if we gain performance by adding the URLs to the hosts file at least while the reverse proxy is running on the same server as some services.
It’s always bad to depend on sources you can’t control, but in many cases, you won’t have a choice. The key goal is to build a reliable service even if foreign systems fail. We tried to build a setup that is as flexible and reliable as possible. If a service needs such data it connects an own service (service D), which is connected to the external interface, with another service (External Service Mapper).
There are two options to provide data, depending on the type:
- If you can store the data in your own database, do it. Collect the data from the foreign API (push or pull) within the best interval (e.g. store weather data every hour). This guarantees availability, even if the external service is offline.
- If you can’t store the data due to the size or complexity (e.g. Google Maps), you’ll have to live with it. Build the best possible fallback.
Why do we need an additional service (External Service Mapper) and don’t implement its logic into our service D?
This modularization helps us if we add a second foreign interface (e.g. OpenStreetMap) or the external API provides an updated version. So we will not have to change anything in our internal connected service (Service D) if anything outside of our system changes.
Within the microservice setup, connections between all services are possible. Due to security in our example setup, every service will always be connected to the auth service. Every request, no matter if it was send by a user outside of the system or an interservice connection, will be checked by the requested service asking the Auth service. We will take a deeper look in the authentication and authorization process in our third part of the blog series. Just keep in mind that these are necessary connections we implement in every service. Even if we hide some services from the public, before processing a request we make sure that it’s valid. We don’t build a wall and once someone passed it, he would get access to the whole system. As a side effect, we will be able to make a service public anytime in the future, without having to implement any additional security functions.
If you have lots of services, monitoring their activities is very important. Similar to the auth service, every service has implemented a connection to the monitoring service, to push or get data pulled. There are two different kinds of data to monitor:
- Heartbeat: To monitor the running services, their system resources and availability, we need a heartbeat that is send or requested in a specified interval. The monitoring service can then provide an overview for the operator/developer, about the whole system.
- Logs: We need to collect the logs of all services, to process them and gather additional information. The services should at least log all incoming requests and outgoing responses with a timestamp. More detailed logging, e.g. inside the application logic, could be stored in a service specific log file.
In the next blog post we will take a closer look on how caching improves the heavy and frequent communication within our setup.
Continue with Part II – Caching
Kost, Christof [firstname.lastname@example.org]
Kuhn, Korbinian [email@example.com]
Schelling, Marc [firstname.lastname@example.org]
Mauser, Steffen [email@example.com]
Varatharajah, Calieston [firstname.lastname@example.org]