Spring Cloud Zuul example


Goal of this example

This example demonstrates the main features of the Zuul API gateway integrated into spring cloud :

  • Service auto registration via eureka
  • Service registration by address
  • Service registration by service ID
  • Filters (logging, authentication)
  • Serving static content

Technology Used

  • Spring boot 1.3.5.RELEASE
  • Eureka
  • Zuul

This article is part of a Spring Cloud / Netflix OSS series

API Gateway Overview

The Zuul API Gateway is part of the Netflix OSS package. It is very lightweight and integrates well with Eureka. API Gateway is the single entry point into the microservice ecosystem from the outside world.

zuul-api-gateway

Main features of Zuul

  • Provides a unified access to multiple different microservices
  • Hides internal details of the microservice ecosystem
  • Load balances across multiple service instances
  • Allowes access to services
  • Restricts access to internal only services
  • Looks up services from Eureka
  • Implements filters for authentication or logging purposes

Zuul and NGINX are very similar the key differences are

  • Unlike for NGINX Zuul can’t be downloaded as a binary distribution, instead it is running inside a Spring Boot application using the Spring Cloud integration
  • Zuul is written in Java, therefore integration with the Java, Spring Boot, Spring Cloud and Netflix OSS ecosystem is quite easy

To start up a Spring Boot application with Zuul in it all we need to do is add the below dependency

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>

and activate it using the @EnableZuulProxy annotation

@EnableZuulProxy
@SpringBootApplication
public class SpringCloudZuulExampleApplication {

Defining routing rules

Routing rules are configured in the application.properites / application.yml The below simple example shows how the /api/customer-service/** URI is mapped to an internal only service

zuul:
  #Service will be mapped under the /api URI
  prefix: /api
  routes:
    customer-service:
      path: /customer-service/**
      url: http://localhost:9098

Zuul integration with Eureka

Eureka is the service registry and part of Netflix OSS and Spring Cloud stack just like Zuul. This tutorial covers more in detail how Eureka works, but in essence, services register themself with a name and URL into the registry, and other services can query the available instances by service name. If Zuul is connected to Eureka then it will query all services and make them available. E.g.: if there is a service named CUSTOMER-SERVICE it will be available under the /customer-service URI. Configuring the connection is as simple as this :

eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:8761/eureka/

This is very convenient, but can be dangerous because exposes every service even if if one was meant to be internal only. A more fine grained approach is to enable services one by one.

zuul:
  #Service will be mapped under the /api URI
  prefix: /api
#  Uncomment to disable auto-registering all services read from Eureka
#  ignoredServices: '*'
  routes:
    test:
      path: /redirect/**
      url: http://google.com
    customer-by-address:
      path: /customer-by-address/**
      url: http://localhost:9098
    customer-by-service:
      path: /customer-by-service/**
      serviceId: CUSTOMER-SERVICE

Given a service available at http://localhost:9098 that registers itself into Eureka under the name “customer-service” the above config will results the following URLs be available in Zuul

URI Function
http://localhost:9090/routes Display all available Routes (services)
http://localhost:9090/api/customer-service/customer/1 Service auto registered from Eureka
http://localhost:9090/api/customer-by-address/customer/1 Service registered by address
http://localhost:9090/api/customer-by-service/customer/1 Service registered by Eureka service id

Zuul filters

If a spring Bean extends ZuulFilter then it will be auto registered as a filter. Filters are most commonly used for

  • Authentication
  • Logging
  • Serving static content
  • Dynamic routing based on some conditions (A/B testing)

Logging filter

@Component
public class StaticFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        String path = RequestContext.getCurrentContext().getRequest().getRequestURI();
        return "/api/static".equals(path);
    }

    @Override
    public Object run() {

        RequestContext ctx = RequestContext.getCurrentContext();
        // Set the default response code for static filters to be 200
        ctx.setResponseStatusCode(HttpServletResponse.SC_OK);
        // first StaticResponseFilter instance to match wins, others do not set body and/or status
        if (ctx.getResponseBody() == null) {
            ctx.setResponseBody("static content");
            ctx.setSendZuulResponse(false);
        }
        return null;
    }
}

Logging filter

@Component
public class AccessLogFilter extends ZuulFilter {

    Logger logger = LoggerFactory.getLogger(AccessLogFilter.class);

    @Override
    public String filterType() {
        return "post";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        HttpServletResponse response = RequestContext.getCurrentContext().getResponse();

        logger.info("REQUEST :: < " + request.getScheme() + " " + request.getLocalAddr() + ":" + request.getLocalPort());
        logger.info("REQUEST :: < " + request.getMethod() + " " + request.getRequestURI() + " " + request.getProtocol()); logger.info("RESPONSE:: > HTTP:" + response.getStatus());

        return null;
    }
}

Redirect filter

The below filter redirects requests to a different server. It also demonstrates that filters can be written in groovy. The advantage (and danger) of groovy scripts is that they can be loaded from classpath, so the operations team can modify them without the need to release a jar / war file itself

import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.RequestContext
import com.netflix.zuul.util.HTTPRequestUtils

public class ExampleSurgicalDebugFilterBean extends ZuulFilter {

    @Override
    String filterType() {
        return "pre"
    }

    @Override
    int filterOrder() {
        return 96
    }

    @Override
    boolean shouldFilter() {
        RequestContext.currentContext.getRequest().getRequestURI().matches("/api/redirect.*")
    }

    @Override
    Object run() {

        RequestContext.currentContext.routeHost = new URL("http://example.com");
        if (HTTPRequestUtils.getInstance().getQueryParams() == null) {
            RequestContext.getCurrentContext().setRequestQueryParams(new HashMap<String, List<String>>());
        }
        HTTPRequestUtils.getInstance().getQueryParams().put("debugRequest", ["true"])
        RequestContext.currentContext.setDebugRequest(true)
        RequestContext.getCurrentContext().zuulToZuul = true

    }

}

beans {
    exampleSurgicalDebugFilterBean(ExampleSurgicalDebugFilterBean) {
        new ExampleSurgicalDebugFilterBean();
    }
}

Externalizing dynamic routing rules

By default routing rules are statically defined in the application.yml file. By using filters these rules can be made dynamic. Spring Boot has support to read groovy scripts from external locations, what allows rules to be defined externally. It can be configured in Spring Boot’s main class

@EnableZuulProxy
@SpringBootApplication
public class SpringCloudZuulExampleApplication {

    public static void main(String[] args) throws MalformedURLException {

//        Object[] sources = {SpringCloudZuulExampleApplication.class, new UrlResource(new URL("file:C:/dev/abc_dynamic.groovy"))};
        Object[] sources = {SpringCloudZuulExampleApplication.class, new ClassPathResource("groovy/ExampleSurgicalDebugFilterBean.groovy")};
        SpringApplication.run(sources, args);

    }

In a local development environment starting up a complete microservice ecosystem with Eureka is a complex task. Docker can help a lot in this, details are discussed in this blog post : https://exampledriven.wordpress.com/2016/06/24/spring-boot-docker-example/

7 comments

  1. Could you please elaborate how the routing is happeing? Would like to know if somebody tries to access /service1 is it getting forwarded or routed to /service1??

  2. Pingback: Spring Cloud hystrix example | Example Driven (Self) Development

  3. Pingback: Spring Cloud, Eureka, Ribbon, Feign example | Example Driven (Self) Development

  4. Pingback: Microservice ecosystem with Docker, Spring Cloud and Spring Boot | Example Driven (Self) Development

  5. Pingback: How to enable ZuulProxy in spring mvc | ASK Dev Archives

  6. Pingback: Zuul – Edge Server – Learn. Write. Repeat.

Leave a comment