Spring Cloud로 개발하는 마이크로서비스(Eureka, Gateway)

Seung jun Cha·2022년 7월 2일
0

1. Spring Cloud Netflix Eureka

  • Eureka는 서비스를 등록하는 서비스 레지스트리로 애플리케이션에 있는 모든 마이크로서비스의 중앙 집중 레지스트리로 작동한다. 서비스들이 서로를 찾는데 도움을 주는 역할을 한다. 원하는 서비스를 찾았을 때, 어떤 인스턴스를 사용해야 할 지 결정해야하는데 매번 이런 선택을 피하기 위해 로드 밸런싱을 사용한다

  • 여러 개의 유레카 서버가 있을 경우, 하나에 문제가 발생하더라도 문제의 발생을 막을 수 있으므로 실무에서는 여러 개의 유레카 서버들이 클러스터로 구성되어 유레카는 다른 유레카 서버로부터 서비스 레지스트리를 가져오거나 다른 유레카 서버의 서비스로 자신을 등록하기도 함

1-1 Service Discovery(=Registry)

1-1-1 Eureka 구성하기

모든 서비스는 Eureka에 등록되며, 하나의 서비스는 여러 개의 인스턴스로 분산이 가능하다. 그러나 여러 개의 인스턴스들은 모두 서비스와 같은 이름으로 Eureka에 등록된다

  1. Eureka가 apigateway에 주기적으로 등록/해제된 서비스들의 정보를 전달
  2. apigateway-service가 주기적으로 전달받은 정보를 기억
  3. 클라이언트가 자신의 요청을 API Gateway에 입력
  4. 요청이 Service Discovery에 전달됨
  5. Service의 위치를 찾아 호출해서 결과를 응답 받음
server:
  port: 8761

spring:
  application:
    name: discorveryservice // 유레카 서버의 이름 (프로젝트 명과는 관련없음)

eureka:
  //instance:
  //hostname: localhost  -> 생략가능하고 생략하면 유레카가 환경변수를 참고하여 결정. 하지만 명시적으로 지정해주는 것이 좋음(여기서는 생략함)
  client:
    register-with-eureka: false 
    // 이 서비스를 다른 eureka 서버에 등록하겠는가 -> 단일 유레카서버 자체로 기동만 하면 되므로, 등록할 필요X
    fetch-registry: false
    // 다른 Eureka 서버로부터 인스턴스들의 정보를 주기적으로 가져올 것인지 설정
    단일 유레카서버 자체로 기동만 하면 되므로, 등록할 필요X
    //defaultZone:
      http://${eureka.instance.hostname}:${server.port}/eureka

Eureka Client로써 Eureka Server에 등록하기 위해 사용되는 Endpoint가 http://localhost:8761/eureka이다. application.yml 파일에 /eureka를 빼고 설정하면, Eureka Server 정보를 전달할 수가 없기 때문에, 오류가 발생하고, Service Registry에 등록되지 않는다.

1-1-2 서비스를 등록하고 찾기

  • 등록한 서비스들을 동시에 여러개 구동시키는 방법
  1. 이렇게 포트번호를 다르게 설정

  2. 인텔리제이 terminal 또는 리눅스 등에서 작업
    java -version / javac -version / mvn --version 으로 설치확인 후 진행
    mvn spring-boot:run '-Dspring-boot.run.jvmArguments="-Dserver.port=9003"'
    실행한 서비스를 중지하려면 Ctrl + C

  3. 위처럼 서비스의 포트번호를 정적으로 할당하면 굉장히 번거로움
    포트번호를 0으로 설정하면 동적으로 랜덤한 번호를 할당함
    인텔리제이 terminal에서 랜덤포트를 부여하려면 mvn spring-boot:run 까지만 입력

server:
  port: 0 // 0으로 할당하면 실행할 때마다 랜덤포트번호 부여

spring:
  application:
    name: user-service  -> 유레카 서버에 등록되는 서비스 이름

eureka:
  instance:
    instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}
    //포트 번호를 랜덤으로 주기 위해 0으로 설정해 놓으면 유레카 서버에서 인스턴스를 구분하지 못함 
    따라서, 랜덤으로 포트번호가 부여된 인스턴스가 표시되도록 설정
    (하나의 서비스에 다수의 인스턴스가 있을 때, Gateway가 어떤 인스턴스를 가져왔는지 알 수 있음)
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka
      //마지막에 적은 eureka는 엔드포인트
      // 해당하는 주소의 유레카 서버에 서비스가 등록되도록 구성됨
  • 클라이언트 인텔리제이를 종료해도 서버 레지스트리에 남는 경우
// 서버.yml
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server: #레지스트리 삭제옵션
    enable-self-preservation: false #개발일때만 사용, 운영시 삭제해야함
    eviction-interval-timer-in-ms: 3000 #하트비트 수신점검
//클라이언트.yml
eureka:
 instance:
   lease-renewal-interval-in-seconds: 1 #하트비트 인터벌
   lease-expiration-duration-in-seconds: 2 
   # 디스커버리는 서비스 등록 해제 하기 전에 마지막 하트비트에서부터 2초 기다림

1-2 API GateWay Service

  • 클라이언트가 요청한 서비스에 인스턴스가 여러 개일 경우, 적절한 인스턴스를 선택해 줌(로드 밸런싱)
  • 일종의 Proxy 역할 : 클라이언트 대신해서 요청하고 응답을 받으면 다시 클라이언트에게 전달을 해주는 역할, Gateway는 MicroService에 요청되는 모든 정보들을 일괄적으로 처리 해줌 (Dispathcher Sevlet와 비슷한 듯)
  • 동기 방식인 Tomcat 서버가 아닌 비동기 방식인 Netty 서버로 실행되어, 비동기 처리도 가능해짐
  1. 인증 및 권한 부여
  2. 서비스 검색 통합
  3. 응답 캐싱
  4. 정책,회로 차단기 (클라이언트가 요청한 Micro Service에 문제가 생기면 그 회로를 차단)
  5. 속도 제한
  6. 부하 분산
  7. Logging,추적
    • 하나의 마이크로서비스가 다른 서비스를 호출하는 경우에 시작점, 중간, 그 다음단계 등을 추적
    • Api Gateway는 어떤 클라이언트가 어떤 서비스에 요청했는지 로그 파일을 처리 가능
  8. IP 허용 목록에 추가
    • 목록에 추가 허용할 수 있는 IP와 차단 IP 처리

1-2-1 Spring Cloud Gateway의 구성

  1. Route : Predicate 와 Filter를 조건을 충족하여 만들어진 요청 URI

  2. Predicate : 요청을 처리하기전에 수행되는 로직, path 혹은 리퀘스트 헤더에 포함된 조건

  3. Filter

  • 필터 작동순서
  1. 클라이언트가 Gateway에 요청을 전달

  2. Gateway Handler Mapping이 요청 정보를 Gateway의 설정된 Route로 전달하는 것을 결정하고, Gateway Web Handler에게 전송한다.

  3. Gateway Web Handler는 만들어진 필터들을 거쳐 조건에 맞는 요청을 보낸다.
    (Pre Filter 과정)

  4. 요청의 결과로, proxied server에 원하는 정보를 얻고 사후 필터들에 대한 로직이 동작하여 클라이언트에게 응답이 간다. (Post Filter 을 거쳐 클라이언트에게 응답)

1-3 Custom Filter

  • exchange로 request 및 response를 가져올 수 있고 동기 방식인 Tomcat이 아닌 비동기 방식인 Netty를 사용하고 있기 때문에, 서블릿이 아닌 serverHttpRequest와 serverHttpResponse를 사용.
    사전 처리가 끝나게 되면 chain을 이용하여 post filter를 추가할 수 있음.

webflux에서도 Mono와 Flux로 나뉘게 됨

  • Mono는 0~1개의 결과만을 처리하기 위한 Reactor 객체
    보통 여러 스트림을 하나의 결과로 모아줄 때 사용
    Flux는 0~N개의 결과물을 처리하기 위한 Reactor 객체
    각각의 Mono를 합쳐서 여러개의 값을 처리할 때 사용

1-3-1 GateWay Service Filter(yml)

  • Route, Filter 기능을 application.yml에서 구현

  • 예제로 쓸 마이크로서비스 2개 생성

  • GateWay Service 에 등록

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
#            - CustomFilter
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2

        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
#            - CustomFilter
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
  1. GateWay Service의 predicates 중에 입력받은 URI 검색
  2. 해당하는 Port로 이동하고 URI + Path 경로인 컨트롤러를 호출

1-3-2 GateWay Service Filter(java)

  • Route와 Filter를 application.yml이 아닌 자바코드로 작성
    • ApiGatewayService에 설정정보로 등록
@Configuration
public class FilterConfig {

    @Bean
    public RouteLocator gatewayRoute(RouteLocatorBuilder builder, CustomFilter customFilter){
        return builder.routes()
                .route(r -> r.path("/first-service/**") // 이 경로가 호출되면
                        .filters(f -> f.addRequestHeader("first-request", "first-request-header") // request 와 responseHeader에 이 값을 넣는다
                                       .addResponseHeader("first-response", "first-response-header")
                                .filter(customFilter.apply(new CustomFilter.Config()))) // 내가 만든 필터 적용
                        .uri("http://localhost:8081")) // 여기 uri로 이동한다

                .route(r -> r.path("/second-service/**")
                .filters(f -> f.addRequestHeader("second-request", "second-request-header")
                        .addResponseHeader("second-response", "second-response-header")
                        .filter(customFilter.apply(new CustomFilter.Config())))
                .uri("http://localhost:8082"))
                .build();
    }
}

1-4 Global Filter

  • 공통적으로 적용되는 필터
  • isPreLogger, isPostLogger
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {

    @Data
    public static class Config{
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }


    public GlobalFilter(){
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {

        //Custom Pre Filter
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();  // Pre Filter
            ServerHttpResponse response = exchange.getResponse(); // Post Filter

            log.info("Global Pre Filter : {}" , config.getBaseMessage());

            if (config.isPreLogger()){ // preFilter이라면 다음의 로그를 호출
                log.info("Global Pre Start : request id -> {}" , request.getId());
            }

            //Custom Post Filter 
            return chain.filter(exchange).then(Mono.fromRunnable(() ->{
                if (config.isPostLogger()) { //PostFilter이라면 로그호출
                    log.info("Global filter End : response code -> {}", response.getStatusCode());
                }
            }));
        };
    }
}
  • yml에 작성
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true

1-5 Logging Filter

  • 필터의 순서
  1. Client Request
  2. Gateway Handler
  3. pre-filter 처리(Global Filter -> Custom Filter -> Logging Filter 순으로 처리)
  4. Request 실행
  5. post-filter 처리(Logging Filter -> Custom Filter -> Global Filter 순으로 처리)
  6. Gateway Handler
  7. Client

단, 필터의 적용 순서를 바꾸고 싶을 때에는, new OrderedGatewayFilter를 사용

 @Override
    public GatewayFilter apply(Config config) {
GatewatFilter filter = new OrderedGatewayFilter((exchange, chain) -> 
        //Custom Pre Filter
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();  // Pre Filter
            ServerHttpResponse response = exchange.getResponse(); // Post Filter

            log.info("Global Pre Filter : {}" , config.getBaseMessage());

            if (config.isPreLogger()){ // preFilter이라면 다음의 로그를 호출
                log.info("Global Pre Start : request id -> {}" , request.getId());
            }

            //Custom Post Filter 
            return chain.filter(exchange).then(Mono.fromRunnable(() ->{
                if (config.isPostLogger()) { //PostFilter이라면 로그호출
                    log.info("Global filter End : response code -> {}", response.getStatusCode());
                }  Ordered.HIGHEST_PRECEDENCE); // 필터의 적용 순서 설정
                return filter;

1-6 Load Balancer

  • 라우팅 정보에 LB를 사용하게 되면, 유레카에 등록된 서비스가 여러 개인 경우, Load Balancing (부하 분산) 처리를 지원함
 routes:
        - id: first-service
          uri: lb://my-first-service   // 등록된 서비스의 포트번호가 아닌 서비스의 이름으로 등록(네이밍 서비스) -> 정해진 포트번호가 아닌 랜덤포트 번호를 사용할 수 있음
          predicates:
            - Path=/first-service/**
  • 이렇게 gateway에서 서비스의 이동을 직접하는 방식이 아니라, registry service에 전달해서 해야 할 경우(서비스 이름을 사용해서 이동하는 경우?) , spring cloud gateway와 registry service가 서로 연결되어 있어야 한다.
    따라서, register-with-eurekafetch-registry 코드를 true로 설정해서 Cloud Gateway 애플리케이션을 Eureka Server에 등록하고, 유레카로부터 정보를 받아 주기적으로 서비스 인스턴스들의 정보를 갱신해 줘야 한다.

  • id 는 Gateway에 등록되는 서비스의 이름

  • 클라이언트가 Path를 입력 시 uri로 이동 (routes의 uri + / + Controller의 uri)
    ex) 127.0.0.1:8000/first-service/health_check 입력 -> 127.0.0.1:랜덤포트/my-first-service/health_check 로 이동

등록

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true

1-6-1 Routes 정보 변경

 routes:
#        - id: user-service
#          uri: lb://user-service
#          predicates:
#            - Path=/user-service/**

        - id: user-service
          uri: lb://USER-SERVICE
          redicates:
            - Path=/user-service/login
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
            
        - id: user-service
       	  uri: lb://USER-SERVICE
          predicates:
           - Path=/user-service/**
           - Method=GET
          filters:
           - RemoveRequestHeader=Cookie
           - RewritePath=/user-service/(?<segment>.*), /$\{segment}
           - AuthorizationHeaderFilter
  • RemoveRequestHeader=Cookie : Request Header에서 Cookie를 삭제하지 않으면, user-service와 같은 애플리케이션에서 Cookies를 추가하여 Response body에 저장된 정보가 있을 경우, 보안에 문제가 된다
    RemoveRequestHeader=Cookie를 적용하게 되면, Response Body에 어떤 정보도 남기지 않고, 매번 새로운 Request Header로 요청한다는 의미이다.

  • RewritePath : RewritePath는 클라이언트로부터 요청된 경로를 다시 다른 경로로 변경할 수 있음. URL을 통한 라우팅 작업을 가능하게 함. 실제 구현된 정보를 노출하지 않을 수 있고, 사용자의 요청 정보를 단일화 (또는 통일화) 한 다음, 내부적으로 사용되는 경로로 라우팅 하기 위해 사용하시면 좋을 것 같다.
    ex)127.0.0.1:8000/user-service/login 입력 -> 127.0.0.1:랜덤포트/USER-SERVICE/login 으로 이동 -> RewritePath=/user-service/(?.*), /${segment} 때문에 /user-service/login 경로 전체가 /login 으로 대체되어서 이동 => 127.0.0.1:랜덤포드/login 이 최종 uri

0개의 댓글