Descubrimiento de microservicios Spring Boot y Flask con Netflix Eureka

En esta guía, veremos cómo llamar a un microservicio Python Flask desde un microservicio Spring Boot, utilizando Netflix Eureka y su descubrimiento de servicios.

Introducción

En esta guía, utilizaremos Eureka de Netflix, un servicio de descubrimiento de microservicios para combinar un microservicio Spring Boot con un microservicio Flask, servicios puente escritos en diferentes lenguajes de programación y frameworks.

Construiremos dos servicios: el Servicio de usuario final, que es un servicio Spring Boot orientado al usuario final, que recopila datos y los envía al Servicio de agregación de datos, un servicio de Python, usando Pandas para realizar la agregación de datos y devolver una respuesta JSON al Servicio de usuario final.

Netflix Eureka Serice Discovery

Al cambiar de un código base monolítico a una arquitectura orientada a microservicios, Netflix creó una gran cantidad de herramientas que les ayudaron a revisar toda su arquitectura. Una de las soluciones internas, que posteriormente se lanzó al público se conoce como Eureka.

Netflix Eureka es una herramienta de descubrimiento de servicios (también conocida como servidor de búsqueda o registro de servicios), que nos permite registrar múltiples microservicios y maneja el enrutamiento de solicitudes entre ellos.

Es un hub central donde se registra cada servicio, y cada uno de ellos se comunica con el resto a través del hub. En lugar de enviar llamadas REST a través de nombres de host y puertos, delegamos esto en Eureka y simplemente llamamos al nombre del servicio, tal como está registrado en el concentrador.

Para lograr esto, una arquitectura típica consta de algunos elementos:

arquitectura microservicio eureka

Puede derivar el servidor Eureka en cualquier idioma que tenga un envoltorio Eureka, sin embargo, se hace de manera más natural en Java, a través de Spring Boot, ya que esta es la implementación original de la herramienta, con soporte oficial.

Cada servidor Eureka puede registrar N clientes Eureka, cada uno de los cuales suele ser un proyecto individual. Estos también se pueden hacer en cualquier lenguaje o marco, por lo que cada microservicio usa lo que es más adecuado para su tarea.

Tendremos dos clientes:

  • Servicio de usuario final (Cliente Eureka basado en Java)
  • Servicio de agregación de datos (Cliente Eureka basado en Python)

Dado que Eureka es un proyecto basado en Java, originalmente destinado a soluciones Spring Boot, no tiene una implementación oficial para Python. Sin embargo, podemos usar un envoltorio de Python impulsado por la comunidad para ello:

Con eso en mente, creemos primero un Servidor Eureka.

Creación de un servidor Eureka

Usaremos Spring Boot para crear y mantener nuestro servidor Eureka. Comencemos creando un directorio para albergar nuestros tres proyectos, y dentro de él un directorio para nuestro servidor:

1
2
3
4
$ mkdir eureka-microservices
$ cd eureka-microservices
$ mkdir eureka-server
$ cd eureka-server

El directorio eureka-server será el directorio raíz de nuestro servidor Eureka. Puede iniciar un proyecto de Spring Boot aquí a través de la CLI:

1
$ spring init -d=spring-cloud-starter-eureka-server

Alternativamente, puede usar Spring Initializr e incluir la dependencia Eureka Server:

spring initializr servidor eureka

Si ya tiene un proyecto y solo desea incluir la nueva dependencia, si está utilizando Maven, agregue:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    <version>${version}</version>
</dependency>

O si estás usando Gradle:

1
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka-server', version: ${version}

Independientemente del tipo de inicialización, el servidor Eureka requiere una anotación única para ser marcado como servidor.

En su clase de archivo EndUserApplication, que es nuestro punto de entrada con la anotación @SpringBootApplication, simplemente agregaremos @EnableEurekaServer:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

El puerto predeterminado para los servidores Eureka es 8761, y también lo recomienda el Spring Team. Aunque, por si acaso, configurémoslo también en el archivo application.properties:

1
server.port=8761

Con eso hecho, nuestro servidor está listo para funcionar. Ejecutar este proyecto iniciará el servidor Eureka, disponible en localhost:8761:

eureka server

Nota: Sin registrar ningún servicio, Eureka puede reclamar incorrectamente que una instancia DESCONOCIDA está activa.

Creación de un cliente Eureka: servicio de usuario final en Spring Boot

Ahora, con nuestro servidor activado y listo para registrar servicios, avancemos y hagamos nuestro Servicio de usuario final en Spring Boot. Tendrá un punto final único que acepta datos JSON con respecto a un Estudiante. Luego, estos datos se envían como JSON a nuestro Servicio de agregación de datos que calcula las estadísticas generales de las calificaciones.

En la práctica, esta operación sería sustituida por operaciones mucho más laboriosas, que tienen sentido que se realicen en bibliotecas dedicadas al procesamiento de datos y que justifican el uso de otro servicio, en lugar de realizarlas en el mismo.

Dicho esto, regresemos y creemos un directorio para nuestro Servicio de usuario final:

1
2
3
$ cd..
$ mkdir end-user-service
$ cd end-user-service

Aquí, comencemos un nuevo proyecto a través de la CLI e incluyamos la dependencia spring-cloud-starter-netflix-eureka-client. También agregaremos la dependencia web ya que esta aplicación en realidad estará frente al usuario:

1
$ spring init -d=web, spring-cloud-starter-netflix-eureka-client

Como alternativa, puede usar Spring Initializr e incluir la dependencia Eureka Discovery Client:

spring initializr cliente eureka

Si ya tiene un proyecto y solo desea incluir la nueva dependencia, si está utilizando Maven, agregue:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>${version}</version>
</dependency>

O si estás usando Gradle:

1
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-eureka-client', version: ${version}

Independientemente del tipo de inicialización, para marcar esta aplicación como Cliente Eureka, simplemente agregamos la anotación @EnableEurekaClient a la clase principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@SpringBootApplication
@EnableEurekaClient
public class EndUserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(EndUserServiceApplication.class, args);
    }
    
    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Nota: Como alternativa, puede usar la anotación @EnableDiscoveryClient, que es una anotación más amplia. Puede referirse a Eureka, Consul o Zookeper, según la herramienta que se utilice.

También hemos definido un @Bean aquí, de modo que podamos @Autowire el RestTemplate más adelante en nuestro controlador. Este RestTemplate se utilizará para enviar una solicitud POST al Servicio de agregación de datos. La anotación @LoadBalanced significa que nuestro RestTeamplate debe usar un RibbonLoadBalancerClient al enviar solicitudes.

Dado que esta aplicación es un cliente de Eureka, querremos darle un nombre para el registro. Otros servicios se referirán a este nombre cuando se basen en él. El nombre se define en el archivo application.properties o application.yml:

1
2
3
server.port = 8060
spring.application.name = end-user-service
eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka
1
2
3
4
5
6
7
8
9
server:
    port: 8060
spring:
    application:
        name: end-user-service
eureka:
    client:
      serviceUrl:
        defaultZone: http://localhost:8761/eureka/

Aquí, hemos configurado el puerto para nuestra aplicación, que Eureka necesita saber para enrutar las solicitudes. También hemos especificado el nombre del servicio, al que harán referencia otros servicios.

La ejecución de esta aplicación registrará el servicio en el servidor Eureka:

1
2
3
4
5
6
7
INFO 3220 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8060 (http) with context path ''
INFO 3220 --- [           main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8060
INFO 3220 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_END-USER-SERVICE/DESKTOP-8HAKM3G:end-user-service:8060 - registration status: 204
INFO 3220 --- [           main] c.m.e.EndUserServiceApplication          : Started EndUserServiceApplication in 1.978 seconds (JVM running for 2.276)
INFO 3220 --- [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_END-USER-SERVICE/DESKTOP-8HAKM3G:end-user-service:8060 - Re-registering apps/END-USER-SERVICE
INFO 3220 --- [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_END-USER-SERVICE/DESKTOP-8HAKM3G:end-user-service:8060: registering service...
INFO 3220 --- [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_END-USER-SERVICE/DESKTOP-8HAKM3G:end-user-service:8060 - registration status: 204

Ahora, si visitamos localhost:8761, podremos verlo registrado en el servidor:

eureka server registered service

Ahora, avancemos y definamos un modelo Estudiante:

1
2
3
4
5
6
7
8
9
public class Student {
    private String name;
    private double mathGrade;
    private double englishGrade;
    private double historyGrade;
    private double scienceGrade;
    
    // Constructor, getters and setters and toString()
}

Para un estudiante, querremos calcular algunas estadísticas resumidas de su desempeño, como el medio, mínimo y máximo de sus calificaciones. Dado que usaremos Pandas para esto, aprovecharemos la muy práctica función DataFrame.describe(). Hagamos también un modelo GradesResult, que contendrá nuestros datos una vez devueltos por el Servicio de agregación de datos:

1
2
3
4
5
6
7
8
public class GradesResult {
    private Map<String, Double> mathGrade;
    private Map<String, Double> englishGrade;
    private Map<String, Double> historyGrade;
    private Map<String, Double> scienceGrade;
    
    // Constructor, getters, setters and toString()
}

Con los modelos terminados, hagamos un ‘@RestController’ realmente simple que acepte una solicitud ‘POST’, la deserialice en un ‘Estudiante’ y la envíe al servicio Agregación de datos, que no tenemos hecho todavía:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Autowired
private RestTemplate restTemplate;

@RestController
public class HomeController {
    @PostMapping("/student")
    public ResponseEntity<String> student(@RequestBody Student student) {
        GradesResult grades = restTemplate.getForObject("http://data-aggregation-service/calculateGrades", GradesResult.class);

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(String.format("Sent the Student to the Data Aggregation Service: %s \nAnd got back:\n %s", student.toString(), gradesResult.toString()));
    }
}

Este @RestController acepta una solicitud POST y deserializa su cuerpo en un objeto Student. Luego, enviamos una solicitud a nuestro servicio de agregación de datos, que aún no está implementado, ya que se registrará en Eureka, y empaquetamos los resultados JSON de esa llamada en nuestro objeto GradesResult .

Nota: Si el serializador tiene problemas con la construcción del objeto GradesResult a partir del resultado dado, querrá convertirlo manualmente usando ObjectMapper de Jackson:

1
2
3
String result = restTemplate.postForObject("http://data-aggregation-service/calculateGrades", student, String.class);
ObjectMapper objectMapper = new ObjectMapper();
GradesResult gradesResult = objectMapper.readValue(result, GradesResult.class);

Finalmente, imprimimos la instancia student que enviamos, así como la instancia grades que construimos a partir del resultado.

Ahora, avancemos y creemos el Servicio de agregación de datos.

Creación de un cliente Eureka - Servicio de agregación de datos en Flask

El único componente que falta es el Servicio de agregación de datos, que acepta un Estudiante, en formato JSON y completa un DataFrame de Pandas, realiza ciertas operaciones y devuelve el resultado.

Vamos a crear un directorio para nuestro proyecto e iniciar un entorno virtual para él:

1
2
3
$ cd..
$ mkdir data-aggregation-service
$ python3 -m venv flask-microservice

Ahora, para activar el entorno virtual, ejecute el archivo activar. En Windows:

1
$ flask-microservice/Scripts/activate.bat

En Linux/Mac:

1
$ source flask-microservice/bin/activate

Haremos funcionar una aplicación Flask simple para esto, así que instalemos las dependencias para Flask y Eureka a través de pip en nuestro entorno activado:

1
(flask-microservice) $ pip install flask pandas py-eureka-client

Y ahora, podemos crear nuestra aplicación Flask:

1
$ touch flask_app.py

Ahora, abra el archivo flask_app.py e importe las bibliotecas Flask, Pandas y Py-Eureka Client:

1
2
3
from flask import Flask, request
import pandas as pd
import py_eureka_client.eureka_client as eureka_client

Usaremos Flask y request para manejar nuestras solicitudes entrantes y devolver una respuesta, así como para activar un servidor. Usaremos Pandas para agregar datos y usaremos py_eureka_client para registrar nuestra aplicación Flask en el servidor Eureka en localhost:8761.

Avancemos y configuremos esta aplicación como un cliente Eureka e implementemos un controlador de solicitud POST para los datos de los estudiantes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
rest_port = 8050
eureka_client.init(eureka_server="http://localhost:8761/eureka",
                   app_name="data-aggregation-service",
                   instance_port=rest_port)

app = Flask(__name__)

@app.route("/calculateGrades", methods=['POST'])
def hello():
    data = request.json
    df = pd.DataFrame(data, index=[0])
    response = df.describe().to_json()
    return response

if __name__ == "__main__":
    app.run(host='0.0.0.0', port = rest_port)

Nota: Tenemos que configurar el host en 0.0.0.0 para abrirlo a servicios externos, para que Flask no se niegue a conectarse.

Esta es una aplicación de Flask bastante mínima con un único @app.route(). Extrajimos el cuerpo de la solicitud ‘POST’ entrante en un diccionario de ‘datos’ a través de ‘request.json’, después de lo cual creamos un ‘DataFrame’ con esos datos.

Dado que este diccionario no tiene ningún índice, hemos configurado uno manualmente.

Finalmente, hemos devuelto los resultados de la función describe() como JSON. No hemos usado jsonify aquí ya que devuelve un objeto Response, no una cadena. Un objeto Respuesta, cuando se devuelve, contiene caracteres \ adicionales:

1
2
3
{\"mathGrade\":...}
vs
{"mathGrade":...}

Estos tendrían que escaparse, para que no se desprendan del deserializador.

En la función init() de eureka_client, hemos configurado la URL de nuestro servidor Eureka, así como el nombre de la aplicación/servicio para el descubrimiento, y también hemos proporcionado un puerto en el que se encuentra. Seré accesible. Esta es la misma información que proporcionamos en la aplicación Spring Boot.

Ahora, ejecutemos esta aplicación Flask:

1
(flask-microservice) $ python flask_app.py

Y si revisamos nuestro servidor Eureka en localhost:8761, está registrado y listo para recibir solicitudes:

eureka server registered service

Llamar al servicio Flask desde el servicio Spring Boot mediante Eureka

Con ambos servicios en funcionamiento, registrados en Eureka y capaces de comunicarse entre sí, enviemos una solicitud POST a nuestro Servicio de usuario final, que contenga algunos datos de los estudiantes, que a su vez enviará un Solicitud POST al Servicio de agregación de datos, recupere la respuesta y envíenosla:

1
$ curl -X POST -H "Content-type: application/json" -d "{\"name\" : \"David\", \"mathGrade\" : \"8\", \"englishGrade\" : \"10\", \"historyGrade\" : \"7\", \"scienceGrade\" : \"10\"}" "http://localhost:8060/student"

Esto da como resultado una respuesta del servidor al usuario final:

1
2
3
Sent the Student to the Data Aggregation Service: Student{name='David', mathGrade=8.0, englishGrade=10.0, historyGrade=7.0, scienceGrade=10.0}
And got back:
GradesResult{mathGrade={count=1.0, mean=8.0, std=null, min=8.0, 25%=8.0, 50%=8.0, 75%=8.0, max=8.0}, englishGrade={count=1.0, mean=10.0, std=null, min=10.0, 25%=10.0, 50%=10.0, 75%=10.0, max=10.0}, historyGrade={count=1.0, mean=7.0, std=null, min=7.0, 25%=7.0, 50%=7.0, 75%=7.0, max=7.0}, scienceGrade={count=1.0, mean=10.0, std=null, min=10.0, 25%=10.0, 50%=10.0, 75%=10.0, max=10.0}}

Conclusión

En esta guía, hemos creado un entorno de microservicios, donde un servicio depende de otro, y los conectamos usando Netflix Eureka.

Estos servicios se construyen utilizando diferentes marcos y diferentes lenguajes de programación, aunque, a través de las API REST, la comunicación entre ellos es directa y fácil.

El código fuente de estos dos servicios, incluido el servidor Eureka, está disponible en Github.