API Gateway Service
사용자가 설정한 라우팅 설정에 따라서 각각 엔드 포인트로 클라이언트를 대신해서 요청하고 응답을 받으면 다시 클라이언트에게 전달을 하는 프록시 역할을 하게 된다. 시스템의 내부 구조는 숨기고 외부 요청에 대해서 적절한 형태로 가공해서 응답할 수 있는 장점이 있다.
클라이언트는 오직 게이트 웨이만 상대하게 된다.
- 인증 및 권한 부여
- 서비스 검색 통합 (마이크로 서비스 검색 통합)
- 응답 캐싱
- 정책, 회로 차단기 및 QoS 다시 시도
- 속도 제한
- 부하 분산 (로드 밸런싱)
- 로깅, 추적, 상관 관계
- 헤더, 쿼리 문자열 및 청구 변화
- IP 허용 목록에 추가
Netflix Ribbon과 Zuul
Netflix Ribbon
Spring Cloud에서 MSA간 통신
1. RestTemplate
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/", User.class, 200);
2. Feign Client
@FeignClient("stores")
public interface StoreClinet{
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
}
Ribbon -> 비동기가 되지 않아서 최근에 잘 사용하지 않음
Client side Load Balancer (클라이언트 측에서 마이크로 서비스 주소를 관리)
- 서비스 이름으로 호출 (ip, port 알지 않아도 됨)
- Health Check
Spring Cloud Ribbon 은 Spring boot 2.4에서 Maintenance 상태 (모듈에 더 이상 새로운 기능을 추가하지 않음. 지원 X)
Netflix Zuul
게이트 웨이 역할 (클라이언트는 Zuul을 상대. Routing, API gateway)
Spring Cloud Zuul 은 Spring boot 2.4에서 Maintenance 상태 (모듈에 더 이상 새로운 기능을 추가하지 않음. 지원 X)
Ribbon 과 Zuul이 어떤 역할을 하는지 Spring Boot 버전을 낮춰서 확인
Netflix Zuul 구현
First Service, Second Serivce
- Spring Boot : 2.3.8
- Dependencies : Lombok, Spring Web, Eureka Discovery Client
server:
port: 8081
spring:
application:
name: my-first-service
eureka:
client:
fetch-registry: false
register-with-eureka: false
server:
port: 8082
spring:
application:
name: my-second-service
eureka:
client:
fetch-registry: false
register-with-eureka: false
Zuul Service
- Spring Boot : 2.3.8
- Dependencies : Lombok, Spring Web, Zuul
server:
port: 8000
spring:
application:
name: my-zuul-service
zuul:
routes:
first-service:
path: /first-service/**
url: http://localhost:8081
second-service:
path: /second-service/**
url: http://localhost:8082
package com.example.zuulservice.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@Component
public class ZuulLoggingFilter extends ZuulFilter {
@Override
public Object run() throws ZuulException {
log.info("**************** printing logs: ");
RequestContext ctx = RequestContext.getCurrentContext(); // Request 정보를 가진 최상위 객체
HttpServletRequest request = ctx.getRequest(); // Request 정보
log.info("**************** " + request.getRequestURI());
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
}
ZuulFilter
비지니스 서비스의 사전 처리(예: 인증), 사후 처리(예: 로깅)를 해줌
run 메서드 - 실제 어떤 동작을 하는지
filterType 메서드 - pre(사전 필터), post(사후 필터)
Spring Cloud Gateway
Zuul에서 호환성 문제가 있던 비동기 처리 가능해졌다.
apigateway-service
- Spring Boot : Zuul 사용하지 않기 때문에 최신 버전 사용 가능
- DevTools, Eureka, Discovery Client, Gateway
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
Netty started on port 8000 -> 기존에는 톰캣서버가 작동했는데 gateway에선 비동기 서버 작동
http://localhost:8000/first-service/welcome으로 요청이 들어오면 인스턴스 서버에 넘겨줄때 기존과 다르게 그대로 넘겨주게 되어 404 에러 발생
-> 인스턴스 서버들의 RequestMapping도 first-service, second-service로 설정해줘야 한다.
Spring Cloud Gateway - Filter
filter 작업 방식 두가지
- java code
- property(application.yml)
java code
package com.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/first-service/**")
.filters(f -> f.addRequestHeader("first-request", "first-request-header")
.addResponseHeader("first-response", "first-response-header"))
.uri("http://localhost:8081"))
.route(r -> r.path("/second-service/**")
.filters(f -> f.addRequestHeader("second-request", "second-request-header")
.addResponseHeader("second-response", "second-response-header"))
.uri("http://localhost:8082"))
.build();
}
}
addRequestHeader - 클라이언트가 header에 넣지 않은 값이 필요하다면 넣을 수 있다.
addResponseHeader - 클라이언트에게 값을 넘겨줄때 헤더값을 넣어서 반환할 수 있다.
property
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
filters:
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
filters:
- AddRequestHeader=second-request, second-request-header2
- AddResponseHeader=second-response, second-response-header2
predicates:
- Path=/second-service/**
@RequestHeader 로 헤더에 어떤 값이 들어있는지 확인
@GetMapping("/message")
public String message(@RequestHeader("first-request") String header) {
log.info(header);
return "Hello World in First Service.";
}
@GetMapping("/message")
public String message(@RequestHeader("second-request") String header) {
log.info(header);
return "Hello World in Second Service.";
}
Eureka의 정확한 용도는 Service Registry와 Discovery의 역할이며, Gateway는 Routing을 처리한다고 보는 것이 맞습니다. Load Balancer를 위해서는 외부의 Service Mesh를 사용하시거나, 기존의 Ribbon을 대신하는 Spring Cloud LoadBalancer(https://spring.io/blog/2020/03/25/spring-tips-spring-cloud-loadbalancer)를 사용해 보시면 좋을 것 같습니다.
Custom Filter
package com.example.apigatewayservice.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
// CustomFilter는 상속 받아야 함
public CustomFilter() {
super(Config.class);
}
public static class Config {
// configuration 정보있다면 내부 클래스 이용해서 넣을 수 있음
}
@Override
public GatewayFilter apply(Config config) {
// Custom PreFilter
return (exchange, chain) -> {
// netty 라는 비동기 방식의 서버 사용 중 (tomcat X)
// ServerHttp 라는 객체를 사용 (ServletHttp X)
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom Pre Filter: request id -> {}", request.getId());
// Custom Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// chain.filter(exchange) 필터 적용
// 이전에 썼던 ServletRequest, Response 가 아니라 스프링 5에서 지원되는 Web Flux 를 이용해서
// 서버를 구축할 때 반환값으로 Mono데이터 타입을 사용할 수 있다. (데이터타입을 하나 주겠다는 의미)
// Mono : 0 ~ 1개 데이터 전달, Flux : 0 ~ N개 데이터 전달
log.info("Custom Post Filter: response code -> {}", response.getStatusCode());
}));
};
}
}
chain.filter(exchange) 어떤 필터를 적용할지 선택
application.yml 에도 넣을 수 있고 config 파일에도 넣을 수 있는데 여기선 application.yml 에 적용
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
filters:
# - AddRequestHeader=first-request, first-request-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
filters:
# - AddRequestHeader=second-request, second-request-header2
# - AddResponseHeader=second-response, second-response-header2
- CustomFilter
predicates:
- Path=/second-service/**
Global Filter
일반적인 Filter와 동일한 방법으로 만들지만 모든 route에 공통으로 적용되어야 해서 개별적으로 적용하지 않고 한번에 적용 가능
모든 Filter 중에 가장 먼저 실행되고 가장 마지막에 종료된다.
package com.example.apigatewayservice.filter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter() {
super(Config.class);
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Global Filter baseMessage: {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Global Filter Start: request id -> {}", request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("Global Filter End: response code -> {}", response.getStatusCode());
}
}));
};
}
}
Config 파라미터 preLogger, postLogger 정보는 application.yml 파일에서 선언
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: http://localhost:8081/
filters:
# - AddRequestHeader=first-request, first-request-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
filters:
# - AddRequestHeader=second-request, second-request-header2
# - AddResponseHeader=second-response, second-response-header2
- CustomFilter
predicates:
- Path=/second-service/**
application.yml 파일에 있는 정보는 내장되어 있는 데이터라서 데이터가 변경되면 새롭게 갱신해줘야 함
but 외부에 있는 데이터를 반영할 수 도 있다. 추후에 할 예정
Logging Filter
package com.example.apigatewayservice.filter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() {
super(Config.class);
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
@Override
public GatewayFilter apply(Config config) {
// return (exchange, chain) -> {
// ServerHttpRequest request = exchange.getRequest();
// ServerHttpResponse response = exchange.getResponse();
//
// log.info("Logging Filter baseMessage: {}", config.getBaseMessage());
//
// if (config.isPreLogger()) {
// log.info("Logging Filter Start: request id -> {}", request.getId());
// }
//
// return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// if (config.isPostLogger()) {
// log.info("Logging Filter End: response code -> {}", response.getStatusCode());
// }
// }));
// };
/*
람다식이 어떻게 구현되어 있는지 확인
GatewayFilter를 반환해야 함
GatewayFilter는 인스턴스라서 new OrderedGatewayFilter()로 생성해야함
spirng web flex는 servlet을 지원하지 않아서 serverRequest, serverResponse를 사용해야함
*/
GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Logging Filter baseMessage: {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Logging Pre Filter Start: request id -> {}", request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("Logging Post Filter End: response code -> {}", response.getStatusCode());
}
}));
}, Ordered.HIGHEST_PRECEDENCE);
return filter;
}
}
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: http://localhost:8081/
filters:
# - AddRequestHeader=first-request, first-request-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
filters:
# - AddRequestHeader=second-request, second-request-header2
# - AddResponseHeader=second-response, second-response-header2
- name: CustomFilter
- name: LoggingFilter # 변수가 있으면 name으로 설정해줘야 한다.
args:
baseMessage: Hi, there.
preLogger: true
postLogger: true
predicates:
- Path=/second-service/**
원래는 Global -> Custom -> Logging 순으로 진행되어야 하지만 Ordered.HIGHEST_PRECEDENCE로 우선순위를 잡아서 위와 같은 실행 순서로 실행된다.
Ordered.HIGHEST_PRECEDENCE (우선순위 가장 높음) < - > Ordered.LOWEST_PRECEDENCE (우선순위 가장 낮음)
Spring Cloud Gateway - Eureka 연동
Eureka라는 naming service에 지금까지 등록한 service 등록
Cloud Gateway 어플리케이션을 Eureka Server에 등록하는 이유??
spring cloud gateway에서 사용자의 요청 정보를 해당 서비스로 직접 이동 (http://~) 하는 경우라면, spring cloud gateway와 registry service(유레카 등)는 서로의 연결되지 않아도 됩니다. 대신, gateway에서 서비스의 이동을 직접하는 방식이 아니라, registry service에 전달해서 해야 할 경우 (라우팅 정보 등록 시 LB라고 등록합니다)에는 spring cloud gateway와 registry service가 서로 연결되어 있어야 합니다. 즉, 말씀하신 내용처럼 fetch-registry 설정으로 유레카로부터 주기적으로 서비스 인스턴스들의 정보를 갱신해 줘야 합니다. 라우팅 정보에 LB를 사용하게 되면, 유레카에 등록된 서비스가 여러 개의 서비스로 구성되어 있는 경우, Load Balancing (부하 분산) 처리를 지원합니다.
pom.xml(apigateway-service, first-service, second-service 모두)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml(apigateway-service, first-service, second-service 모두)
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
application.yml (apigateway-service)
uri 설정
http 프로토콜을 이용하는 것이 아니라 Eureka Server로 가서 요청하게 한다.
lb -> DiscoverySerivce으로 가서 해당하는 이름을 찾겠다.
uri: lb://MY-FIRST-SERVICE
uri: lb://MY-SECOND-SERVICE
Eureka Server 기동 후 실행 (discoveryservice)
Spring Cloud Gateway - Load Balancer
First Service, Second Service를 각각 2개씩 기동해서 로드밸런스 동작
방법
1. VM Options -> -Dserver.port=[다른 포트] (인텔리제이 내부에서)
2. $ mvn spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9003' (메이븐 실행)
3. $mvn clean compile package (메이븐 컴파일 해서 jar 파일 실행)
$ java -jar -Dserver.port=9004 ./target/user-service-0.0.1-SNAPSHOT.jar
포트번호를 0으로 바꿔서 random port 사용 -> Eureka Server에서 인스턴스 하나밖에 확인되지 않음
인스턴스 Id 추가해서 해결
server:
port: 0
spring:
application:
name: my-first-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${spring.application.instance_id:${random.value}}
prefer-ip-address: true
둘 중에 어떤 인스턴스가 실행될까??
Environment(환경 변수) 로 확인
package com.example.firstservice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Enumeration;
@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController {
Environment env;
@Autowired
public FirstServiceController(Environment env) {
this.env = env;
}
@GetMapping("/welcome")
public String welcome() {
return "Welcome to the First service.";
}
@GetMapping("/message")
public String message(@RequestHeader("first-request") String header) {
log.info(header);
return "Hello World in First Service.";
}
@GetMapping("/check")
public String check(HttpServletRequest request) {
log.info("Server port={}", request.getServerPort());
log.info("spring.cloud.client.hostname={}", env.getProperty("spring.cloud.client.hostname"));
log.info("spring.cloud.client.ip-address={}", env.getProperty("spring.cloud.client.ip-address"));
return String.format("Hi, there. This is a message from First Service on PORT %s"
, env.getProperty("local.server.port"));
}
}
호출할 때마다 다른 포트번호로 실행되는거 확인할 수 있다.
gateway 를 사용하면 라우팅, 로드밸런서 기능을 사용할 수 있다.
'Spring > [인프런] Spring Cloud' 카테고리의 다른 글
Catalogs, Orders Microservice (0) | 2022.06.19 |
---|---|
Users Microservice (0) | 2022.06.18 |
E-commerce 어플리케이션 (0) | 2022.06.18 |
Service Discovery (0) | 2022.05.29 |
Microservice와 Spring Cloud 소개 (0) | 2022.05.28 |