Cloud/SpringCloud로 개발하는 MSA

API Gateway Service

Tony Lim 2022. 10. 14. 16:05
728x90

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

728x90