A partir do momento que passamos a usar uma abordagem de microserviços ou da separação do código monolito, começamos a enfrentar alguns desafios por lidarmos com diversas aplicações, um deles é trabalhar com as diferentes URLs das aplicações distribuídas, onde conforme a necessidade deve ser direcionado para aplicação correta.
Uma das formas de minimizar esse desafio é utilizando um proxy, onde todas chamadas são centralizadas em um único local e a solução de proxy faz o direcionamento para aplicação correta, com isso, utilizando Spring Cloud em conjunto tecnologias da Netflix (Zuul e Eureka), é possível configurar de forma simples toda essa infraestrutura para aplicações Spring Boot.
Spring Cloud: é um conjunto de ferramentas que prove umas série de padrões e frameworks para a acelerar a construções de aplicações distribuídas.
Zuul: é uma solução de roteamento dinâmico que possibilita monitoramento, resiliência e segurança para aplicações, que também pode ser encontrada no sub-projeto spring-cloud-netflix.
Eureka: é uma solução de service discovery, que em conjunto com outras ferramentas possibilita gerenciamento dinâmico e escalabilidade para as aplicações, o Eureka também faz pode ser encontrado no sub-projeto spring-cloud-netflix.
Como comentado anteriormente, o Zuul e Eureka fazem parte do projeto spring-cloud-netflix, com isso, já uma grande integração nativa das soluções em conjunto com o Spring Cloud e Spring Boot.
Funcionamento
O funcionamento é dividido em três papéis:
- Spring Cloud: Responsável por integrar todas as soluções em aplicações Spring Boot, onde toda configuração é feita através de anotações e propriedades do application.properties;
- Eureka: Responsável por registar as aplicações através de apelidos, o que torna todo roteamento dinâmico;
- Zuul: Responsável por ser a porta de entrada das chamadas e direcionar as chamadas para as aplicações registradas no Eureka.
Dessa forma, as chamadas podem ser centralizadas em um único local (no Zuul) através do endereço /api, o qual traça a rota para as APIs internas (/api1 e /ap2), encapsulando os reais endereços das aplicações, o que possibilita maior segurança por não ter acesso direto aos serviços, centralização de URLs, onde a aplicações tem uma única porta de entrada e a estrutura pronta para balanciamento de carga, onde do Zuul e o Eureka podem suportar esse papel em conjunto com outras soluções, como Ribbon.
Exemplo
Colocando em prática o que foi comentado anteriormente, vamos implementar o cenário ilustrador anteriormente.
Como estamos utilizando Spring Cloud Netflix, suas dependências disponibilizam anotações para que aplicações Spring Boot se tornem servidores das soluções da Netflix, assim vamos configurar o Eureka e Zuul em projetos Spring Boot para facilitar a construção do ambiente.
Eureka
O Eureka é executado a partir de uma simples aplicação Spring Boot, que quando executado, sobe a instância do Eureka server.
A única dependência a ser adicionada para configurar o Eureka é a spring-cloud-netflix-eureka-server, que já carrega outras dependências necessárias para inicializar o Spring Boot no projeto (spring-boot-starter).
pom.xml
org.springframework.cloud spring-cloud-netflix-eureka-server
Para tornar a aplicação um Eureka Server basta adicionar a anotação @EnableEurekaServer em conjunto com a anotação do Spring Boot (SpringBootApplication) que no startup da aplicação será inicializado o Eureka.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaAppConfig { public static void main(String[] args) { SpringApplication.run(EurekaAppConfig.class, args); } }
E por fim, algumas configurações precisam ser feitas nos arquivos de configuração do Spring Boot (application.properties).
server.port=8761 eureka.client.register-with-eureka=false eureka.client.fetch-registry=false
- server.port: Define a porta que o Eureka vai estar disponível, por padrão o Eureka usa a porta 8761;
- eureka.client.register-with-eureka: Indica que a instância do Eureka não precisa ser registrada, até mesmo porque ela é a responsável pelos registros, assim não precisa registrar ela mesmo;
- eureka.client.fetch-registry: Indica que essa aplicação não precisa buscar informações de registro no Eureka, pelo mesmo motivo anterior.
Aplicações
São criadas duas aplicações de back-end para simular um cenário com contexto de APIs diferentes, onde uma aplicação será responsável por listar os customers e outra para listar os products.
Primeira aplicação: Customers
As dependências adicionadas são: spring-boot-starter-web para fazer a disponibilização da API Rest e spring-cloud-starter-eureka para registrar a aplicação no Eureka.
org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-eureka
A classe de configuração é igual qualquer aplicação Spring Boot, apenas adicionando a anotação @EnableDiscoveryClient para fazer o registro da aplicação no Eureka.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class CustomersAppConfig { public static void main(String[] args) { SpringApplication.run(CustomersAppConfig.class, args); } }
Observação: O registro no Eureka é realizado no startup da aplicação, onde a aplicação se registra no service discovery.
Para fins de demonstração, apenas é criado um serviço /customer no método GET, que retorna uma lista de customers.
import com.google.common.collect.ImmutableMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/customers") public class CustomerController { @GetMapping public List findAll() { return ImmutableMap.of( 1L, "Customer 1", 2L, "Customer 2", 3L, "Customer 3", 4L, "Customer 4"); } }
E por fim, as configurações a serem feitas no application.properties, são: o host do Eureka e a definição de nome da aplicação para registro no Eureka.
server.port=8060 spring.application.name=customers eureka.instance.hostname=localhost
-
- server.port: A porta que a aplicação estará disponível;
- spring.application.name: A identificação da aplicação que será registrada no Eureka;
- eureka.instance.hostname: O hostname do servidor do Eureka.
Segundo aplicação: Products
A aplicação products segue o mesmo padrão da anterior, apenas mudando o controller que disponibiliza na API /products e as informações para registro no Eureka feitas no application.properties.
import com.google.common.collect.ImmutableMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/products") public class ProductController { @GetMapping public List findAll() { return ImmutableMap.of( 1L, "Product 1", 2L, "Product 2", 3L, "Product 3", 4L, "Product 4"); } }
spring.application.name=products eureka.instance.hostname=localhost server.port=8070
Zuul
Por fim, a configuração do Zuul, que também é uma aplicação Spring boot.
As dependências do são: spring-cloud-starter-zuul para inicializar o Zuul na aplicação e a do Eureka para registrar a aplicação no service discovery.
org.springframework.cloud spring-cloud-starter-zuul org.springframework.cloud spring-cloud-starter-eureka
A ativação do Zuul segue os padrões anteriores, apenas anotando com @EnableZuulProxy na classe de configuração do Spring Boot, e assim o Zuul é inicializado no startup da aplicação.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableZuulProxy @EnableDiscoveryClient public class ZuulAppConfig { public static void main(String[] args) { SpringApplication.run(ZuulAppConfig.class, args); } }
As configurações também são as propriedades para definir porta e registar no Eureka, mas também as configurações do Zuul para mapear os caminho das APIs e qual seu destino.
A configuração das rotas do Zuul no application.properties segue o seguinte padrão: zuul.routes. + nome do serviço + . propriedade a ser configurada para aquele serviço. Essa configuração apenas é valida quando utilizado no Zuul no Spring Boot.
spring.application.name=zuul eureka.instance.hostname=localhost server.port=8080 zuul.prefix=/api zuul.ignored-services=* zuul.routes.customers.path=/customers/** zuul.routes.customers.serviceId=customers zuul.routes.customers.strip-prefix=false zuul.routes.products.path=/products/** zuul.routes.products.serviceId=products zuul.routes.products.strip-prefix=false
- zuul.prefix: Configura para que o contexto de entrada seja no /api, assim todos os serviços vão ser acessados pela URI /api;
- zuul.ignored-services: Quando configurado como ‘*’, todos os serviços são ignorados por padrão, assim nenhum serviço vai ser acessado pelo Zuul, apenas vão estar disponível os que estiverem mapeados explícitamente como os de customers e products;
- zuul.routes.customers.path: Define a URI para acessar os dados de customer, no caso /api/customers;
- zuul.routes.customers.serviceId: Informa o ID da aplicação registrada no Eureka, que é o valor atribuído em cada aplicação na propriedade spring.application.name do application.properties;
- zuul.routes.products.strip-prefix: Configurado com false, o prefixo do serviço configurado no path não tera nenhum efeito no path original da sua chamada, assim quando requisitado /api/customers será redirecionado para /customers.
Executando o cenário
Fazer o build do projeto rodando um mvn clean package na raiz do projeto.
mvn clean package [INFO] Reactor Summary: [INFO] [INFO] proxy-apis ......................................... SUCCESS [ 0.153 s] [INFO] eureka ............................................. SUCCESS [ 3.102 s] [INFO] zuul ............................................... SUCCESS [ 0.683 s] [INFO] customers .......................................... SUCCESS [ 0.431 s] [INFO] products ........................................... SUCCESS [ 0.560 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
Inicializar o Eureka, executando o mvn spring-boot:run dentro da pasta /eureka.
cd eureka mvn spring-boot:run s.b.c.e.t.TomcatEmbeddedServletContainer: Tomcat started on port(s): 8761 (http) s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8761 br.com.emmanuelneri.EurekaAppConfig : Started EurekaAppConfig in 6.465 seconds (JVM running for 10.401)
Após o startup, o Eureka está disponível no endereço: http://localhost:8761/eureka, onde é sua tela administrativa.
Inicializar a API de customers, executando o mvn spring-boot:run dentro da pasta /customers.
cd customers mvn spring-boot:run com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERS/192.168.11.247:customers:8060: registering service... s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8060 (http) s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8060 br.com.emmanuelneri.CustomersAppConfig : Started CustomersAppConfig in 6.736 seconds (JVM running for 10.549) com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERS/192.168.11.247:customers:8060 - registration status: 204
Após o startup, o serviço de listar customers está disponível no endereço http://localhost:8060/customers e a aplicação está registrada no Eureka como demonstrado no log acima.
Inicializar a API de products, executando o mvn spring-boot:run dentro da pasta /products.
cd products mvn spring-boot:run com.netflix.discovery.DiscoveryClient : DiscoveryClient_PRODUCTS/192.168.11.247:products:8070: registering service... s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8070 (http) s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8070 br.com.emmanuelneri.ProductsAppConfig : Started ProductsAppConfig in 5.503 seconds (JVM running for 8.769) com.netflix.discovery.DiscoveryClient : DiscoveryClient_PRODUCTS/192.168.11.247:products:8070 - registration status: 204
Após o startup, o serviço de listar products está no endereço http://localhost:8070/products e a aplicação está registrada no Eureka.
Por fim, inicializar o Zuul para fazer o proxy dos serviços.
cd zuul mvn spring-boot:run com.netflix.discovery.DiscoveryClient : DiscoveryClient_ZUUL/192.168.11.247:zuul:8080: registering service... s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080 br.com.emmanuelneri.ZuulAppConfig : Started ZuulAppConfig in 7.765 seconds (JVM running for 11.081) com.netflix.discovery.DiscoveryClient : DiscoveryClient_ZUUL/192.168.11.247:zuul:8080 - registration status: 204
Após o startup, as APIs de customers e products estão disponíveis nas URLs http://localhost:8080/api/customers e http://localhost:8080/api/products, assim as chamadas estão centralizadas em um único endereço, do Zuul, deixando transparente o acesso as APIs dos back-ends.
Conclusão
Com a adesão de microserviços ou de outras abordagens arquiteturais que tem como tendência a utilização de diversos serviços distribuídos, o Zuul é uma boa alternativa e de fácil implantação para realizar o proxy de APIs, e quando utilizado em conjunto com o Eureka oferecem o roteamento dos serviços de forma dinâmica, através do nome dos serviços registros no service discovery(Eureka), assim possibilitando maior flexibilidade na gestão dos serviços, além de preparar a estrutura para outras solução como balanciamento de carga (com o Ribbon) e circuit breaker (com hystrix).
O código fonte dos testes está disponível no github.
Sensacional, parabens
CurtirCurtir
Ótimo artigo. Talvez eu colocasse um diagrama pra mostrar o resultado da arquitetura com o proxy. Mas parabéns pelo material.
CurtirCurtir
Obrigado Carlos. Boa sugestão!
CurtirCurtir
ótimo artigo, me ajudou muito, minha dúvida é como aplicar o spring security diante de todo esse cenário, pois entre o zuul e o eureka, qual deles deveria ser implementado o websecurity do spring boot? desde já agradeço pela atenção.
CurtirCurtir
Que bom Bruno! Você pode ter um serviço responsável pela autenticação e o Zuul fazer o direcionamento e o proxy do token de autenticação entre os serviços, pois todas as chamadas passam pelo Zuul, que é responsável pelas configurações de rotas e o Eureka é o responsável por registrar os endereços reais do serviços que vão ser roteados. Tem também outro projeto da Spring, o spring-cloud-security, que ajuda simplificar o cenário de segurança em aplicações que utilizam spring cloud. Abraço!
CurtirCurtir
Como deixar dinâmico a implementação de um novo microservice no ambiente sem precisar ter que alterar os properties do Zuul ? o Zuul fornece esse tipo de implementção fora dos properties ? tem algum material sobre isso ou indica alguma coisa para eu pesquisar. Obrigado pela atenção;
CurtirCurtir
Por padrão, o roteamento dinâmico do Zuul é referente a quantidade de instâncias registradas no Eureka para um serviço, ou seja, é necessário a configuração do serviço(do microservice) no Zuul para aplicar as configurações de proxy, como path e endereço ou serviceId do destino do microservice. Acredito que para fazer uma configuração totalmente dinâmica dessa forma, você tenha que criar uma filtro customizado no Zuul, que acesse o Eureka (ou qualquer service discovery) e faça o registros das rotas, ou o service discovery notificar o Zuul a cada novo microservice.
Segue exemplos de implementações de filtros customizados:
https://github.com/spring-cloud-samples/sample-zuul-filters
Observação: Outros soluções também trabalham dessa forma, exigindo as configurações das rotas, por exemplo o AWS API Gateway também é baseado em um arquivo de configuração onde precisa informar as rotas dos serviços.
CurtirCurtir
Ótimo artigo, parabéns!
Poderia criar um novo post implementado o websecurity do spring boot, seria fodastico!
Abraço!
CurtirCurtir
Valeu! Vou adicionar aqui na lista para fazer! Abraço
CurtirCurtir
Obrigado, ajudou bastante
CurtirCurtir
Que ótimo! Fico feliz! abraço
CurtirCurtir