Spring Cloud Series

Subscribe to my newsletter to receive updates when content like this is published.

  1. Developing Microservices using Spring Boot, Jersey, Swagger and Docker
  2. Integration Testing using Spring Boot, Postgres and Docker
  3. Services registration and discovery using Spring Cloud Netflix Eureka Server and client-side load-balancing using Ribbon and Feign
  4. Centralized and versioned configuration using Spring Cloud Config Server and Git
  5. Routing requests and dynamically refreshing routes using Spring Cloud Zuul Server (you are here)
  6. Microservices Sidecar pattern implementation using Postgres, Spring Cloud Netflix and Docker
  7. Implementing Circuit Breaker using Hystrix, Dashboard using Spring Cloud Turbine Server (work in progress)

1. OVERVIEW

Having covered infrastructure services Spring Cloud Config server here with Refreshable Configuration here and Registration and Discovery here with Multi-versioned service support here, in this post I’ll cover the Spring Cloud Netflix Zuul server, another infrastructure service used in a microservice architecture.

Zuul is an edge server that seats between the outside world and the downstream services and can handle cross-cutting concerns like security, geolocation, rate limiting, metering, routing, request / response normalization (encoding, headers, urls). Developed and used by Netflix to handle tens of billions of requests daily, it has also been integrated for Spring Boot / Spring Cloud applications by Pivotal.

A core component of the Zuul server is the Zuul filters, which Zuul provides four types of:

Filter type Description
pre filters Executed before the request is routed.
routing filters Handles the actual routing of the request.
post filters Executed after the request has been routed.
error filters Executed if an error happens while handling the request.

This post shows how to configure a Spring Cloud Netflix Zuul server to route requests to a demo downstream service using the provided routing filter RibbonRoutingFilter and how to dynamically refresh the Zuul routes using Spring Cloud Eureka and Spring Cloud Config servers.

Routing traffic using Spring Cloud Netflix Zuul

2. REQUIREMENTS

  • Java 7+.
  • Maven 3.2+.
  • Familiarity with Spring Framework.
  • A Eureka server instance for the Spring Cloud Netflix Zuul servers to read the registry from and to match routes with services.
  • (Optional) Spring Cloud Config server instance for the Zuul servers to read externally configured routes and refresh them when they are updated.
  • (Optional) A RabbitMQ host for the Config server to publish changes to and for the subscribed Zuul servers to get notifications from, when the routes are updated.

3. CREATE THE ZUUL SERVER

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.7.RELEASE -d dependencies=actuator,cloud-zuul,cloud-eureka -d language=java -d type=maven-project -d baseDir=zuulserver -d groupId=com.asimio.cloud -d artifactId=zuul-server -d version=0-SNAPSHOT | tar -xzvf -

This command will create a Maven project in a folder named zuulserver with most of the dependencies used in the accompanying source code for this post.

Some of its relevant files are:

pom.xml:

...
<properties>
  ...
  <spring-cloud.version>Camden.SR7</spring-cloud.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
...
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
...

spring-cloud-starter-zuul will bring the required Zuul edge server dependencies while spring-cloud-starter-eureka dependency will allow the Zuul server to proxy requests to the registered services with Eureka first looking up their metadata using the service id mapped to the route used in the request.

ZuulServerApplication.java:

package com.asimio.cloud.zuul;
...
@SpringBootApplication
@EnableZuulProxy
public class ZuulServerApplication {

  public static void main(String[] args) {
    SpringApplication.run(ZuulServerApplication.class, args);
  }
}

This is the execution entry point of the Zuul server webapp, @EnableZuulProxy set this application up to proxy requests to other services.

application.yml:

...
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/

# ribbon.eureka.enabled: false
zuul:
  ignoredServices: "*"
  routes:
    zuulDemo1:
      path: /zuul1/**
# serviceId as registed with Eureka. Enabled and used when ribbon.eureka.enabled is true.
      serviceId: demo-zuul-api1
# zuul.routes.<the route>.url used when ribbon.eureka.enabled is false, serviceId is disabled.
#      url: http://localhost:8600/
# stripPrefix set to true if context path is set to /
      stripPrefix: true
...

The Eureka client is configured to retrieve the registry from the server at the specified location but Zuul itself doesn’t register with it.

Zuul configuration defines the route zuulDemo1 mapped to /zuul1/**. There are a couple of options to proxy requests to this Zuul’s path to a service:

  • Using the serviceId. The Zuul server uses this value to retrieve the service metadata from the Eureka registry, in case of multiple servers are found, load-balancing between them is already taken care of and proxies the requests accordingly.

  • Using the url. The url of the destination is explicitly configured.

4. CREATE THE DEMO SERVICE

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.7.RELEASE -d dependencies=actuator,cloud-eureka,web -d language=java -d type=maven-project -d baseDir=demo-zuul-api-1 -d groupId=com.asimio.demo.api -d artifactId=demo-zuul-api-1 -d version=0-SNAPSHOT | tar -xzvf -

Creates a Maven project in a folder named demo-zuul-api-1 with the dependencies needed to demo this post.

pom.xml:

...
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
...

spring-boot-starter-web is included to implement an endpoint using Spring MVC Rest and also allows to enable and use Spring Boot actuators.
spring-cloud-starter-eureka is included to register this service with a Eureka server in order for the Zuul server to discover it and proxy the requests that matches the configured path.

ActorResource.java

com.asimio.api.demo1.rest
...
@RestController
@RequestMapping(value = "/actors", produces = "application/json")
public class ActorResource {

  @RequestMapping(value = "/{id}", method = RequestMethod.GET)
  public Actor getActor(@PathVariable("id") String id) {
    return this.buildActor(id, String.format("First%s", id), String.format("Last%s", id));
  }
...
}

A simple /actors/{id} implementation using Spring MVC Rest.

application.yml:

...
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/
  instance:
    hostname: localhost
    statusPageUrlPath: ${management.context-path}/info
    healthCheckUrlPath: ${management.context-path}/health
    preferIpAddress: true
    metadataMap:
      instanceId: ${spring.application.name}:${server.port}
...

Familiar Eureka client configuration that has been covered here and here.

5. RUNNING THE EUREKA AND ZUUL SERVERS AND THE DEMO SERVICE

To keep this post simple let’s just run a single instance of the Eureka server using its standalone Spring profile.

mvn spring-boot:run -Dspring.profiles.active=standalone
...
2017-09-27 23:49:58.810  INFO 87796 --- [      Thread-10] e.s.EurekaServerInitializerConfiguration : Started Eureka Server
2017-09-27 23:49:58.893  INFO 87796 --- [           main] b.c.e.u.UndertowEmbeddedServletContainer : Undertow started on port(s) 8000 (http)
2017-09-27 23:49:58.894  INFO 87796 --- [           main] c.n.e.EurekaDiscoveryClientConfiguration : Updating port to 8000
2017-09-27 23:49:58.898  INFO 87796 --- [           main] c.a.c.eureka.EurekaServerApplication     : Started EurekaServerApplication in 5.629 seconds (JVM running for 9.09)
...

Now lets start a single instance of the Zuul server:

mvn spring-boot:run
...
2017-09-28 00:21:23 INFO  TomcatEmbeddedServletContainer:198 - Tomcat started on port(s): 8200 (http)
2017-09-28 00:21:23 INFO  EurekaDiscoveryClientConfiguration:168 - Updating port to 8200
2017-09-28 00:21:23 INFO  ZuulServerApplication:57 - Started ZuulServerApplication in 8.684 seconds (JVM running for 13.123)

And lastly lets start the Proxied-Demo service:

mvn spring-boot:run
...
2017-09-28 00:31:47 INFO  TomcatEmbeddedServletContainer:198 - Tomcat started on port(s): 8600 (http)
2017-09-28 00:31:47 INFO  EurekaDiscoveryClientConfiguration:168 - Updating port to 8600
2017-09-28 00:31:47 INFO  Application:57 - Started Application in 4.885 seconds (JVM running for 7.569)

Lets’s now send request to the zuulDemo1 route (via zuul1 path) which proxies the request to the Demo service:

curl -v http://localhost:8200/zuul1/actors/1
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8200 (#0)
> GET /zuul1/actors/1 HTTP/1.1
> Host: localhost:8200
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: Zuul-Server:default:8200
< Date: Thu, 28 Sep 2017 04:45:07 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1","lastUpdate":null}

Here are some of the logs from the Zuul server while processing such request:

...
2017-09-28 00:45:07 INFO  BaseLoadBalancer:185 - Client:demo-zuul-api1 instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=demo-zuul-api1,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2017-09-28 00:45:07 INFO  DynamicServerListLoadBalancer:214 - Using serverListUpdater PollingServerListUpdater
2017-09-28 00:45:07 INFO  ChainedDynamicProperty:115 - Flipping property: demo-zuul-api1.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2017-09-28 00:45:07 INFO  DynamicServerListLoadBalancer:150 - DynamicServerListLoadBalancer for client demo-zuul-api1 initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=demo-zuul-api1,current list of Servers=[120.240.1.192:demo-zuul-api1:8600],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone;	Instance count:1;	Active connections count: 0;	Circuit breaker tripped count: 0;	Active connections per server: 0.0;]
...

demo-zuul-api1 is the Demo application service id registered with the Eureka server and mapped to the zuulDemo1 route in Zuul server’s application.yml. In case there would be more than one instance of the Demo service registered with the Eureka server, Zuul would load-balance the requests.

6. ADDING SUPPORT TO DYNAMICALLY UPDATE THE ZUUL ROUTES

One problem of the approach described so far is that adding, updating or removing Zuul routes requires the Zuul server to be restarted.

This section takes the Refreshable Configuration using Spring Cloud Config Server, Spring Cloud Bus, RabbitMQ and Git approach and configures the Zuul routes in an external properties file, backed by Git and retrieved via the Spring Cloud Config server so that when Zuul routes are updated, the Zuul server which would be subscribed to a RabbitMQ exchange will be notified about the changes and will refresh its routes without the need for it to be bounced.

Routing traffic using Spring Cloud Netflix Zuul Routing traffic using Spring Cloud Netflix Zuul

Next are explained the files changes to be included to accomplish such behavior:

pom.xml:

...
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
+<dependency>
+  <groupId>org.springframework.cloud</groupId>
+  <artifactId>spring-cloud-starter-config</artifactId>
+</dependency>
+<dependency>
+  <groupId>org.springframework.cloud</groupId>
+  <artifactId>spring-cloud-starter-bus-amqp</artifactId>
+</dependency>
...

spring-cloud-starter-config dependency implements reading properties from a Spring Cloud Config server backed by a Git backend, in this case, the routes are going to be configured remotely.
spring-cloud-starter-bus-amqp includes the dependencies implementing subscribing to a RabbitMQ exchange where the Config server will send messages with the updated properties.

bootstrap.yml:

spring:
  application:
    name: Zuul-Server
+  cloud:
+    bus:
+      enabled: false
+    config:
+      enabled: false
+
+---
+spring:
+  profiles: refreshable
+  cloud:
+    bus:
+      enabled: true
+    config:
+      enabled: true
+      # Or have Config server register with Eureka (registration-first approach)
+      # as described at http://tech.asimio.net/2016/12/09/Centralized-and-Versioned-Configuration-using-Spring-Cloud-Config-Server-and-Git.html
+      uri: http://localhost:8101
+  rabbitmq:
+    host: cat.rmq.cloudamqp.com
+    port: 5672
+endpoints:
+  refresh:
+    enabled: true

The first few lines turn RabbitMQ exchange subscription and Spring Cloud Config client configuration off, so that the Zuul server works standalone, with routes configured locally as it did before.

The second set of lines defines a Spring profile configuring the Zuul server to read properties from a remote Config server and to subscribe to an exchange to receive notifications when those properties are updated.

Zuul-Server-refreshable.yml (New file):

# ribbon.eureka.enabled: false
zuul:
  ignoredServices: "*"
  routes:
    zuulDemo1:
      path: /zuul2/**
# serviceId as registed with Eureka. Enabled and used when ribbon.eureka.enabled is true.
      serviceId: demo-zuul-api1
# zuul.routes.<the route>.url used when ribbon.eureka.enabled is false, serviceId is disabled.
#      url: http://localhost:8600/
# stripPrefix set to true if context path is set to /
      stripPrefix: true

Similar settings Zuul server’s previous application.yml but now these settings are store in a Git file: Zuul-Server-refreshable.yml

ZuulServerApplication.java

@SpringBootApplication
+@EnableAutoConfiguration(exclude = { RabbitAutoConfiguration.class })
@EnableZuulProxy

In the Zuul server main class, RabbitAutoConfiguration is left out so that this server works standalone, without any need to connect to a RabbitMQ exchange.

AppConfig.java (New file):

package com.asimio.cloud.zuul.config;
...
@Configuration
@ConditionalOnProperty(name = "spring.cloud.bus.enabled", havingValue = "true", matchIfMissing = false)
@Import(RabbitAutoConfiguration.class)
public class AppConfig {

  @Primary
  @Bean(name = "zuulConfigProperties")
  @RefreshScope
  @ConfigurationProperties("zuul")
  public ZuulProperties zuulProperties() {
    return new ZuulProperties();
  }
}

This @Configuration-annotated class is going to instantiate beans and auto-configure a RabbitMQ exchange only when spring.cloud.bus.enabled is true, which happens when using the refreshable Spring profile.
The zuulConfigProperties bean provides Zuul properties read from the Config server and the @RefreshScope annotation indicates this bean to be re-created and injected in components using it.

6.1. SETTING UP RABBITMQ

Already covered in Setting up RabbitMQ from Refreshable Configuration using Spring Cloud Config Server Spring Cloud Bus RabbitMQ.

Basically an exchange with these settings is needed:

name springCloudBus
type topic
durable true
autoDelete false
internal false

6.2. RUNNING THE EUREKA SERVER

Running a standalone (via a Spring profile) instance as previously described:

mvn spring-boot:run -Dspring.profiles.active=standalone
...

6.3. RUNNING THE CONFIG SERVER

The Config server has already been updated in this section from Refreshable Configuration using Spring Cloud Config Server, Spring Cloud Bus, RabbitMQ and Git, so I’ll just go ahead and run it using the config-monitor Spring profile.

mvn spring-boot:run -Dserver.port=8101 -Dspring.profiles.active=config-monitor -Dspring.rabbitmq.virtual-host=<your virtual host> -Dspring.rabbitmq.username=<your username> -Dspring.rabbitmq.password=<your password>
...
2017-10-03 00:45:56 INFO  Http11NioProtocol:179 - Starting ProtocolHandler [http-nio-8101]
2017-10-03 00:45:56 INFO  NioSelectorPool:179 - Using a shared selector for servlet write/read
2017-10-03 00:45:56 INFO  TomcatEmbeddedServletContainer:185 - Tomcat started on port(s): 8101 (http)
2017-10-03 00:45:56 INFO  ConfigServerApplication:57 - Started ConfigServerApplication in 7.863 seconds (JVM running for 11.178)

The reason the Config server is listening on port 8101 is because the Bitbucket has already configured a Webhook to POST to and my router has a port forward entry allowing it.

6.4. RUNNING THE ZUUL SERVER

mvn spring-boot:run -Dspring.profiles.active=refreshable -Dspring.rabbitmq.virtual-host=<your virtual host> -Dspring.rabbitmq.username=<your username> -Dspring.rabbitmq.password=<your password>
...
2017-10-03 00:53:14 INFO  ConfigServicePropertySourceLocator:80 - Fetching config from server at: http://localhost:8101
...
2017-10-03 00:53:24 INFO  AmqpInboundChannelAdapter:97 - started inbound.springCloudBus.anonymous.eDmai3-BTcetp0JCIc-bRQ
2017-10-03 00:53:24 INFO  EventDrivenConsumer:108 - Adding {message-handler:inbound.springCloudBus.default} as a subscriber to the 'bridge.springCloudBus' channel
2017-10-03 00:53:24 INFO  EventDrivenConsumer:97 - started inbound.springCloudBus.default
2017-10-03 00:53:24 INFO  DefaultLifecycleProcessor:343 - Starting beans in phase 2147483647
2017-10-03 00:53:24 INFO  HystrixCircuitBreakerConfiguration$HystrixMetricsPollerConfiguration:138 - Starting poller
2017-10-03 00:53:24 INFO  Http11NioProtocol:179 - Initializing ProtocolHandler ["http-nio-8200"]
2017-10-03 00:53:24 INFO  Http11NioProtocol:179 - Starting ProtocolHandler ["http-nio-8200"]
2017-10-03 00:53:24 INFO  NioSelectorPool:179 - Using a shared selector for servlet write/read
2017-10-03 00:53:24 INFO  TomcatEmbeddedServletContainer:198 - Tomcat started on port(s): 8200 (http)
2017-10-03 00:53:24 INFO  EurekaDiscoveryClientConfiguration:168 - Updating port to 8200
2017-10-03 00:53:24 INFO  ZuulServerApplication:57 - Started ZuulServerApplication in 10.991 seconds (JVM running for 13.872)

Notice once starting the Zuul server using the refreshable Spring profile it retrieves configuration properties from the Config server and subscribes to the springCloudBus exchange.

6.5. RUNNING THE PROXIED-DEMO API

mvn spring-boot:run
...
2017-10-03 00:54:11 INFO  TomcatEmbeddedServletContainer:198 - Tomcat started on port(s): 8600 (http)
2017-10-03 00:54:11 INFO  EurekaDiscoveryClientConfiguration:168 - Updating port to 8600
2017-10-03 00:54:11 INFO  Application:57 - Started Application in 4.693 seconds (JVM running for 7.421)

6.6. SENDING API REQUESTS VIA ZUUL, UPDATING REMOTE PROPERTIES AND RETRY

curl -v http://localhost:8200/zuul2/actors/1
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8200 (#0)
> GET /zuul2/actors/1 HTTP/1.1
> Host: localhost:8200
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: Zuul-Server:refreshable:8200
< Date: Tue, 03 Oct 2017 04:55:07 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1","lastUpdate":null}

It can be seen the request is sent to /zuul2 which is now mapped to the zuulDemo1 route which is specified in the Git-backed file Zuul-Server-refreshable.yml. New requests to path /zuul1 should now fail with HTTP status 404.

In order for the next section to work, the Git repo including the Zuul server configuration properties file (Zuul-Server-refreshable.yml) where the Zuul routes are configured need to be setup with a Webhook to POST to the Config server’s /monitor endpoint when changes are pushed.

Bitbucket Config server webhook Bitbucket Config server webhook

Let’s now update the zuulDemo1 route from /zuul2/** to /zuul3/** and push the change:

Zuul-Server-refreshable.yml:

-      path: /zuul2/**
+      path: /zuul3/**

The Config server logs now shows:

2017-10-03 01:03:49 INFO  PropertyPathEndpoint:89 - Refresh for: *
...
2017-10-03 01:03:50 INFO  RefreshListener:27 - Received remote refresh request. Keys refreshed []
2017-10-03 01:03:51 INFO  MultipleJGitEnvironmentRepository:296 - Fetched for remote master and found 1 updates
2017-10-03 01:03:51 INFO  AnnotationConfigApplicationContext:582 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@f1057aa: startup date [Tue Oct 03 01:03:51 EDT 2017]; root of context hierarchy
2017-10-03 01:03:51 INFO  AutowiredAnnotationBeanPostProcessor:155 - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2017-10-03 01:03:51 INFO  NativeEnvironmentRepository:228 - Adding property source: file:/var/folders/lg/n7p2jx0j4qjb0jdy09_lzc7m0000gp/T/config-repo-8155767605200864957/Zuul-Server-refreshable.yml
...

The Zuul server logs now shows a Refresh event has been received:

...
2017-10-03 01:03:52 INFO  RefreshListener:27 - Received remote refresh request. Keys refreshed [config.client.version, zuul.routes.zuulDemo1.path]

And sending a new request to the Proxied-Demo service results in:

curl -v http://localhost:8200/zuul3/actors/1
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8200 (#0)
> GET /zuul3/actors/1 HTTP/1.1
> Host: localhost:8200
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: Zuul-Server:refreshable:8200
< Date: Tue, 03 Oct 2017 05:07:58 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1","lastUpdate":null}

Requests to the previous path /zuul2 should now returns in 404.

And that’s the end of this post.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

7. SOURCE CODE

Accompanying source code for this blog post can be found at:

8. REFERENCES