backend

Spring Cloud를 뜯어보자 (Gateway, Config, Netflix Eureka)

downfa11 2024. 11. 19. 15:43

마이크로 서비스 아키텍처(MSA)는 하나의 모놀리식 아키텍처에서 각 도메인별로 분해하여 비즈니스에 따른 유연한 대응을 목적으로 한다.

구조는 복잡해져도, 기술만을 중요시하지 않고 도메인을 신경써야하는 요즘 시대에 걸맞는다고 볼 수 있어서 시장의 대세로 자리잡았다.

그런 MSA의 단점으로 꼽히는 문제점들이 있고, 각 서비스들은 이 문제를 어떤 방식으로 해결하는가? 가 중요해졌다.

 

예를 들자면

  • 여러 마이크로 서비스들이 어디 있는지 어떻게 찾아야 하는가?
  • 각 서비스는 어떻게 통신하는가?
  • 보안적 요소나 속도 제한처럼 서비스의 접근을 어떻게 제어하는가?
  • 문제 상황 발생시, 개별 서비스가 응답하지 않으면 비즈니스를 어떻게 처리할건가?

이런 복잡한 문제점들을 해결하기 위해 Spring Cloud에서 여러 서비스를 제공하고 있다.

 

Spring Cloud 시작하기

스프링 프레임워크 기반의 클라우드 네이티브 애플리케이션을 개발하기 위한 프로젝트이다.

 

로드밸런싱, 분산/버전 구성, 라우팅과 서비스간의 통신 등 여러 기능을 제공해서 특히 MSA 구현할때 유용하게 쓰인다.

 Netflix OSS + Spring Cloud = Spring Cloud Netflix(Spring Cloud로 총칭)

 

  • 서비스 디스커버리(Service Discovery) : MSA 환경에서 서비스를 검색하고 호출하는 기능으로 Netflix OSS에서 제공하는 Eureka나 Apache Zookeeper가 유명하다!
  • API 게이트웨이(API Gateway) : 여러 마이크로 서비스에서 제공하는 API를 단일 게이트웨이로 노출시키는 역할로 Spring Cloud Gateway(SCG)를 쓸거다.
    • 얜 좀 중요하다고 생각해서 일부러 게시글 하나 할애해서 뒤에 작성중이다...ㅎㅎ
  • 분산 추적(Distributed tracing) : 분산 시스템 환경에서 트랜잭션을 추적하기 위한 기능

 

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, short lived microservices and contract testing)

 

 

https://spring.io/projects/spring-cloud

 

Spring Cloud

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, short lived microservices and

spring.io

 

 

Spring Cloud Config

각 마이크로 서비스들이 갖는 구성 환경을 중앙(Git 등)에서 관리하도록 구현한 기능

서버-클라이언트 구조로, 여기서 클라이언트는 단순히 비즈니스를 수행하는 애플리케이션으로 마이크로 서비스들을 지칭한다.

각 마이크로 서비스들이 Config 서버에 접근하여 본인의 환경에 맞는 구성을 적용해가는 방식이다.

내부적으로 파일 형태나 Git Repository 형태로 제공되며, profile과 label로 환경을 구분한다.

우선 ssh 접속에 사용할 비대칭키를 만든다.

$ ssh-keygen -m PEM -t rsa -b 4096 -C "downfa11"
$ cd ~/.ssh

 

만들어서 따로 저장해두자

Github에 pirvate repository를 하나 생성한다.

내용은 그냥 membership이라는 마이크로 서비스의 dev 버전의 properties 파일이다.

네이밍 형태는 틀리면 안된다.

포트를 80으로 지정하는거밖에 없이 간략하게 실습을 진행하겠다.

1. Config Server

스프링 프로젝트에서 Config Server, Security 정도 의존성을 가져오자

사실 프로젝트 내에 이렇다할 기술적 어려움은 없다. 딸깍 몇번이면 끝난다.

Main 클래스에 @EnableConfigServer 어노테이션을 명시해주고 Configuration 클래스를 하나 작성해주면 끝난다.

사실 Spring Security에 대한 클래스라 실습 환경에서는 작성하지 않아도 된다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf((auth) -> auth.disable());
        http.authorizeHttpRequests((auth) -> auth.anyRequest().authenticated());
        http.httpBasic(Customizer.withDefaults());
        
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {

        UserDetails user1 = User.builder()
                .username("account")
                .password(bCryptPasswordEncoder().encode("password"))
                .roles("admin")
                .build();
        
        return new InMemoryUserDetailsManager(user1);
    }
}

 

Config Server로의 접근은 관리자 이외에 누구도 접근해선 안되기에, 별다른 필터링 없이 admin 계정으로 접속하도록 구현했다.

application.properties 설정이 좀 귀찮은데, 여기에 개인키를 넣을거다.

server.port=9000
spring.cloud.config.server.git.uri=git@github.com:downfa11/test-config.git
spring.cloud.config.server.git.ignoreLocalSshSettings=true

spring.cloud.config.server.git.private-key=\
  -----BEGIN RSA PRIVATE KEY-----\n\
MIIJJwIBAAKCAgEAtlkvMPzfSNAS8MzilrDYHyrL45m6eGxlsQ5eXGMw2nkp2L4/\n\
qa9ovJtXDPZgzTMbDtAn+81bugSFU46J+zzRcbzga+iAydnvFIbif/CnAnXDYaQe\n\
...
중략

 

보이는 것처럼.. \n\를 넣어주면서 줄바꿈을 인식시켜줘야한다.

자, 이제 설정 정보 데이터를 얻기 위해 Config Client가 Server에 접근하는 주소는 다음과 같다.

http://ip:port/저장소이름/저장소환경

이름-환경.properties

이름-환경.yml

네이밍 형태는 틀리면 안된다.

Spring Security 설정에서 지정한 admin 계정으로 접속하면 private Repository에서 원하는 버전의 구성 환경을 가지고 온다.

여기선 결과값으로 다음과 같이 반환받았다.

{
"name":"membership",
"profiles":["dev"],
"label":null,
"version":"6af531a1ec0e15f44462af8d9bd5177a389588a6",
"state":null,
"propertySources":[
  {"name":"git@github.com:downfa11/test-config.git/membership-dev.properties",
   "source":{"server.port":"8080"}}]
}

 

Config 서버에서 이렇게 깃허브에서 관리하는 각 마이크로 서비스의 버전에 맞는 구성 환경을 가져오도록 하는데 성공했다.

이제 클라이언트에서 Config 서버로 요청을 보내 이 요청을 받아오면 끝난다.

2. Config Client

다음과 같이 마이크로 서비스에 해당하는 프로젝트를 생성해서 Spring Cloud Config에 대한 의존성을 추가해준다.

ext {
  set('springCloudVersion', "2022.0.4")
}

dependencies {
  implementation 'org.springframework.cloud:spring-cloud-starter-config'
}

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}

 

나는 Git Repository에서 membership-dev의 Port 정보를 80으로 지정했었다.

application.properties를 다음과 같이 작성해보자.

 

spring.application.name=membership
spring.profiles.active=dev
spring.config.import=optional:configserver:http://account:password@localhost:9000

 

 

헷갈릴 수도 있는데, 아까 우리는 Config Server의 Port는 9000으로 지정했었다.

이해를 돕기 위한 설명

스프링 프로젝트에서 따로 Port를 명시하지 않으면 default로 8080으로 접속된다.

테스트 할겸 실행해서 Config Client를 localhost:8080으로 켜보면 접속이 거부된다.

서버 포트가 8080이 아니라 config.import에서 불러온 설정에 맞게 변경되었단 말씀

그럼 repository에서 지정한 80 포트로 마이크로 서비스를 접속시키면 어떻게 될까?

Config Client는 /로 접속시 main을 반환하도록 따로 코딩해뒀다.

 

원래 ConfigClient에서 RestController로 “main”을 반환하게 했으니 Port:80 으로 잘 접속된다!

Spring Cloud Netflix Eureka

다음 글에 쓸려고 했는데 서비스 디스커버리는 아마 앞으로도 내가 쓸거 같지 않고 내용도 적어서 마저 쓰고 끝내겠다.

서비스 디스커버리의 가장 대표적인 Netflix Eureka는 가동중인 노드를 확인한 뒤, Gateway에 리스트를 전달하고 마이크로 서비스들을 모니터링하는 감시자 역할을 한다.

보통 부하가 늘어나면 서버 개수를 늘리는 오토스케일링을 하는데, Cloud Gateway에서 새로 생긴 노드들의 주소를 모른다!

Cloud Config와 마찬가지로 서버-클라이언트 구조로 이뤄진다. 마이크로 서비스가 클라이언트를 담당하고 Eureka 서버가 서비스 디스커버리를 담당한다.

예시로 Cloud Config와 Eureka Config를 함께 사용하는 경우를 들어보겠다.

Config Server, Eureka Server, 마이크로 서비스(Config Client+Eureka Client) 로 되는 셈이다.

1. Eureka Discovery Server

새로운 프로젝트를 생성해서 Eureka Server와 필요에 따라 Spring Security를 함께 의존성 추가해준다.

Config Server 생성할때 했던 것처럼 관리자만 접속할 수 있도록 설정해줘야한다.

어노테이션을 새로 등록하기

@EnableEurekaServer 어노테이션을 Main 클래스에 함께 작성

application.properties 작성

server.port = 8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

 

localhost:8761로 들어가면 다음과 같이 대시보드가 뜬다.

2. Eureka Discovery Client

마이크로 서비스에서 Eureka Client를 등록하면 Eureka 서버에서 모니터링 및 관리가 가능하다

 

의존성 주입

Discovery Client 의존성을 추가

ext {
    set('springCloudVersion', "2023.0.3")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

 

나머지는 마이크로 서비스의 비즈니스에 필요한 내용을 실습 차원에서 넣은거다. 안따라해도됨

어노테이션을 새로 등록하기

@EnableDiscoveryClient 어노테이션을 마찬가지로 Main 클래스에 작성

application.properties 작성

server.port=8080
spring.application.name=client

eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.service-url.defaultZone=http://account:password@localhost:8761/eureka

 

클라이언트를 실행하면 Eureka 서버의 대시보드에 다음과 같이 추가됨을 볼 수 있다.

 

 

Spring Cloud Gateway(SCG)

MSA 가장 앞단에서 클라이언트들로 부터 오는 요청을 받은 후 경로와 조건에 알맞은 마이크로서비스 로직에 요청을 전달하는 Gateway

단순하지만, 중지되지 않으면서 모든 요청을 받아야 하기에 설정하기 까다롭다.

특성

  • 기존의 Blocking 기반 tomcat 서버가 아니라 Non-Blocking 기반 Netty 서버를 사용한다.
  • IO 처리를 중점적으로 진행하기 때문

https://spring.io/projects/spring-cloud-gateway

 

Spring Cloud Gateway

This project provides a libraries for building an API Gateway on top of Spring WebFlux or Spring WebMVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitor

spring.io

 

webflux라서 JDBC로 통신하는 JPA가 안되니 R2DBC를 써야해서 러닝 커브 소요

프로젝트를 생성해서 의존성을 추가해보자자

Reactive???? 얜 모지

Gateway 라우팅 설정

  • 설정 파일 방식(yaml, properties)
spring.cloud.gateway.routes[0].id=membership
spring.cloud.gateway.routes[0].predaicates[0].name=Path
spring.cloud.gateway.routes[0].predicates[0].args.pattern=/membership/**
spring.cloud.gateway.routes[0].uri=http://localhost:8081

spring.cloud.gateway.routes[1].id=board
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args.pattern=/board/**
spring.cloud.gateway.routes[1].uri=http://localhost:8082

 

이런식으로 routes[]를 추가해 관리한다

RouteConfig 클래스

@Configuration
public class RouteConfig {

    @Bean
    public RouteLocator serviceRouter(RouteLocatorBuilder builder) {

        return builder.routes()
                .route("ms1", r -> r.path("/membership/**")
                        .uri("http://localhost:8081"))
                .route("ms2", r -> r.path("/board/**")
                        .uri("http://localhost:8082"))
                .build();
    }
}

 

id는 별명일뿐이고, predicateds는 Path 전략을 사용할 것이라는 소리이다.

spring.cloud.gateway.routes[0].predicates[0].args.pattern=/membership/**

Gateway에서 /membership/** 를 통해 접근하면 routes[0]인 localhost:8081로 접속하도록 지정하는 패턴이다.

라우팅

http://localhost:8080/board/**로 요청이 들어오면 이를 http://localhost:8082/board/**로 전달하는 역할

SCG는 localhost:8080으로 구동중이고, kucis에 사용하던 웹서버를 localhost:8082에서 실행했다.

의도대로라면, 8080 포트에서 웹서버의 엔드포인트로 라우팅해줘야한다.

Swagger라서 CORS 오류때문인지 좀 삽질했었다.

 

@Bean
public RouteLocator msRoutes(RouteLocatorBuilder builder) {
  return builder.routes()
       .route("kucis-client", r -> r.path("/board/**")
             .uri("http://localhost:8082"))
       .build();
}

 

 

membership의 user 관련 서비스중에서 랜덤으로 사용자를 생성하는 로직을 가져와서 테스트해보겠음.

그럼, Swagger를 통해서 생성한뒤, 유저 목록을 가져오는 함수를 라우팅된 Gateway에서 확인해보자

짜자잔 우리 애 잘나오죠???

Cloud Gateway의 Eureka 연동

라우팅할때 부하를 분산하기 위한 로드밸런싱 수행

eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.service-url.defaultZone=http://account:password@localhost:8761/eureka
  • register-with-eureka : 나도 서버에 등록할건지
  • fetch-registry : 다른 클라이언트들의 정보를 가져올건지

오토스케일링 등의 이유로 새로 들어온 노드가 존재함을 Cloud Gateway가 알수 있게 설정

spring.cloud.gateway.routes[0].id=membership
spring.cloud.gateway.routes[0].predicates[0].name=Path
spring.cloud.gateway.routes[0].predicates[0].args.pattern=/membership/**
spring.cloud.gateway.routes[0].uri=lb://ureka-client

 

 

유레카 서버에 등록된 eureka-client에 전달

같은 비즈니스를 책임지는 마이크로서비스의 경우는 spring.application.name은 동일하게 지정

이때 여러 노드들이 모두 EUREKA-CLIENT를 가지고 8081, 8082포트를 사용한다고 가정해보자.

Cloud Gateway(localhost:8080)에서 /membership 엔드포인트로 접속시 localhost:8081와 localhost:8082 를 번갈아 접속시키며 로드밸런싱을 수행한다.

SCG 서버 실행중에 동적 라우팅 추가

서비스 상용화 이후로는 Cloud Gateway는 절대 멈춰선 안된다.

그럼 새로운 업데이트로 비즈니스 발생시, Gateway를 멈추고 다시 추가해야하는가?

 

Actuator 방식의 동적 라우팅

스프링 애플리케이션의 기능을 엔드 포인트로 전달하는 의존성인 Actuator를 이용해서 SCG 실행중에도 라우터를 추가할 수 있다.

 

actuator 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-actuator'

 

 

application.properties 작성 (지정된 명령. 수정X)

management.endpoint.gateway.enabled=true
management.endpoints.web.exposure.include=gateway

 

아맞다. 당연히 얘 할때도 Spring Security로 관리자만 접근할 수 있도록 해야한다.

다양한 actuator 명령어가 있지만... 여기선 소개하지 않겠다.

게이트웨이 글로벌 필터(Gateway Global Filter)

글로벌 필터는 모든 라우팅에 대해 적용되는 필터를 지칭한다.

  • 클라이언트의 요청시
  • 필터(pre) → 마이크로 서비스 → 필터(post) 형태로 이동

각각의 필터는 Order값을 가지며, pre필터는 값이 작을수록 빠르게 동작한다.

반대로 post 필터의 경우는 값이 적을수록 느리게 동작한다.

필터를 Component로 생성해서 Bean에 등록해보자

@Component
public class GlobalFilter implements GlobalFilter, Ordered {


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("pre global filter order -1");
        return chain.filter(exchange)
                .then(Mono.fromRunnable(() -> {
                    System.out.println("post global filter order -1");
                }));
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

 

Ordered를 implement해서 함수를 오버라이드하면 반환값이 Order로 자동 인식한다.

Order가 -1,-2인 Order가 있다고 가정하자.

동작 과정 : pre(-2) → pre(-1) → micro service → post(-1) → post(-2)

글로벌 필터 말고 지역 필터(Local Filter)

특정한 하나의 마이크로 서비스만 필터 적용하고 싶은 경우, 특정 라우터에서만 적용하는 필터를 지칭한다.

다음과 같이 클래스 변수를 통해 받을 수 있다.

@Component
public class testFilter extends AbstractGatewayFilterFactory<testFilter.Config> {

    public testFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (config.isPre())
                System.out.println("pre local filter 1");
            
            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPost())
                            System.out.println("post local filter 1");
                    }));
        };
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Config {
        private boolean pre;
        private boolean post;
    }
}

 

 

 

 

근데 나처럼 MSA를 배우겠다! 하면서 k8s부터 시작한 사람들한테는 이 Config 서버나 Eureka같은게 굉장히 이해가 안될거다.

 

Secret이나 ConfigMap 리소스에 버전별로 구성 환경 관리하면 되는데 이거 왜쓰는거임? 서비스 디스커버리도 쿠버네티스에서 자체적으로 제공해주는데?

 

개인적인 생각일뿐이지만, 세상 사람들이 모두 MSA를 쿠버 환경에서 배포하진 않는다.

그냥 로드밸런서 + 오토스케일링 정도로 배포한다고 생각하면 '아하 여러 마이크로 서비스들이 어디 있는지 어떻게 찾지?' 라고 생각이 들거다. 유레카가 유명한 이유가 있다