Post

서비스 네트워크 자세히 정리하기

서비스 네트워크에 대해서

💡 KANS 스터디
CloudNet에서 주관하는 KANS(Kubernetes Advanced Networking Study)으로 쿠버네티스 네트워킹 스터디입니다. 아래의 글은 스터디의 내용을 기반으로 작성했습니다.

스터디에 관심이 있으신 분은 CloudNet Blog를 참고해주세요.

들어가며

실습환경은 kind(kubernetes in docker)로 진행됩니다. 해당 포스트에서는 실습보다는 이론 위주로 진행되며, 서비스의 필요성과 작동 방식에 대해 알아봅니다.

스터디에서 제공해주신 kind 설정파일은 아래와 같습니다.

  • CNI: kind(default)
  • node: controlplane + worker x 3ea
  • podCIDR: 10.10.0.0/16
  • serviceCIDR: 10.200.1.0/24
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
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
  "MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
- role: worker
  labels:
    mynode: worker1
- role: worker
  labels:
    mynode: worker2
- role: worker
  labels:
    mynode: worker3
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24

서비스(Service)

배경

쿠버네티스의 기본 애플리케이션 단위는 파드이다. 애플리케이션에 문제가 생기면 쿠버네티스는 리소스를 재생성한다. 이때 IP를 포함한 상태값은 달라진다. 그러다보니 파드의 IP는 고정적인 엔드포인트를 제공할 수 없다. 이 문제를 해결하기위해 서비스라는 리소스가 등장한다. 서비스는 고정적인 엔드포인트를 제공하며, 파드를 연결해준다. 즉, 서비스를 통해 파드의 상태가 변해도 연결할 수 있다.

image.png

출처: https://kubernetes.io/docs/tutorials/kubernetes-basics/expose/expose-intro/

type

서비스의 종류는 다음과 같이 3개의 종류가 있다. 종류가 순서대로 단점을 보완한 형태로 진화한다.

ClusterIP

고정적인 엔드포인트를 제공하며 로드밸런서 기능을 제공한다. 하지만, 외부에서 해당 IP를 사용할 수 없다.

NodePort

외부에서 접속할 수 있는 서비스이다. 모든 노드의 특정 포트로 들어오는 트래픽을 포착하여 파드로 라우팅한다. 하지만, 특정 노드가 죽었을 때 문제가 발생할 수 있다. 예를 들어, 노드1의 IP를 고정적으로 사용한다고 가정하면, 노드1이 죽었을 때 다른 노드들이 살아있어도 문제가 발생한다.

image.png

출처: https://www.geeksforgeeks.org/kubernetes-nodeport-service/

LoadBalancer

외부의 로드밸런서와 연동된 서비스 타입이다. 로드밸런서로 들어온 트래픽을 살아있는 노드로 트래픽을 전달한다. 로드밸런서에 Health check 기능이 있기에 NodePort와 같은 문제를 보완할 수 있다. 또한 externalTrafficPolicy옵션을 Local로 설정하면 들어온 노드에 있는 파드로 라우팅하여, 네트워크 hop을 최적화할 수 있다. 대신 이 경우, 모든 노드에 적어도 하나 이상의 파드가 존재해야 한다.

kube proxy

CNI는 IP 할당 및 컨테이너 네트워크 연결에 대한 책임을 가진 반면, kube-proxy는 서비스 리소스의 구현을 담당한다. iptables , ipvs, nftables 모드를 지원한다. 모두 Netfiler framework을 사용한다. Netfiler는 리눅스 커널 기능으로, 규칙 기반으로 패킷을 처리한다.

예를 들어 POD1 IP의 80포트로 향하는 패킷을 POD2 IP의 8080포트로 향하도록 하는 규칙이 있다면 커널 단에서 이를 처리해준다.

image.png

(ClusterIP로 다른 파드에 접근할 때 흐름도) 출처: https://docs.tigera.io/calico/latest/about/kubernetes-training/about-kubernetes-services

netfilter

쿠버네티스는 리눅스 커널 기능 중 하나인 netfilter와 user space에 존재하는 인터페이스인 iptables라는 소프트웨어를 이용하여 패킷 흐름을 제어한다. netfilter란 규칙기반 패킷 처리 엔진이며, kernel space에 위치하여 모든 오고 가는 패킷을 관찰한다. 그리고 규칙에 매칭되는 패킷을 발견하면, SRC/DST등을 바꾸는 등 미리 정의된 행동을 수행한다.

1_Pz-NixxBDbSnuPGyp8UWHQ.webp

iptables

iptables는 리눅스 시스템에서 네트워크 트래픽을 제어하는 방화벽 도구이다. kube-proxy는 iptables(netfiler)를 통해 서비스 리소스를 구현한다. netfiler는 5가지 훅이 존재한다. 훅에 맞는 패킷을 발견하면, 규칙에 맞게 패킷을 가공한다.

  1. PREROUTING: 패킷이 수신된 직후, 목적지 변경 또는 NAT 적용.
  2. INPUT: 로컬로 향하는 패킷을 처리하고 필터링.
  3. FORWARD: 시스템을 경유하는 패킷을 필터링.
  4. OUTPUT: 로컬에서 생성된 패킷을 외부로 보내기 전에 처리.
  5. POSTROUTING: 패킷이 송신되기 직전에 NAT나 수정 적용.

image.png

출처: https://netpple.github.io/2022/netfilter-iptables/

worker 노드에 접속하여, iptables 규칙을 간단하게 확인해보면 아래와 같다.

현재 default 서비스(kubernetes)만 존재하는데도 상당히 많다. 위에서 본 Hook을 확인할 수 있다.

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
root@myk8s-worker:/# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N DOCKER_OUTPUT
-N DOCKER_POSTROUTING
-N KIND-MASQ-AGENT
-N KUBE-EXT-7EJNTS7AENER2WX5
-N KUBE-KUBELET-CANARY
-N KUBE-MARK-MASQ
-N KUBE-NODEPORTS
-N KUBE-POSTROUTING
-N KUBE-PROXY-CANARY
-N KUBE-SEP-2XZJVPRY2PQVE3B3
-N KUBE-SEP-2ZVL7EJZGLLRN3QG
...
-N KUBE-SVC-NPX46M4PTMTKRN6Y
-N KUBE-SVC-TCOU7JCQXEZGVUNU
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A PREROUTING -d 192.168.65.254/32 -j DOCKER_OUTPUT
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -d 192.168.65.254/32 -j DOCKER_OUTPUT
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A POSTROUTING -d 192.168.65.254/32 -j DOCKER_POSTROUTING
-A POSTROUTING -m addrtype ! --dst-type LOCAL -m comment --comment "kind-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom KIND-MASQ-AGENT chain" -j KIND-MASQ-AGENT
-A DOCKER_OUTPUT -d 192.168.65.254/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:34103
-A DOCKER_OUTPUT -d 192.168.65.254/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:50632
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 34103 -j SNAT --to-source 192.168.65.254:53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 50632 -j SNAT --to-source 192.168.65.254:53
-A KIND-MASQ-AGENT -d 10.10.0.0/16 -m comment --comment "kind-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
-A KIND-MASQ-AGENT -m comment --comment "kind-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE
-A KUBE-EXT-7EJNTS7AENER2WX5 -m comment --comment "masquerade traffic for kube-system/kube-ops-view:http external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-7EJNTS7AENER2WX5 -j KUBE-SVC-7EJNTS7AENER2WX5
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "kube-system/kube-ops-view:http" -m tcp --dport 30000 -m nfacct --nfacct-name  localhost_nps_accepted_pkts -j KUBE-EXT-7EJNTS7AENER2WX5
-A KUBE-NODEPORTS -p tcp -m comment --comment "kube-system/kube-ops-view:http" -m tcp --dport 30000 -j KUBE-EXT-7EJNTS7AENER2WX5
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
...
-A KUBE-SVC-7EJNTS7AENER2WX5 ! -s 10.10.0.0/16 -d 10.200.1.253/32 -p tcp -m comment --comment "kube-system/kube-ops-view:http cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-SVC-7EJNTS7AENER2WX5 -m comment --comment "kube-system/kube-ops-view:http -> 10.10.0.5:8080" -j KUBE-SEP-2ZVL7EJZGLLRN3QG
-A KUBE-SVC-ERIFXISQEP7F7OF4 ! -s 10.10.0.0/16 -d 10.200.1.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-MARK-MASQ
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp -> 10.10.0.2:53" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-XVHB3NIW2NQLTFP3
...

위의 옵션 해석이 어렵다. 해석을 위해 iptables 옵션 및 명령어를 알아야하는데 GPT를 통해서 간단하게 정리하면 아래와 같다.

💡 관련 옵션 및 명령어

  • S: 현재 설정된 iptables 규칙을 출력합니다.

  • -t nat: ‘nat’ 테이블을 대상으로 작업을 수행합니다. NAT(Network Address Translation)은 IP 패킷의 TCP/UDP 포트 번호와 소스 및 목적지 IP 주소를 재작성하는 방법입니다.

  • N: 새로운 체인을 생성합니다. 예를 들어, N KUBE-SVC-ERIFXISQEP7F7OF4는 ‘KUBE-SVC-ERIFXISQEP7F7OF4’라는 이름의 새 체인을 생성합니다.

  • A: 규칙추가

  • d: 목적지 주소를 지정합니다. 예를 들어, d 10.43.239.29/32는 목적지 IP 주소가 ‘10.43.239.29/32’인 패킷에 규칙을 적용하라는 의미입니다.

  • s 옵션은 소스 IP 주소를 지정하는 데 사용됩니다. 예를 들어, -s 10.42.0.0/16은 ‘10.42.0.0/16’ 네트워크에서 시작하는 패킷에 규칙이 적용된다는 것을 의미합니다.

  • p: 프로토콜을 지정합니다. p tcp는 TCP 프로토콜을 사용하는 패킷에 규칙을 적용하라는 의미입니다.

  • m comment: comment 모듈을 사용하라는 의미입니다. 이 모듈은 iptables 규칙에 주석을 추가하는 데 사용됩니다.

  • -comment: comment 모듈에 대한 옵션으로, 주석을 지정합니다.

  • -sport, -dport: 대상 포트를 지정합니다. 예를 들어, -dport 80는 대상 포트가 80인 패킷에 규칙을 적용하라는 의미입니다.

  • j: 패킷이 규칙에 일치할 경우 수행할 작업을 지정합니다.

  • -i, -o : input interface, output interface

  • ! 기호는 그 뒤에 오는 조건을 부정하는 데 사용됩니다. 예를 들어, ! -s 10.42.0.0/16은 ‘10.42.0.0/16’ 네트워크에서 시작하지 않는 패킷에 규칙이 적용된다는 것을 의미합니다.

쿠버네티스 서비스에서는 iptables를 아래와 같이 적용한다고 한다.

💡 iptables 정책 적용시 순서

  1. PREROUTING

  2. KUBE-SERVICES

  3. KUBE-SVC-###

  4. KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>, ….

image.png

위의 그림은 스터디에서 보기 좋게 정리해주신 패킷흐름 예시이다.

실습

이제 위에서 학습한 내용을 실습한다. 우선, 서비스가 정말 고정적인 엔드포인트를 제공하고 부하분산을 진행하는지 테스트한다.

리소스 YAML

  • 테스트 파드
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
apiVersion: v1
kind: Pod
metadata:
  name: webpod1
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod2
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker2
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod3
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker3
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0

  • 클라이언트 파드
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Pod
metadata:
  name: net-pod
spec:
  nodeName: myk8s-control-plane
  containers:
  - name: netshoot-pod
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
  • 서비스
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        # 서비스 IP 에 접속 시 사용하는 포트 port 를 의미
      targetPort: 80    # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
  selector:
    app: webpod         # 셀렉터 아래 app:webpod 레이블이 설정되어 있는 파드들은 해당 서비스에 연동됨
  type: ClusterIP       # 서비스 타입

3개의 리소스를 배포한다. 배포 결과 아래와 같이 총 4개의 파드와 svc-clusterip 서비스를 확인할 수 있다.

image.png

서비스는 :8000 포트로 들어오는 트래픽 webpod1,2,3:80 포트로 라우팅한다.

ClusterIP

  • net-pod에 접속하여 clusterIP에 접근해본다.
1
2
3
4
5
6
7
8
9
10
11
curl 10.200.1.201:9000
Hostname: webpod2
IP: 127.0.0.1
IP: ::1
IP: 10.10.2.2
IP: fe80::c896:3aff:fe4f:109b
RemoteAddr: 10.10.0.6:34036
GET / HTTP/1.1
Host: 10.200.1.201:9000
User-Agent: curl/8.7.1
Accept: */*

정상적으로 접근이 잘이뤄진다. 이제 반복적으로 서비스에 접근하고 hostname을 카운팅하여 로드밸런서 기능을 확인해본다.

1
2
kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uni
q -c | sort -nr"

image.png

35,33,32로 33%에 수렴하는 것을 확인할 수 있다.

이제 서비스에 대한 iptables를 확인해본다.

docker exec를 통해 worker노드에 접속한다. 서비스의 IP를 기반으로 필터링해보면, 아래와 같이 서비스에 대한 chain을 확인할 수 있다.

1
2
3
iptables -S -t nat | grep 10.200.1.201
-A KUBE-SERVICES -d 10.200.1.201/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.201/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ

이제 이 KUBE-SVC-KBDEBIL6IU6WL7RF chain으로 필터링을 해보면, 각 파드의 IP로 1/3의 확률로 라우팅하는 것을 볼 수 있다. (여기서 잘보면 정확한 1/3의 값은 아니다.) 첫번째 chain의 확률은 1/3, 두번째 체인은 자신에게 왔다는 것은 첫번째 체인을 통과한 것이므로 1/2, 나머지 chain은 1/1의 확률로 지정했다.

1
2
3
4
5
6
7
iptables -S -t nat | grep KUBE-SVC-KBDEBIL6IU6WL7RF
-N KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SERVICES -d 10.200.1.201/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.201/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.1.2:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-TBW2IYJKUCAC7GB3
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.2.2:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-DOIEFYKPESCDTYCH
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.3.3:80" -j KUBE-SEP-K7ALM6KJRBAYOHKX

NodePort

위에서 사용한 svc-clusterip 서비스를 NodePort Type으로 수정한다.

1
k edit svc svc-clusterip

image.png

iptables에서 nodeport로 필터링을 하면 아래와 같이 svc-clusterip에 대한 규칙을 확인할 수 있다.

1
2
3
4
5
iptables -S -t nat | grep NODEPORT
-N KUBE-NODEPORTS
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m tcp --dport 30007 -m nfacct --nfacct-name  localhost_nps_accepted_pkts -j KUBE-EXT-KBDEBIL6IU6WL7RF
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m tcp --dport 30007 -j c
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS

30007번 포트로 들어오는 패킷은 KUBE-EXT-KBDEBIL6IU6WL7RF 체인으로 보내고, 이는 KUBE-SVC-KBDEBIL6IU6WL7RF 으로 향하는 것을 확인할 수 있다.

1
2
3
4
5
6
iptables -S -t nat | grep KUBE-EXT-KBDEBIL6IU6WL7RF
...
-A KUBE-EXT-KBDEBIL6IU6WL7RF -j KUBE-SVC-KBDEBIL6IU6WL7RF
...
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m tcp --dport 30007 -m nfacct --nfacct-name  localhost_nps_accepted_pkts -j KUBE-EXT-KBDEBIL6IU6WL7RF
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m tcp --dport 30007 -j KUBE-EXT-KBDEBIL6IU6WL7RF

KUBE-SVC-KBDEBIL6IU6WL7RF 체인은 위의 ClusterIP와 같이 파드의 엔드포인트로 라우팅하는 체인이다. 다시 한번 확인해보면

1
2
3
4
5
6
7
8
iptables -S -t nat | grep KUBE-SVC-KBDEBIL6IU6WL7RF
-N KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-EXT-KBDEBIL6IU6WL7RF -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SERVICES -d 10.200.1.201/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.201/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.1.2:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-TBW2IYJKUCAC7GB3
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.2.2:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-DOIEFYKPESCDTYCH
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.3.3:80" -j KUBE-SEP-K7ALM6KJRBAYOHKX

파드로 라우팅되는 것을 확인할 수 있다.

즉, ClusterIP와 비교하여 KUBE-NODEPORTKUBE-EXT-... 체인이 추가된다.

정리

서비스의 동작은 ‘kube-proxy’가 담당하며, 별다른 리소스를 소모하지 않고, iptables(netfilter)를 이용하여 커널영역에서 규칙에 맞는 패킷이 오면 정해둔 행동을 취한다. 여기서는 주로, Network Address Translation “NAT” 기능을 수행한다. 이를 통해, 실질적인 인터페이스 없이 reverse-proxy 역할을 서비스가 할 수 있게 된다. 이를 위해 kube-proxy는 kube-proxy는 API 서버로 파드의 Health check, 혹은 파드의 부하상태를 통해 엔드포인트를 관리한다. 현재의 엔드포인트에 맞게 Netfilter의 규칙을 수정한다. 즉, kube-proxy는 로드밸런서에서의 manager server의 역할을 담당한다.

This post is licensed under CC BY 4.0 by the author.