Eureka
는 서비스를 등록하는 서비스 레지스트리로 애플리케이션에 있는 모든 마이크로서비스의 중앙 집중 레지스트리로 작동한다. 서비스들이 서로를 찾는데 도움을 주는 역할을 한다. 원하는 서비스를 찾았을 때, 어떤 인스턴스를 사용해야 할 지 결정해야하는데 매번 이런 선택을 피하기 위해 로드 밸런싱
을 사용한다
여러 개의 유레카 서버가 있을 경우, 하나에 문제가 발생하더라도 문제의 발생을 막을 수 있으므로 실무에서는 여러 개의 유레카 서버들이 클러스터로 구성되어 유레카는 다른 유레카 서버로부터 서비스 레지스트리를 가져오거나 다른 유레카 서버의 서비스로 자신을 등록하기도 함
모든 서비스는 Eureka에 등록되며, 하나의 서비스는 여러 개의 인스턴스로 분산이 가능하다. 그러나 여러 개의 인스턴스들은 모두 서비스와 같은 이름으로 Eureka에 등록된다
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에 등록되지 않는다.
이렇게 포트번호를 다르게 설정
인텔리제이 terminal 또는 리눅스 등에서 작업
java -version / javac -version / mvn --version 으로 설치확인 후 진행
mvn spring-boot:run '-Dspring-boot.run.jvmArguments="-Dserver.port=9003"'
실행한 서비스를 중지하려면 Ctrl + C
위처럼 서비스의 포트번호를 정적으로 할당하면 굉장히 번거로움
포트번호를 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초 기다림
Netty 서버
로 실행되어, 비동기 처리도 가능해짐Route : Predicate 와 Filter를 조건을 충족하여 만들어진 요청 URI
Predicate : 요청을 처리하기전에 수행되는 로직, path 혹은 리퀘스트 헤더에 포함된 조건
Filter
클라이언트가 Gateway에 요청을 전달
Gateway Handler Mapping
이 요청 정보를 Gateway의 설정된 Route
로 전달하는 것을 결정하고, Gateway Web Handler에게 전송한다.
Gateway Web Handler
는 만들어진 필터들을 거쳐 조건에 맞는 요청을 보낸다.
(Pre Filter 과정)
요청의 결과로, proxied server에 원하는 정보를 얻고 사후 필터들에 대한 로직이 동작하여 클라이언트에게 응답이 간다. (Post Filter 을 거쳐 클라이언트에게 응답)
exchange
로 request 및 response를 가져올 수 있고 동기 방식인 Tomcat이 아닌 비동기 방식인 Netty를 사용하고 있기 때문에, 서블릿이 아닌 serverHttpRequest와 serverHttpResponse를 사용.chain
을 이용하여 post filter를 추가할 수 있음.webflux에서도 Mono와 Flux로 나뉘게 됨
Mono
는 0~1개의 결과만을 처리하기 위한 Reactor 객체Flux
는 0~N개의 결과물을 처리하기 위한 Reactor 객체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
@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();
}
}
@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());
}
}));
};
}
}
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
단, 필터의 적용 순서를 바꾸고 싶을 때에는, 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;
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-eureka
와 fetch-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
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