소스 IP 사용하기

소스 IP 사용하기 (Using Source IP)

쿠버네티스 클러스터에서 실행되는 애플리케이션은 Service 추상화를 통해 서로, 그리고 외부 세계와 통신합니다. 이 문서는 다른 유형의 Service로 보내진 패킷의 소스 IP에 무슨 일이 일어나는지, 그리고 필요에 따라 이 동작을 어떻게 전환할 수 있는지 설명해요.

출처: 문서

본문

시작하기 전에

용어 (Terminology)

이 문서는 다음 용어를 사용해요.

NAT - 네트워크 주소 변환(Network address translation)

Source NAT - 패킷에서 소스 IP를 교체하는 것. 이 페이지에서는 보통 노드의 IP 주소로 교체하는 것을 의미해요.

Destination NAT - 패킷에서 대상 IP를 교체하는 것. 이 페이지에서는 보통 파드의 IP 주소로 교체하는 것을 의미해요.

VIP - 가상 IP 주소. 쿠버네티스의 모든 Service에 할당되는 것 같은 것.

kube-proxy - 모든 노드에서 Service VIP 관리를 조정하는 네트워크 데몬.

전제 조건 (Prerequisites)

쿠버네티스 클러스터가 필요하고, kubectl 명령줄 도구가 클러스터와 통신하도록 설정돼 있어야 해요. 이 튜토리얼은 제어 플레인 호스트 역할을 하지 않는 노드가 최소 두 개 있는 클러스터에서 실행하는 것을 권장해요. 아직 클러스터가 없다면 minikube를 사용하거나 다음 쿠버네티스 플레이그라운드 중 하나를 사용해 만들 수 있어요.

  • iximiuz Labs
  • Killercoda
  • KodeKloud

예시는 HTTP 헤더를 통해 받은 요청의 소스 IP를 에코해 주는 작은 nginx 웹서버를 사용해요. 다음과 같이 만들 수 있어요.

참고

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10

출력은 다음과 같아요.

deployment.apps/source-ip-app created

목표 (Objectives)

  • 다양한 유형의 Service를 통해 간단한 애플리케이션 노출하기
  • 각 Service 유형이 소스 IP NAT을 어떻게 처리하는지 이해하기
  • 소스 IP 보존에 관련된 트레이드오프 이해하기

Type=ClusterIP인 Service의 소스 IP

클러스터 내부에서 ClusterIP로 보내진 패킷은 iptables 모드(기본값)로 kube-proxy를 실행 중이라면 절대 source NAT되지 않아요. kube-proxy가 실행 중인 노드에서 http://localhost:10249/proxyMode를 가져와 kube-proxy 모드를 쿼리할 수 있어요.

kubectl get nodes

출력은 다음과 비슷해요.

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

노드 중 하나에서 프록시 모드를 가져와요(kube-proxy는 포트 10249에서 수신 대기).

# 쿼리하려는 노드의 셸에서 이 명령을 실행하세요.
curl http://localhost:10249/proxyMode

출력은 다음과 같아요.

iptables

source-ip 앱 위에 Service를 만들어 소스 IP 보존을 테스트할 수 있어요.

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

출력은 다음과 같아요.

service/clusterip exposed
kubectl get svc clusterip

출력은 다음과 비슷해요.

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

그리고 같은 클러스터의 파드에서 ClusterIP를 호출해요.

kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

출력은 다음과 비슷해요.

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

그런 다음 그 파드 안에서 명령을 실행할 수 있어요.

# "kubectl run" 터미널 안에서 이 명령을 실행하세요
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

…그런 다음 wget으로 로컬 웹서버를 쿼리해요.

# "10.0.170.92" 를 "clusterip"라는 Service의 IPv4 주소로 교체하세요
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

client_address는 클라이언트 파드와 서버 파드가 같은 노드에 있든 다른 노드에 있든 항상 클라이언트 파드의 IP 주소예요.

Type=NodePort인 Service의 소스 IP

Type=NodePort인 Service로 보내진 패킷은 기본적으로 source NAT돼요. NodePort Service를 만들어 이를 테스트할 수 있어요.

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

출력은 다음과 같아요.

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)

NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

클라우드 제공자에서 실행 중이라면 위에서 보고된 nodes:nodeport에 대한 방화벽 규칙을 열어야 할 수도 있어요. 이제 위에서 할당된 노드 포트를 통해 클러스터 밖에서 Service에 도달해 볼 수 있어요.

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

출력은 다음과 비슷해요.

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

이것들은 올바른 클라이언트 IP가 아니라 클러스터 내부 IP라는 점을 주목하세요. 이렇게 일어나요.

  • 클라이언트가 node2:nodePort로 패킷을 보냄
  • node2가 패킷의 소스 IP 주소를 자신의 IP 주소로 교체(SNAT)
  • node2가 패킷의 대상 IP를 파드 IP로 교체
  • 패킷이 node 1로 라우팅된 다음 엔드포인트로 이동
  • 파드의 응답이 node2로 라우팅됨
  • 파드의 응답이 클라이언트로 다시 보내짐

시각적으로:

그림. SNAT을 사용하는 Type=NodePort 소스 IP

이것을 피하기 위해 쿠버네티스에는 클라이언트 소스 IP를 보존하는 기능이 있어요. service.spec.externalTrafficPolicyLocal 값으로 설정하면, kube-proxy는 요청을 로컬 엔드포인트에만 프록시하고 다른 노드로 트래픽을 전달하지 않아요. 이 접근 방식은 원래 소스 IP 주소를 보존해요. 로컬 엔드포인트가 없으면 노드로 보내진 패킷은 버려지므로, 엔드포인트에 도달하는 패킷에 적용하는 어떤 패킷 처리 규칙에서도 올바른 source-ip에 의존할 수 있어요.

service.spec.externalTrafficPolicy 필드를 다음과 같이 설정하세요.

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

출력은 다음과 같아요.

service/nodeport patched

이제 테스트를 다시 실행해요.

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

출력은 다음과 비슷해요.

client_address=198.51.100.79

엔드포인트 파드가 실행 중인 노드 하나에서만, 올바른 클라이언트 IP와 함께 응답 하나를 받았음을 주목하세요.

이렇게 일어나요.

  • 클라이언트가 엔드포인트가 없는 node2:nodePort로 패킷을 보냄
  • 패킷이 버려짐
  • 클라이언트가 엔드포인트가 있는 node1:nodePort로 패킷을 보냄
  • node1이 올바른 소스 IP로 엔드포인트에 패킷을 라우팅함

시각적으로:

그림. Type=NodePort 소스 IP가 클라이언트 소스 IP 주소를 보존

Type=LoadBalancer인 Service의 소스 IP

Type=LoadBalancer인 Service로 보내진 패킷은 기본적으로 source NAT돼요. Ready 상태의 모든 스케줄링 가능한 쿠버네티스 노드가 로드 밸런싱 트래픽에 적격이기 때문이에요. 그래서 패킷이 엔드포인트가 없는 노드에 도착하면, 시스템은 그것을 엔드포인트가 있는 노드로 프록시하면서 패킷의 소스 IP를 노드의 IP로 교체해요(이전 섹션에서 설명한 대로).

source-ip-app을 로드 밸런서를 통해 노출해 이를 테스트할 수 있어요.

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

출력은 다음과 같아요.

service/loadbalancer exposed

Service의 IP 주소를 출력해요.

kubectl get svc loadbalancer

출력은 다음과 비슷해요.

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

다음으로 이 Service의 external-ip로 요청을 보내요.

curl 203.0.113.140

출력은 다음과 비슷해요.

CLIENT VALUES:
client_address=10.240.0.5
...

하지만 GKE(Google Kubernetes Engine)/GCE에서 실행 중이라면, 같은 service.spec.externalTrafficPolicy 필드를 Local로 설정하면 Service 엔드포인트가 없는 노드가 의도적으로 상태 검사(health check)에 실패하게 해 로드 밸런싱 트래픽에 적격한 노드 목록에서 스스로를 제거하도록 강제해요.

시각적으로:

어노테이션을 설정해 이를 테스트할 수 있어요.

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

쿠버네티스가 할당한 service.spec.healthCheckNodePort 필드를 즉시 볼 수 있어야 해요.

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

출력은 다음과 비슷해요.

  healthCheckNodePort: 32122

service.spec.healthCheckNodePort 필드는 /healthz에서 상태 검사를 서비스하는 모든 노드의 포트를 가리켜요. 이를 테스트할 수 있어요.

kubectl get pod -o wide -l app=source-ip-app

출력은 다음과 비슷해요.

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

curl로 여러 노드의 /healthz 엔드포인트를 가져와요.

# 선택한 노드에서 로컬로 이 명령을 실행하세요
curl localhost:32122/healthz
1 Service Endpoints found

다른 노드에서는 다른 결과를 얻을 수 있어요.

# 선택한 노드에서 로컬로 이 명령을 실행하세요
curl localhost:32122/healthz
No Service Endpoints Found

제어 플레인에서 실행되는 컨트롤러가 클라우드 로드 밸런서를 할당하는 책임이 있어요. 같은 컨트롤러가 각 노드의 이 포트/경로를 가리키는 HTTP 상태 검사도 할당해요. 엔드포인트가 없는 2개 노드가 상태 검사에 실패할 때까지 약 10초 기다린 다음, curl로 로드 밸런서의 IPv4 주소를 쿼리해요.

curl 203.0.113.140

출력은 다음과 비슷해요.

CLIENT VALUES:
client_address=198.51.100.79
...

플랫폼 간 지원 (Cross-platform support)

일부 클라우드 제공자만 Type=LoadBalancer인 Service를 통한 소스 IP 보존을 지원해요. 실행 중인 클라우드 제공자는 loadbalancer에 대한 요청을 몇 가지 다른 방식으로 충족할 수 있어요.

  • 클라이언트 연결을 종료하고 노드/엔드포인트에 새 연결을 여는 프록시 사용. 이런 경우 소스 IP는 항상 클라우드 LB의 것이고, 클라이언트의 것이 아니에요.
  • 패킷 포워더 사용. 클라이언트가 loadbalancer VIP로 보낸 요청이 중간 프록시의 것이 아닌 클라이언트의 소스 IP로 노드에 도착하게 해요.

첫 번째 범주의 로드 밸런서는 HTTP Forwarded 또는 X-FORWARDED-FOR 헤더나 proxy 프로토콜 같은 실제 클라이언트 IP를 전달하기 위해 로드 밸런서와 백엔드 사이에 합의된 프로토콜을 사용해야 해요. 두 번째 범주의 로드 밸런서는 Service의 service.spec.healthCheckNodePort 필드에 저장된 포트를 가리키는 HTTP 상태 검사를 만들어 위에서 설명한 기능을 활용할 수 있어요.

정리하기 (Cleaning up)

Service를 삭제해요.

kubectl delete svc -l app=source-ip-app

Deployment, ReplicaSet, Pod를 삭제해요.

kubectl delete deployment source-ip-app

더 알아보기 (Learn more)