Search results
Routing requests and dynamically refreshing routes using Spring Cloud Zuul Server
Spring Cloud Series
Subscribe to my newsletter to receive updates when content like this is published.
- Developing Microservices using Spring Boot, Jersey, Swagger and Docker
- Integration Testing using Spring Boot, Postgres and Docker
- Services registration and discovery using Spring Cloud Netflix Eureka Server and client-side load-balancing using Ribbon and Feign
- Centralized and versioned configuration using Spring Cloud Config Server and Git
- Routing requests and dynamically refreshing routes using Spring Cloud Zuul Server (you are here)
- Microservices Sidecar pattern implementation using Postgres, Spring Cloud Netflix and Docker
- 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.
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.
localhost:8000
, this service would have to be started using: -Deureka.client.serviceUrl.defaultZone=http://<localhost>:<port>/eureka/
.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.
localhost:8000
, this service would have to be started using: -Deureka.client.serviceUrl.defaultZone=http://<localhost>:<port>/eureka/
.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.
standalone
and peerAware
modes.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.
/routes
actuator endpoint, automatically configured by the Zuul server will return the configured routes when sending a GET request, and will refresh the routes configuration when sending a POST request.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
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
Zuul-Server-refreshable.yml
matches the application name Zuul-Server
and Spring profile refreshable
. This is the criteria the Config server uses to locate this file.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
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
NEED HELP?
I provide Consulting Services.ABOUT THE AUTHOR
