API 게이트웨이에 대해서 알아보고 Zuul API Gateway, Eureka Server, MicroService(EurekaClient) 구성 후 Docker를 이용해 컨테이너화 해보자.

API Gateway 란?

API Gateway 패턴은 마이크로서비스 아키텍처를 적용하여 개발할 때 고려된다.
클라이언트와 마이크로서비스 사이에 위치해서 클라이언트가 서비스에 요청을 보낼 때 사용되는데
API Gateway가 Single entry point를 제공하고, 클라이언트의 요청에 대해서 내부 마이크로 서비스의 엔드포인트로 라우팅하는 프록시 서버와 같은 역할을 한다.

API Gateway를 사용하게되면 클라이언트 앱에 대해서 단일 엔드포인트(URL)을 제공할 수 있고, 마이크로 서비스와 클라이언트 앱을 분리할 수 있다.
마이크로 서비스에서 공통적으로 필요한 인증 및 권한 부여, 응답에 대한 캐싱, 부하분산, 속도제한 등의 구현을 게이트웨이에서 처리할 수 있고, 따라서 각 마이크로 서비스의 구현을 간소화 할 수 있다.
반면에 단일 실패 지점이 만들어질 수 있고 추가 네트워크 호출로 응답시간이 증가 할 수 있는 등의 단점이 있을 수 있다.

참고1 참고2

Spring Cloud Netflix 란?

Spring Cloud Netflix를 이용해서 빠르게 API Gateway 서버를 구성해보자.
게이트웨이로 Zuul을 사용하고, 라우팅 및 부하분산을 위해서 Eureka, Ribbon을 사용한다.

Spring Cloud Netflix는 Spring Boot 어플리케이션을 위한 Netflix OSS 통합을 제공한다.

Spring Cloud Netflix

This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few simple annotations you can quickly enable and configure the common patterns inside your application and build large distributed systems with battle-tested Netflix components. The patterns provided include Service Discovery (Eureka), Circuit Breaker (Hystrix), Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon).

reference

Netflix OSS(Open Soure Software)

Zuul

Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application

As an edge service application,

Zuul is built to enable dynamic routing, monitoring, resiliency and security

..

Eureka

관련 포스팅 : 유레카 서버와 유레카 클라이언트 어떻게 동작할까?

purpose of load balancing and failover of middle-tier servers

Eureka Server, Eureka Client

eureka-goodnote

Eureka 에 대해서 이해할 때 Service Discovery 에 대해 알고가자.

클라우드에서 인스턴스는 동적으로 할당된다. IP주소 및 포트 정보도 변경된다.

  • EurekaServer (Service Registry Server) 에 등록된 서비스들은 Eureka Client가 Eureka Server로 부터 레지스트리 정보를 가져와서 로컬에 캐시하고, 캐시된 레지스트리 정보를 이용해서 다른 서비스들을 찾을 수 있다.

Ribbon

Client Side Load Balancing Library

Ribbon Client는 Eureka Client가 알려주는 사용가능한 서비스들을 대상으로 LoadBalancing (기본 Round Robin 방식) 할 수 있다.

API Gateway 서버 구성

Gradle Project, Spring Boot 2.1, Java 8

Spring Initializr 를 통해서 프로젝트를 생성 했다. (EurekaServer, ZuulAPIGateway, TestService)

각 프로젝트의 주요 dependency 설정과 어플리케이션 설정 내용에 대해서만 작성 함.

Eureka Server

Dependencies

Spring Boot Actuator, Spring Security, Eureka Server

1
2
3
4
5
6
7
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

설정 (bootstrap.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
spring:
application:
name: ${EUREKA_SERVER_NAME:eureka-server}

server:
port: ${PORT:8081}

eureka:
instance:
hostname: ${EUREKA_SERVER_NAME:eureka-server}
prefer-ip-address: true
lease-expiration-duration-in-seconds: 5
lease-renewal-interval-in-seconds: 5
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${EUREKA_SERVER_NAME:eureka-server}:${PORT:8081}/eureka/
server:
enable-self-preservation: false
max-threads-for-peer-replication: 0

management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
health:
show-details: ALWAYS

ZuulAPIGateway

Dependencies

Spring Boot Actuator, Spring Security, Eureka Discovery Client, Zuul, Ribbon

1
2
3
4
5
6
7
8
9
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-ribbon'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

설정 (bootstrap.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
spring:
application:
name: zuul-api-gateway

server:
port: ${PORT:8080}

eureka:
instance:
hostname: zuul-api-gateway
prefer-ip-address: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://${EUREKA_SERVER_NAME:eureka-server}:${EUREKA_SERVER_PORT:8081}/eureka/

zuul:
ignored-services: '*'
prefix: /api
ribbonIsolationStrategy: THREAD
threadPool:
useSeparateThreadPools: true
routes:
test-service:
path: /test-service/**
sensitive-headers:
service-id: test-service

ribbon:
eureka:
enabled: true
okhttp:
enabled: true

management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
health:
show-details: ALWAYS

TestService

Dependencies

Spring Boot Actuator, Spring Security, SpringWeb, Eureka Discovery Client

1
2
3
4
5
6
7
8
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

설정 (bootstrap.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
spring:
application:
name: test-service

server:
port: ${PORT:8090}

eureka:
instance:
hostname: test-service
prefer-ip-address: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://${EUREKA_SERVER_NAME:eureka-server}:${EUREKA_SERVER_PORT:8081}/eureka/
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
health:
show-details: ALWAYS

로드밸런싱이 잘되는지 확인 용 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class TestController {
@Value("${server.port}")
private int port;

@RequestMapping(value = "/hello", method = RequestMethod.GET)
ResponseEntity<Map<String, String>> sample() {
Map<String, String> result = new HashMap<String, String>();
result.put("msg", String.format("eureka client! PORT: %s", port));

return ResponseEntity.ok(result);
}

}

Docker를 이용한 컨테이너화

Docker Compose를 이용해 컨테이너 생성 후 테스트.

docker-comopse.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
version: '3'
services:
eureka-server:
image: 'eureka-server:0.1'
ports:
- '8081:8081'
environment:
- EUREKA_SERVER_NAME=eureka-server
- PORT=8081

test-service1:
image: 'test-service:0.1'
ports:
- '8090:8090'
environment:
- EUREKA_SERVER_NAME=eureka-server
- EUREKA_SERVER_PORT=8081
- PORT=8090
links:
- eureka-server

test-service2:
image: 'test-service:0.1'
ports:
- '8091:8091'
environment:
- EUREKA_SERVER_NAME=eureka-server
- EUREKA_SERVER_PORT=8081
- PORT=8091
links:
- eureka-server

zuul-api-gateway:
image: 'zuul-api-gateway:0.1'
ports:
- '8080:8080'
environment:
- EUREKA_SERVER_NAME=eureka-server
- EUREKA_SERVER_PORT=8081
- PORT=8080
links:
- eureka-server
- test-service1
- test-service2

동일 host 상에 배포된 container 사이는 Private IP 를 이용해 통신이 가능 하지만 IP 기반의 설정은 권고되지 않는다
그래서 컨테이너 간의 통신을 위해서 link 방식을 사용 했다.
link 방식을 사용했을 경우에 동적 IP 이슈를 피할 수 있지만 동일 docker host에 존재할 경우만 가능하고, 타 host에 존재할 경우에는 통신이 불가하다.

추가. link 방식을 사용하지 않고, 네트워크 설정을 통한 컨테이너간의 통신방식
동일한 네트워크에 컨테이너가 생성될 경우 컨테이너 간의 통신이 가능하다.
test네트워크를 만들고 컨테이너를 생성할 때 네트워크를 지정(test)하도록 함

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
version: '3'
services:
eureka-server:
image: 'eureka-server:0.1'
networks:
- test
ports:
- '8081:8081'
environment:
- EUREKA_SERVER_NAME=eureka-server
- PORT=8081

test-service1:
image: 'test-service:0.1'
networks:
- test
ports:
- '8090:8090'
environment:
- EUREKA_SERVER_NAME=eureka-server
- EUREKA_SERVER_PORT=8081
- PORT=8090
depends_on:
- eureka-server

test-service2:
image: 'test-service:0.1'
networks:
- test
ports:
- '8091:8091'
environment:
- EUREKA_SERVER_NAME=eureka-server
- EUREKA_SERVER_PORT=8081
- PORT=8091
depends_on:
- eureka-server

zuul-api-gateway:
image: 'zuul-api-gateway:0.1'
networks:
- test
ports:
- '8080:8080'
environment:
- EUREKA_SERVER_NAME=eureka-server
- EUREKA_SERVER_PORT=8081
- PORT=8080
depends_on:
- eureka-server
- test-service1
- test-service2

networks:
test:

결과

Eureka Server Dashboard

http://localhost:8081/

eureka-server-dashboard

GATEWAY 1개, SERVICE 2개가 Eureka registry에 등록된 것을 확인할 수 있다.

Gateway API URL

http://localhost:8080/<API Prefix>/<Endpoint Service ID>/<Service API>

http://localhost:8080/api/test-service/hello

로드밸런싱 확인

test-service1(8090), test-service2(8091) 로 로드밸런싱이 잘되는지 확인.

1
2
{"msg":"eureka client! PORT: 8090"}
{"msg":"eureka client! PORT: 8091"}