Implementing Eureka and Zuul for Service Discovery and Dynamic Routing in JavaScript Microservices Running on Node.js

April 22, 2019
Written by
Maciej Treder
Contributor
Opinions expressed by Twilio contributors are their own

Gw87W418humMuagmwCQwtUyO2SOfwrWPG7TdI2vz7z_y86RFhQPQorXMBTcuPGWz1qCgkzBfvzX51Lgf1K1y493wa4Apwoq-y0zQf8Sig9UDXo5XaZi-y_YdGYoecOCIvX4Jfawz

Building your JavaScript applications as a collection of microservices give you a number of advantages. Your applications can be more modular, uniform, and testable as you build them and they can be more robust, scalable, and available when you deploy them to the production environment. Including a service discovery registry and dynamic routing capabilities will help you achieve scalability and availability in the production.

This post will show you how to integrate service discovery and intelligent routing into a Node.js application built with a microservices architecture. You'll see how you can do this with two Netflix open source projects, Eureka and Zuul, that run in the Java SE Runtime Environment.

The Netflix Eureka server provides service discovery. This gives your application's services the ability to find other services without knowing where they are hosted or the full URL required to reach them, so you don't have to provide complete URLs for each service that needs to reach another service.

The Netflix Zuul service provides dynamic routing. Using Zuul in your app enables your services to use the information from the Eureka service directory to reach other services.

Because both Eureka and Zuul are Java applications they can be implemented with Spring Boot, part of the  Spring Framework for Java. With Spring Boot you can package Java applications in Java Archive (.jar) file that can be run in the Java SE Runtime Environment. This enables you to easily deploy your Node.js server in a container along with Eureka and Zuul.

Spring has built configurations for common usage scenarios in the Spring Cloud Netflix, including Service Discovery with Eureka and Intelligent Routing with Zuul. The .jar files are built with Apache Maven using Project Object Model (pom.xml) files for Eureka and Zuul available as part of the Spring Guides repository on GitHub.

Prerequisites

To accomplish the programming tasks in this post you will need the following:

To learn most effectively from this post you should have the following:

  • Working knowledge of JavaScript and Node.js
  • Some exposure to the HTTP protocol

There is a companion repository for this post available on GitHub.

Build the basic JavaScript distributed system

If you have completed the project from the first post in this series, Building JavaScript Microservices with Node.js, you can continue with the code you wrote for that post: the Node.js project for this post is built on that code base. If you are familiar with building JavaScript microservices, or want to start fresh, you can get the code from GitHub.

Clone the project by executing the following command-line instructions in the directory where you would like to create the root project directory:

git clone https://github.com/maciejtreder/introduction-to-microservices.git
cd introduction-to-microservices/heroes
git checkout step2
npm install
cd ../threats
npm install

Register services with Eureka

The existing application is a simple system with two services and hardcoded URLs. What if you want to add more instances of heroes-service? How could threats-service determine which one to use? Should you hardcode two URLs?

There's a better way. With Eureka you can register your services so other services don't have to rely on hardcoded URLs to find them, even when there are multiple instances of a service running on different servers in different locations.

Zuul will provide the intelligent routing needed to reach the services listed in the registry so you don't need to create and maintain a complex and brittle routing system.

The architecture you are going to implement is illustrated in the following diagram:

Eureka architecture

There will be two kinds of communication flow in the completed application. First, there will be a continuously running sequence represented by the green lines. Whenever service is running it registers with Eureka and sends a heartbeat, informing Eureka that it is up and running. Zuul will poll Eureka for all available services and map them to specific routes.

The second kind of flow, represented by the blue lines, occurs when the system receives a request:

  1. A request is made to assign a hero to a threat!
  2. Zuul, based on information obtained from Eureka, provides this request to the desired service, threats-service,
  3. The threats-service sends Zuul a request to “tell the hero service to set the designated hero's status to ‘busy’”.
  4. Zuul forwards that request to the heroes-service.

Download the following Java archive files and place them in your project's root directory:

eureka-service-0.0.1-SNAPSHOT.jar (42.6 MB)

zuul-0.0.1-SNAPSHOT.jar (39.6 MB)

Take a look at the Eureka user interface by executing the following command-line instruction in the root directory of your application. Windows users should execute the instruction in a Windows Command Prompt (cmd.exe) window, not a PowerShell window.

java -jar eureka-service-0.0.1-SNAPSHOT.jar

Navigate to the Eureka application at http://localhost:8761 with your favorite browser.

Eureka application

By default, the Eureka service is running on port 8761. This can be  changed by using the --server.port parameter; but don't do this unless you have a good reason, like a port conflict.

Register a service instance in Eureka using a RESTful API

Although services can be configured with Eureka using the  Spring Framework, Eureka also has a  RESTful API that can be used with non-Java applications. To register a new service you can perform a POST request against the http://localhost:8761/eureka/apps/my-service endpoint. This can be done with a curl command, as demonstrated below.

In the root directory for the application, create a eureka-curl-payload.json file and insert the following JSON data:

{
    "instance": {
        "hostName": "localhost",
        "app": "MY-SERVICE",
        "vipAddress": "my-service",
        "instanceId": "unique-instance-id",
        "ipAddr": "0.0.0.0",
        "status": "UP",
        "port": {
            "$": 8585,
            "@enabled": true
        },
        "dataCenterInfo": {
            "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
            "name": "MyOwn"
        }
    }
}

The value for app, shown as "my-service", is the handle used for the service name. All the included data elements are required to register a service with Eureka.

Execute the following curl command-line instruction in the root directory of the application:

curl -i --request POST --header "Content-Type: application/json" --data @eureka-curl-payload.json http://localhost:8761/eureka/apps/my-service

After executing this request you should see the following in the terminal output:

HTTP/1.1 204 
Content-Type: application/xml
Date: Mon, 22 Apr 2019 13:37:43 GMT

Also, my-service should be listed in the Instances currently registered with Eureka table at http://localhost:8761, as shown below:

Instances registered with Eureka

Note that you may not see your service listed if you don't refresh the browser tab quickly after changing the contents of the JSON file. The Eureka application is configured by default to automatically unregister clients if they don’t send a heartbeat request within 60 seconds. If this happens to you, resend the curl request.

Leave the console window open and leave Eureka running. You'll be needing both again.

Here's an explanation of the information Eureka needs to properly register a service instance:

hostname – hostname of the service

app – name of the service (must be equal name from the URI)

vipAdress – Virtual hostname

instanceId – unique id of the service instance

ipAddr – IP address of the machine where the instance is running (Use 0.0.0.0 to use the hostname instead of an IP address.)

status – status of the service (UP, DOWN, STARTING, OUT_OF_SERVICE, UNKNOWN)

port – a JSON object containing information about the port on which service instance is running

dataCenterInfo – a property required when running Eureka in the Amazon Web Services environment. It should be set to ‘cloud’ when running on AWS.

Register the Node.js application in Eureka

You are ready now to implement a registration mechanism in the case study project. Because you are going to use the same mechanism in both the heroes-service and threats-service services, you can create a separate npm project which will be used by both services to register themselves with Eureka.

Create a /eureka-helper directory at the same level as the /heroes and /threats directories.

Change to the /eureka-helper directory and create a eureka-helper.js file.

In the /eureka-helper directory, initialize the npm project and install the necessary dependencies by executing the following command-line instructions:

npm init -y
npm install request 
npm install ip

Insert following JavaScript code in the  /eureka-helper/eureka-helper.js file:

const request = require('request');
const ip = require('ip');

const eurekaService = `http://localhost:8761/eureka`;

module.exports = {
   registerWithEureka: (appName, port) => {
       console.log(`Registering ${appName} with Eureka`);
       request.post({
           headers: {'content-type': 'application/json'},
           url: `${eurekaService}/apps/${appName}`,
           body: JSON.stringify({
               instance: {
                   hostName: `localhost`,
                   instanceId: `${appName}-${port}`,
                   vipAddress: `${appName}`,
                   app: `${appName.toUpperCase()}`,
                   ipAddr: ip.address(),
                   status: `UP`,
                   port: {
                       $: port,
                       "@enabled": true
                   },
                   dataCenterInfo: {
                       "@class": `com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo`,
                       name: `MyOwn`
                   }
               }
           })
       },
       (error, response, body) => {
           if(!error) {
               console.log(`Registered with Eureka.`);
               setInterval(() => {
                   request.put({
                       headers: {'content-type': 'application/json'},
                       url: `${eurekaService}/apps/${appName}/${appName}-${port}`
                   }, (error, response, body => {
                       if (error) {
                           console.log('Sending heartbeat to Eureka failed.');
                       } else {
                           console.log('Successfully sent heartbeat to Eureka.');
                       }
                   }));
               }, 50 * 1000);
      
           } else {
               console.log(`Not registered with eureka due to: ${error}`);
           }
       });
   }
};

The eureka-helper project exposes the registerWithEureka method, which performs the HTTP POST request you tried while testing Eureka. When the method gets a positive response it starts sending a heartbeat to Eureka to keep it from unregistering the service, as noted above. This is done in the setInterval method:

setInterval(() => {
    request.put({
        headers: {'content-type': 'application/json'},
        url: `${eurekaService}/apps/${appName}/${appName}-${port}`
    }, (error, response, body => {
        if (error) {
            console.log('Sending heartbeat to Eureka failed.');
        } else {
            console.log('Successfully sent heartbeat to Eureka.');
        }
    }));
}, 50 * 1000);

Now you can call the registerWithEureka method from each of the services.

Modify the  /threats/threats.js file so the last three lines of code are as follows:

require('../eureka-helper/eureka-helper').registerWithEureka('threats-service', port);

console.log(`Threats service listening on port ${port}`);
app.listen(port);

Modify the /heroes/heroes.js file so the last three lines read as follows. Note that one of the arguments for registerWithEureka is different:

require('../eureka-helper/eureka-helper').registerWithEureka('heroes-service', port);

console.log(`Heroes service listening on port ${port}`);
app.listen(port);

Open a console window in the /heroes directory and execute the following command-line instruction to start the service. Windows users should use a command (cmd.exe) window, not PowerShell:

node heroes.js 3838

The output should look like the following:

Registering heroes-service with Eureka
Heroes service listening on port 3838
Registered with Eureka.

Leave this console window open. You'll eventually have five console windows open, so you may want to start arranging your desktop now so you can see them along with your browser tabs if you have sufficient space.

Open another console window in the /threats directory and start the service with the following command-line instruction:

node threats.js 3939

The output should look like the following:

Threats service listening on port 3939
Registering threats-service with Eureka
Registered with Eureka.

In your browser, look at the tab for Eureka (http://localhost:8761) and verify both of the services are listed under Instances currently registered with Eureka, as follows:

Eureka services instances registered

If you haven't been following along with the coding and you want to catch up to this step using the code from the GitHub repository, execute the following commands in the directory where you’d like to create the project directory:

git clone https://github.com/maciejtreder/introduction-to-microservices.git
cd introduction-to-microservices/heroes
git checkout step3
npm install
cd ../threats
npm install
cd ../eureka-helper
npm install
cd ..

Find services with Zuul

Now that the services are registered with Eureka they can be located with Zuul's dynamic routing capabilities.

Open a new console window (this will be the fourth command window you'll have open) in the root directory of the application.

Start the Zuul service by executing the following command-line instruction in a new console window. This will be the fifth console window you'll have open:

java -jar zuul-0.0.1-SNAPSHOT.jar

This command will start Zuul on HTTP port 8080. By default, Zuul will look for the Eureka service at http://localhost:8761. If you've changed the port for Eureka you can override Zuul's default settings by using inline arguments, as follows, where the first port number is Zuul's and the second is Eureka's:

java -jar zuul-0.0.1-SNAPSHOT.jar --server.port=9090 --eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

Open a new browser tab and navigate to http://localhost:8080/routes (or the port to which you've assigned Zuul, if you've changed it). You should see list of routes registered in Zuul as follows:

{"/hero-service/**":"heroes-service","/threats-service/**":"threats-service"}

Eureka JSON

Leave the Zuul service running.

Use Zuul to find heroes-service

Zuul is running, but threats-service needs to be changed to use Zuul to find the heroes-service; threats-service is currently using a hardcoded URL, which is not very scalable. Changing the service to use Zuul is easy.

In the console window in which you're running threats-service, stop it and leave the window open.

Find the following line in the threats/threats.js file:

const heroesService = 'http://localhost:8081';

Change it so that it points to the port on which Zuul is running, followed by the name of heroes-service as it was registered in eureka-helper/eureka-helper.js:

const heroesService = 'http://localhost:8080/heroes-service';

Save the file and restart threats-service.

Verify Eureka and Zuul are working

You now have a service directory and dynamic routing in place in your application. You can see it in action by executing a post command that assigns a specific "hero" to a specific "threat", which marks the hero as busy saving the world from that particular peril.

You can use Postman, curl, PowerShell Invoke-WebRequest, or your browser. If you'd like to use curl, open a console window and execute the following command-line instruction:

curl -i --request POST --header "Content-Type: application/json" --data "{\"heroId\": 1, \"threatId\": 1}" localhost:8080/threats-service/assignment

Note that you'll need to change the port number if you changed the port number on which you're running Zuul.

If the service is working correctly you should see results similar to the following console output from curl:

HTTP/1.1 202 
X-Application-Context: application:8080
X-Powered-By: Express
ETag: W/"79-ER1WRPW1305+Eomgfjq/A/Cgkp8"
Date: Fri, 05 Apr 2019 18:05:54 GMT
Content-Type: application/json;charset=utf-8
Transfer-Encoding: chunked

{"id":1,"displayName":"Pisa tower is about to collapse.","necessaryPowers":["flying"],"img":"tower.jpg","assignedHero":1}

In the console window for heroes-service you should see output similar to:

Heroes service listening on port 3838
Registered with Eureka.
Successfully sent heartbeat to Eureka.
Successfully sent heartbeat to Eureka.
Set busy to true in hero: 1

Congratulations! You now have a more scalable and robust microservices application!

If you want to catch up to this step using the code from the GitHub repository, execute the following commands in the directory where you’d like to create the project directory:

git clone https://github.com/maciejtreder/introduction-to-microservices.git
cd introduction-to-microservices/heroes
git checkout step4
npm install
cd ../threats
npm install
cd ../eureka-helper
npm install
cd ..

Summary

In this post you learned how to use service discovery in Eureka. You saw how to delegate responsibility for different tasks to separate applications and to communicate between services. Both applications communicate with each other by exposed REST APIs. Each manipulates only the data for which it is responsible and can be maintained, extended, and deployed without involving the other service. Both applications are accessible under the same domain, localhost:8080. To access them you need only add their designated handles to the URL.

Additional Resources

Architectural Styles and the Design of Network-based Software Architectures, Roy Thomas Fielding, 2000 – Fielding’s doctoral dissertation describes Representational State Transfer (chapter 5) and other architectural styles.

Microservices – Although flawed, the Wikipedia article is a good starting place for finding more information about microservices architecture and implementation.

Node.js reference documentation

Spring Cloud NetflixSpring Cloud Netflix project " … provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms."

Maciej Treder is a Senior Software Development Engineer at Akamai Technologies. He is also an international conference speaker and the author of @ng-toolkit, an open source toolkit for building Angular progressive web apps (PWAs), serverless apps, and Angular Universal apps. Check out the repo to learn more about the toolkit, contribute, and support the project. You can learn more about the author at https://www.maciejtreder.com. You can also contact him at: contact@maciejtreder.com or @maciejtreder on GitHub, Twitter, StackOverflow, and LinkedIn.