Springboot MSA 구성2 - Spring Cloud Gateway
Spring Cloud Gateway는 API로 라우팅할 수 있는 간단하면서도 효과적인 방법을 제공하고 보안, 모니터링/메트릭스, 복원력 등과 같은 다양한 우려 사항을 제공하는 것을 목표로 한다. 서비스 엔드포인트를 하나로 통일해서 요청의 특성별로 알맞는 서비스로 라우팅 할 수 있는 기능을 제공한다. 라우팅 설정은 무중단 적용이 가능하다. 클라이언트는 엔드포인트가 한군데로 통일되어 관리포인트가 줄어드는 장점이 있다.
API Gateway의 역할
프록시의 역할과 로드밸런싱 - URI에 따라 서비스 엔드포인트를 다르게 가져가는 동적 라우팅이 가능하다.
인증 서버로서의 기능 - 모든 요청/응답을 관리할 수가 있어 앞단에 인증 및 보안을 적용하기가 용이하다.
로깅 서버로서의 기능 - 모든 요청/응답을 관제할 수 있는 모니터링 시스템 구성이 단순해진다.
Config서버에서 관리하는 config-repo 모듈에 Gateway Route 설정 추가
Gateway Route 설정추가
# gateway-local.yml
zuul:
routes:
member:
stripPrefix: false
path: /v1/member/**
serviceId: resource
pay:
stripPrefix: false
path: /v1/pay/**
serviceId: resource2
else:
stripPrefix: false
path: /v1/**
serviceId: resource2
---
# gateway-prod.yml
zuul:
routes:
member:
stripPrefix: false
path: /v1/member/**
serviceId: resource
pay:
stripPrefix: false
path: /v1/pay/**
serviceId: resource2
else:
stripPrefix: false
path: /v1/**
serviceId: resource2
Config 서버확인
GET http://localhost:9000/gateway/local
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 25 Aug 2021 03:39:41 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "gateway",
"profiles": [
"local"
],
"label": null,
"version": "46e8eb3c8d007a6aef573aa6c815950d0a56fd3b",
"state": null,
"propertySources": [
{
"name": "https://github.com/sisipapa/Springboot-MSA/file:C:\\Users\\user\\AppData\\Local\\Temp\\config-repo-10700435317011410158\\config-repo\\local\\gateway-local.yml",
"source": {
"spring.profiles": "local",
"zuul.routes.member.stripPrefix": false,
"zuul.routes.member.path": "/v1/member/**",
"zuul.routes.member.serviceId": "resource",
"zuul.routes.pay.stripPrefix": false,
"zuul.routes.pay.path": "/v1/pay/**",
"zuul.routes.pay.serviceId": "resource2",
"zuul.routes.else.stripPrefix": false,
"zuul.routes.else.path": "/v1/**",
"zuul.routes.else.serviceId": "resource2"
}
}
]
}
Response code: 200; Time: 1120ms; Content length: 843 bytes
Gateway 모듈 구성
Gateway 서버의 경우 클라이언트들의 엔드포인트로 트래픽이 높기 때문에 단일 서비스로 운영하기 보다는 여러대로 구성하고 LoadBalancer로 묶어 HA를 확보하는 것이 좋다.
build.gradle
Multi Module 프로젝트 구성의 build.gradle의 Gateway 모듈에 해당하는 부분만 정리. spring-cloud-starter-netflix-zuul dependency 추가한다. gradle에서 zuul 라이브러리를 내려받지 못해 2.2.9.RELEASE 버전을 입력해 주었다.
project(':Gateway') {
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul:2.2.9.RELEASE'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
}
Gateway 모듈 bootstrap-{env}.yml
local, prod 내용은 동일하다.
# bootstrap-local.yml
server:
port: 9100
spring:
application:
name: gateway
config:
import: "optional:configserver:http://localhost:9000"
management:
endpoints:
web:
exposure:
include: refresh
---
# bootstrap-local.yml
server:
port: 9100
spring:
application:
name: gateway
config:
import: "optional:configserver:http://localhost:9000"
management:
endpoints:
web:
exposure:
include: refresh
Application.java
EnableZuulProxy 어노테이션을 추가한다.
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Resource 서버에서 확인을 위한 Gateway Routing 확인을 위한 Controller 추가
Resource 서버(8080) 추가된 Controller
@RequestMapping("/v1")
@RestController
@RefreshScope
public class MemberController {
@GetMapping("/member/health")
public String memberHealth() {
return "MemberController running";
}
@GetMapping("/member2/health")
public String member2Health() {
return "MemberController running";
}
}
Resource2 서버(8081) 추가된 Controller
PayController
@RequestMapping("/v1/pay")
@RestController
@RefreshScope
public class PayController {
@GetMapping("/health")
public String health() {
return "PayController running";
}
}
ProductController
@RequestMapping("/v1")
@RestController
@RefreshScope
public class ProductController {
@GetMapping("/product/health")
public String productHealth() {
return "PayController running";
}
@GetMapping("/product2/health")
public String product2Health() {
return "PayController running";
}
}
Gateway Route Test 테스트
테스트 중 Exception 발생
java.lang.NoSuchMethodError: org.springframework.boot.web.servlet.error.ErrorController.getErrorPath()Ljava/lang/String;
at org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping.lookupHandler(ZuulHandlerMapping.java:87) ~[spring-cloud-netflix-zuul-2.2.9.RELEASE.jar:2.2.9.RELEASE]
at org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.getHandlerInternal(AbstractUrlHandlerMapping.java:152) ~[spring-webmvc-5.3.9.jar:5.3.9]
at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:498) ~[spring-webmvc-5.3.9.jar:5.3.9]
at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1258) ~[spring-webmvc-5.3.9.jar:5.3.9]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.3.9.jar:5.3.9]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.9.jar:5.3.9]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.52.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.52.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) ~[spring-boot-actuator-2.5.4.jar:2.5.4]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.52.jar:9.0.52]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
spring-boot-starter 버전이 업그레이드 되면서 spring-cloud-starter-netflix-zuul의 ZuulHandlerMapping 클래스에서 ErrorController.getErrorPath() NoSuchMethodError 발생했고 오류 두시간 이상 오류 해결이 되지 않아 springboot, spring cloud 라이브러리 버전을 변경하게 되었다.
변경전
Springboot - 2.5.4
Spring Cloud - 2020.0.3
변경후
Springboot - 2.3.12.RELEASE
Spring Cloud - Hoxton.SR5
라이브러리 변경 후 Resource,Resource2 서버에서 config 서버에 접속하기 위한 설정변경
#AS-IS(Spring Cloud-2020.0.3)
spring:
config:
import: "optional:configserver:http://localhost:9000"
---
#TO-BE(Spring Cloud-Hoxton.SR5)
spring:
cloud:
config:
uri: http://localhost:9000
라이브러리 버전 변경후 Config 서버 구동시 오류
Springboot 2.5.3 버전에서는 applicaition.yml 파일내에서 encrypt설정을 정상적으로 로드가 되지만 Springboot 버전을 2.3.12.RELEASE로 변경 후 아래 오류가 발생해서 Config 서버의 application-{env}.yml 파일의 이름을 bootstrap-{env}.yml로 변경
***************************l
APPLICATION FAILED TO START
***************************
Description:
Field rsaProperties in org.springframework.cloud.config.server.config.EncryptionAutoConfiguration$KeyStoreConfiguration required a bean of type 'org.springframework.cloud.bootstrap.encrypt.RsaProperties' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=false)
Action:
Consider defining a bean of type 'org.springframework.cloud.bootstrap.encrypt.RsaProperties' in your configuration.
장애와 관련된 링크 이다.
관련해서 해결책을 못찾아서 현재는 라이브러리 버전 다운그레이드….
장애해결 후 재테스트!!!
Gateway 서버로 요청을 보내면 Resource, Resource2 서버로 Routing 되는 것을 확인할 수 있다.
Gateway(9000) 서버로 요청 > Resource 서버로 Routing
GET http://localhost:9100/v1/member/health
HTTP/1.1 200
Date: Wed, 25 Aug 2021 09:03:23 GMT
Keep-Alive: timeout=60
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
MemberController running
Response code: 200; Time: 195ms; Content length: 24 bytes
Gateway(9000) 서버로 요청 > Resource2 서버로 Routing
GET http://localhost:9100/v1/product/health
HTTP/1.1 200
Date: Wed, 25 Aug 2021 08:56:42 GMT
Keep-Alive: timeout=60
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
PayController running
Response code: 200; Time: 56ms; Content length: 21 bytes
여기까지 Spring Cloud Gateway 설정을 통한 Routing 기능을 확인해 보았다.
Spring Cloud Gateway 필터 적용
- Pre Filter - 라우팅 전 실행되고 logging 및 인증 등 처리
- Routing Filter - 요청에 대한 라우팅 처리
- Post Filter - 라우팅 후 실행되고 사용자 정의 헤더 추가/제거 또는 통계 및 matrix 수집
- Error Filter - 에러 발생시 핸들링
Filter class 생성
@Slf4j
public class GatewayPreFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("Using Pre Filter : "+request.getMethod() + " request to " + request.getRequestURL().toString());
return null;
}
}
@Slf4j
public class GatewayRouteFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
log.info("GatewayRouteFilter");
return null;
}
}
@Slf4j
public class GatewayPostFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
log.info("GatewayPostFilter");
return null;
}
}
@Slf4j
public class GatewayErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
log.info("GatewayErrorFilter");
return null;
}
}
Filter Bean 등록
@Configuration
public class ZuulFilterConfig {
@Bean
public GatewayPreFilter preFilter() {
return new GatewayPreFilter();
}
@Bean
public GatewayPostFilter postFilter() {
return new GatewayPostFilter();
}
@Bean
public GatewayRouteFilter routeFilter() {
return new GatewayRouteFilter();
}
@Bean
public GatewayErrorFilter errorFilter() {
return new GatewayErrorFilter();
}
}
Filter 테스트
Filter는 확인을 위한 로그만 출력한다.
GET http://localhost:9100/v1/product/health
HTTP/1.1 200
Date: Wed, 25 Aug 2021 14:29:17 GMT
Keep-Alive: timeout=60
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
PayController running
Response code: 200; Time: 181ms; Content length: 21 bytes
Filter 적용 후 로그
2021-08-25 23:37:17.758 INFO 13004 --- [nio-9100-exec-7] c.s.s.m.gateway.filter.GatewayPreFilter : Using Pre Filter : GET request to http://localhost:9100/v1/product/health
2021-08-25 23:37:17.758 INFO 13004 --- [nio-9100-exec-7] c.s.s.m.g.filter.GatewayRouteFilter : GatewayRouteFilter
2021-08-25 23:37:17.775 INFO 13004 --- [nio-9100-exec-7] c.s.s.m.g.filter.GatewayPostFilter : GatewayPostFilter
참고
DaddyProgrammer Spring CLoud MSA(2) - Gateway(Routing & Filter) Server by Netflix zuul