Guide to Microservices Versioning & Architecture
Problem
The architecture of Microservice deployment is in a perpetual state of flux due to the complex web of interdependencies between independent components. Therefore to effectively manage a microservice architecture we need tools that can track the operating state of multiple interdependent components.
With such interdependence it makes no sense to deploy them on production separately - if we deploy only one microservice that is dependent on another - the application will not work as expected. Another critical feature to consider is the ability to deal with unexpected problems following a new feature deployment. In this scenario, we need the ability to roll back versions of individual microservices including all their interdependent features from multiple components.
Existing approaches
There are 3 approaches for managing versions in a microservice architecture: independent deployment for each component, distributed monolith, and combined approach. We will consider an abstract microservice platform with 3 services (service a, service b, service c) and 3 environments (DEV, STAGE, PROD).
Three ways to manage versions in a microservice architecture:
-
Independent deployment for each component;
-
distributed monolith;
-
combined approach.
We will consider an abstract microservice platform with 3 services (service a, service b, service c) and 3 environments (DEV, STAGE, PROD).
Independent deployment
In this case, we manage versioning only for components and each component has its own deployment lifecycle without knowing about other components' states. Each time we create a version of a component, this version automatically goes to the DEV environment, and then if it passes tests/checks, we can deploy it to further environments independently. This approach makes sense if these components are not tightly coupled to each other.
A good example of where it makes sense to use the independent deployment approach is in the AWS platform where components such as EC2, S3, and IAM can be deployed independently and don’t affect each other. Further, the independent deployment approach is a good choice when there are dedicated teams supporting each service separately.
Distributed monolith
In this approach, we manage versioning not only for components but for a platform as well. You can consider the platform as a snapshot of the components’ versions list. For example, we have 3 components: service-a, service-b, and service-c. The Platform will be a combination of these components’ versions, e.g. service-a - 0.1.1 + service-b - 0.0.5 + service-c - 2.0.3. And each combination will have its own version.
In this case, after each component version creation, we automatically deploy it to the DEV environment and besides that, we need to create a new version for the platform that is currently in DEV. If we need to deploy components to the next environment (in our case STAGE), we deploy not a separate component version, but a specific platform version, i.e. combination of components’ versions.
We take a version of the DEV platform and deploy it to the STAGE environment. The same deployment process is used for PROD and other environments. The general rule is we deploy platform versions instead of individual component versions. This approach may work slower than independent deployment, but it’s very useful if your services are tightly coupled with each other and you care about compatibility between components.
Combined approach
In the third approach, we combine the first and the second approaches. The idea behind this approach is that you can deploy your microservices in an independent way to each environment except PROD. But the synchronization between PROD and pre-production environment (in our case it’s STAGE) should be done through whole platform deployment, which is the same as in distributed monolith.
Platform versioning should be applied only for pre-production and PROD environments. This approach is very flexible, but harder to implement compared to the 2 previous approaches. It is a good option if you have a lot of non-production environments like DEV, QA, UAT, DEMO, etc and you have tight coupling. In this case, your services quickly go to the pre-production environment, and with distributed monolithic deployment, you can mitigate the problems with components coupling.
Suggested approach
Let's say we chose a distributed monolith deployment approach for our product. In the current section, we will describe some details relating to its implementation.
For example, let’s imagine we’re building a logistics application that can track vehicles and their mileage, fuel level, cargo status, geofencing, and has the built-in authorization structure with access for both managers, drivers, and company clients. The application is composed from several levels, which implement user UI, track deliveries, and create data visualizations for every level of user access.
Git strategy
We are using a trunk-based development strategy for our components. It means that we don’t have one branch for each environment as in git flow strategy. Instead, we have one mainline branch (trunk) in which we merge all our features. And each commit in that branch will produce a new version for the component and will be automatically deployed to our development environment.
Semantic versioning
For versioning, we are using the SemVer approach. In this case, we automatically can increment the patch number after we have committed in the mainline branch. In case of release, we can increment minor/major numbers and continue working on them with the same patch-increment model.
State of components
To version the whole platform we are creating a tag on the repo that contains a list of components and their versions after each component’s commit. In our case, we are using Helm to package all our components. Because we used Helm, we experienced 2 approaches to having a list of components.
The first approach is the umbrella chart. In this case, we have a platform as an umbrella chart, and each microservice is a sub-chart of this umbrella chart. We can store it in an external repository (for example, in a chartmuseum) and deploy it as one helm chart to the environment.
The problem is that we need to build not only a component chart, but also an umbrella chart after each commit to the component repo. To mitigate this problem, we decided to migrate our umbrella chart to the helmfile tool. In this case, we can just tag the git repository, which contains a file in which we store a combination of component versions with the new tag, and use the specific tag of the file with the combination in the future.
Release
The release job creates a release branch with major and minor numbers of the version in the branch name (e.g., 1.2-release) for each component, as well as for the platform as a whole. To continue the development of the next version, we bump each component’s and the last platform’s major/minor version and continue working with components in a patch-incrementing manner. To do some fixes in previous releases, we can commit to corresponding release branches and increment patch versions of the components and platform.
Summary
In this article, we discussed the necessity of versioning, discussed 3 approaches: independent deployment, distributed monolith, and combined, and considered their use-cases, pros, and cons. Also, we discussed details about how we implemented a distributed monolith approach in our product.
While Microservice architecture has the same functional scope and option to connect either to a single or to separate databases, it has a decreased scaling bottleneck possibility compared to the monolithic architecture, and is ideal for big enterprise apps as well as for startup applications that are yet to scale up. Connect with our experts if you want to develop and deploy an application with specific problem solutions or requirements.