티스토리 뷰
<출처> https://kubernetes.io/docs/tutorials/services/source-ip/
K8S 클러스터에서 동작중인 애플리케이션은 서비스 추상화를 통해 서로 서로, 그리고 외부 세상를 인지하고 커뮤니테이션 한다, 이 문서는 상이한 형태의 서비스에 전송된 패킷의 출발지 IP에 무슨 일들이 일어나고 있는지 설명하고, 어떻게 필요에 따라 이 동작방식을 토글(toggle) 시킬 수 있는지에 대해 설명한다.
Objectives
- 다양한 서비스 방식을 통해 간단한 애플리케이션을 노출한다.
- 각 서비스 방식이 어떻게 출발지 IP NAT를 다루는지 이해한다.
- 출발지 IP 보존에 관한 트레이드오프(tradeoff)를 이해한다.
Before you begin
K8S 클러스터가 필요하며, 클러스터와 커뮤니케이션할 수 있도록 구성되어진 kubctl 커맨드-라인 툴도 필요하다. 아직 클러스터를 보유하지 않고 있다면, Minikube 를 이용하여 생성할 수 있으며, 또는 다음 두 개의 (웹 기반 대화형 가상 터미널 환경의) K8S 실습 도구를 이용할 수 있다.
버전 확인을 위해 kubectl version 을 수행한다.
Terminology
이 문서에서는 다음의 용어를 이용한다.
- NAT: 네트워크 주소 변환
- 출발지 NAT: 패킷상의 출발지 IP를 노드의 IP로 대체 시키는 것
- 목적지 NAT: 패킷상의 목적지 IP를 파드의 IP로 대체 시키는 것
- VIP: 모든 K8S 서비스에 부여된 IP와 같은 가상 IP
- Kube-proxy: 모든 노드상에 서비스 VIP 관리를 오케스트레이션 해주는 네트워크 데몬
Prerequisites
이 문서의 예제를 동작시켜 보기 위해 동작하는 K8S 1.5 클러스터를 보유해야 한다. 이 예제는 HTTP 헤더를 통해 수신된 서비스 요청에 대한 출발지 IP 를 되돌려 에코해주는 경량의 Nginx 웹서버를 이용한다. 다음과 같이 생성할 수 있다.
1 2 | [root@docker-registry zerobig]# kubectl run source-ip-app --image=k8s.gcr.io/echoserver:1.4 deployment.apps/source-ip-app created | cs |
Source IP for Services with Type=ClusterIP
만약 iptables mode에 kube-proxy가 동작상태에 있다면, 클러스터 내에서 클러스터 IP로 전송된 패킷은 절대 출발지 NAT 처리되지 않는다. 이는 K8S 1.2 이후로 기본값이 되었다. Kube-proxy는 "proxyMode" 엔드포인트를 통해 모드를 노출시켜준다.
1 2 3 4 5 | [root@docker-registry zerobig]# kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-node1.zerobig.com Ready <none> 49d v1.10.5-rancher1 sptek-devops Ready <none> 49d v1.10.5-rancher1 zerodesk-k8s.zerobig.com Ready <none> 49d v1.10.5-rancher1 | cs |
출발지 IP 앱에 서비스를 생성하여 출발지 IP 보존에 대한 테스트를 수행할 수 있다.
1 2 3 4 5 | [root@docker-registry zerobig]# kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080 service/clusterip exposed [root@docker-registry ~]# kubectl get svc clusterip NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE clusterip ClusterIP 10.43.96.184 <none> 80/TCP 11s | cs |
동일 클러스터 내 파드에서 "ClusterIP" 에 대한 힌트를 얻을 수 있다.
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 | [root@docker-registry zerobig]# kubectl run busybox -it --image=busybox --restart=Never --rm If you don't see a command prompt, try pressing enter. / # ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 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@if1280: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1402 qdisc noqueue link/ether 02:cc:f3:d8:7e:63 brd ff:ff:ff:ff:ff:ff inet 10.42.80.4/16 brd 10.42.255.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::a0cf:a9ff:fe6d:ff2/64 scope link valid_lft forever preferred_lft forever / # wget -qO - 10.43.96.184 CLIENT VALUES: client_address=10.42.80.4 command=GET real path=/ query=nil request_version=1.1 request_uri=http://10.43.96.184:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: connection=close host=10.43.96.184 user-agent=Wget BODY: -no body in request- | cs |
클라이언트 파드와 서버 파드가 동일 노드 상에 위치한다면, client_address는 클라이언트 파드의 IP주소가 된다. 그러나, 클라이언트 파드와 서버 파드가 다른 노드 상에 위치한다면, client_address는 클라이언트 파드의 노드 flannel IP 주소가 된다.
Source IP for Services with Type=NodePort
K8S 1.5부터 Type=NodePort로 (노출된) 서비스에 전송된 패킷은 기본값으로 출발지 NAT가 처리된다. "NodePort" 서비스를 생성하여 이를 테스트해 볼 수 있다.
1 2 3 4 | [root@docker-registry zerobig]# kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort service/nodeport exposed [root@docker-registry zerobig]# NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport) [root@docker-registry zerobig]# NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="ExternalIP")].address }') | cs |
클라우드 제공사업자 환경에서 동작시키고 있다면, 위에서 언급한 "nodes:nodeport"에 대한 방화벽 룰을 열어야 할 수도 있다.
이제 위에서 할당된 노드 포트를 통해 클러스터 외부로부터 서비스에 접근할 수 있는지 테스트 해볼 수 있다.
1 2 3 4 | [root@docker-registry zerobig]# for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done client_address=10.42.46.142 client_address=10.42.31.168 client_address=10.42.0.1 | cs |
실제 GKE 환경에서는 포트에 대한 방화벽 룰이 허용되지 않았으므로, 당연하게 다음과 같이 아무런 결과를 리턴하지 못한다.)
1 | [root@docker-registry k8s]# for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done | cs |
올바른 클라이언트 IP가 아닌 점을 주목하자. 이것들은 클러스터 내부의 IP들이다. 이렇게 된 것이다.
- 클라이언트가 "node2:nodePort"에 패킷을 전송한다.
- "node2"는 자신의 IP주소를 가지고 패킷 내 출발지 IP 주소(SNAT)를 대체한다.
- "node2"는 파드 IP를 가지고 패킷 상에 목적지 IP를 대체한다.
- 패킷이 node1로 라우팅 되고 나서 엔드포인트로 라우팅된다.
- 파드의 응답은 node2로 되돌려 라우팅된다.
- 파드의 응답은 클라이언트로 되돌려 전송된다.
도식화하면,
client \ ^ \ \ v \ node 1 <--- node 2 | ^ SNAT | | ---> v | endpoint
이러한 결과를 회피하기 위해, K8S는 클라이언트 출발지(가용한 특성에 대한 정보는 여기에서 확인)를 보존하는 특성을 갖는다. "service.spec.externalTrafficPolicy "의 값을 "Local"로 설정하면, 오직 로컬 엔드포인트로 요청을 프록시하며, 절대 다른 노드에 트래픽을 전달하지 않게 된다. 이로 인해 원래 출발지 IP 주소를 보존하게 될 것이다. 로컬 엔드포인트가 존재하지 않는다면, 노드로 전송된 패킷은 사라지게 되며, 따라서 하나의 패킷이 엔드포인트로 통하도록 적용할 경우 임의의 패킷 처리 규칙 내 올바른 source-ip에 대한 신뢰를 할 수 있게 된다.
"service.spec.externalTrafficPolicy" 필드를 다음과 같이 설정한다.
1 2 | [root@docker-registry zerobig]# kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}' service/nodeport patched | cs |
이제, 다시 테스트를 수행해본다.
1 2 | [root@docker-registry zerobig]# for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done client_address=124.136.X.X | cs |
엔드포인트 파드가 동작하고 있는 하나의 노드로부터, 올바른 클라이언트 IP 정보값을 가지고, 하나만의 응답결과를 얻게 됨을 주목한다.
이렇게 된 것이다.
- 클라이언트가 "node2:nodePort"로 패킷을 전송한다. 여기에는 어떠한 엔드포인트도 없다.
- 패킷이 사라진다.
- 클라이언트가 "node1:nodePort"로 패킷을 전송한다. 여기에는 엔드포인트를 가지고 있다.
- node1은 올바른 출발지 IP를 가지고 엔드포인트로 패킷을 라우팅 한다.
도식화 하면:
client ^ / \ / / \ / v X node 1 node 2 ^ | | | | v endpoint
Source IP for Services with Type=LoadBalancer
K8S 1.5부터, Type=LoadBalancer를 가진 서비스로 전송된 패킷은 기본값으로 출발지 NAT가 이루어 진다. "Ready" 상태에 있는 모든 스케줄 처리 가능한 K8S 노드는 로드밸런스 된 트래픽을 받을 자격을 갖는다. 그래서 엔드포인트가 없는 노드에 패킷이 유입된다면, (이전 섹션에서 설명한 바와 같이) 시스템은 노드의 IP 정보를 가지고 패킷 상의 출발지 IP를 대체하여, 엔드포인트를 갖는 노드로 패킷을 프록시한다.
로드밸런서를 통해 source-ip-app 을 노출시켜 테스트를 해볼 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | [root@docker-registry zerobig]# kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer service/loadbalancer exposed [root@docker-registry ~]# kubectl get svc loadbalancer NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE loadbalancer LoadBalancer 10.43.219.123 124.136.X.X 80:30822/TCP 14s [root@docker-registry ~]# curl 124.136.X.X CLIENT VALUES: client_address=10.42.46.142 command=GET real path=/ query=nil request_version=1.1 request_uri=http://124.136.X.X:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* host=124.136.X.X user-agent=curl/7.29.0 BODY: -no body in request- | cs |
그러나, 구글 쿠버네티스 엔진/GCE에서 동작 시키는 중이라면, 똑같은 "service.spec.externalTrafficPolicy" 필드를 "Local"로 설정하면, 노드에서 서비스 엔드포인트가 없어지도록 하여 의도적으로 헬스체크가 실패나도록 함으로써 로드밸런스된 트래픽 처리에 대한 자격을 갖춘 노드의 리스트에서 제거시킨다.
도식화하면:
client | lb VIP / ^ v / health check ---> node 1 node 2 <--- health check 200 <--- ^ | ---> 500 | V endpoint
주석을 설정하여 테스트 할 수 있다.
1 2 | [root@docker-registry zerobig]# kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}' service/loadbalancer patched | cs |
K8S에 의해 할당된 "service.spec.healthCheckNodePort" 필드를 바로 볼 수 있을 것이다.
1 2 | [root@docker-registry zerobig]# kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort healthCheckNodePort: 31181 | cs |
"service.spec.healthCheckNodePort" 필드는 "/healthz"에서 헬스체크를 제공해주는 모든 노드 상에 하나의 포트를 지정한다. 이를 테스트 할 수 있다.
1 2 3 | [root@docker-registry zerobig]# kubectl get pod -o wide -l run=source-ip-app NAME READY STATUS RESTARTS AGE IP NODE source-ip-app-8687dbf9f-zdxtx 1/1 Running 0 5h 10.42.150.39 zerodesk-k8s.zerobig.com | cs |
(서비스 엔드포인트가 없는 노드에서는 연결이 거부된다.)
1 2 | [root@docker-registry zerobig]# curl localhost:32122/healthz curl: (7) Failed connect to localhost:32122; 연결이 거부됨 | cs |
(다음 노드에서는 서비스 엔드포인트를 찾았기에 정상 출력한다.)
1 2 3 4 5 6 7 | root@sptek-devops:~# curl localhost:31181/healthz { "service": { "namespace": "default", "name": "loadbalancer" }, "localEndpoints": 0 | cs |
마스터에서 동작 중인 서비스 컨트롤러는 클라우드 로드밸런서 할당에 대한 책임을 갖는다. 그리고 그리 할 경우, 모든 노드에 이 포트/경로를 지정하여 HTTP 헬스체크 또한 할당한다. 헬스체크가 실패 나도록 엔드포인트 없는 2 노드에 대해 약 10초를 기다린 다음, lb 아이피에 curl을 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [root@docker-registry zerobig]# curl 124.136.X.X CLIENT VALUES: client_address=124.136.X.X command=GET real path=/ query=nil request_version=1.1 request_uri=http://124.136.X.X:8080/ SERVER VALUES: server_version=nginx: 1.10.0 - lua: 10001 HEADERS RECEIVED: accept=*/* host=124.136.X.X user-agent=curl/7.29.0 BODY: -no body in request- | cs |
Cross platform support
K8S 1.5부터 , Type=LoadBalancer로 서비스를 통한 출발지 IP 보존을 지원하는 것은 오직 클라우드 제공사업자(GCP와 Azure)의 서브셋에서 실행된다. 동작 시키고 있는 클라우드 제공사업자는 몇 가지 상이한 방식으로 로드밸런스에 대한 요청을 수행하게 된다.
클라이언트 커넥션을 종료시키고 노드/엔드포인트에 새로운 커넥션을 오픈해주는 프록시를 이용하는 경우, 항상 출발지 IP는 클라이언트가 아닌, 클라우드 LB의 IP가 될 것이다.
패킷 전달자를 이용하는 경우, 클라이언트에서 로드밸런서 VIP로의 요청은 중간 프록시가 아닌, 클라이언트의 출발지 IP를 가지고 노드에서 마치게 된다.
첫 번째 카테고리에서 로드밸런서는 HTTP X-FORWARDED-FOR 헤더, 또는 proxy protocol과 같은 전정한 클라이어트와 커뮤니케이션 하기위해 로드밸런서와 백엔드 간에 동의된 프로토콜을 이용해야만 한다. 두번 째 카테고리에서 로드밸런서는 서비스에서 "service.spec.healthCheckNodePort" 필드 내 저장된 포트를 가리키도록 간단하게 HTTP 헬스체크를 생성함으로써 위에서 설명했던 기능을 강화할 수 있다.
Cleaning up
서비스를 삭제한다.
1 2 3 4 | [root@docker-registry zerobig]# kubectl delete svc -l run=source-ip-app service "clusterip" deleted service "loadbalancer" deleted service "nodeport" deleted | cs |
디플로이먼트, 리플리카 셋 그리고 파드를 삭제한다.
1 2 | [root@docker-registry zerobig]# kubectl delete deployment source-ip-app deployment.extensions "source-ip-app" deleted | cs |
What's next
Learn more about connecting applications via services
Learn more about loadbalancing
'Kubernetes' 카테고리의 다른 글
25 Kubernetes 201 (2) | 2018.09.27 |
---|---|
24 Kubernetes 101 (0) | 2018.09.27 |
22 Clusters - AppAmor (0) | 2018.09.27 |
21 CI/CD Pipeline - Set Up CI/CD for a Distributed Crossword Puzzle App on Kubernetes (Part 4) (0) | 2018.09.11 |
20 CI/CD Pipeline - Run and Scale a Distributed Crossword Puzzle App with CI/CD on Kubernetes (Part 3) (0) | 2018.09.11 |