Micro Service/mSVC개발

[SC12] Spring Cloud Gateway 란 ?

Happy@Cloud 2021. 2. 14. 03:19

1. Spring Cloud Gateway(SCG) 이해

1) WHY ?

SCG는 API Gateway의 하나입니다. 

따라서 API Gateway가 왜 필요한지 먼저 이해하는것이 필요합니다. 

Zuul server편에서 설명한바와 같이

API Gateway가 필요한 이유는 안전한 API유통과 Client 요청별로 유연하게 대처하기 위해서입니다. 

그리고 API Gateway는 인증/인가, L/B & 라우팅, 로깅, Circuit Breaker의 역할을 합니다. 

 

2) HOW ?

SCG는 Zuul과 어떤 차이가 있을까요 ?

그 차이를 이해하면서 SCG의 동작원리를 설명하겠습니다.

 

차이점 1: Blocking vs non-Blocking

SCG를 만든 Spencer Gibb은 아래와 같이 설명하고 있습니다. 

Zuul is built on servlet 2.5 (works with 3.x), using blocking APIs. 
It doesn't support any long lived connections, like websockets

Gateway is built on Spring Framework 5, Project Reactor and 
Spring Boot 2 using non-blocking APIs. 
Websockets are supported and it's a much better developer experience 
since it's tightly integrated with Spring.

Zuul은 서블릿 2.5 위에서 Blocking API들을 사용하여 개발되었습니다. 
그래서 웹소켓과 같은 길게 지속되는 연결을 지원하지 않습니다. 

SCG는 Spring Framework 5, Project Reactor 그리고 Spring Boot2 위에서 
non-Blocking API들을 사용하여 개발되었습니다. 
웹소켓이 지원되고, Spring과 잘 연동되기 때문에 개발자 경험을 매우 크게 향상시킵니다. 

stackoverflow.com/questions/47092048/how-is-spring-cloud-gateway-different-from-zuul

Blocking 방식은 요청을 보내고 응답이 올때까지 다음으로 진행하지 않고 기다립니다. 

non-Blocking방식은 요청을 보내고 바로 다음으로 진행하여 다른 일을 하다가, 응답이 오면 그에 맞는 처리를 합니다. 

Blocking과 non-Blocking에 대해 조금 더 이해하려면

마이크로서비스패턴: 핵심패턴만 빠르게 이해하기편의 '동기와 비동기 처리 이해'를 참고하세요.

 

Zuul 1.x는 blocking방식의 단점을 해결하기 위해 Thread pool을 사용했습니다.

각 트랜잭션이 별도의 Pool에서 수행되므로, 어느 정도는 blocking방식의 문제를 해결할 수 있었습니다. 

zuul:
  ...
  ribbon-isolation-strategy: thread
  thread-pool:
    use-separate-thread-pool: true
    thread-pool-key-prefix: zuul-

하지만 한계가 있었던지, Zuul 2.x에서는 non-blocking방식으로 바꿨습니다. 

그러나, Zuul의 태생이 Netflix OSS여서 그런지 Spring Cloud와는 잘 안 맞는 면이 좀 있었던것 같습니다. 

그래서 Spring Cloud 커뮤니티에서 내놓은 새로운 API Gateway가 Spring Cloud Gateway입니다. 

Zuul 1.x와 SCG의 성능을 비교한 몇몇 사이트가 있는데, 초기에는 Zuul이 더 좋은것으로 보고하는 사이트가 있었지만, 

장기 지속 연결에 적합치 않은 측정툴을 사용하였기 때문이라는 반박을 받았습니다. 

최근의 테스트에서는 SCG가 훨씬 더 나은 성능을 보이는것으로 나옵니다. 

www.bytesville.com/zuul-spring-cloud-gateway-comparison-benchmarks-loadtesting/ 

 

차이점 2: Filter only vs Predicates+Filters

Zuul과 SCG는 동작원리 측면에서도 많이 다릅니다. 

Zuul이 Filter들만으로 동작하는 반면에, SCG는 Predicates(수행을 위한 사전 요구조건)와 Filter를 조합하여 동작합니다. 

그림출처: https://velog.io/@tlatldms/서버개발캠프-MSA-아키텍쳐의-API-Gateway-프레임워크-결정
그림출처: https://velog.io/@tlatldms/서버개발캠프-MSA-아키텍쳐의-API-Gateway-프레임워크-결정

Gateway Handler Mapping이 Predicates에 지정한 경로와 일치하는지 판단하고, 

Gateway Web Handler는 지정된 필터들을 통해 요청을 전송합니다. 

필터들은 요청과 응답에 대한 처리를 수행합니다. 

 

차이점 3: Tomcat vs Netty

Zuul은 Web/WAS로 Tomcat을 사용하고, SCG는 Netty를 사용합니다.

Netty는 비동기 네트워킹을 지원하는 어플리케이션 프레임워크입니다. 

 

2. SCG 서버 개발

그동안은 STS(Spring Tool Suite)에서 개발했는데, 앞으로는 MS Visual Studio Code(vscode)에서 개발하도록 하겠습니다. 

보다 가볍고 플러그인도 많아 최근에 Intellij IDEA와 더불어 가장 많이 사용하는 IDE이기 때문입니다. 

vscode 설치는 이 글을 참고하십시오. -> vscode 설치, 구성, 삭제 

저는 Mac에서 개발하기 때문에 Window에서는 화면이 약간 다를 수 있습니다. 

 

1) 프로젝트 생성

View > Command Palette 또는 Command+Shift+p (window는 CTRL+Shift+p)를 누르십시오. 

- spring initializr을 입력하고, Maven Project를 선택합니다.

- Spring Boot Version는 2.3.8, Language는 Java를 선택합니다. 

- Group Id는 'com.springcloud'를 입력합니다. 변경하셔도 됩니다. 

 - Artifact Id는 application명인 'scg'를 입력합니다. 역시 바꿔도 됩니다.  

package명은 GroupId+Artifact Id로 자동 생성됩니다. 즉, com.springcloud.scg로 생성됩니다. 

- Package Type은 'Jar', Java version은 '1.8'을 선택합니다. 

- Depedency는 'gateway', 'Config client', 'Eureka Discovery Client', 'Spring boot Actuator'를 추가합니다. 

- project가 만들어질 상위 폴더(예에서는 sc-hklee)를 선택하고, [Generate into this folder]를 클릭합니다.  

※ 좌측 'JAVA PROJECTS', 'SPRING BOOT DASHBOARD'에 자동으로 추가가 안됩니다. 

'Clean Workspace'를 선택하고, vscode를 재시작하십시오.  

 

- pom.xml 수정

vscode에서 생성한 프로젝트의 pom.xml에는 spring cloud의 dependency관리를 위한 'spring-cloud-dependencies'가 빠져 있습니다. 

이게 빠져 있으면, spring cloud 관련 라이브러리 추가할 때 version을 모두 명시해 줘야 합니다. 

spring-cloud-dependencies는 정의된 spring cloud 버전(예: Hoxton.SR9)에 해당되는 각 spring cloud 라이브러리가

자동으로 로딩 되도록 해줍니다. 

아래와 같이 추가해 주십시오.

...

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
	</properties>

...
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
 ...

 

2) Main Class 수정

@EnableDiscoveryClient만 추가합니다. 

@SpringBootApplication
@EnableDiscoveryClient
public class ScgApplication {

	public static void main(String[] args) {
		SpringApplication.run(ScgApplication.class, args);
	}

}

 

3) bootstrap.yaml 작성

[JAVA PROJECTS] 탭에서는 파일 추가가 안됩니다. 아래와 같이 [Folders]탭에서 추가하십시오. 

server: 
  port: ${service_port:8082}
spring:
  application:
    name: scg
  profiles: 
    active: ${profile:local}
    include: common
  cloud:
    config:
      uri: ${config_servers:http://localhost:9001}
      searchPaths: ${spring.application.name}
      default-label: main

 

4) dependency, finalName 설정

 

...
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-bus-amqp</artifactId>
		</dependency>
	</dependencies>
	...
	<build>
		<finalName>scg</finalName>
		<plugins>
...

 

5) Application 개발

없습니다. 

6) configmng에 config 파일 추가

configmng하위에 scg디렉토리를 만들고, 아래 파일들을 추가합니다. 

scg-cicd-common.properties

# Container Image info
image_registry=harbor.io
image_project=sc-hklee
image_repository=scg
image_tag=0.0.1

# resources
req_cpu=64m
req_mem=64Mi
limit_cpu=1024m
limit_mem=1024Mi

scg-cicd-dev.properties

# namespace, sa
namespace=hklee
serviceaccount=sa-hklee

# Service info
service_target_port=8082
service_port=8082
service_host=scg.169.56.84.41.nip.io
service_replicas=1

image_pull_policy=Always

scg-cicd-prod.properties

# namespace, sa
namespace=hklee
serviceaccount=sa-hklee

# Service info
service_target_port=8082
service_port=8082
service_host=scg.169.56.84.41.nip.io
service_replicas=2

image_pull_policy=Always

scg-common.yaml

테스트 할 gateway설정을 추가합니다. 

http://<scg ingress>/webhook/greeting/Hi를 webhook으로 proxying합니다.

이때 첫번째 경로에 있는 'webhook'은 제거하고 http://<webhook service>:<webhook port>/greeting/Hi로 proxying합니다.  

spring.cloud.gateway:
  routes: 
  - id: webhook-test 
    uri: lb://webhook
    predicates:
    - Path=/webhook/**   
    filters:
    - RewritePath=/webhook/(?<uri>.*), /${uri} 

scg-secret-common.properties

mq_pw=guest

 

git repository에 푸시합니다. 

좌측 3번째 아이콘을 클릭하고, SOURCE CONTROL탭에서 configmng로 이동합니다. 

하단에 comment를 입력하고 체크 아이콘을 클릭합니다. 

more아이콘을 클릭한 후 'Push'를 수행합니다. 

 

7) Local 테스트

[SPRING BOOT DASHBOARD]에서 config, eureka, webhook을 먼저 디버그모드로 실행합니다.

디버그모드로 실행안하고, 그냥 실행해도 되는데 그냥 실행하면 실행여부를 나타내는 왼쪽 동그라미 색깔이 안변해 불편합니다.  

scg는 위 DASHBOARD에서 실행해도 되지만, 실행할때마다 새 터미널이 생겨서 불편합니다.  자꾸 닫아야 하니까요.

그래서 터미널에서 콘솔명령으로 실행하는것이 편합니다. 

~/Documents/springboot/sc-hklee
❯ cd scg

~/Documents/springboot/sc-hklee/scg
❯ ./mvnw clean package -DskipTests && java -jar target/scg.jar 

SCG서버가 실행된 후 웹브라우저에서 아래와 같이 테스트 합니다. 

 

8) 환경변수 파일 생성

추가로 만들 환경변수는 없습니다. 

 

9) git repository 생성 및 Push

'scg'라는 git repository를 만들고 push합니다.  

❯ cd ~/Documents/springboot/sc-hklee/scg
❯ git init
❯ git remote add origin https://happycloudpak@github.com/sc-hklee/scg.git
❯ git checkout -b main
새로 만든 'main' 브랜치로 전환합니다

❯ git add . && git commit -m "first" && git push -u origin main

※ git remote add origin할때 git repository주소의 'happycloudpak@'을 추가하는 것은
어떤 git계정으로 push하는지 지정해주는 겁니다.  여러 계정으로 git을 사용하는 경우 이렇게 git 계정을 명시해 줘야 합니다. 

scg의 소스를 아무거나 고치고(예: 빈줄 추가) 저장하면, 자동으로 아래와 같이 'Source Control'아이콘에 변경 count가 늘어납니다. 

그 아이콘을 클릭하면 어디 파일이 변했는지도 알려줍니다. 물론, 여기서 comment를 입력하고 git push할 수도 있습니다. 

 

10) 빌드&배포

NFS서버로 로그인한 후, hklee계정으로 바꾸고, run-cicd를 이용하여 배포합니다. 

[root@nfs ~]# su - hklee
마지막 로그인: 일  2월  7 05:02:28 CST 2021 일시 pts/0
[hklee@nfs ~]$ cd work
[hklee@nfs work]$ git clone https://github.com/sc-hklee/scg.git
Cloning into 'scg'...
...

[hklee@nfs work]$ cd scg
[hklee@nfs scg]$ run-cicd hklee passw0rd . dev . java config

 

SCG서버의 ingress주소로 아래와 같이 테스트합니다. 

 

 

3. SCG 실습

SCG실습은 아래와 같이 API Gateway의 기능별로 해보겠습니다. 

1) L/B & 라우팅: yaml과 filter를 이용한 라우팅 설정. Spring Cloud Load Balancer를 이용한 자세한 L/B는 별도의 장에서 다룸.

2) 로깅 & 메트릭: filter, Sleuth & Zipkin을 이용한 로깅과 Actuator를 이용한 메트릭 제공  

3) Circuit Breaker: 별도의 장에서 다룸

4) 인증/인가: 내용이 복잡하고 길기 때문에 별도의 장에서 다룸 

주로, spring.io의 Spring Cloud Gateway 매뉴얼을 참조하였습니다. 

 


SCG실습: L/B & 라우팅

yaml과 filter를 이용하여 라우팅을 설정합니다.  

실습을 위한 어플리케이션은 지금까지 작성한 어플리케이션을 이용합니다. 

 

1) yaml configuration 문법

predicates와 filters를 설정하는 2가지 방법입니다. 

- 한줄로 설정하기 

아래와 같이 한줄로 설정합니다. 

spring.cloud.gateway:
  routes: 
  - id: webhook-test1
    uri: lb://webhook
    predicates:
    - Path=/webhook/**   
    filters:
    - RewritePath=/webhook/(?<uri>.*), /${uri}

- 길게 설정하기

name과 parameter들로 길게 쓸 수 있습니다. parameter명은 filter소스에 정의되어 있는데 spring.io의 매뉴얼을 보면 됩니다. 

파라미터가 2개 이하인 경우는 한줄 설정 방법을 많이 사용합니다.  

spring.cloud.gateway:
  routes: 
  - id: webhook-test1
    uri: lb://webhook
    predicates:
    - name: Path
      regexp: /webhook/**
    filters:
    - name: RewritePath 
      regexp: /webhook/(?<uri>.*)
      replacement: /${uri}   
    

 

predicates와 filters 설정을 실습하기 위해 아래와 같이 사전작업 하여 주십시오. 

- 어플리케이션을 k8s환경에 배포: config, eureka, webhook, consumer, hystrix-consumer, hystrix-producer, scg

- vscode에서 configmng의 sc-common.yaml파일 오픈 

실습은 아래와 같은 방법으로 합니다. 

- Terminal에서 Pod 'scg-0'의 콘솔을 오픈함 

k logs -f scg-0

- scg-common.yaml파일 수정 

- git repository에 push : vscode의 기능 사용 

- git push시 webhook 동작하여, config서버에서 자동으로 갱신된 설정을 읽어 scg에 반영

scg-0의 콘솔을 보면 갱신되는것을 확인할 수 있습니다. -> 'Received remote refresh request.' 

- 웹브라우저에서 'http://{scg ingress host}/{Path}/*'를 열어 확인함

 

2) Predicates 

라우팅이 동작하는 조건을 지정합니다.  지정할 수 있는 조건은 아래와 같습니다. 

DateTime(Before, After, Between), Cookie, Http Header, Host, Http Method, Query

그리고, Proxying되는 비율(Weight)을 줄 수도 있습니다. 

 

sc-common.yaml에 아래 각 설정을 추가하면서 실습합니다. 

- Before, After, Between

아래 설정을 예를 들어 풀어 쓰면 아래와 같습니다. 

- Between: 2021-02-08 하루 동안은, '/hystrix-producer/coffees/pass'를 http://{hystrix-procucer}/api/coffees/pass로 proxying

- Before: 2020-12-31일 전까지는, '/api/coffees/pass'를 http://{hystrix-procucer}/api/coffees/pass로 proxying 

- After: 2021-01-01일 이후에는, '/hystrix-producer/coffees/pass'를 http://{hystrix-procucer}/api/coffees/pass로 proxying

  - id: hystrix-producer-event 
    uri: lb://hystrix-producer
    predicates:
    - Path=/hystrix-producer/**
    - Between=2021-02-08T00:00:00+09:00[Asia/Seoul],2021-02-08T23:59:59+09:00[Asia/Seoul]
    filters:
    - RewritePath=/hystrix-producer/(?<segment>.*), /coffees/delay  # Rewrite rule
    - PrefixPath=/api  #무조건 앞에 붙이는 문자열 
  
  - id: hystrix-producer-old
    uri: lb://hystrix-producer
    predicates: 
    - Path=/api/coffees/**
    - Before=2020-12-31T23:59:59+09:00[Asia/Seoul]

  - id: hystrix-producer-new
    # ex) /hystrix-producer/coffees/pass => <uri>/api/coffees/pass
    uri: lb://hystrix-producer
    predicates:
    - Path=/hystrix-producer/**
    - After=2021-01-01T00:00:00+09:00[Asia/Seoul]
    filters:
    - RewritePath=/hystrix-producer/(?<apiuri>.*), /${apiuri}  # 그룹갭처 '(?<group>pattern)', group명에 특수문자 쓰지 않기 
    - PrefixPath=/api  #무조건 앞에 붙이는 문자열 
   

※ uri에 'lb://{eureka 등록된 service id}'형식으로 등록하면 eureka와 연동하고 Load balancing되어 proxying합니다.  

  - id: hystrix-producer-event 
    uri: lb://hystrix-producer
  ...  

물론, 직접 host를 지정해도 됩니다. 아래는 k8s service의 주소를 지정한 예입니다.

사실 k8s에서는 k8s service가 L/B역할을 하므로, 이렇게 하셔도 됩니다. 

  - id: hystrix-producer-event 
    uri: http://hystrix-producer:8013

그리고, 당연하겠지만 아래와 같이 L4/L7에 등록된 host를 지정하셔도 됩니다. 

  - id: hystrix-producer-event 
    uri: http://hystrix-producer.ondal.com

 

※ Path에 지정하는 값은 정규식이 아니라 Ant-Style pattern입니다. 

? : 1개의 문자와 매칭 (matches single character)

* : 0개 이상의 문자와 매칭 (matches zero or more characters)

** : 0개 이상의 디렉토리와 파일 매칭 (matches all files / directories)

 

※ RewritePath의 값은 정규식입니다. 

맨 마지막 설정의 RewritePath에서는 정규식 group을 사용하였습니다. 정규식 그룹은 '(?<group>pattern)'형식으로 작성해야 합니다. 

...
	filters:
    - RewritePath=/hystrix-producer/(?<apiuri>.*), /${apiuri}  # 그룹갭처 '(?<group>pattern)', group명에 특수문자 쓰지 않기 
    - PrefixPath=/api  #무조건 앞에 붙이는 문자열 

정규식에 대해선 '정규식 쉽게'로 구글링하시면 많이 나옵니다. => j2doll.tistory.com/646

 

yaml의 Before, After, Between의 값을 바꾸면서 웹브라우저에서 테스트하십시오. 

 

- Cookie: Cookie의 key와 value가 일치하는 경우 proxying

예) '/cookie/'로 시작하고, 요청 header에 LTPAToken이라는 cookie가 있을때, hystrix-producer의 /api/coffees/pass로 proxying함

# Predicates: Cookie
  - id: hystrix-producer-cookie
    uri: lb://hystrix-producer
    predicates:
    - Path=/cookie/**
    - Cookie=LTPAToken,* 
    filters: 
    - RewritePath=/(.*), /api/coffees/pass

 

- Header: Request Http Header에 지정된 key와 값이 있는 경우에만 proxying

예) 요청 주소가 '/header'로 시작하고 Chrome브라우저이며 Host가 'scg'로 시작할때만, hystrix-producer의 /api/coffees/pass로 proxying함. 

# Predicates: Header
  - id: hystrix-producer-header 
    uri: lb://hystrix-producer
    predicates:
    - Path=/header/**
    - Header=User-Agent, .+Chrome.*   #only chrome support
    - Header=Host, ^scg.*         #only from host which start 'scg' 
    filters: 
    - RewritePath=/(.*), /api/coffees/pass    

 

- Host: 특정 host에서 요청하는 경우에만 Proxying

요청주소가 /host로 시작하고 요청host가 scg로 시작하거나 myorg.com으로 끝나는 경우만, hystrix-producer/api/coffees/pass로 proxyin함

# Predicates: Host
  - id: hystrix-producer-host 
    uri: lb://hystrix-producer
    predicates:
    - Path=/host/**
    - Host=scg.**, **.myorg.com   #not regrexp, but Ant-style pattern(?, *. **).
    filters: 
    - RewritePath=/(.*), /api/coffees/pass

 

- Method: 특정 Method로 요청하는 경우에만 Proxying

요청주소가 /method로 시작하고 요청 Method가 GET 또는 POST인 경우에만, hystrix-producer의 /api/coffees/pass로 Proxying

# Predicates: Method
  - id: hystrix-producer-method 
    uri: lb://hystrix-producer
    predicates:
    - Path=/method/**
    - Method=Get,POST   # only allow Get, POST.
    filters: 
    - RewritePath=/(.*), /api/coffees/pass

 

- Query: 특정 Query parameter로 요청하는 경우에만 Proxying

요청 주소가 /query로 시작하고 Query 파라미터가 param=pass인 경우에만, hystrix-producer의 /api/coffees/pass로 Proxying

# Predicates: Query
  - id: hystrix-producer-query 
    uri: lb://hystrix-producer
    predicates:
    - Path=/query/**
    - Query=param, ^pass$   # param값이 정확히 pass일때만 routing 
    filters: 
    - RewritePath=/(.*), /api/coffees/pass

 

- Weight: 지정된 비율로 Proxying하며 새 버전을 일부 유저에게만 공개하여 시험하는 카나리 배포에 사용할 수 있음

요청 주소가 /weight로 시작하면 구 버전 60%, 신 버전 40%의 비율로 연결함

# Predicates: Weight -> use it for canari deploy
  - id: hystrix-producer-v1 
    uri: lb://hystrix-producer
    predicates:
    - Path=/weight/**
    - Weight=group1, 6 
    filters: 
    - RewritePath=/(.*), /api/coffees/no-pass
  - id: hystrix-producer-v2
    uri: lb://hystrix-producer
    predicates:
    - Path=/weight/**
    - Weight=group1, 4
    filters: 
    - RewritePath=/(.*), /api/coffees/pass

웹브라우저에서 http://{scg ingress host}/weight를 오픈하면, 어떨때는 느리고 어떨때는 빠르게 결과가 나올겁니다.

/api/coffees/no-pass로 proxying될때는 느리고, /api/coffees/pass로 proxying될때는 빠르게 응답이 오기 때문입니다. 

 

3) GatewayFilter Factory

Proxying을 하면서 수행할 행위를 정의합니다. 아래와 같은 행위를 할 수 있습니다. 

- Http Request Header의 생성, 수정, 삭제

- Http Response Header의 생성, 수정, 삭제

- Http Request Parameter의 생성, 수정, 삭제 

- Circuit Breaker 

- Target Path의 추가, 수정, 삭제

- Redirect

- Retry

그럼 각각의 방법을 예제를 통해서 실습하겠습니다.

실습을 위해 consumer어플리케이션의 '/greeting/{param}' API에 Request Header를 로깅하는 수행을 추가하겠습니다. 

public class Controller {
	...

	public String greeting(@PathVariable String message, @RequestHeader HttpHeaders headers) {
		...
        
		headers.forEach((key, value) -> {
			log.info(String.format("***** Header '%s' => %s", key, value));
		});

		...
	}
...
}

 

- Http Request Header 추가 : AddRequestHeader

# Request Header: Create
  # watch console of consumer-0 POD 
  - id: consumer-reqheader-create 
    uri: lb://consumer
    predicates:
    - Path=/addreqheader/**
    filters:
    - RewritePath=/addreqheader/(?<msg>.*), /greeting/${msg}
    - AddRequestHeader=x-req-msg, Happy

웹에서 /addreqheader를 오픈하고, POD 'consumer-0'의 콘솔을 보면 아래와 같이 x-req-msg값이 Happy로 나오는 것을

확인할 수 있습니다.

아래와 같이 Predicates의 Path값을 변수로 받아 처리할 수도 있습니다. 

  - id: consumer-reqheader-create2
    uri: lb://consumer
    predicates:
    - Path=/addreqheader2/{greeting}
    filters:
    - AddRequestHeader=x-req-msg, {greeting}
    - RewritePath=/.*/(?<msg>.*), /greeting/${msg} 

 

- Http Request Header 수정: SetRequestHeader

# Request Header: Update
  # /setreqheader/max-age=86400
  - id: consumer-reqheader-update
    uri: lb://consumer
    predicates:
    - Path=/setreqheader/{cache-control} 
    filters: 
    - RewritePath=/.*/(?<msg>.*), /greeting/${msg} 
    - SetRequestHeader=cache-control, {cache-control}

Path에 넘어온 cache-control값을 이용하여 Header 'cache-control'의 값을 바꿉니다.  

 

- Http Request Header 삭제: RemoveRequestHeader 

# Request Header: Delete
  # /rmreqheader/x-removed-header
  - id: consumer-reqheader-delete 
    uri: lb://consumer 
    predicates:
    - Path=/rmreqheader/{rmheader}
    filters:
    - RewritePath=/.*/(?<header>.*), /greeting/${header}
    - AddRequestHeader=x-removed-header, {rmheader}
    - RemoveRequestHeader=x-removed-header

Header 'x-removed-header'를 추가했다가, 바로 삭제하므로 consumer-0의 콘솔에서 보면 x-removed-header는 표시되지 않습니다. 

 

- Http Response Header 추가, 수정, 삭제: AddResponseHeader, SetResponseHeader, RemoveResponseHeader

# Response Header: Create
  - id: consumer-resheader-create 
    uri: lb://consumer
    predicates:
    - Path=/addresheader/{header}
    filters: 
    - RewritePath=/.*/(?<header>.*), /greeting/${header}
    - AddResponseHeader=x-resheader-create, {header}

# Response Header: Update
  # /setcontenttype/text
  - id: consumer-resheader-update
    uri: lb://consumer 
    predicates:
    - Path=/setcontenttype/{content-type}
    filters: 
    - RewritePath=/.*/(?<type>.*), /greeting/${type}
    - SetResponseHeader=Content-Type, {content-type}

# Response Header: Delete
  - id: consumer-resheader-delete
    uri: lb://consumer 
    predicates:
    - Path=/rmresheader/{rmheader}
    filters: 
    - RewritePath=/.*/(?<header>.*), /greeting/${header}
    - AddResponseHeader=x-header, {rmheader}
    - AddResponseHeader=x-removed-header, x-header
    - RemoveResponseHeader=x-header

Http Response Header는 아래와 같이 브라우저 디버깅 툴을 이용하면 볼 수 있습니다. 

 

- Http Request Parameter 추가: AddRequestParameter

주석에 있듯이   '/addreqparam/hello'는 '/consumer/hello?svc=consumer&api=greeting'로 Proxying되고,

두번째 설정(id=consumer-routes-by-query)에 의해   'http://{consumer}/greeting/hello'로 Proxying됩니다.  

  - id: consumer-routes-by-query
    uri: lb://consumer
    predicates: 
    - Path=/consumer/** 
    - Query=svc, ^consumer$
    - Query=api, ^greeting$
    filters: 
    - RewritePath=/.*/(?<msg>.*), /greeting/${msg}

  - id: scg-reqparam-create
    uri: lb://scg
    predicates:
    - Path=/addreqparam/**
    filters:
    - RewritePath=/.*/(?<param>.*), /consumer/${param}
    - AddRequestParameter=svc, consumer 
    - AddRequestParameter=api, greeting 

- Http Request Parameter 수정: SetRequestParameter가 있어야 할것 같지만 없습니다. 대신 AddRequestParameter를 사용합니다. 

# Request Parameter: Update - No SetRequestParameter. use AddRequestParameter
  - id: consumer-reqparam-update 
    uri: lb://scg
    predicates: 
    - Path=/setreqparam/**
    - Query=api, ^greeting$   
    filters:
    - RewritePath=/.*/(?<msg>.*), /consumer/${msg} 
    - AddRequestParameter=svc, consumer
    - AddRequestParameter=api, greeting 

- Http Request Parameter 삭제: RemoveRequestParameter

# Request Parameter: Delete 
  - id: consumer-reqparam-delete 
    uri: lb://scg
    predicates: 
    - Path=/rmreqparam/** 
    - Query=svc, webhook  
    - Query=api, greeting
    filters: 
    - RewritePath=/.*/(?<msg>.*), /consumer/${msg} 
    - RemoveRequestParameter=svc 
    #- AddRequestParameter=svc, consumer

Parameter 'svc'가 삭제되므로, http://{scg host}/consumer/{msg}?api=greeting으로 proxying되어, 에러가 납니다.

맨 마지막 줄의 주석을 제거하면, 정상 동작할겁니다. 

 

- Path 추가: PrefixPath

/prefixpath -> /api/coffees/pass 로 proxying합니다. 

  - id: prefixpath
    uri: lb://hystrix-producer
    predicates:
    - Path=/prefixpath
    filters: 
    - RewritePath=/.*, /coffees/pass 
    - PrefixPath=/api

- Path 수정: RewritePath, SetPath

RewritePath는 계속 실습했으니, SetPath만 실습합니다. 

/setpath/{api}/{param} -> {consumer}/{api}/{param}. 예를 들어, /setpath/greeting/hi => {consumer}/greeting/hi로 프락싱

# SetPath
  # /setpath/hystrix/pass 
  - id: setpath 
    uri: lb://consumer
    predicates:
    - Path=/setpath/{api}/{param}
    filters:
    - SetPath=/{api}/{param}

- Path 삭제: StripPath

요청되는 Path에서 특정번째 디렉토리를 제거하고 proxying합니다. 

아래 예에서는 /strip-prefix/webhook/greeting/hello에서 1번째 디렉토리를 제거하고 http://{scg host}/webhook/greeting/hello로 proxying합니다. 

# StripPrefix
  # /strip-prefix/prefixpath -> /prefixpath 
  - id: strip-prefix
    uri: lb://scg
    predicates:
    - Path=/strip-prefix/**
    filters: 
    - StripPrefix=1 

 

- RedirectTo

지정된 사이트로 이동합니다. Path의 변수를 받아서 처리하진 못합니다.  

# RedirctTo 
  - id: redirect-to
    uri: lb://consumer 
    predicates:
    - Path=/redirect
    filters:
    #- RedirectTo=302, http://{domain}   # 변수 받는건 안됨
    - RedirectTo=302, http://google.com

 

- Retry: Proxying 실패 시 재시도 처리

Proxying 실패 시 {firstBackoff}후에 첫번째 재시도를 합니다. 

{maxBackoff}시간마다 재시도를 {retries}번 합니다. 

※ 원래 재시도 주기는 {firstBackoff} * ({factor}^{retries})입니다. 이 값이 {maxBackoff}보다 크면, {maxBackoff}가 재시도 주기가 됩니다. 

아래 예에서 계산된 재시도 주기는 500ms * (10 ^ 5) = 500ms * 100,000 = 50,000,000ms 이고, maxBackoff인 2000ms보다 훨씬 크므로, 재시도 주기는 2000ms가 됩니다. 

factor를 사용하여 재시도 주기를 지정하면 복잡하므로, factor를 충분히 크게 지정하고 maxBackoff에 주기를 지정하는것이 간편합니다. 

 

※ metadata로 connect와 reponse timeout을 지정해야 제대로 동작합니다. 

이 값이 없고  Global Http time설정도 없다면 기본값(아마 30초)이 적용되어, Retry가 제대로 수행되지 않습니다.

Global Http timeout설정은 6) Http timeout에서 설명합니다.  

# Retry
  - id: consumer-retry 
    uri: lb://consumer 
    predicates: 
    - Path=/retry
    filters: 
    - RewritePath=/.*, /greeting/hi-retry 
    - name: Retry 
      args: 
        retries: 5              # 재시도 횟수 
        statuses: BAD_GATEWAY, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE        
        methods: GET, POST 
        backoff: 
          firstBackoff: 500ms   #첫번째 재시도는 실패 후 0.5초 후 수행  
          maxBackoff: 2000ms    #재시도 간격 
          factor: 10            #firstBackoff * (factor^retries)가 재시도 간격임. maxBackoff보다 클 수는 없음.   
        #exceptions:             # Connect가 안되는 경우에만 retry
         #- java.net.ConnectException
    metadata:
      connect-timeout: 1000     
      response-timeout: 1000

테스트 결과 예시) 아래는 Global Filter를 만들어 Retry 시간을 본 결과입니다.  Global Filter는 바로 다음에 실습합니다. 

최초 실패 후에 500ms후에 재시도 된걸 볼 수 있습니다. 

response timeout이 1000ms이니, 1초 동안 시도하다 timeout이 납니다. 그리고 재시도 주기인 2000ms를 더 기다립니다.  

결과적으로 로그에는 3초마다 재시도하는걸로 나옵니다. 

 

※ POST요청 Retry때 중복 처리 방지

POST요청을 잘못 Retry하면 데이터가 중복 등록될 위험이 있습니다. 

이를 방지하기 위해 POST일때는 'exceptions'에 'java.net.ConnectException'을 추가하시는것이 좋습니다. 

즉 연결이 안되는 예외가 발생하는 경우에만 재시도를 하게 하는겁니다. 

 

4) Global Filter 

Global Filter는 Request가 올때마다 항상 수행되는 필터입니다.  이 필터에 프로그래밍하여, 인증/인가 처리나 로깅을 할 수 있습니다. 

여기서는 간단히 필터를 만드는 방법만 설명합니다. 

- Spring Bean만들기 : @Configuration과 @Bean 이용

아래와 같이 'CustomGlobalFilter'라는 Spring Bean을 생성합니다. 어플리케이션 구동 시 생성되어 Bean Pool에 등록됩니다. 

package com.springcloud.scg;

import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RouteConfig {
    @Bean
    public GlobalFilter filter1() {
        return new CustomGlobalFilter();
    }
}

- CustomGlobalFilter 작성

Global Filter는 GlobalFilter와 Ordered라는 추상 Class를 상속받아 만들어야 합니다. 

Global Filter가 여러개인 경우, getOrder()에 지정된 순서로 수행됩니다. 

package com.springcloud.scg;

import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

public class CustomGlobalFilter implements GlobalFilter, Ordered {
    private final Logger log = LoggerFactory.getLogger(getClass());

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("FIRST custom global filter->Order is 10");
        
        return chain.filter(exchange);
    }

    @Override 
    public int getOrder() { return 10; } 


}

위와 같이 Global Filter를 만들고, 위 Retry필터를 테스트 해 보시기 바랍니다.

 

5) RemoveHopByHop Filter : Http Request Header 삭제 

remove-hop-by-hop 필터는 Http Request Header에서 지정된 header를 삭제합니다. 

아래와 같이 테스트 해 봅니다.

- POD 'consumer-0'의 console을 오픈합니다. 

k logs -f consumer-0

- 웹브라우저에서 http://{scg host}/retry를 입력합니다. http://{consumer}/greeting/{msg}로 프락싱됩니다. 

consumer의 Controller.java에서 '/greeting/{msg}'가 호출될 때 Http Request Header의 값을 콘솔에 찍게 했으므로, 

header의 값들이 표시됩니다. connection, keep-alive, x-real-ip가 나오는지 확인합니다.  

- 아래 설정을 configmng의 scg-common.yaml에 추가합니다. 

'---' 를 추가하고, 그 다음줄에 추가해야 합니다. 

...
---
spring.cloud.gateway:
  filter:
    remove-hop-by-hop:
      headers:
        - connection
        - keep-alive
        - x-real-ip

- 웹브라우저에서 http://{scg host}/retry를 다시 오픈하고, consumer-0의 콘솔을 확인합니다. 

위에서 지정한 header들이 안 나오는걸 확인합니다. 

 

6) Http timeout: connection과 response timeout 설정 

- Global하게 설정하려면 아래와 같이 하십시오. 

connect-timeout은 반드시 단위 없이 milisecond값을 지정하고,  response-timeout은 단위 있는 형식으로 지정합니다. 

spring.cloud.gateway:
  ...
  # http timeout
  httpclient: 
    connect-timeout: 1000
    response-timeout: 2000ms  # or 2s 

- 각 request마다 지정하려면 아래와 같이 하십시오. 이미 위 /retry에 대해 설정해 봤습니다. 

timeout이 너무 짧아 에러가 날것입니다.  1000 정도로 늘려주면 정상적으로 수행됩니다. 

# Timeout
  - id: timeout 
    uri: lb://consumer 
    predicates: 
    - Path=/timeout
    filters: 
    - RewritePath=/.*, /greeting/timeout
    metadata:
      connect-timeout: 10     
      response-timeout: 10

 

7) CORS

CORS(Cross-Origin Resource Sharing)는 요청하는 도메인과 프락싱 대상 도메인이 다를때 이를 허용해 주는 방법입니다. 

아래 예와 같이 요청 Path별로 허용되는 도메인과 메소드를 지정하면 됩니다. 

  # CORS 
  globalcors:
    cors-configurations:
      '[/**]':
        allowedOrigins: "https://direct-order.ondal.com"
        allowedMethods: 
        - GET
      '[/consumer/**]':
        allowedOrigins: "https://consumer.ondal.com"
        allowedMethods:  
        - GET 
        - PUT 
        - POST 

 

8) Custom Gatewayfilter Factory 만들기 

Gatewayfilter Factory는 위 scg-common.yaml에서 'filters' 항목 밑에 정의했던 필터들을 말하는겁니다. 

아래 설정과 같이, 요청 Path의 첫번째 디렉토리에 proxying할 service id가 있으면 그 어플리케이션으로 proxying할 수는 없을까요 ? 

불행히도 이걸 yaml로 할 수는 없습니다. 그래서, yaml로 하려면 어플리케이션이 생길때마다 추가해 줘야 합니다.

어플리케이션의 서비스명이 변경되거나, 어플리케이션이 없어질때도 yaml을 수정해 줘야 하구요. 

 

# Dynamic routing
  - id: dynamic-routing 
    uri: lb://{serviceid}
    predicates: 
    - Path=/{serviceid}/**
    filters: 
    - RewritePath=/.*/(?<api>.*), /${api}
    

이걸 동적으로 하려면 프로그램으로 Custom Gatewayfilter Factory를 만들어 줘야 합니다. 

- yaml에 custom filter 지정

먼저 scg-common.yaml에 routing 설정을 추가합니다. 

uri에는 아무값이나 넣으면 됩니다. 어차피 custom filter에서 바뀌기 때문입니다. 

# Dynamic routing using Custom GgatewayFilterFactory
  # ref: https://github.com/spring-cloud/spring-cloud-gateway/issues/608#issuecomment-431852899
  - id: dynamic-routing 
    uri: lb://tbd
    predicates: 
    - Path=/*/**
    filters: 
    - StripPrefix=1
    - name: UriHostPlaceholderFilter
      args: 
        order: 10001

 

- dependency  'lombok' 추가 

lombok은 Getter와 Setter를 자동으로 만들어주는 모듈입니다.

아래와 같이 '+'를 누르고, 'lombok'을 입력한 후 엔터를 쳐서 검색하십시오. 결과 중 org.projectlombok을 선택하십시오.  

 

- UriHostPlaceholderFilter.java를 작성합니다. 

package com.springcloud.scg;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

//ref: https://github.com/spring-cloud/spring-cloud-gateway/issues/608#issuecomment-431852899

@Component 
public class UriHostPlaceholderFilter extends AbstractGatewayFilterFactory<UriHostPlaceholderFilter.Config> {
    private final Logger log = LoggerFactory.getLogger(getClass()); 
    
    public UriHostPlaceholderFilter() {
        super(Config.class);
    }

    @Override 
    public GatewayFilter apply(Config config) {
        return new OrderedGatewayFilter((exchange, chain) -> {
            String serivceID = "";
            String downStreamPath ="";
            URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
            LinkedHashSet<URI> originalURI = exchange
                    .getRequiredAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);			
            addOriginalRequestUrl(exchange,  uri);					
            String path = originalURI.stream().findFirst().get().getPath();
            Pattern pattern = Pattern.compile("/(?<eurekaSerivceId>[^/]*)/(?<downStreamPath>.*)");
            if (path != null) { 
                Matcher matcher = pattern.matcher(path);
                boolean flag = matcher.matches();
                serivceID = matcher.group("eurekaSerivceId");
                downStreamPath = matcher.group("downStreamPath");
            }
            String newurl = "lb://"+serivceID.toUpperCase()+"/"+downStreamPath;
            log.info(String.format("****** URI: %s => %s", path, newurl));

            URI newUri =null;
            try {
                newUri = new URI(newurl);
            } catch (URISyntaxException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }		
            
            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newUri);			
            return chain.filter(exchange);
        },config.order);
    }

    @Getter
    @Setter 
    public static class Config {
        private int order;
    
    }
}

@Getter, @Setter가 lombok을 이용하여, 자동으로 Getter와 Setter를 생성하라는 어노테이션입니다. 

참고) args의 key인 'order'는 위 필터에서 Config의 property명과 동일해야 합니다. 

아래와 같이 yaml에 설정하면, order값을 10001로 바꾸는 겁니다. 

order값은 filter가 적용될 우선순위입니다.

이 Custom filter는 다른 필터를 적용하고 맨 마지막에 적용하는게 좋으므로, 값을 매우 높게 셋팅한겁니다. 

  - id: dynamic-routing 
	...
	- name: UriHostPlaceholderFilter
      args: 
        order: 10001

 

- 배포 및 테스트

configmng를 git push하고, scg를 다시 배포하십시오. 

아래 예와 같이 첫번째 경로에 service id를 지정하여, 동적으로 프락싱이 가능해졌습니다. 

 

9) Custom Routing 프로그램 만들기 

yaml로 다양한 routing 설정을 할 수 있는데, 상황에 따라 프로그램으로 라우팅 설정을 하는것이 필요할 수 있습니다. 

예를 들어, DB에서 설정값을 읽어 라우팅 설정에서 사용하는 경우처럼 말이죠. 

SCG의 RouteLocator라는 객체를 이용하면 custom routing 프로그램을 만들 수 있습니다. 

Spring bean으로 등록해야 하므로, 위에서 만든 RouteConfig.java에 추가하여 만듭니다. 

package com.springcloud.scg;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GlobalFilter;
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 RouteConfig {
    private final Logger log = LoggerFactory.getLogger(getClass());

	...
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        // 순서 : order -> predicates -> filters -> uri -> metadata -> id
        return builder.routes()
            // /spring-images/spring-logo-9146a4d3298760c2e7e49595184e1975.svg
            
            .route(r -> 
                r.order(0)
                .path("/spring-images/**")
                .filters(f -> 
                    f.addRequestHeader("x-header1", "springcloud")
                    .rewritePath("/.*/(?<image>.*)", "/images/${image}")
                )
                .uri("https://spring.io")
                .metadata("response-timeout", 1000)
                .metadata("connect-timeout", 1000)
                .id("images")
            )

            // /daum-images/20200723055344399.png
            .route(r -> 
                r.order(-1)
                .path("/daum-images/**")
                .filters(f -> f.rewritePath("/.*/(?<image>.*)", "/daumtop_chanel/op/${image}"))
                .uri("https://t1.daumcdn.net")

            ).build();
    }
}

주석에 있듯이, RouteLocator는 order -> predicates -> filters -> uri -> metadata -> id 순으로 작성해야 합니다.

위 예제는 '/spring-images/spring-logo-9146a4d3298760c2e7e49595184e1975.svg'를 입력하면, spring.io의 Logo를 proxying하고, 

'/daum-images/20200723055344399.png'로 요청하면, daum의 이미지를 proxying해줍니다. 

filter가 여러개 일때는 위 예제와 같이 '.'으로 chaining하면 됩니다. 

 

 

SCG실습: 로깅 & 메트릭

1) default-filters : 전체에 적용될 Gatewayfilter Factory를 지정

아래 예와 같이 지정하면, 모든 요청 header에 'x-serviceid'가 추가 됩니다. 

  # default filters 
  default-filters: 
  # Try /setpath/greeting/hey, Watch console of consumer-0 to check 'x-serviceid'.
  - AddRequestHeader=x-serviceid, spring-gateway   
  

default-filters를 이용하여, 요청 시와 응답 시에 로깅을 하는 Custom Gatewayfilter Factory를 만들 수 있습니다. 

 

2) Pre Logging Gatewayfilter 만들기 : Request를 Proxying하기 전에 수행됩니다. 

- scg-common.yaml에 Pre Logging filter를 설정합니다. 

'PreLogger'라는 filter 이름과, baseMessage와 logging이라는 Argument이름은 다른걸로 바꾸셔도 됩니다. 

  spring.cloud.gateway:
  
  ...
  
  # default filters 
  default-filters: 
  
  ...
  
  # GatewayFilterFactory sample 
  - name: PreLogger
    args:
      baseMessage: PRE GATEWAYFILTER FACTORY 
      logging: true 
  

 

- 'PreLogger.java' 개발

GatewayFilter는 AbstractGatewayFilterFactory에서 상속받아야 합니다. 

package com.springcloud.scg;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Component
public class PreLogger extends AbstractGatewayFilterFactory<PreLogger.Config> {
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    public PreLogger() {
        super(Config.class);
    }

    @Override 
    public GatewayFilter apply(Config config) {
        //grab configuration from Config object 
        return (exchange, chain) -> {
            log.info(">>>>> Base Message => " + config.baseMessage);

            if(config.logging) log.info("============= START >>>>> "+exchange.getRequest());
            
            ServerHttpRequest.Builder builder = exchange.getRequest().mutate();

            return chain.filter(exchange.mutate().request(builder.build()).build());
        };
    }

    @Getter
    @Setter 
    public static class Config {
        private String baseMessage; 
        private boolean logging;
        
    }
}

 

 

3) Post Logging Gateway filter 만들기 : Response를 리턴하기 전에 수행합니다. 

- scg-common.yaml에 Post Logging filter를 설정합니다. 

'PostLogger'라는 filter 이름과, baseMessage와 logging이라는 Argument이름은 다른걸로 바꾸셔도 됩니다.

  spring.cloud.gateway:
  
  ...
  
  # default filters 
  default-filters: 
  
  ...
   
  - name: PostLogger
    args: 
      baseMessage: POST GATEWAYFILTER FACTORY
      logging: true

 

- PostLogger.java 작성

package com.springcloud.scg;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;
import reactor.core.publisher.Mono;

@Component
public class PostLogger extends AbstractGatewayFilterFactory<PostLogger.Config> {
    private final Logger log = LoggerFactory.getLogger(getClass());
    
    public PostLogger() {
        super(Config.class);
    }

    @Override 
    public GatewayFilter apply(Config config) {
        //grab configuration from Config object 
        return (exchange, chain) -> {
            return chain.filter(exchange).then(Mono.fromRunnable( () -> {
                log.info(">>>>> PostGatewayFilterFactory Message => " + config.baseMessage);

                ServerHttpResponse response = exchange.getResponse(); 
                if(config.logging) log.info("============= END >>>>> "+exchange.getResponse());
            }));
        };
    }

    @Getter 
    @Setter
    public static class Config {
        private String baseMessage; 
        private boolean logging;        
    }
}

 

4) Pre/Post Logger 테스트 

configmng와 scg를 git에 push하고, scg를 재배포 합니다. 

그리고, 웹브라우저에서 아무 url이나 열었을때, scg-0의 콘솔에 어떻게 찍히는지 확인합니다. 

위와 같이 PreLogger filter -> Global filter -> UriHostPlaceholderFilter -> PostLogger filter 순으로 수행됩니다.

 

5) Spring Cloud Sleuth와 Zipkin 설정

scg의 pom.xml에 아래 2개 dependency를 추가합니다. 

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-sleuth</artifactId>
			
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-zipkin</artifactId>
			
		</dependency>

configmng의 scg-common.yaml에 설정을 추가합니다. 

zipkin서버의 주소는 올바르게 입력해야 합니다. Sleuth와 Zipkin편을 참고하세요.

---
spring:
  sleuth: 
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://zipkin:9411

configmng와 scg를 git push하고, scg를 재배포합니다. 

Log에 Trace ID와 Span ID가 삽입되었는지 확인합니다. Zipkin에서 확인하는 방법은  Sleuth와 Zipkin편을 참고하세요.

 

6) 메트릭: Actuator 

/actuator/gateway라는 endpoint에서 다양한 메트릭을 제공합니다. 

configmng의 scg-common.yaml에 아래 설정을 추가합니다. 사실은 default로 Actuator는 활성화 되어 있어서 안해도 됩니다. 

# Enable gateway Actuator: default is enabled
# gateway endpoints: https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#recap-the-list-of-all-endpoints
management:
  endpoint.gateway.enabled: true

주의) 'management.endpoints.web.exposure.include: gateway'를 추가하면 안됩니다.

configmng의 application.yaml에 설정된 'management.endpoints.web.exposure.include: *'을 덮어 써서, 

노출되는 Actuator endpoint가 gateway만 남게 됩니다. 

 

주석에도 있지만, /actuator/gateway/ 하위에서 제공하는 endpoint는 아래 링크를 참조하세요.

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#recap-the-list-of-all-endpoints