Strangler pattern is a way of migrating from legacy applications to new ones with both being operational at the same time and the upgrade happening behind the scenes gradually.
The picture below shows the crux of this pattern:
The idea is that you build and deploy the new system side-by-side to the existing legacy system and route traffic between them accordingly. Gradually, the new system will grow while the legacy system will shrink as functionality gets re-written in the new system and eventually, the legacy system will go away and the new one will take over. All through this process, users have no idea that the entire system has been replaced while still remaining operational.
Ever since reading about this pattern I had been thinking of trying it out in some way but wasn’t sure how so I decided to use IIS and the URL Rewrite module to do simple path based URL re-writing to distribute traffic between the legacy and new application, effectively turning IIS into a reverse proxy (IIS already works as a reverse proxy to Kestrel for ASP.NET Core apps). To simulate the applications I created a dummy ASP.NET WebForms project which will play the legacy application role and a new ASP.NET Core 2.0 MVC project which will play the new application role, in Visual Studio 2017.
Disclaimer: this may not be, infact, I am certain it’s not, the most efficient or the best way to implement strangler pattern but I think it demonstrates the idea at its very core and I am certainly open to ideas to make this design scale better because this is still an ongoing study for me. 🙂 I am also assuming the readers are fairly comfortable with IIS, URL Rewrite module and Regular Expressions.
I added a little footer in both apps to distinguish them:
To simulate migration, I decided to serve the Contact page from the new app and the rest from the legacy app. This means all the other links in the legacy app will use legacy resources but the Contact link will be re-routed to the new app and the response will be rendered as though it was being served by the legacy app. I made sure all the navigation layout and naming is consistent across both apps which is crucial for the stealth of the migration.
Enter URL Rewrite Module.
URL rewriting rules can either be set up at the server level where they will apply to all the requests coming in regardless of the application or they can be set up at the application level where they will only apply to that application in question. Because I am doing this in 2018, it seems the ASP.NET WebForms project template has undergone a bit of transformation and it now includes an ASP.NET routing module called “FriendlyUrls” which converts URLs like:
It registers these application level routes at app start in Global.asax.cs:
You can see the RouteConfig file enables these friendly urls:
So now if you set up a URL rewriting rule like this on the application level (you can either set these in the web.config of the legacy or via IIS Manager application):
It would be ignored as ASP.NET routing seems to take precedence and navigates to the route registered during Application_Start above (this is my theory at this point perhaps someone has a clearer idea?).
The URL rewriting module is handler agnostic and rewrites the URL based on configuration and kicks in early on in the pipeline. The ASP.NET routing however, kicks in when the application handler (instance of HttpApplication) has been selected to handle the request, it registers the routes in the Application_Start event and proceeds on to make requests to the URL. You can check out this Microsoft link for more information on this.
One way to solve this issue would have been to simply disable friendly url settings for urls that I wanted to rewrite (by commenting the code out and manually re-jigging all links) but that would’ve required a bit of work in the app specifically for migration reasons and I wanted to avoid that as far as possible. I wanted to make strangulation changes on the outside relying more on a rules based proxying rather than code level changes.
Therefore a more approach I settled for was to put the inbound rules on the server level in the applicationHost.config file (found in C:\Windows\System32\inetsrv\config). Something of note here, since this rule is set up at server level, it gets activated for every single request to the server even to other apps on the server. To prevent this, I added a SERVER_PORT condition to make sure it redirects requests only when made by the legacy app i.e. the one on port 88.
This way the URL rewriter was effectively able to by-pass the ASP.NET application routing for the strangled URLs, in this case “/Contact” and I didn’t need to modify anything else in the application for this. Other urls in the legacy app still continued to work “as is”. Having done this, I navigated to http://localhost:88/Contact and Voila! it navigated to the new Contact page hosted in the new app (you can tell that by the footer):
More importantly, the URL in the browser address bar didn’t change (stealth FTW!):
This was only half the work as now the redirection worked but any links inside that page were still relative to the new app and navigating to those links was causing a 404.
To fix this, I set-up outbound rules that dynamically changed the URLs in the HTML response to point relative to the legacy app. These outbound rules, in contrast to inbound rules, were set up at the application level in the legacy app’s web.config:
This changed the HTML response from the new app on the fly and updated the anchor tags according to the rule above:
I also updated all the custom CSS/JS content links and the form action attribute in the response HTML by writing 2 more outbound rules:
Any third party CSS/JS content (like Bootstrap, jQuery etc) should ideally be served via a CDN rather than from the app root this will keep the number of re-write operations required low. The rules above only rewrote custom CSS/JS content links this made sure that the new app page rendered with its own CSS/JS without disturbing any in the legacy app which were not changed.
The FormActionFixer rule changed the form action attribute from:
but this form post still didn’t work because there was no URL called http://localhost:88/Submit in the legacy app. To fix this, I just re-directed this url to http://localhost:8021/Home/Submit using another server level inbound rule:
The form post now posted correctly but since I was redirecting back to the original Contact action in my HomeController:
the response contained the Location header pointing to “/Home/Contact” which was once again relative to the new app on port 8021 and navigating to it resulted in a 404 error:
To fix this, I added one last outbound rule to my legacy app config. This time instead of matching the URL path, I needed to match the value of the RESPONSE_LOCATION server variable, look for “/Home/Contact” and rewrite the header value to point to “/Contact” so that the pre-existing inbound rule can take care of routing the Contact page. Of course the pre-condition to this rule was that only re-direct response types needed to be handled i.e. the value of RESPONSE_STATUS server variable should be either 301/302/307:
Now when I retried submitting the form post, the redirection back to Contact page finally worked with the comment I posted being displayed back on the page:
The final inbound rules list looked like this (in applicationHost.config):
The final outbound rules list looked like this (in web.config of the legacy app):
There is some stuff that I couldn’t get round to :
a) How would I flow the authentication cookies from the legacy app to the new app without having to authenticate twice? Security is an important consideration in this kind of architecture.
b) The finer details like re-writing URLs for AJAX calls and caching of responses at the proxy level etc.
b) How would I do rate-limiting? This is kind of venturing into application gateway territory that is usually put in front of back end services in a micro-services architecture but could still be useful here.
c) How would this particular approach scale? The approach to accessing back end services via a reverse proxy is going to be a bit slower due to extra hops and URL and content rewriting. I did some preliminary load testing (on localhost) with Apache Bench (both the applications were only running in single worker processes with all other settings in IIS at their defaults):
At concurrency level of 300 (averaged over several runs):
Accessing the new app directly with the same test parameters, I got:
A difference of around 400 RPS which on localhost doesn’t mean much, plus the result is only static text but the question is would we rather have the users experience a slightly degraded app performance while the migration happens but still be able to provide value out of both the systems or we maintain both applications but the new app doesn’t get deployed and used until ready which means a slower feedback loop and chances of getting it wrong. Slow but likely right or fast but potentially wrong? Perhaps there is a happy medium here that could be achieved if this design was tweaked a bit.
d) I also need to figure out how this architecture would work in cloud environment. Perhaps an exercise for the next post.