Netflix Zuul - 요청을 다른데로 보내고 싶다면
Zuul 을 만나다
사건의 발단
서비스용 Stat 페이지를 운영툴에 붙이고 싶었다. 이 Stat 페이지는 vue 코드가, elastic search API를 찔러야하는 구조로 되어있고, elastic search가 설치된 서버는 물론~ 운영툴의 도메인과는 다른 도메인이다.
같은 vue로 되어있는 운영툴 frontend에 떡하니 붙여보니, 운영툴 FE 도메인이 다른 도메인에 요청을 보내므로 CORS가 뜬다.
운영툴의 backend 는 frontend와 같은 도메인이므로 CORS 에러가 나지 않는다.
따라서 이런 결론에 다다르게 되었다.
backend에서 특정 uri로 들어온 요청을 elastic search로 프록시해주자!
그래서 이미 프로젝트에서 같은 목적으로 쓰고 있던 zuul을 사용 해보게 되었다.
Zuul?
참고한 링크를 보면 Zuul에 대한 설명이 나와있다. 'Zuul은 모든 디바이스/웹사이트에서 넷플릭스 백엔드에 보내는 요청의 앞문이다. Zuul은 동적 라우팅, 모니터링, 탄성, 그리고 보안을 가능하게 만들어졌다.'
즉, client가 요청을 zuul에 보내고, zuul 을 통해서 넷플릭스의 백엔드 서비스로 퍼지게 된다.
넷플릭스는 시스템들이 복잡하게 엮여 있고, 1,000 개 이상의 디바이스 종류를 지원하며, 초당 5만 건 이상의 요청을 처리하는 API를 갖고 있다. 또한 매일매일 피쳐를 새롭게 개발하며, 계속해서 변경점이 생긴다. 새로운 AWS 리전에 배포를 시작하고, UI에서도 변경점이 계속 만들어진다. 이런 변화에 대응하기 위해서, 빠른 개발, 높은 유연성, 인사이트가 필요했다. 이를 해결해줄 것이 바로 Zuul인것이고!
어떻게 작동하나?
Zuul의 핵심에는 HTTP 요청과 응답을 라우팅하는 과정에서 특정 액션을 수행하는 필터들이 있다. Zuul 필터의 핵심 특징은 다음과 같다.
- Type: 라우팅 플로우 중에 필터가 어디에 적용될지를 의미한다 ( 라우팅 훨씬 전에, 직전에, 이후에 등)
- Execution Order: 여러 필터 중 몇번째로 실행될 것인지 의미한다. Type과 관련되어 있음.
- Criteria: 필터가 작동할 조건을 의미한다.
- Action: 조건(위의 Criteria)가 만족할때 어떤 행동을 할 것인지 정의한다.
아래는 simple filter의 예이다.
Zuul 은 이런 필터를 동적으로 읽고, 컴파일하고, 동작하는 프레임워크를 제공한다. 필터는 서로 직접적으로 통신하진 않고, 각 Request 마다 유니크하게 생성되는 RequstContext라는 상태를 공유한다. 현재는 필터가 Groovy로 쓰여있지만, Zuul은 JVM 기반 언어를 모두 지원한다. 각 필터의 소스코드는 줄 서버의 특정 디렉토리에 들어가 있고, 변경사항이 있는지 주기적으로 poll 을 돌린다. 업데이트된 필터를 디스크에서 가져와서, 현재 돌고 있는 서버에 동적으로 컴파일되고 이후 요청부터는 변경된 필터로 동작한다.
Zuul의 핵심 아키텍처 구조.
요청이 들어오면 통과하게되는 필터 타입들을 알아보자.
- PRE : 라우팅되기 전에 실행된다. 여기에서는 대개 요청에 대한 인증, origin server 선택, 디버깅 정보를 로깅하는 등의 일을 한다. (origin은 요청이 원래 도달해야하는 서버를 의미한다.)
- ROUTING: origin으로의 요청 라우팅을 다룬다. origin에 보낼 HTTP Request가 생성되는 곳이자 Apache HttpClient 혹은 Netflix ribbon을 사용해서 보내지는 곳이다.
- POST : 요청이 origin 에 도달한 이후 실행된다. 클라이언트에 보낼 응답에 HTTP header를 붙이거나, 통계나 지표 수집, origin 에서 client로 응답을 스트리밍하는 등에 사용한다.
- ERROR: 다른 단계에서 에러가 발생하면 실행된다.
Zuul에는 기본 필터들이 차례대로 실행되고, 그 중간에 custom filter를 넣을 수 있다.
내 경우 origin에 해당하는 서버에 따로 인증을 했어야해서, Custom filter를 사용해서 라우팅 때 인증 정보를 함께 보내려고 맘을 먹었다. 이에 필요한 정보는 https://supawer0728.github.io/2018/03/11/Spring-Cloud-Zuul/ 를 참고했다.
프로젝트에 적용
- build.gradle에 의존성을 추가해준다.
- Application.java 혹은 Config 파일에
@EnableZuulProxy
Annotation을 설정해준다. 프로젝트에서는 다음과 같은 ZuulConfig를 사용했다.
- application.yml에 라우팅할 루트를 설정한다.
이렇게 하면, 프로젝트 백엔드 서버 <host명>/api/custom
하위로 요청이 들어가면 [https://another-api.com/search](https://another-api.com/search)
에 요청이 라우팅된다. another api
서버에 인증이 필요해서 custom filter에 authorization header를 따로 붙여줄 것이므로, sensitive-headers에 다른 헤더 명들만 명시해준다(Cookie, Set-Cookie).
- Custom filter를 만든다.
내 경우 크게 두가지를 해주어야 했다.
- origin 서버에 대한 인증
- uri 변경
- 위의 설정처럼 하면, origin으로 요청이 /search/api/custom으로 간다.
- 백엔드에서는 /api/custom 으로 요청을 보내고, origin에서는 요청이 search로 가도록 해주고 싶다.
이 요구조건을 Custom Filter로 해결할 수 있다.
인증은 보통 Pre filter에서 한다-라고 대부분의 문서에 나와있지만, 이는 API GATEWAY에 대한 인증이었나보다... origin 에 대한 인증은 Route 필터에서 해야 동작했다. 이 필터는 이 custom api 에 대해서만 동작했으면 해서 shouldFilter로 조건을 부여했다. 이 부분이 위에서 말한 Criteria에 해당한다.
filter가 실행되면
RequestContext에 인증 헤더를 붙이고
URI를 변경하는 작업을 한다.
Filter를 bean으로 등록한다.
아까 설정한 Config 내부에 bean으로 등록한다. application.java에서 해도 되지만 좀더 깔끔한 표현을 위해!
- 테스트 해본다.
POSTMAN으로 backend에 요청을 보내서 origin 에서 내려주는 응답이 오는지 확인해본다.
결론
zuul은 적용하기도 쉽고, 손쉽게 CORS 오류를 피할 수 있는 방법이라 편리하게 사용했다.
대용량 시스템에서의 로드밸런싱 등에도 효과적으로 사용할 수 있는 것으로 보이는데, 다음에 쓸일이 있으면 더 정리하도록 하겠다.
참고
http://woowabros.github.io/r&d/2017/06/13/apigateway.html
https://medium.com/netflix-techblog/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee