Spring Cloud- Netflix Zuul + Eureka Simple Example ครบ จบในบทเดียว
โดยปกติแล้ว 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 กัน ผมพยายามเลือกตัวใหม่ที่สุดแล้ว
- 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 เราจะเห็นแบบนี้