제4장 서비스 디스커버리 (1)
서비스 디스커버리
서버
- 스프링 부트 애플리케이션으로 실행한다.
- 서버 API 구성
- 등록된 서비스의 목록을 수집하기 위한 API
- 새로운 서비스를 네트워크 위치 주소와 함께 등록하기 위한 API
- 서버의 상태를 다른 서버로 복제함으로써 안정성과 가용성을 높일 수 있다.
클라이언트
- 마이크로서비스 애플리케이션에 의존성을 포함시켜 사용한다.
- 기능
- 애플리케이션 시작 후 서버에 등록한다.
- 종료 전 서버에서 등록 해제를 담당한다.
- 유레카 서버로부터 주기적으로 최신 서비스 목록을 받아온다.
서버 측에서 유레카 서버 실행하기
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaServer
//@EnableEurekaClient <-- 그 Client 아님요
public class MsaServerApplication {
public static void main(String[] args) {
SpringApplication.run(MsaServerApplication.class);
}
}
eureka-server의 dependency만 추가 하였는데 @EnableEurekaClient 역시 활성화 가능하다.
하지만 이것은 디스커버리 인스턴스를 고가용성 모드로 동작할 경우 디스커버리 인스턴스 사이의 동료 간 통신(peer-to-peer)일 경우에만 유용하다.
이 상태로 실행을 하면, 에러 로그가 많이 찍힐 것이다.(그래도 브라우저에서 확인은 가능하다.)
책에서는 의존성에서 spring-cloud-netflix-eureka-client를 의존성에서 제외 하는것도 하나의 방법이라고 적혀 있는데 그렇게 하면 특정 Bean을 찾지 못해 에러가 나더라..
아무튼 아래와 같이 yml 파일에서 처럼 디스커버리 클라이언트를 비활성화 하는 것이 가장 좋은 방법이다.
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
이제 Spring Cloud Application을 시작한다.
그리고 localhost:{port} 로 이동하자.
위 이미지 처럼 Spring Cloud Application에 대한 정보를 확인 할 수 있다.
그리고 /eureka/apps 로 이동하여 서버에 등록된 마이크로서비스의 목록을 확인 가능하다.
이제 Eureka 서버에 등록될 서비스를 만들 차례다.
클라이언트에서 유레카 활성화 하기
다음과 같이 dependency가 추가 되어 있다는걸 확인할 수 있다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
책에서는
spring-cloud-starter-netflix-eureka-client 만 추가되어 있지만, 실제로 client를 실행 시켜보면 등록 즉시 shutdown 되는 것을 확인할 수 있다. 따라서 아래와 같이 dependency를 하나 더 추가해야 한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
클라이언트는
- 자신을 Eureka 서버에 등록하고 호스트, 포트, 상태 정보, URL, 홈페이지 URL을 보낸다.
- Eureka 서버는 서비스의 각 인스턴스로부터 생존신호(Heartbeat) 메시지를 받는다.
- 설정된 기간 동안 생존신호 메시지를 받지 못하면 레지스트리에서 서비스가 삭제된다.
- 서버로부터 데이터를 가져와서 캐싱하고 주기적으로 변경사항을 점검한다.
- @EnableDiscoveryClient Annotation을 메인 클래스에 추가
- 컨설, 유레카, 주키퍼 등 다수의 클라이언트 구현체가 classpath에 있을 경우 @EnableEurekaClient Annotation을 추가
나는 @EnableDiscoveryClient를 추가한다.
@SpringBootApplication
@EnableDiscoveryClient
public class MsaClientApplication {
public static void main(String[] args) {
SpringApplication.run(MsaClientApplication.class, args);
}
}
그리고 yml에 다음과 같이 설정을 한다.
spring:
application:
name: client-service
server:
port: ${PORT:8081}
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
이제 빌드를 하고 실행을 해보자.
java -jar -DPORT=8081 target/msa-client-0.0.1-SNAPSHOT.jar
java -jar -DPORT=8082 target/msa-client-0.0.1-SNAPSHOT.jar
그리고 나서 아까 실행했던 Server의 대시보드 화면을 다시 확인해보자.
포트 8081, 8082로 실행한 두 개의 client 인스턴스가 존재한다.
종료 시 등록 해제
스프링 엑추에이터의 /shutdown API를 이용하여 애플리케이션을 '우아하게' 중지하기
- spring-boot-starter-actuator를 pom.xml 에 추가한다.
- 기본으로 비활성화 되어 있기 때문에 속성을 통해 활성화 시킨다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
.yml 설정 (책의 버전이랑 달라서 그런지 아래처럼 해줘야 한다.)
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown,info
이렇게 추가를 한 다음에 다시 클라이언트를 실행한다.
이제 애플리케이션을 종료하려면 /actuator/shutdown API 를 POST 메서드로 호출한다. (포스트맨 등을 사용해서)
문제가 없다면 다음과 같은 Response를 받을 것이다.
{
"message": "Shutting down, bye..."
}
Client의 로그를 확인 해보자.
com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient ...
com.netflix.discovery.DiscoveryClient : Unregistering ...
com.netflix.discovery.DiscoveryClient : DiscoveryClient_CLIENT-SERVICE/kkt0929.MIDASIT.local:client-service:8081 - deregister status: 200
com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient
o.apache.catalina.core.StandardService : Stopping service [Tomcat]
o.a.c.c.C.[Tomcat].[localhost].[/] : Destroying Spring FrameworkServlet 'dispatcherServlet'
이제 서버 대시보드를 가보면 포트 8082만 남아 있을 것이다.
이 외에도 유레카 서버 대시보드는 등록되고 취소된 서비스 이력을 확인 가능하다.
현실에선 항상 우아할 수 없다.
- 서버 머신이 재시작하거나 애플리케이션의 장애
- 서버와 클라이언트 간의 네트워크 인터페이스 문제
프로그램 방식으로 디스커버리 클라이언트 사용하기
- com.netflix.discovery.EurekaClient
- 유레카 서버가 노출하는 모든 HTTP API를 구현한다. 유레카 API 영역에 설명돼 있다.
- org.springframework.cloud.client.discovery.DiscoveryClient
- 넷플릭스 EurekaClient를 대체하는 스프링 클라우드의 구현체. 이것은 모든 디스커버리 클라이언트용으로 사용하는 간단한 범용 API다. 여기에는 getService와 getInstances의 두 가지 메서드가 있다.
@RestController
public class MainController {
private static Logger LOGGER = LoggerFactory.getLogger(MainController.class);
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/ping")
public List<ServiceInstance> ping(){
List<ServiceInstance> instances = discoveryClient.getInstances("client-service");
LOGGER.info("INSTANCES: count={}", instances.size());
instances.stream().forEach(
it -> LOGGER.info("INSTANCE: id={}, port={}", it.getServiceId(), it.getPort()));
return instances;
}
}
여기서 주의해야 할 사항이 있는데 DiscoveryClient의 package는 org.springframework.cloud.client.discovery 이다.
com.netflix.discovery.DiscoveryClient << 이놈으로 하지 말고..
/ping을 확인하면 다음과 같다.
고급 컨피규레이션 설정
서버
- 서버의 행동을 재정의 한다.
- eureka.server.* 를 접두어로 사용하는 모든 속성을 포함한다.
- 전체 속성 목록 URL
클라이언트
- 유레카 클라이언트에서 사용할 수 있는 두 가지 속성 중 하나다. 이것은 클라이언트가 레지스트리에서 다른 서비스의 정보를 얻기 위해 질의하는 방법의 컨피규레이션을 담당한다.
- eureka.client.* 를 접두어로 사용하는 모든 속성을 포함한다.
- 전체 속성 목록 URL
인스턴스
- 이것은 포트나 이름 등의 현재 유레카 클라이언트의 행동을 재정의한다.
- eureka.instance.* 를 접두어로 사용하는 모든 속성을 포함한다.
- 전체 속성 목록 URL
레지스트리 갱신하기
이전 내용중에 '우아하게' 종료하기를 하면서 '자기 보존 모드(self-preservation-mode)' 가 기억이 나는가?
'self-preservation-mode'를 비활성화 시켜도 여전히 서버가 임대를 취소하는 시간은 오래 걸린다.
그 이유는
- 모든 클라이언트 서비스가 30초(default)마다 서버로 하트비트를 보내기 때문이다.
- eureka.instance.leaseRenewalIntervalInSeconds 속성으로 설정 가능하다.
- 서버가 하트비트를 받지 못하면 레지스트리에서 인스턴스를 제거하기 전에 90초를 기다린다.
- 등록을 해제해서 인스턴스로 더 이상 트래픽이 가지 못하도록 차단할 수 있다.
- eureka.instance.leaseExpirationDurationInSeconds 속성으로 설정 가능하다.
- leaseExpirationDurationInSeconds 에 지정된 기간 동안 heartbeat가 수신되지 않으면 eureka 서버에서 instance를 제거한다.
클라이언트 Configuration
eureka:
instance:
lease-renewal-interval-in-seconds: 1
lease-expiration-duration-in-seconds: 2
서버 Configuration
eureka:
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 3000
이제 디스커버리 서버를 다시 실행하고 클라이언트 인스턴스를 다시 실행해보자.
그리고 아까 처럼 클라이언트를 강제 종료한다.
이번에는 비활성화 된 클라이언트가 유레카 서버에서 제거 되는 것을 확인할 수 있다.
다음은 유레카 서버의 로그 일부다.
: Evicting 1 items (expired=1, evictionLimit=1)
: DS: Registry: expired lease for CLIENT-SERVICE/kkt0929.MIDASIT.local:client-service:8081
: Cancelled instance CLIENT-SERVICE/kkt0929.MIDASIT.local:client-service:8081 (replication=false)
이런 속성들을 조작해서 서비스의 등록과 만료에 대한 유지 관리를 정의할 수 있다.
하지만 중요한 것은 정의된 Configuration으로 인해 시스템의 성능에 악영향을 미치지 않도록 하는 것이다.
이런 설정에 민감한 요소에는
- 부하 분산
- 게이트웨이
- 서킷 브레이커
인스턴스 식별자 변경하기
server:
port: 808${SEQUENCE_NO}
eureka:
instance:
instance-id: ${spring.application.name}-${SEQUENCE_NO}
그리고 실행을 해보자. 한...3개정도 해보자...
예)
java -jar -DSEQUENCE_NO=1 target/msa-client-0.0.1-SNAPSHOT.jar
java -jar -DSEQUENCE_NO=2 target/msa-client-0.0.1-SNAPSHOT.jar
java -jar -DSEQUENCE_NO=3 target/msa-client-0.0.1-SNAPSHOT.jar
Eureka 대시보드를 확인해보면
설정한대로 instanceId 가 만들어졌고 실행이 되었다. 유레카 서버에 잘 나온다.
IP 주소 우선하기
- eth01 : 내부 네트워크
- eht01 : 공인 IP
암튼.. 이럴 때는 네트워크 인터페이스를 선택해야 한다.
application.yml 에 무시할 패턴의 목록을 정의 하면 된다.
spring:또는 원하는 네트워크 주소를 정의하는 방법도 있다.
cloud:
inetutils:
ignored-interfaces:
- eth1*
spring:
cloud:
inetutils:
preferred-networks:
- 192.168
응답 캐시
서버
유레카 서버는 기본적으로 응답을 캐시한다. 그리고 30초마다 캐시 데이터를 지운다.
유레카 서버를 실행하고 /eureka/apps API를 호출해보자. 아무것도 안나온다.
그리고 클라이언트 인스턴스를 실행 하고 서버의 /eureka/apps를 다시 호출해보자. 여전히 안나온다.
30초가 지나고 나서 다시 호출해보자 추가된 클라이언트 인스턴스가 조회될 것이다.
Cache Timeout 은 reponseCacheUpdateIntervalMs 속성으로 재정의 가능하다.
eureka:
server:
response-cache-update-interval-ms: 3000
재미있는 것은 REST API 와는 반대로 유레카 서버의 대시보드에 등록된 인스턴스가 표실될 때에는 캐시를 사용하지 않는다.
클라이언트
Client의 registryFetchIntervalSeconds 을 3초로 변경해보자.
책에서는 shouldDisableDelta 로 되어 있지만 내가 사용하는 버전에는 아래와 같이 disable-delta로 된다.
eureka:
client:
registry-fetch-interval-seconds: 3
disable-delta: true
클라이언트와 서버 간의 보안 통신 사용학
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
아래 취소선으로 되어 있는 내용은 책의 내용이다.
이렇게 해도 된다. 하지만 난 이렇게 안할거다.
취소선 내용은 건너뛰자.
그리고 yml 에도 또 추가하자.
spring:
security:
user:
name: admin
password: admin123
책에서는 security가 root 지만 옛날 버전간다. spring이 루트다.
그리고 책에서는 basic.enabled 가 있지만 deprecated 된 속성이다.
뭐 이렇게만 추가한 뒤에 Eureka 서버를 다시 실행해보자. 그럼 로그인을 해야 할 것이다.
yml 파일에 정의 한 대로 Username 과 Password 를 입력하고 로그인 하자.
그럼 대시보드로 잘 들어갈거다.
이제 클라이언트 에서 인증된 상태로 서버에 접근 하려면 다음 설정과 같이 URL 연결 주소에 자격 증명을 제공한다.
eureka:
client:
serviceUrl:
defaultZone: http://admin:admin123@localhost:8761/eureka/
yml에 설정하지 말고 Security 설정을 JavaConfig로 하자.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder.encode("admin123"))
.roles("SYSTEM");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login", "/logout", "favicon.ico").permitAll()
.and().authorizeRequests().anyRequest().authenticated()
.and().httpBasic().and().formLogin()
.and().csrf().disable();
}
}
사용자 인증은 inMemory를 사용하자. passwordEncoder를 이용하는데 Bcrypt를 사용 할 것이다.
그리고 configure 에서는 인증 방식에 대한 내용을 정의하자.
authorizeRequests().antMatchers("/login", "/logout", "favicon.ico").permitAll()
"login", "/logout", "/favicon.ico" 얘네들은 인증이 필요 없이 접근 가능하다. 말 그대로 전부 허용이다.
authorizeRequests().anyRequest().authenticated()
그 외에는 다 인증이 필요하게 설정했다.
httpBasic()
httpBasic 을 추가 하면 BasicAuthenticationFilter(Basic Auth) 가 Spring Security Filter Chain에 추가가 되는데
filter 내용은 다음과 같다. 필요한 부분만 몇줄만 적는다.
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}~~~~~~~~~~~~~~~~~~블라블라
request 객체의 Header 정보중에 Authorization 을 가져온다.
가져와서 Null 이거나 Authorization으로 시작 되는 것이 아니면 Filter Chain으로 넘긴다.
Authorization 헤더 정보가 존재하면 다음과 같이 진행된다.
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String username = tokens[0];
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
요약하면 인증을 할 필요가 있는지 체크를 하고 필요하면 Authorization 정보를 가지고 인증을 완료한다.
Authorization 에 어떤 정보가 있는가 하면....이것도 요약하면...
사용자 username과 password 를 가지고 base64로 인코딩 한 다음 앞에 Basic+공백+{인코딩값} 으로 만들어서
그 전체를 Authorization header에 추가해서 요청을 보내는 것이다.
예) admin:admin123 = Basic YWRtaW46YWRtaW4xMjM=
이야기가 나온김에 조금 더 하면 Postman에서 테스트 해볼수 있다.
요청을 보내기 전에 인증 타입을 Basic Auth 로 선택하고 Username, Password 를 입력하자. 그리고 요청을 보내보자.
그리고 나서 Headers 탭으로 이동해 보면, Authorization Header에 Basic YWRtaW46YWRtaW4xMjM= 이 추가된 것을 확인 할 수 있다.
또 다른 방법이 있는데 다음과 같이 url에 username과 password 를 포함 시키는 것이다.
이렇게 보내면 Basic Auth 방식으로 요청을 보내게 된다.
잠시 후 설명할 Client 에서 이 방식을 사용한다.
갑자기 이야기가 산으로 갔지만 괜찮다. 내가 블로그 주인이니까.
formLogin()
그리고 이건 formLogin을 사용하겠다라는 이야기다.
eureka Server에 로그인 하는 방식이다.
아무튼 SecurityConfig를 만들고 나면 passwordEncoder 에 빨간줄이 생길 것이다.
왜냐하면 Bean으로 추가하지 않았기 때문이다.
PasswordEncoder를 Bean으로 등록하자.
@SpringBootApplication
@EnableEurekaServer
public class MsaServerApplication {
public static void main(String[] args) {
SpringApplication.run(MsaServerApplication.class);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
이제 실행해보자 그럼 아래처럼 나올 것이다.
yml 파일에 정의 한 대로 Username 과 Password 를 입력하고 로그인 하자.
그럼 대시보드로 잘 들어갈거다.
이제 클라이언트 에서 인증된 상태로 서버에 접근 하려면 다음 설정과 같이 URL 연결 주소에 자격 증명을 제공한다.
eureka:
client:
serviceUrl:
defaultZone: http://admin:admin123@localhost:8761/eureka/
이렇게 클라이언트에서 serviceUrl의 defaultZone만 변경하면 이제 기본적인 인증이 추가 된 초간단 MSA 샘플이 되었다.