¿Estas desarrollando un sistema basado en microservicios? ¿Tenes que consumir múltiples servicios? ¿No conoces la ubicación de red donde se despliegan? ¡Entonces necesitas implementar el patrón de descubrimiento de servicios! En este post veremos cómo hacerlo usando Spring Boot y Netflix Eureka, ¡manos a la obra!
Consumir servicios en la nube
En una arquitectura de microservicios, una aplicación se suele dividir en múltiples módulos ejecutables que poseen una responsabilidad bien definida llamados servicios. Estos módulos como instancias independientes de la aplicación definen un API mediante el cual se comunican con otros módulos del sistema (o de otro sistema) para cumplir con una funcionalidad definida por los requerimientos del negocio.
Esto deriva eventualmente en que para resolver un caso de uso, probablemente muchos servicios deban colaborar (es decir, comunicarse) y efectuar la operaciones necesarias.
En un contexto de despliegue de la aplicación donde:
- Los servicios se instancian, se replican y se destruyen de forma dinámica.
- La cantidad de servicios que existen y colaboran entre sí es elevada.
- Ambas cosas.
La configuración de acceso a los distintos servicios deja de ser una tarea trivial. Parte de la solución a este problema la provee la utilización de un servidor central de configuraciones (como vimos en este post) sin embargo, con esto no alcanza.
Para solucionar este problema podemos implementar el patrón de descubrimiento de servicios:
Este patrón tiene como elemento clave al registro de servicios.
Un registro de servicios puede modelarse como un componente encargado de mantener una tabla volátil que contiene un mapeo entre el nombre de los distintos servicios que conforman la nube de microservicios y las ubicaciones de red en las que están desplegados y escuchando peticiones:
Este registro de servicios, en la implementación que vamos a ver, es actualizada por los distintos servicios clientes, que se registran y reciben como respuesta un listado con todos los servicios disponibles en ese momento.
Adicionalmente, cada cliente implementa algún tipo de mecanismo que le avisa periódicamente al registro de servicios que la instancia sigue “viva”, a este mecanismo se lo suele denominar “heartbeat”.
La forma de implementar el patrón de diseño de service discovery descrita se denomina client side service discovery y es la implementación que sigue Netflix Eureka, la tecnología que vamos a ver en este post. Cabe destacar que en contraposición a esta forma de implementación podemos hacer una implemementación desde el lado del servidor denominada server side service discovery que no vamos a tratar aquí por brevedad.
Implementación del registro de servicios
Para esta implementación de un registro de servicios vamos a usar Spring Boot junto a Spring Cloud Config y en particular la dependencia de Netflix Eureka Server.
Primero vamos a necesitar agregar al pom.xml de nuestra aplicación de spring boot el siguiente gestor de dependencias :
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Hecho esto, necesitaremos las siguientes dependencias en nuestro proyecto:
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
</dependencies>
La dependencia de spring cloud config no es necesaria realmente, pero en este ejemplo veremos como se integra con el servidor de configuraciones que ya implementamos en un post anterior de esta serie.
En el archivo bootstrap.yml necesitaremos la siguiente configuración para que nuestro registro de servicios obtenga su configuración del servidor de configuraciones:
spring.application.name: lab-4-eureka-server
spring.cloud.config.uri: http://localhost:8001
En el archivo de properties alojado en el repositorio de configuraciones necesitamos la siguiente configuración:
server.port: 8010 eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Esta configuración le dice a Netflix Eureka que instancie a nuestro servidor de registros de forma local y que funcione de forma autónoma sin tratar de conectarse a otros registros de servicio (comportamiento que tiene por defecto por cuestiones de disponibilidad del servicio).
Finalmente, en el main de nuestra aplicación necesitamos de la anotación @EnableEurekaServer:
package com.example.lab4eurekaserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class Lab4EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(Lab4EurekaServerApplication.class, args); } }
Implementación de servicio:
Vamos a necesitar del mismo gestor de dependencias que utilizó el registro de servicios y las siguientes dependencias:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</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-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
El servicio cliente, como cualquier otra aplicación que estamos implementando, obtiene su configuración del servidor de configuraciones, para ello necesitará del archivo bootstrap.yml:
spring.application.name: lab-4-nombres-api spring.cloud.config.uri: http://localhost:8001
En el servidor de configuraciones podemos tener un archivo con el nombre application.yml, las configuraciones que estén en este archivo serán servidas a todos los clientes que requieran una configuración, sin importar su nombre:
eureka.client.serviceUrl.defaultZone: http://localhost:8010/eureka/ server.port: ${PORT:${SERVER_PORT:0}}
La primer configuración es la ubicación del servidor Eureka. La segunda le dice a nuestra aplicación que se levante en un puerto aleatorio disponible. Esto nos permitirá levantar más de una instancia de forma dinámica en un mismo servidor.
La aplicación, para registrarse como un servicio, sólo necesita de la anotación @EnableDiscoveryClient en su clase main:
package com.example.lab4nombresapi; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Implementación de cliente del servicio:
La implementación de un cliente de nuestros servicios es exactamente igual a la de un servicio, a fin de cuentas, un servicio puede actuar como cliente de otro. Ahora bien, ¿cómo consumimos un servicio registrado?
El siguiente ejemplo nos da una muestra sencilla:
package com.example.lab3client.controller; import java.net.URI; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class NombresEquiposController { @Autowired private DiscoveryClient client; @GetMapping("/") public String mostrarSaludo() { return obtenerString("LAB-4-NOMBRES-API"); } private String obtenerString(String service) { List<ServiceInstance> list = client.getInstances(service); if (list != null && list.size() > 0) { URI uri = list.get(0).getUri(); if (uri != null) { return (new RestTemplate()).getForObject(uri, String.class); } } return null; } }
Usando una instancia de DiscoveryClient vamos a poder acceder al listado de servicios registrados que fue provisto a la aplicación cliente por el servidor Netflix Eureka.
Esto es todo por el momento, en futuros artículos veremos una forma más amigable de consumir estos servicios en lugar de acceder directamente al DiscoveryClient y crear restTemplates a mano.
¡Hasta la próxima!