Kubernetes는 컨테이너 오케스트레이션을 구현하기 위해 독특한 네트워킹 구조를 가지고 있습니다.
이런 Kubernetes의 네트워킹 구조에서 단연 중요한 것은 Service라는 개념입니다.
Service는 외부 네트워크와 격리되어 있으며 임시적이라는 특성을 가진 Pod들을 외부와 통신할 수 있도록 고정적인 주소로 노출하는 방법을 말합니다.
이 Service의 가상 IP를 구현하기 위해서는 kube-proxy라는 컴포넌트가 전적으로 맡게 되며, 각 node에 위치해 netfilter라는 linux 패킷 조작 툴을 사용해 패킷의 생명주기를 조작합니다.
예를 들어 kube-proxy가 Service의 구현을 위해 사용하는 mode 중 iptables 모드는 패킷이 node로 들어오면 kube-proxy에 의해 변경된 iptables를 기반으로 적절한 pod에 부하 분산됩니다.
이렇게 Service는 Kubernetes 네트워크에서 중요한 부분을 차지하는 만큼 다양한 옵션의 설정들을 적용할 수 있습니다.
그 중 하나가 이번 포스팅의 주제인 externalTrafficPolicy 설정인데요.
이번 포스팅을 통해 externalTrafficPolicy 설정의 옵션 별 기능과 한계, 그리고 이 한계를 극복할 수 있는 GCP의 Container-native LoadBalancer에 대해 알아보겠습니다.
1. externalTrafficPolicy란?
externalTrafficPolicy에 대해 소개하기 전에 Kubernetes 네트워크의 단점 중 하나인 Double-hop dilema부터 짚고 넘어가겠습니다.
위에서 소개한 Kubernetes 네트워킹의 단점이 있는데, 이는 LoadBalancer가 트래픽을 목적지 Pod가 존재하지 않는 Node로 보낸 경우에는 Pod가 존재하는 Node로 한 번 더 트래픽을 보내야 한다는 것입니다.
이처럼 Kubernetes 네트워크 구조 상 하나의 홉이 더 추가되는 현상을 Double-hop dilemma라고 합니다.
Double-hop dilema는 LoadBalancer 입장에서 봤을 때 어떤 Node에 어떤 Pod가 있을 것인지 모르기 때문에 발생하는 현상입니다.
운 좋게 목적지 Pod가 존재하는 Node로 트래픽을 한 번에 보낼 수도 있겠지만, 그럴 확률은 총 Node 개수/Pod가 존재하는 Node 개수 밖에 되지 않습니다.
이처럼 외부에서 들어오는 트래픽은 하나의 네트워크 홉이 추가될 수도 있다는 손해를 감수해야 하는데요. 특히 대량의 트래픽을 다루어야 하는 서비스의 경우에 Latency가 대폭 증가할 수 밖에 없습니다.
Kubernetes에는 이미 이러한 단점을 완화할 수 있는 Service 설정이 존재합니다.
그것이 바로 externalTrafficPolicy 설정인데요.
Kuberentes 공식 문서에서 externalTrafficPolicy는 다음과 같이 명시되어 있습니다.
.spec.externalTrafficPolicy denotes if this Service desires to route external traffic to node-local or cluster-wide endpoints. There are two available options: Cluster (default) and Local. Cluster obscures the client source IP and may cause a second hop to another node, but should have good overall load-spreading. Local preserves the client source IP and avoids a second hop for LoadBalancer and NodePort type Services, but risks potentially imbalanced traffic spreading.
externalTrafficPolicy 설정은 Cluster(default)와 Local, 2가지 옵션을 가질 수 있으며 Cluster의 경우 Source IP가 변경되고 Second hop을 야기할 수 있으나 고른 부하 분산을 할 수 있고, Local의 경우 Source IP를 보존할 수 있으나 불균형한 부하 분산을 한다고 되어 있습니다.
externalTrafficPolicy 옵션에 따른 네트워킹 차이를 더 자세히 알아보겠습니다.
externalTrafficPolicy=Cluster
externalTrafficPolicy를 Cluster로 설정할 경우, 트래픽은 Node간 이동이 가능합니다.
트래픽이 Node를 넘을 수 있기 때문에 두 번째 홉이 추가될 수도 있지만, 이 덕분에 모든 Pod에 고른 확률로 부하 분산이 된다는 장점이 있습니다.
추가적으로 Cluster 옵션은 Client의 Source IP를 보존하지 못한다는 특징도 존재합니다.
이 또한 트래픽이 Node를 넘나들 수 있다는 점에 기인하는데요.
externalTrafficPolicy=Cluster로 설정한 서비스에서는 Client에서 출발한 트래픽이 Node를 통과하면서 SNAT를 수행하기 때문에 엔드포이트 컨테이너가 받는 Source IP는 Node 2의 IP가 됩니다.
그래서 결과적으로 컨테이너는 Client의 Source IP를 모른 채로 작업을 수행해야 합니다.
따라서 이 옵션은 Latency에 크게 구애받지 않거나, 트래픽 양이 많지 않은 경우, 혹은 Source IP를 보존할 필요가 없는 경우에 적합합니다.
externalTrafficPolicy=Local
externalTrafficPolicy를 Local로 설정한 경우에는 트래픽의 Node간 이동이 불가능합니다.
그렇기 때문에 Node 간 이동으로 인해 발생했던 Double-hop delima가 해결된 것을 볼 수 있습니다.
하지만 단일 Node 내에서만 Pod간 부하 분산이 가능하기 때문에 분산이 불균형해진다는 단점이 존재합니다.
이 같은 단점을 완화하기 위해서는 pod anti-affinity를 설정해 최대한 Node별로 Pod가 균등하게 배치되도록 해야 합니다.
또 Cluster 옵션과 다르게 Local 옵션은 Client의 Source IP를 보존할 수 있다는 특징이 있는데요.
이 또한 트래픽이 Node간 넘어다니지 못하기 때문에 SNAT가 수행되지 않는 데서 기인합니다.
그렇기 때문에 Local 옵션은 Latency가 크게 중요하거나 네트워크를 효율적으로 운영해야 할 경우, 또는 Source IP를 보존해야 하는 경우에 적합합니다.
2. GCP의 Container-native Load balancer
지금까지 Kubernetes의 Service가 가질 수 있는 구성 중 externalTrafficPolicy의 옵션 별 특징과 장단점을 알아봤습니다.
알아본 결과 externalTrafficPolicy의 두 옵션 Cluster, Local 모두 장점과 단점이 Trade-off 관계로 뚜렷하게 나타났는데요.
그렇다면 Double-hop dilema를 해결하면서 Pod간 고른 부하 분산까지 잡을 수는 없는 것일까요?
이를 해결하기 위해 GCP의 Kuberentes 서비스 GKE는 Container-native LoadBalancer를 도입했습니다.
Container-native LoadBalancer는 기존의 Kubernetes 네트워킹이 가지는 한계를 극복하기 위해 등장한 LoadBalancer 타입입니다.
이 타입의 LB는 Node 간 트래픽이 이동함으로써 발생하는 Second hop 문제와 이를 해결하기 위해 부담했어야 했던 불균형한 Pod 부하 분산 문제를 모두 해결했습니다.
이는 Container-native LoadBalancer가 기존의 LoadBalancer와는 다른 유형의 Backend를 가지기 때문에 가능한 것인데요.
기존의 LB가 단순히 VM을 묶어 놓은 Instance Group을 Backend로 사용하는데 비해, Container-native LB는 Pod IP : VM hostname의 값 쌍을 가지는 Network EndPoint Group(NEG)를 Backend로 사용합니다.
이 차이가 어떤 결과를 나타내는지 자세히 보도록 하겠습니다.
Legacy LoadBalancer
기존의 LoadBalancer가 가지고 있는 정보는 각 Node의 주소가 전부이며 Pod 주소에 대한 정보는 가지고 있지 않습니다.
LB는 Node안에 어떤 Pod가 존재하는지 모르므로 기존 LB가 유지하고 있던 부하 분산 규칙에 따라서만 트래픽을 Node로 보내게 됩니다.
이렇게 되면 Pod로의 라우팅 및 부하 분산은 온전히 Node 내부의 iptables에 맡기기 때문에 Double-hop delima가 나타납니다.
따라서 이 방식은 기존의 Kubernetes 네트워킹이 가진 한계를 그대로 가지고 있다고 볼 수 있습니다.
Container-native LoadBalancer
반면에 Container-Native LoadBalancer는 Network Endpoint Group(NEG)에 등록된 Node의 주소와 Pod의 주소가 매칭된 정보를 가지고 있습니다.
따라서 어떤 Node에 어떤 Pod가 존재하는지 알고 있으므로 곧바로 올바른 Node에 트래픽을 보낼 수 있습니다.
이렇게 되면 아무 Node에게나 트래픽을 보내지 않으므로 Double-hop dilema를 해결할 수 있을 뿐만 아니라, LB단에서 Pod 부하 분산을 구현할 수도 있습니다.
iptables가 아닌 LB에서 라우팅과 부하 분산을 모두 맡았기 때문에 기존의 Kubernetes 네트워킹이 가지던 한계를 극복할 수 있게 된 것입니다.
그렇다면 어떻게 Cluster 외부에 존재하는 Load balancer가 Node 내부의 Pod 위치를 어떻게 알 수 있었을까요?
GCP는 이를 GKE의 VPC-native routing에서 사용하는 Alias-IP를 지원하기 때문입니다.
Alias-IP란 VM에게 추가적인 IP range를 할당할 수 있는 GCP의 VPC 네트워킹 기능입니다.
GCP의 VPC는 기본적으로 서브넷이 가진 Primary CIDR range 외에 추가적인 Secondary CIDR range를 할당할 수 있는데요.
Secondary CIDR range를 할당받은 서브넷의 VM은 Primary CIDR range 내에서 단 하나의 Primary IP만 할당받을 수 있지만, Secondary CIDR range에서는 추가적인 Alias IP를 range 단위로 할당받을 수 있기 때문에 하나의 NIC에 여러 IP를 매칭할 수 있게 됩니다.
이는 추가적인 NIC를 가지는 것이 아니라, Primary IP의 네트워크 인터페이스에 내부 IP range를 앨리어스하는 것이기 때문에 NIC 증가에 따른 고갈이나 복잡성 문제는 일어나지 않습니다.
결과적으로 NIC 추가 없이 여러 내부 IP를 할당할 수 있기 때문에 특히 컨테이너 단위의 서비스를 운영한다면 유용한 기능이라고 할 수 있습니다.
GKE는 VM의 Alias-IP를 Kubernetes 클러스터의 Pod address range로 사용함으로써 Pod의 주소만 알아도 이에 매칭된 Node의 Primary IP까지 알 수 있습니다.
이 Node와 Pod address 쌍을 Network Endpoint Group(NEG)에 등록함으로써 Load Balancer가 이를 보고 pod 단위 부하 분산을 구현할 수 있게 된 것입니다.
3. Container-native Load balancer 구현
지금까지 Container-native Load balancer의 개념을 알아보았는데요, 이제 이 기능을 실제로 어떻게 사용할 수 있는지 GKE Kubernetes 클러스터에서 구현해보겠습니다.
3-1. GKE 클러스터 환경 구성
먼저 아래 명령어로 VPC-native 기능이 활성화된 GKE 클러스터를 생성합니다.
1
2
3
4
5
|
gcloud container clusters create neg-demo-cluster \
--enable-ip-alias \
--create-subnetwork="" \
--network=default \
--zone=us-central1-a
|
cs |
콘솔 상에서는 Networking 탭의 Enable VPC-native traffic routing (uses alias IP)를 체크하는 것으로 해당 기능을 활성화할 수 있습니다.
클러스터가 생성되었다면 아래 명령어로 테스트용 이미지를 사용한 Deployment yaml을 생성합니다.
1
|
kubectl create deployment test-server --image k8s.gcr.io/serve_hostname --port 9376 --dry-run=server -o yaml >> server.yaml
|
cs |
생성된 yaml파일을 클러스터에 적용합니다.
1
|
kubectl apply -f ./server.yaml
|
cs |
Deployment가 생성된 것을 확인하고 아래 명령어로 Deployment의 Pod 개수를 3개로 늘립니다.
1
|
kubectl scale deployment test-server --replicas=3
|
cs |
이후 아래 명령어로 Deployment를 LoadBalancer 타입의 Service를 사용해 노출합니다.
1
|
kubectl expose deployment server --type LoadBalancer --port 80 --target-port 9376
|
cs |
생성된 LoadBalancer Service를 살펴보면 아래와 같은 yaml 구성으로 되어있습니다.
여기서 metadata.annotation 항목의 cloud.google.com/neg: '{"ingress":true}' 값이 Container-native Load balancer를 enable해주는 값입니다.
현재 GKE에서 LoadBalancer, 혹은 NodePort 타입의 서비스를 생성하면 해당 annotation이 기본적으로 붙은 채로 생성됩니다.
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
|
apiVersion: v1
kind: Service
metadata:
annotations:
cloud.google.com/neg: '{"ingress":true}'
creationTimestamp: "2022-02-19T06:16:32Z"
finalizers:
- service.kubernetes.io/load-balancer-cleanup
labels:
app: server
name: server
namespace: default
resourceVersion: "2099229"
uid: 76803e7c-7435-48f8-a177-44ec55f913ac
spec:
clusterIP: 10.72.8.59
clusterIPs:
- 10.72.8.59
externalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- nodePort: 31022
port: 81
protocol: TCP
targetPort: 9376
selector:
app: server
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}
|
cs |
하지만 여기까지의 진행으로는 아직 Container-native Load balancer가 생성되지 않습니다.
Container-native LB는 Ingress 오브젝트를 생성했을 때 사용할 수 있습니다. 다음 명령어로 Ingress 오브젝트를 생성해 서비스를 노출합니다.
1
|
kubectl create ingress demo-ingress --default-backend=server:80
|
cs |
여기까지 Container-native Load balancer 사용을 위한 GKE 클러스터 환경을 모두 구성했습니다.
3-2. GCP 리소스 생성 확인
이렇게 모든 구성을 마친 상황에서 GCP 상에서는 어떤 리소스가 생성되어 Container-native LB를 구현하는지 알아보겠습니다.
gcloud compute forwarding-rules list
GCP의 Load balancer는 Forwarding rule + Target proxy + Backend service 로 이루어져 있는데요.
GKE 환경을 구성한 결과 GCP 상에서는 2개의 forwarding rule이 생성되는 것을 확인할 수 있습니다.
Region이 지정되어 있는 2번째 리소스가 GKE의 LoadBalancer Service가 사용하는 rule,
Global 속성인 1번째 리소스가 Ingress에 사용하는 rule로, LoadBalancer 및 Ingress의 Forwarindg rule이 각각 1개씩 생성된 것을 알 수 있습니다.
gcloud compute backend-services list
Backend는 Ingress, 즉 Container-native Loadbalancer가 사용하는 Backend 1개만 생성이 됩니다. Ingress 엔드포인트로 들어오는 트래픽은 이 Backend로 도달하게 됩니다.
더 자세히 보기 위해 아래 명령어로 해당 Backend를 확인합니다.
gcloud compute backend-services describe k8s1-ed23c2f4-default-server-80-ac858206 --global
명령어로 Backend를 확인해보면 현재 NEG를 Backend로 사용하고 있는 것을 알 수 있습니다.
이렇게 기본적으로 GKE 환경에서 Ingress로 노출하면 GCP에선 NEG를 Backend로 하는 LB를 생성한다는 것을 알 수 있습니다.
gcloud compute network-endpoint-groups list
Network Endpoint Group(NEG)를 확인해보면 위에서 본 것처럼 Container-native Load balancer는 NEG를 Backend로 가지기 때문에 NEG 리소스가 1개 생성된 것을 확인할 수 있습니다.
여기서 얻은 NEG 이름 속성을 기반으로 어떤 endpoint가 NEG에 등록되어 있는지 확인할 수 있습니다.
gcloud compute network-endpoint-groups list-network-endpoints k8s1-ed23c2f4-default-server-80-ac858206 --zone us-central1-c
NEG에 등록된 endpoint를 확인해보면 3개의 endpoint가 등록되어 있는 것을 볼 수 있습니다.
이 endpoint들의 주소는 이전에 생성한 Pod의 주소 및 포트와 동일한 것으로 보아 GKE 내 Pod들의 주소 목록임을 확인할 수 있습니다.
결과적으로 Ingress에 사용되는 LB는 INSTANCE : IP_ADDRESS : PORT 로 이루어진 주소 목록을 통해 어떤 node에 어떤 pod를 어떤 port로 접근해야 하는지 알 수 있게 됩니다.
현재까지 생성된 GKE 리소스 현황을 토대로 보면 LB는 다음과 같이 Pod의 주소를 식별할 수 있습니다.
3-3. Container-native Load balancer 사용 및 검증
이제 Container-native LB가 기존 LB와 달리 Double-hop dilema를 해결할 수 있는지에 대한 검증을 해보겠습니다.
GKE 클러스터 내에 총 3개 Node가 존재하는 상황에서 단 1개 Node에만 pod를 띄웠을때, 각 LB별로 어떻게 트래픽을 보내는지 확인해보겠습니다.
먼저 아래 명령어로 pod의 수를 1개로 변경합니다.
1
|
kubectl scale deployment server --replicas=1
|
cs |
1개 남은 pod는 현재 ...n3j7 Node에 존재하는 것을 확인했습니다.
먼저 Container-native LB를 사용하는 구성에서 트래픽을 보내보겠습니다. 테스트는 GCP의 networking tool인 Connectivity Tests를 이용해 진행했습니다.
테스트 결과 Container-native LB로 보내진 트래픽이 Pod가 존재하는 n3j7 Node로만 간 것을 확인할 수 있었습니다.
이로써 NEG를 Backend로 사용하는 Container-native LB는 정확히 Pod가 존재하는 Node에만 트래픽을 보낸다는 것을 확인했습니다.
다음으로 Container-native가 아닌 기존 LB를 사용한 환경에 트래픽을 보내보겠습니다.
기존 LB를 사용하는 환경으로 변경하기 위해 LoadBalancer Service의 구성 중 다음과 같이 cloud.google.com/neg: '{"ingress":true}' annotation을 삭제합니다.
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
|
apiVersion: v1
kind: Service
metadata:
annotations:
creationTimestamp: "2022-02-19T06:16:32Z"
finalizers:
- service.kubernetes.io/load-balancer-cleanup
labels:
app: server
name: server
namespace: default
resourceVersion: "2099229"
uid: 76803e7c-7435-48f8-a177-44ec55f913ac
spec:
clusterIP: 10.72.8.59
clusterIPs:
- 10.72.8.59
externalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- nodePort: 31022
port: 81
protocol: TCP
targetPort: 9376
selector:
app: server
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}
|
cs |
생성된 Backend를 확인해보면 대상이 NEG에서 InstanceGroup으로 바뀐 것을 확인할 수 있습니다.
이제 Ingress를 통해 Pod로 트래픽을 보내는 테스트를 진행합니다.
테스트 결과 Ingress로 보낸 트래픽이 Pod가 띄워진 Node는 물론 Pod가 없는 나머지 2개의 Node에도 도달한 것을 확인할 수 있습니다.
이로써 Instance Group을 Backend로 가지는 기존 LB는 Pod가 존재하는 Node 뿐만이 아닌 모든 Node에게 트래픽을 보낸다는 것을 확인했습니다.
4. 마무리
이렇게 Kubernetes의 네트워킹과 externalTrafficPolicy에 따른 변화, 그리고 Container-native LB를 이용한 GKE 클러스터의 특징에 대해 알아봤습니다.
Kubernetes의 서비스 구성 중 하나인 externalTrafficPolicy는 트래픽이 Node간 이동 가능하지만 그만큼 비효율적인 통신이 발생하는 Cluster 옵션과 트래픽이 Node 내에서만 이동 가능하지만 부하 분산이 불균형해지는 Local 옵션을 사용할 수 있었습니다.
두 옵션은 서로의 장단점이 Trade-off 관계에 있기 때문에 어느 한 옵션의 단점을 감수할 수 밖에 없었는데요.
GKE에서 이용할 수 있는 Container-natie LB는 기존 Kubernetes 네트워킹의 단점이었던 Double-hop dilema를 해결할 수 있었으며, Pod간 고른 부하분산까지 구현할 수 있기 때문에 이같은 문제를 해결할 수 있었습니다.
이는 GCP의 VPC 네트워킹 기능인 Alias-IP를 이용해 NEG를 Backend로 등록하면 LB가 어떤 Pod가 어떤 Node에 존재하는지 알 수 있기 때문이었습니다.
마지막으로 Kubernetes 클러스터에 네트워크 테스트를 해봄으로써 Container-native LB가 실제로 Pod가 존재하는 Node에만 트래픽을 보낸다는 것을 확인했습니다.
이 포스팅을 읽는 분들이 Kuberntes 네트워크의 특징과 GCP의 GKE containver-native LB 기능에 대해 잘 알아갔으면 합니다.
'GCP' 카테고리의 다른 글
GKE에서 Autoscaling을 더 잘 사용할 수 있는 5가지 방법(HPA, VPA, MPA, CA, NAP) (0) | 2022.07.26 |
---|---|
Secret Manager에 저장된 중요 데이터를 Kubernetes Secret과 연동해보자(with GCP) (1) | 2022.04.11 |
Prometheus+Grafana로 Apache Hadoop 및 Hive모니터링 하기(with GCP Dataproc) (0) | 2021.11.23 |
Google Cloud의 Cloud Deploy로 자동화된 CI/CD Pipeline 구성하기 (0) | 2021.11.05 |
GCP Cloud armor의 DDoS protection 기능 사용 및 검증 (0) | 2021.10.07 |