Cloud/SpringCloud로 개발하는 MSA

API Gateway Service

Tony Lim 2022. 10. 14. 16:05

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 이다.