Spring Cloud - API Gateway

SeungTaek·2021년 9월 27일
0
post-thumbnail

본 게시물은 스스로의 공부를 위한 글입니다.
틀린 내용이 있을 수 있습니다.


📒 API Gateway Service란?

  • 만약 Client side에서 각 서비스들을 직접 연결하는 엔드포인트를 가지고 있다고 하자.
  • 그럼 엔드 포인트가 변경되거나 추가되면 클라이언트도 업데이트 해야하는 문제가 생긴다.
  • 위 문제를 해결하기 위해 Backend에 API Gateway를 두고 각각의 마이크로 서비스로 요청되는 모든 정보에 대해서 일괄적으로 처리하게 만든다.
  • Client Side -> API Gateway -> Microservice

📒 API Gateway Service의 역할

  • 인증 및 권한 부여
    • 유입되는 모든 요청/응답이 통하기 때문에 적용하기 좋다.
  • 서비스 검색 통합
  • 응답 캐싱
  • 정책, 회로 차단기 및 QoS 다시 시도
  • 속도 제한
  • 부하 분산
  • 로깅, 추적, 상관 관계
  • 헤더, 쿼리 문자열 및 청구 변환
  • IP 허용 목록에 추가
  • 저번 게시물에서 배운 EurekaService Registry 서비스 중 하나이다.
  • EurekaAPI Gateway 프로세스는 아래 이미지와 같다.



스프링에서 사용하는 API Gateway로는 다음과 같은 서비스들이 있다.

  • Netflix Ribbon
  • Netflix zuul
  • Spring Cloud Gateway

📒 Netflix Ribbon, zuul

  • Ribbon : Client side Load Balancer
    • 별도의 API Gateway를 두는게 아니라, 클라이언트 내부에 서비스를 설치
    • 서비스 이름으로 호출
    • Health Check : 해당하는 서비스가 정상 작동 중인지 확인
    • Spring Boot 2.4부터 사용되지 않음(Maintenance mode)
      • Spring Cloud Loadbalancer로 대체
  • Zuul : API Gateway
    • Routing, API Gateway기능
    • 마찬가지로 Spring Boot 2.4부터 Maintenance mode
      • Spring Cloud Gateway로 대체

Maintenance mode란?

더 이상 지원 서비스를 하지 않는 보류 상태.


📒 Netflix zuul 사용해보기

📌 1. 서비스 2개 생성(First Service, Second Service)

  • Spring boot : 2.3.x로 생성해야 한다. (2.4부터는 지원하지 않기 때문)
  • Dependencies : Lombok, Spring Web

📌 2. Service 코드 작성

  • 간단한 RestController 생성
  • 로컬 환경에서 하는 테스트이기 때문에 First Service와 Second Service 포트 번호를 서로 다르게 해준다.
@RestController
@RequestMapping("/first-service")
public class FirstServiceController {
    @GetMapping("/welcome")
    public String welcome(){
        return "Welcome to the First service";
    }
}
server:
  port: 8081 # second service는 8082

spring:
  application:
    name: my-first-service

Test

http://localhost:8081/welcomeWelcome to the First service 출력 확인

http://localhost:8082/welcomeWelcome to the Second service 출력 확인



📌 3. Zuul Service 생성

  • Spring boot : 2.3.x로 생성해야 한다. (2.4부터는 지원하지 않기 때문)
  • Dependencies : Lombok, Spring Web, Zuul

📌 4. Zuul Service 코드 작성

  • main 클래스에 @EnableZullProxy 추가
  • 설정 파일에 zuul.routes 추가
    • path 요청이 들어오면 url로 포워딩 시켜줌
@SpringBootApplication
@EnableZuulProxy
public class ZuulServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(FirstServiceApplication.class, args);
    }
}
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

테스트

first service, second service, zuul serivce를 모두 실행 후..

http://localhost:8000/first-service/welcom으로 접속하면 first-service로 연결되는거 확인 가능

http://localhost:8000/second-service/welcom으로 접속하면 second-service로 연결되는거 확인 가능

이로써 gateway 기능을 사용할 수 있다.


📌 5. ZuulFilter

  • 어떤 요청의 사전 처리(인증 처리 등)과 사후 처리(로깅 등)를 설정할 수 있음
@Component
@SLF4J
public class ZullLoggingFilter extends ZuulFilter{
    @Override
    //실제 동작
    public Object run() throws ZullException {
        log.info("사전필터입니다!");
        
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info("Request url = "+ request.getRequestURL());
        return null;
    }
    
	@Override
    public String filterType(){
        return "pre"; //사전 처리 필터로 결정
    }
    
    @Override
    public int filterOrder(){
        return 1; //필터가 여러개인경우 순서 결정
    }
    
    @Override
    public boolean shouldFilter(){
        return true; //필터를 사용할지, 사용 안할지
    }
}



📒 Spring Cloud Gateway 사용해보기 (+ Filter)

📌 1. 서비스 2개 생성(First Service, Second Service)

  • Dependencies : Lombok, Spring Web

📌 2. service 코드 작성

  • 간단한 RestController 생성
  • 로컬 환경에서 하는 테스트이기 때문에 포트 번호를 서로 다르게 해준다.
@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController {
    @GetMapping("/message")
    public String message(@RequestHeader("first-request") String header){
        log.info(header);
        return "Hello world in First Service";
    }
}
server:
  port: 8081 # Second Service는 8082

spring:
  application:
    name: my-first-service

📌 3. Gateway Service 생성

  • 아래에서 Eureka 서비스와 연동해서 사용할 예정이므로 미리 Eureka 관련 서비스도 추가

  • Dependencies : Spring Boot DevTools, Eureka Discovery Client, Gateway


📌 4. gateway service 코드 작성

  • application.yml 또는 자바 코드로 작성 가능

    4-1. application.yml로 설정

server:
  port: 8000

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header2
            - AddResponseHeader=second-response, second-response-header2
  • predicates 에 있는 path로 접속한다면 해당하는 url로 포워딩 시켜준다.
  • 이때, 포워딩 주소는 (url+접속한 path)이다.
  • filtersrequestHeaderresponseHeader을 추가할 수 있다. (key, value 형식)

​ 4-2. 자바 파일로 설정

@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();  
	}
}

5. 테스트

  • first service, second service, Gateway Service를 모두 실행 후 http://localhost:8000/first-service/message 으로 접속하면 first-service로 연결되는거 확인 가능
  • 필터 적용 확인
    • requestHeader와 responseHeader가 추가된 것을 확인할 수 있다.



📒 Custom Filter 적용하기

  • 모든 요청은 API Gateway를 통과하므로 인증 등의 필터를 적용하기 적합하다.

📌 1-1. Gateway Service에 필터 클래스 추가

  • pre filter은 service 호출 전 실행

  • post filter은 service 호출 후 실행

  • CustomFilter을 위해선 AbstractGatewayFilterFactory을 상속 받아야함

@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
    }
}

📌 1-2. Logging Filter 클래스 추가

  • 다음과 같은 방법으로도 클래스를 추가할 수 있다.
  • application.yml에서 설정한 args를 받아와 사용할 수 있다.
  • Ordered.HIGHEST_PRECEDENCE, Ordered.LOWEST_PRECEDENCE로 커스텀 필터의 실행 우선순위를 정해줄 수 있다.
@Component
@Slf4j
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("Logginf Pre Filter: request id-> {}", request.getId());
            }
            // Custom post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(()-> {
                if (config.isPostLogger()) {
                    log.info("Log Filter End: response code -> {}", response.getStatusCode());
                }
            }));
        }, Ordered.HIGHEST_PRECEDENCE); //필터 체인 방식에서 우선순위를 정해줄 수 있다.
        return filter;
    }
    
    @Data
    public static class Config{
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

📌 2. application.yml 수정

server:
  port: 8000

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - CustomFilter  # Custom Filter만 적용
         
       - id: second-service
         uri: http://localhost:8081/
         predicates:
            - Path=/second-service/**
         filters:
            - name: CustomFilter  # filter을 여러개 적용가능
            - name: LogginFilter 
              args:
                baseMessage: ~
                preLogger: ~
                ~
  • 필터를 여러개 적용하고 싶다면 name을 적어줘야 한다.
  • args는 위 코드와 같이 사용하면 된다.



📒 Global Filter 설정

Custom Filter와 Global Filter의 작동 순서

  • Global Pre Filter -> Custom Pre Filter -> Service -> Custome Post Filter -> Global Post Filter

📌 1. Gateway Service에 클래스 추가

@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
   public GlobalFilter(){
       super(Config.class);
   }
   @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());
               }
           }));
       };
   }

   @Data
    public static class Config{ // application.yml에서 설정한 args를 받아올 수 있음
       private String baseMessage;
       private boolean preLogger;
       private boolean postLogger;
    }
}

📌 2. application.yml에 코드 추가

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters: # default-filters가 글로벌 필터이다.
        - name: GlobalFilter
          args: # 인자 전달 가능
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
            
       ...



📒 Eureka + API Gateway를 사용한 Load Balancer

📌 1. Eureka Server 생성


📌 2. Gateway Service의 application.yml 수정

server:
  port: 8000
eureka: # Eureka에 등록하기 위해 추가
  client:
    register-with-eureka: true
    fetch-registry: true
    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: lb://MY-FIRST-SERVICE
          predicates:
            - Path=/first-service/**
          filters:
            - CustomFilter
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
            - CustomFilter
  • uri에는 eureka에 등록되어 있는 서비스 이름으로 작성한다. lb://{등록된 서비스 이름} 형식

📌 3. first service, second service 수정

server:
  port: 0 # 랜덤 포트

spring:
  application:
    name: my-first-service

eureka: # eureka에 등록
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController {
    Environment env;
    @Autowired
    public FirstServiceController(Environment env){
        this.env=env;
    }

    @GetMapping("/check")
    public String check(HttpServletRequest request){
        log.info("server port={}", request.getServerPort());
        return String.format("Hi, there. This is a message from First Service on Port %s",
                env.getProperty("local.server.port"));
    }
}

📌 4. first service, second service 각각 2개씩 실행

  • 터미널, 콘솔, 인텔리제이 구성편집 등을 통해 실행 가능
    • 다른 게시물 참고

📌 5. 테스트

  • http://localhost:8000/first-service/check 에 접속
  • 새로고침시 계속 사용하는 인스턴스가 바뀌는걸 확인 가능
    • 즉, 라운드로빈 방식으로 Load Balancing 한다.
  • 이로써 Eureka + API Gateway를 사용할 수 있다.

인프런의 'Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)(Dowon Lee)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요.

profile
I Think So!

0개의 댓글