Scenario
I have a legacy service which has been re-engineered to a more modern version(assumption is that as part of this migration the uri's of the endpoints have not changed). I want to migrate users slowly over from the legacy application over to the modern version.Implementation using Spring Cloud Netflix - Zuul Support
This can be easily implemented using Netflix Zuul support in Spring Cloud project.
Zuul is driven by a set of filters which act on a request before(pre filters), during(route filters) and after(post filters) a request to a backend. Spring Cloud adds it custom set of filters to Zuul and drives the behavior of these filters by configuration that looks like this:
zuul: routes: ratio-route: path: /routes/** strip-prefix: false
This specifies that Zuul will be handling a request to Uri with prefix "/routes" and this prefix will not be stripped from the downstream call. This logic is encoded into a "PreDecorationFilter". My objective is to act on the request AFTER the PreDecorationFilter and specify the backend to be either the legacy version or the modern version. Given this a filter which acts on the request looks like this:
import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; ... @Service public class RatioBasedRoutingZuulFilter extends ZuulFilter { public static final String LEGACY_APP = "legacy"; public static final String MODERN_APP = "modern"; private Random random = new Random(); @Autowired private RatioRoutingProperties ratioRoutingProperties; @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return FilterConstants.PRE_DECORATION_FILTER_ORDER + 1; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return ctx.containsKey(SERVICE_ID_KEY) && ctx.get(SERVICE_ID_KEY).equals("ratio-route"); } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); if (isTargetedToLegacy()) { ctx.put(SERVICE_ID_KEY, LEGACY_APP); } else { ctx.put(SERVICE_ID_KEY, MODERN_APP); } return null; } boolean isTargetedToLegacy() { return random.nextInt(100) < ratioRoutingProperties.getOldPercent(); } }
The filter is set to act after the "PreDecorationFilter" by overriding the filterOrder() method. The routing logic is fairly naive but should work for most cases. The serviceId being set in the Zuul context has a value of "legacy" or "modern" and represents a "named" Ribbon client, a handle using which the details of the backend can be set. So with Spring Cloud, my named clients are mapped to the legacy and modern versions of the app the following way:
legacy: ribbon: listOfServers: http://localhost:8081 modern: ribbon: DeploymentContextBasedVipAddresses: modern-app
Here just for a little more variation I am making a direct call to an endpoint for the legacy app and making a call via Eureka for the modern version of the application.
If you are interested in exploring the entirety of the application it is available in my github repo
With the entire set-up in place, a small test with the legacy handling 20% of the traffic confirms that the filter works effectively: