Back

TechnologyApr 23, 2019

There Is No Zuul, Only Eureka: Zero-Touch Zuul Routing

Taylor Gregston

This article assumes working knowledge of Netflix’s Eureka and Zuul, specifically Spring Cloud’s implementation.

In the DevOps world, one thing we can do to make our lives easier is reduce application, infrastructure, and operational coupling in order to provide maximal flexibility to deploy and evolve individual components of our architecture. Consider a typical service discovery setup using the Spring Cloud version of the Netflix OSS stack, where Zuul routes requests to a suite of back-end services based on metadata available through Eureka, a service registry. In most cases the routing rules are defined in a YAML or properties file that’s packaged along with the Zuul server code into a jar. This means that introducing a new service with distinct routing rules requires changes to the Zuul configuration, and therefore a new build and deployment. Now we have to coordinate the deployment of the two components—our new service and Zuul. So how can we eliminate this dependency on Zuul and avoid a coordinated deployment?

role reversal

One way to accomplish this would be to have each Eureka client service register its own route mapping with Eureka instead of defining the route mappings for every service in the Zuul server configuration. Zuul would then need to fetch the route mappings from Eureka (in addition to its own internal configuration) and dynamically build the full route tree based on what Eureka returned.

With this strategy, the routing definitions can be completely removed from the Zuul configuration since each Eureka client will provide its own routes. This behavior is not supported “out of the box”, so we will need to extend the existing functionality. Fortunately, Eureka already provides a mechanism, the instance metadata map, for passing arbitrary data from one Eureka client to another (in this case from a service to the Zuul server).

eureka instance metadata map

The instance details that are provided to Eureka (and shared with other clients) are defined by the ServiceInstance class. This class contains a field called metadata that is simply a map of String values that we can use to define our custom routes. To add an entry called routes to the map, we need to add the following to our application.yml:

eureka.instance.metadata-map.routes: /myroute/**

With this change alone, the route defined in the service will be accessible by Zuul, but we still need to configure Zuul to make use of it. Thanks to Spring, this can be done without modifying the Zuul library. We need only to extend the DiscoveryClientRouteLocator class.

extending the route locator

Assuming the @EnableZuulProxy annotation on our main class, the default route definitions are gathered via the DiscoveryClientRouteLocator bean. We can extend this class and effectively override the default route building behavior in Zuul. All we need to do is place this file in the base package of our Zuul application:

MyRouteLocator.java

package com.gregstont.zuul   import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator; import org.springframework.cloud.netflix.zuul.filters.discovery.ServiceRouteMapper; import org.springframework.stereotype.Component;   import java.util.LinkedHashMap; import java.util.List;   @Component public class MyRouteLocator extends DiscoveryClientRouteLocator {   private static final Log log = LogFactory.getLog(MyRouteLocator.class);   private DiscoveryClient discoveryClient;   MyRouteLocator(ServerProperties server, DiscoveryClient discoveryClient, ZuulProperties properties, ServiceRouteMapper mapper, ServiceInstance localServiceInstance) { super(server.getServlet().getContextPath(), discoveryClient, properties, mapper, localServiceInstance); this.discoveryClient = discoveryClient; }   @Override protected LinkedHashMap<String, ZuulProperties.ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = super.locateRoutes(); for (String serviceId : this.discoveryClient.getServices()) { List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId); if (!instances.isEmpty()) { String routes = instances.get(0).getMetadata().get("routes"); if (routes != null) { for (String route : routes.split("\n")) { routesMap.put(route, new ZuulProperties.ZuulRoute(route, serviceId)); } } } } return routesMap; }}

By providing our own DiscoveryClientRouteLocator, we are forcing the ZuulProxyAutoConfiguration bean to use our modified route locator rather than the default. (This works because of the @ConditionalOnMissingBean annotation that the Spring Cloud developers so graciously provided).

The locateRoutes() method on MyRouteLocator will be called during the refresh of the Zuul routing definitions. It will first gather the routes defined by the default behavior (via super.locateRoutes()), and then loops through the available services, gathering the metadata to build our custom, client-defined routes. Our client-defined routes will take precedence over routes provided by the default behavior.

the default behavior

I should mention, by default Zuul will create a route for newly discovered services. For example, if an application named my-app-1 registers with Eureka, Zuul will route all traffic with a /my-app-1/ prefix to that service. However, this doesn’t provide any flexibility whatsoever, and the only way to modify the route would be to rename the application. This also poses an issue if we’d like to version our API, since an application name like /api/v1/my-app-1/ is not supported.

what it means

By defining the route mappings in the services instead of Zuul, we loosen the coupling between our components in a way that allows us to introduce new services to our ecosystem without needing a new build and deployment of the Zuul server. Ultimately this means less operational overhead, easier deployments, and potentially less downtime for our applications. Employ the above changes now and your application deployment team will thank you later!