Real-world application refactoring using Sidecars and StranglersMichael Seifert, 2020-08-21, Updated 2020-10-07
It's easy to break up a monolithic application on the whiteboard. That's until you realize that your monolith lives in a complex landscape that limits the decisions you can make. This article presents the modularization of a monolithic application that faced these problems. Specifically, it shows how the Sidecar and Strangler patterns can be leveraged to solve complex authentication problems.
A monolith and its pain points
The initial architecture of our system consisted of a monolithic application that both provided various API endpoints and served a frontend. The frontend consisted of some server-side rendered HTML pages and a bundled single-page application (SPA). As much as I like monoliths, the team really started to feel the pain of this one: For one, the build pipeline was running for a long time. A deployment took around 12 to 15 minutes given everything was successful. Since there was no local development environment that could be used for testing, a developer had to run the pipeline multiple times a day. That's a lot of time spent waiting and switching contexts!
Over the course of the project, we identified three different domains of the application. The different domains had different availability requirements. The frontend had to be available virtually all the time and down times had to be announced in advance. This affected the team's ability to deploy code for other domains as well, because every deployment had to go through the process of announcing a downtime. This was the case, even if the changes did not affect the frontend which was only associated with one of domains.
Target architecture and constraints
To address the issues with the different domains and long running build pipelines, the target architecture was supposed to be more modular with the ability to deploy each domain separately.
Factoring out domain B as a whole was not feasible time and budget wise. However, both management and the development team were convinced that factoring out just the single-page application was a benefit on its own. Extracting a single-page application to a separate webserver should be easy, right? That's until reality hits you… The SPA was served by the web server of the monolithic backend and the user was already logged in when accessing the SPA. API calls from the SPA to the backend APIs relied on the existence of a session cookie. Factoring out the SPA would either mean that users would have to log in twice—once for the SPA and once for the non-SPA frontend—or that we had to implement single sign-on (SSO) across both services. Users should not have to log in twice within the same application, so single sign-on was the way to go.
In fact, the SPA already implemented OAuth2 functionality in order to authenticate against other services in the system. However, the backend APIs were only accessible via basic authentication or SAML. Adding OAuth2 support required heavy customization of a closed-source third-party application. Any implementation would have added a huge amount of complexity and had a high risk of breakage, if it worked in the first place. A rewrite of the APIs used by the SPA would have increased the migration risk considerably and exceeded time and budget requirements, as mentioned previously.
Transition architecture and migration
In the end, we came up with the following solution:
I introduced an nginx reverse proxy that intercepts API calls made by the SPA and injects the credentials of a technical user. These calls are then forwarded to the monolith's API endpoints which successfully authenticated those requests. This worked like a charm, but it left all APIs accessible by anyone due to a lack of authentication.
Up to this point, the authentication issue was treated as an implementation problem, i.e. OAuth2 support cannot be added to the APIs. However, it can also be treated as an operational problem: Louketo proxy, formerly known as Keycloak Gatekeeper, is a minimal Go application that supports OAuth2 authorization flows. If configured correctly, unauthenticated requests are redirected to the login page of the identity provider. Authorized requests are forwarded to the nginx proxy and unauthorized requests are denied. By adding Louketo proxy as Sidecar in front of the request chain, we added OAuth2 support to the nginx reverse proxy and, by extension, to the monolith's APIs. All that was left was to introduce an on/off switchable redirect in the monolith to the new, factored-out SPA and direct the SPA API calls to the authorization proxy.
This solution posed a way to factor out the SPA within the time and budget constraints while minimizing the risk of transition. Furthermore, the solution opened up a way to gradually improve the existing design towards the desired target architecture. The nginx reverse proxy serves a strangler façade for the APIs of the monolith and the domain-specific APIs. This allows API endpoints to be moved individually from the monolith to the domain service by adjusting the routing of the reverse proxy.