API Gateway Service
API gateway 역할을 해주는 spring cloud zuul 과 Ribbon 은 spring boot 2.4 까지는 maintenance.
- 인증 및 권한 부여
- 서비스 검색 통합
- 응답 캐싱
- 정책 , 회로 차단기 및 Qos(Quality of Service) 다시시도
- 속도 제한 , 부하 분산
- 로깅 , 추적 , 상관 관계
- 헤더 , 쿼리 문자열 및 청구 변환
- IP허용 목록에 추가
Spring cloud에서 msa간의 통신
1. RestTemplate
2. Feign Client
Zull 실습 spring boot 2.3 이하의 버전만 가능
server:
port: 8000
spring:
application:
name: my-zuul-service
zuul:
routes:
first-service:
path: /first-service/**
url: http://localhost:8081
second-service:
path: /first-service/**
url: http://localhost:8082
@EnableZuulProxy 를 작성해 준후에 application.yml 에 service instance들을 연결해주면 된다.
firstservice , secondservice 각각 어떤 url 로 갈지 적어준것이다.
다음으로는 Spring boot 최신 버젼도 호환이되는 Spring Cloud 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/**
이떄 first service ,second service controller에서 주의를 해야한다.
왜냐하면 라우팅되는 실제 uri는 "http://localhost:8081/first-service/**" 로 포워딩되어서 날라가기 때문이다.
각 서비스의 Controller단에서 @RequestMapping에서 ("/first-service/")와 같은 작업을 해줘야 제대로 맵핑이 된다.
Filter 적용
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();
}
}
위의 application yaml 에서 spring cloud 옵션을 주석처리하고 따로 Configuration Bean을 만들어서 위와 동일한 역할 + header를 추가해주는 코드이다.
localhost:8000/first-service/message 쪽으로 request를 보내면
@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController
{
@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";
}
}
이 controller를 타고 gateway에서 입력된 Header 값을 log를 출력하고 string 을 return 한다.
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/**
filters:
- AddRequestHeader=first-request, first-requests-header2
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-requests-header2
- AddResponseHeader=second-response, second-response-header2
동일하게 application yaml에서 filters property를 정의함으로 할 수있다.
Custom Filter
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config>
{
public CustomFilter()
{
super(Config.class);
}
@Override
public GatewayFilter apply(Config config)
{
//Custom Pre Filter
return ((exchange, chain) -> {
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(() -> {
log.info("Custom POST filter: response code -> {}",response.getStatusCode());
}));
});
}
public static class Config
{
// Put the configuration properties
}
}
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/**
filters:
# - AddRequestHeader=first-request, first-requests-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=second-request, second-requests-header2
# - AddResponseHeader=second-response, second-response-header2
- CustomFilter
Mono 인 Reactive stream을 반환값으로 줘야한다.
Custome Filter이기에 모든 라우팅 패스에 알아서 추가해줘야 작동한다.
위에 예시에 first-service, second-service 둘다 추가해서 동작한것이다.
Global Filter
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config>
{
public GlobalFilter()
{
super(Config.class);
}
@Override
public GatewayFilter apply(Config config)
{
//Custom Pre Filter
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());
}
// Custom Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()){
log.info("Global Filter End : response code -> {}",response.getStatusCode());
}
}));
});
}
@Data
public static class Config
{
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
// Put the configuration properties
}
}
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 Gatway Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
# - AddRequestHeader=first-request, first-requests-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=second-request, second-requests-header2
# - AddResponseHeader=second-response, second-response-header2
- CustomFilter
위에 패스에 일일이 추가해줘야 되는 CustomFilter와 다르게 모든 패스에 적용하고 싶을 때 쓰는것이 Global Filter이다.
2022-10-14 12:32:51.475 INFO 4295 --- [or-http-epoll-3] c.e.a.config.GlobalFilter : Global Filter baseMessage: Spring Cloud Gateway Global Filter
2022-10-14 12:32:51.476 INFO 4295 --- [or-http-epoll-3] c.e.a.config.GlobalFilter : Global Filter Start: request id -> c99822f7-1
2022-10-14 12:32:51.476 INFO 4295 --- [or-http-epoll-3] c.e.a.config.CustomFilter : Custom PreFilter: request id -> c99822f7-1
2022-10-14 12:32:51.548 INFO 4295 --- [or-http-epoll-3] c.e.a.config.CustomFilter : Custom POST filter: response -> 200 OK
2022-10-14 12:32:51.548 INFO 4295 --- [or-http-epoll-3] c.e.a.config.GlobalFilter : Global Filter End: response -> 200 OK
Global Filter는 가장 겉에 존재하게 된다. 가장 먼저 적용이되고 안의 CustomFilter가 따로 적용이 된다.
Logging Filter
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
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: request id -> {}", request.getId());
}
// Custom PostFilter
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("Logging Post Filter: response -> {}",response.getStatusCode());
}
}));
}, Ordered.LOWEST_PRECEDENCE);
return filter;
}
@Data
public static class Config {
// put the configuration Properties
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
2022-10-14 13:13:08.607 INFO 9297 --- [or-http-epoll-3] c.e.a.config.GlobalFilter : Global Filter baseMessage: Spring Cloud Gateway Global Filter
2022-10-14 13:13:08.607 INFO 9297 --- [or-http-epoll-3] c.e.a.config.GlobalFilter : Global Filter Start: request id -> f5da4e12-1
2022-10-14 13:13:08.608 INFO 9297 --- [or-http-epoll-3] c.e.a.config.CustomFilter : Custom PreFilter: request id -> f5da4e12-1
2022-10-14 13:13:08.676 INFO 9297 --- [or-http-epoll-3] c.e.a.config.LoggingFilter : Logging Filter baseMessage: hi, there.
2022-10-14 13:13:08.677 INFO 9297 --- [or-http-epoll-3] c.e.a.config.LoggingFilter : Logging Pre Filter: request id -> f5da4e12-1
2022-10-14 13:13:08.677 INFO 9297 --- [or-http-epoll-3] c.e.a.config.LoggingFilter : Logging Post Filter: response -> 200 OK
2022-10-14 13:13:08.677 INFO 9297 --- [or-http-epoll-3] c.e.a.config.CustomFilter : Custom POST filter: response -> 200 OK
2022-10-14 13:13:08.677 INFO 9297 --- [or-http-epoll-3] c.e.a.config.GlobalFilter : Global Filter End: response -> 200 OK
Lowest 순서로 Logging filter를 적용했으므로 가장 맨 뒷단에서 가장 나중에 적용된것을 확인 할 수 있다.
LoadBalance = Eureka Service (service discovery) + api gateway
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
Eureka 의 service discovery 덕분에 더 이상 api gateway 뒤에 있는 msa의 위치정보를 몰라도 된다.
더 이상 api gateway 에서 직접 msa를 호출하는것이 아닌 Eureka registry에 등록되고 등록된 msa를 호출하는 방식으로 변경된 것이다.
이런식으로 discover service에 여러개의 instance가 등록이 되어 LB가 가능해진다. 기본 전략은 round robin 이다.