Spring Cloud- Netflix Zuul + Eureka Simple Example ครบ จบในบทเดียว

Chiwa Kantawong (Pea)
6 min readJun 30, 2021

--

โดยปกติแล้ว Microservice Architecture เราจะมี MS ที่ทำงานร่วมกันอยู่มากมาย และ MS ตัวนึงก็อาจจะ run อยู่หลาย ๆ instance ซึ่งหาเราจะต้อง mapping url หรือ endpoint ต่างๆ ของ MS ที่ทำงานร่วมกันแล้ว มันจะนรกขนาดไหน นั้นก็จึงเกิด Eureka เกิดขึ้นมาเพื่อจัดการเรื่องนี้ โดย Eureka Server จะทำหน้าที่เป็น Service Discovery ให้กับ MS ต่างๆ และ MS ต่างๆ ก็เรียกกันผ่าน ServiceId ที่ register ผ่านทาง Eureka Server โดยที่ไม่ต้องพะวงเลยว่า MS ที่ตัวเองเรียกใช้ จะอยู่ที่ไหน port อะไร

นอกจากนี้เรายังมี ZULL เพื่อทำหน้าที่เป็น API Gateway เพื่อให้ client ข้างนอกเรียกเข้ามาใช้ MS ของเรา โดยเราจะ Authentication & Authorization ที่ Zull เท่านั้น มันคงไม่ดีนักถ้า MS อื่นๆ เวลาจะคุยกันหรือถูกเรียกจะต้องจัดเรื่องพวกนี้ทุกๆ MS และอีกอย่าง Zull ก็จะทำหน้าที่เป็น Load Balancer ให้อัตโนมัติด้วย (Zuul uses Netflix Ribbon) และยังมี filters type อีก 4 แบบ คือ

  • pre-filters — are invoked before the request is routed
  • post-filers — are invoked after the request has been routed
  • route-filters — are used to route the request
  • error-filters — are invoked when an error occurs while handling the request.

แต่ว่าบทความนี้เราจะไม่ยุ่งกะ filters นะครับ

บทความนี้เราไม่ต้องทฤษฎีอะไรกันมากนะครับ ผมไม่ใช่ ทฤษฎีแมน เสียด้วย เห็นเวลาสัมภาษณ์คน ถามกันจัง ทำงานได้ไม่ได้ ไม่ค่อยจะสนใจกัน ขอให้ตอบเป๊ะ แบบนี้ก็อ่านหนังสือไปสัมภาษณ์ เหมือนสมัยเรียนอ่านหนังสือไปสอบกันไปเลยไหม ฮ่าๆ บ่น ๆๆๆๆๆ

โปรเจคส์เราจะใช้ Maven นะครับ และ spring-boot version และ spring-cloud.version ของแต่ละตัวก็จะอาจะไม่เหมือนกันเพราะบางอย่างมัน incompatible กัน ผมพยายามเลือกตัวใหม่ที่สุดแล้ว

  1. Eureka Server

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zengcode</groupId>
<artifactId>eureka-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-server</name>
<description>Eureka Server</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

EurekaServerApplication เราต้อง เพิ่ม @EnableEurekaServer

package com.zengcode.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

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

}

application.yml

eureka:
client:
registerWithEureka: false
fetchRegistry: false
server:
port: 8761

กำหนดค่า registerWithEureka = false เพราะว่าตัวเองเป็น server ไม่ต้อง register ไปหาตัวเอง เพียงเท่านี้เราก็จะได้ Eureka Server ขึ้นมาแล้ว เมื่อรันแล้วก็ลองเข้า

เราจะเห็นว่ายังไม่มี MS ไหน register เข้ามา ต่อไปเราจะมาสร้าง MS ซึ่งจะเป็น Eureka Client ในบทความนี้ผมจะทำ Product Service run 2 instance ซึ่งผมรันอยู่บนเครื่องเดียวกันผมจึง รันคนละ port นะครับ และก็มี Inventory Service อีก 1 instance ซึ่ง Product Service จะเรียกใช้งาน Inventory Service ซึ่ง code จะ simple มากๆ เพราะเราเน้นว่า Eureka ทำงานอย่างไร

ผมจะอธิบายที่สำคัญๆ นะครับ เพราะจะมี souce code ให้ download กัน

2. Eureka Client

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zengcode</groupId>
<artifactId>product-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product-service</name>
<description>Product Service</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Main Application จะต้อง @EnableEurekaClient และ @EnableDiscoveryClient

package com.zengcode.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class ProductServiceApplication {

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}

}

application.yml

ของ Product Service จะมีสองตัว รันสอง port คือ 8081 และ 8083

server:
port: 8081

spring:
application:
name: PRODUCT-SERVICE

eureka:
instance:
appname: PRODUCT-SERVICE
client:
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
healthcheck:
enabled: true

ของ Inventory Service

server:
port: 8082

spring:
application:
name: INVENTORY-SERVICE

eureka:
instance:
appname: INVENTORY-SERVICE
client:
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
healthcheck:
enabled: true

จะกำหนด eureka.instance.appname ให้แต่ละตัว เพื่อให้ไป register ที่ Eureka Server และก็สามารถใช้ชื่อนี้ เรียกใช้งานระหว่าง MS ได้ เช่น

โดย เราสร้าง Controller สำหรับ Product Service

package com.zengcode.product.controler;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class MainController {

@Autowired
private RestTemplate restTemplate;

@GetMapping("/product")
public String getProduct() {
String url = "http://INVENTORY-SERVICE/inventory";
int inventory = restTemplate.getForObject(url, Integer.class);
return "Server 1, SKU=123, name=Soap, inventory = " + inventory;
}
}

เราจะเห็นว่ามีการเรียก Inventory Service ใช้งานโดยผ่านชื่อที่ inventory Service ได้ Register กับทาง Eureka Server คือ INVENTORY-SERVICE

โดยที่ Product Service ไม่ต้องรู้เลยว่า Inventory Service จะ run อยู่ที่ IP ไหน Port อะไร ตัว Eureka Service จะเป็นตัวจัดการ Mapping ให้อัตโนมัติ

สำหรับ controller ของ Inventory Service ก็จะง่ายๆ เหมือนกันครับ

package com.zengcode.inventoryservice.controler;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainController {

@GetMapping("/inventory")
public int getInventory() {
return 159;
}
}

ต่อไปเรารัน Eureka Server และก็ Product Service 2 instance และ Inventory Service 1 instance

เมื่อเรากูที่ Eureka Server เราจะเห็นแบบนี้

เราลองเรียก

เราจะเห็นว่า Product Service เรียกใช้ Inventory Service ได้จากข้างล่าง

String url = "http://INVENTORY-SERVICE/inventory";
int inventory = restTemplate.getForObject(url, Integer.class);

ซึ่ง ไม่ว่า ต่อไป Inventory Service จะรันกี่ instance หรือว่า ip หรือ port จะเปลี่ยนไปยังไงไม่ว่าจะไปรันบน env ไหน ก็ไม่ต้องแก้ไข Product Service เลย

ต่อไปเราจะมาสร้าง Zuul (API Gatway) เพื่อให้ client ข้างนอกใช้นะครับ

3. Zull Serviece

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zengcode</groupId>
<artifactId>zull-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zull-service</name>
<description>Zuul Server</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<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-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Main Application ต้อง @EnableZuulProxy, @EnableEurekaClient และ @EnableDiscoveryClient

package com.zengcode.inventoryservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
@EnableDiscoveryClient
public class ZuulServiceApplication {

public static void main(String[] args) {
SpringApplication.run(ZuulServiceApplication.class, args);
}

}

ต่อไปเราจะมา config routing mapping

application.yml

server:
port: 9000

spring:
application:
name: ZUUL-SERVICE

management:
endpoints:
web:
exposure:
Include: "*" #Note here * to add quotes, expose all, you can also only expose the corresponding endpoint
endpoint:
routes:
Enabled: true # defaults to true, can be omitted

eureka:
instance:
appname: ZUUL-SERVICE
client:
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
healthcheck:
enabled: true

zuul:
#ignored-services: '*' #ignore mapping by Eureka Client Registration (automatic mapping)
routes:
product-service:
path: /product-api/**
stripPrefix: true

เรา mapping routing “/product-api/**” → Product Service (serviceId = product-service ที่ทำการ register ไว้กับ Eureka Server ไว้)

run แล้วเข้า

เราจะเห็น routing ที่เรา manual config ไป และก็ auto config ที่ Eureka จัดการให้

ต่อไปเราจะลองเข้า routing ที่เรา config ไปนะครับว่าจะสามารถเข้าถึง Product Service ได้หรือไม่

และ

จะเห็นว่าเราเข้า Product Service ผ่าน Zuul Api Gateway ได้และ Zull ก็ทำตัวเป็น Load Balancer ให้เราอัตโนมัติ คือโดย Zuul จะใช้ Ribbon แบบ Round Robbin ให้เราอัตโนมัติ

ยังไงก็โหลด ไฟล์ไปลอง run และก็ทำความเข้าใจกันนะครับ

ลิงค์ที่น่าสนใจ

ตัวอย่างการทำ Filter

package com.zengcode.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Random;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;

@Service
public class CustomPreFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (new Random().nextBoolean()) {
ctx.setResponseStatusCode(400);
ctx.setResponseBody("access denied");
ctx.setSendZuulResponse(false);
}
return ctx;
}
}

ถ้า Random ได้ true เราจะเห็นแบบนี้

--

--

Chiwa Kantawong (Pea)
Chiwa Kantawong (Pea)

Written by Chiwa Kantawong (Pea)

Software Development Expert at Central Tech

No responses yet