Friday, April 30, 2021

Stack Abuse: Spring Boot and Flask Microservice Discovery with Netflix Eureka

Introduction

In this guide, we'll be utilizing Netflix Eureka, a microservice discovery service to combine a Spring Boot microservice with a Flask microservice, bridging services written in totally different programming languages and frameworks.

We'll be building two services - The End-User Service, which is a Spring Boot service oriented at the end-user, that collects data and sends it to the Data-Aggregation Service - a Python service, using Pandas to perform data aggregation, and return a JSON response to the End-User Service.

Netflix Eureka Serice Discovery

When switching from a monolith codebase to a microservice-oriented architecture - Netflix built a plethora of tools that helped them overhaul their entire architecture. One of the in-house solutions, which was subsequently released to the public is Eureka.

Netflix Eureka a service discovery tool (also known as a lookup server or service registry), that allows us to register multiple microservices, and handles request routing between them.

It's a central hub where each service is registered, and each of them communicates with the rest through the hub. Instead of sending REST calls via hostnames and ports - we delegate this to Eureka, and simply call the name of the service, as registered in the hub.

To achieve this, a typical architecture consists of a few elements:

eureka microservice architecture

You can spin off the Eureka Server in any language that has a Eureka wrapper, though, it's most naturally done in Java, through Spring Boot, since this is the original implementation of the tool, with official support.

Each Eureka Server can register N Eureka Clients, each of which is typically an individual project. These can also be done in any language or framework, so each microservice uses what's most suitable for their task.

We'll have two clients:

  • End-User Service (Java-based Eureka Client)
  • Data-Aggregation Service (Python-based Eureka Client)

Since Eureka is a Java-based project, originally meant for Spring Boot solutions - it doesn't have an official implementation for Python. However, we can use a community-driven Python wrapper for it:

With that in mind, let's create an Eureka Server first.

Creating a Eureka Server

We'll be using Spring Boot to create and maintain our Eureka Server. Let's start off by making a directory to house our three projects, and within it a directory for our server:

$ mkdir eureka-microservices
$ cd eureka-microservices
$ mkdir eureka-server
$ cd eureka-server

The eureka-server directory will be the root directory of our Eureka Server. You can start a Spring Boot project here through the CLI:

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

Alternatively, you can use Spring Initializr and include the Eureka Server dependency:

spring initializr eureka server

If you already have a project and just wish to include the new depdenency, if you're using Maven, add:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
    <version>${version}</version>
</dependency>

Or if you're using Gradle:

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

Regardless of the initialization type - the Eureka Server requires a single anotation to be marked as a server.

In your EndUserApplication file class, which is our entry-point with the @SpringBootApplication annotation, we'll just add an @EnableEurekaServer:

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

The default port for Eureka Servers is 8761, and it's also recommended by the Spring Team. Though, for good measure, let's set it in the application.properties file as well:

server.port=8761

With that done, our server is ready to run. Running this project will start up the Eureka Server, available at localhost:8761:

eureka server

Note: Without registering any services, Eureka may incorrectly claim an UNKNOWN instance is up.

Creating a Eureka Client - End User Service in Spring Boot

Now, with our server spun up and ready to register services, let's go ahead and make our End-User Service in Spring Boot. It'll have a single endpoint that accepts JSON data regarding a Student. This data is then sent as JSON to our Data Aggregation Service that calculates general statistics of the grades.

In practice, this operation would be replaced with much more labor-intensive operations, which make sense to be done in dedicated data processing libraries and which justify the use of another service, rather than performing them on the same one.

That being said, let's go back and create a directory for our End-User Service:

$ cd..
$ mkdir end-user-service
$ cd end-user-service

Here, let's start a new project via the CLI, and include the spring-cloud-starter-netflix-eureka-client dependency. We'll also add the web dependency since this application will actually be facing the user:

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

Alternatively, you can use Spring Initializr and include the Eureka Discovery Client dependency:

spring initializr eureka client

If you already have a project and just wish to include the new depdenency, if you're using Maven, add:

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

Or if you're using Gradle:

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

Regardless of the initialization type - to mark this application as a Eureka Client, we simply add the @EnableEurekaClient annotation to the main class:

@SpringBootApplication
@EnableEurekaClient
public class EndUserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(EndUserServiceApplication.class, args);
    }
    
    @LoadBalanced
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Note: Alternatively, you can use the @EnableDiscoveryClient annotation, which is a more wide-encompassing annotation. It can refer to Eureka, Consul or Zookeper, depending on which tool is being used.

We've also defined a @Bean here, so that we can @Autowire the RestTemplate later on in our controller. This RestTemplate will be used to send a POST request to the Data Aggregation Service. The @LoadBalanced annotation signifies that our RestTeamplate should use a RibbonLoadBalancerClient when sending requests.

Since this application is a Eureka Client, we'll want to give it a name for the registry. Other services will refer to this name when relying on it. The name is defined in the application.properties or application.yml file:

server.port = 8060
spring.application.name = end-user-service
eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka
server:
    port: 8060
spring:
    application:
        name: end-user-service
eureka:
    client:
      serviceUrl:
        defaultZone: http://localhost:8761/eureka/

Here, we've set the port for our application, which Eureka needs to know to route requests to it. We've also specified the name of the service, which will be referenced by other services.

Running this application will register the service to the Eureka Server:

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

Now, if we visit localhost:8761, we'll be able to see it registered on the server:

eureka server registered service

Now, let's go ahead and define a Student model:

public class Student {
    private String name;
    private double mathGrade;
    private double englishGrade;
    private double historyGrade;
    private double scienceGrade;
    
    // Constructor, getters and setters and toString()
}

For a student, we'll want to calculate some summary statistics of their performance, such as the mean, minimum and maximum of their grades. Since we'll be using Pandas for this - we'll leverage the very handy DataFrame.describe() function. Let's make a GradesResult model as well, that'll hold our data once returned from the Data Aggregation Service:

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()
}

With the models done, let's make a really simple @RestController that accepts a POST request, deserializes it into a Student and sends it off to the Data Aggregation service, which we haven't made yet:

@RestController
public class HomeController {
    @PostMapping("/student")
    public ResponseEntity<String> student(@RequestBody Student student) {
        RestTemplate restTemplate = new RestTemplate();
        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()));
    }
}

This @RestController accepts a POST request, and deserializes its body into a Student object. Then, we're sending a request to our data-aggregation-service, which isn't yet implemented, as it will be registered on Eureka, and we pack the JSON results of that call into our GradesResult object.

Note: If the serializer has issues with constructing the GradesResult object from the given result, you'll want to manually convert it using Jackson's ObjectMapper:

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

Finally, we print the student instance we've sent as well as the grades instance we constructed from the result.

Now, let's go ahead and create the Data Aggregation Service.

Creating a Eureka Client - Data Aggregation Service in Flask

The only missing component is the Data Aggregation Service, which accepts a Student, in JSON format and populates a Pandas DataFrame, performs certain operations and returns the result back.

Let's create a directory for our project and start a virtual environment for it:

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

Now, to activate the virtual environment, run the activate file. On Windows:

$ flask-microservice/Scripts/activate.bat

On Linux/Mac:

$ source flask-microservice/bin/activate

We'll be spinning up a simple Flask application for this, so let's install the dependencies for both Flask and Eureka via pip in our activated environment:

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

And now, we can create our Flask application:

$ touch flask_app.py

Now, open the flask_app.py file and import Flask, Pandas and the Py-Eureka Client libraries:

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

We'll be using Flask and request to handle our incoming requests and return a response, as well a spin up a server. We'll be using Pandas to aggregate data, and we'll use the py_eureka_client to register our Flask application to the Eureka Server on localhost:8761.

Let's go ahead and set this application up as a Eureka Client and implement a POST request handler for the student data:

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)

Note: We have to set the host to 0.0.0.0 to open it to external services, lest Flask refuse them to connect.

This is a pretty minimal Flask app with a single @app.route(). We've extracted the incoming POST request body into a data dictionary through request.json, after which we've made a DataFrame with that data.

Since this dictionary doesn't have an index at all, we've manually set one.

Finally, we've returned the describe() function's results as JSON. We haven't used jsonify here since it returns a Response object, not a String. A Response object, when sent back would contain extra \ characters:

{\"mathGrade\":...}
vs
{"mathGrade":...}

These would have to be escaped, lest they throw off the deserializer.

In the init() function of eureka_client, we've set the URL to our Eureka Server, as well as set the name of the application/service for discovery, as well as supplied a port on which it'll be accessible. This is the same information we've provided in the Spring Boot application.

Now, let's run this Flask application:

(flask-microservice) $ python flask_app.py

And if we check our Eureka Server on localhost:8761, it's registered and ready to receive requests:

eureka server registered service

Calling Flask Service from Spring Boot Service using Eureka

With both of our services up and running, registered to Eureka and able to communicate with each other, let's send a POST request to our End-User Service, containing some student data, which will in turn send a POST request to the Data Aggregation Service, retrieve the response, and forward it to us:

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

This results in a response from the server to the end-user:

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}}

Conclusion

In this guide, we've created a microservice environment, where one service relies on another, and hooked them up using Netflix Eureka.

These services are built using different frameworks, and different programming languages - though, through REST APIs, communicating between them is straightforward and easy.

The source code for these two services, including the Eureka Server is available on Github.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...