Service: ClusterIP, NodePort

Uk·2024년 9월 28일
0

쿠버네티스에서 동작하는 애플리케이션을 내/외부에서 유연하게 접속하기 위한 오브젝트
iptables, ipvs, nftables proxy mode 모두 Netfilter framework 사용

  • 실습 환경 - kind k8s v1.31.0, CNI(Kindnet, Direct Routing mode), IPTABLES proxy mode node - 172.18.0.0/16 pod - 10.10.0.0/16 ⇒ 10.10.1.0/24, 10.10.2.0/24, 10.10.3.0/24, 10.10.4.0/24 service - 10.200.1.0/24

docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity

클러스터 노출 발전 단계

  1. 파드 생성: k8s 클러스터 내부에서만 접속
  2. 서비스(Cluster Type) 연결 : k8s 클러스터 내부에서만 접속
    • 동일한 애플리케이션의 다수 파드의 접속을 용이하게 하기 위한 서비스에 접속
    • 고정 접속 방법을 제공 (고정 VirtualIP, Domain 주소 생성)
  3. 서비스(NodePort Type) 연결: 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속
    • NodePort의 일부 단점을 보완한 LoadBalancer 존재

서비스 종류

  • ClusterIP

  • NodePort

  • LoadBalancer

kube-proxy (optional)

서비스 통신 동작에 대한 설정 관리
데몬셋으로 배포되어, 모든 쿠버네티스 노드에 파드가 생성됨

  • IPTABLES
리눅스의 호스트 방화벽 / NAT 역할

IPTABLES에 정책 설정 시, 커널 영역에 내장된 Netfilter 를 통해 통제 수행

Cluster IP

다음과 같은 환경 구성

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i iptables -t nat -S | grep $SVC1; echo; done

⇒ ClusterIP의 9000번 포트로 넘어오면 어떤 룰에 통해서 jump 처리!

⇒ 목적지 IP와 포트를 iptables Rule에 의해서 처리

클라이언트(TestPod)가 'CLUSTER-IP' 접속 시 해당 노드의 iptables 룰(랜덤 분산)에 의해서 DNAT 처리가 되어 목적지(backend) 파드와 통신!

참고) tcp listen, routing 정보는 없음

kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000

Port 9000 / TargetPort 80

마치 L4 Service처럼 작동하며, NAT는 위와 같이 동작한다.

docker exec -it myk8s-worker bash
**tcpdump** -i eth0 tcp port 80 -nnq
tcpdump -i eth0 tcp port 9000 -nnq

eth0 >> tcp 9000 트래픽은 존재하지 않는다.

⇒ TestPod가 있는 Node에서 iptables에 의해 이미 NAT 처리가 완료 됐기 때문.

IPTABLES 정책 확인

  • TestPod → Cluster IP:9000 접속 마스터노드에서 iptables의 NAT 테이블에 규칙과 매칭돼 목적지 IP, Port 변환 목적지 IP는 app=webpod label을 가지고 있는 파드가 대상, 랜덤 부하분산 선택 NAT 수행시 연결 정보를 기록하여 Return 트래픽을 확인 후 TestPod로 전달

  1. PREROUTING

    모든 트래픽이 매칭, KUBE-SERVICES로 전달 Jump

  2. KUBE-SERVICES

    ClusterIP(Port)에 매칭되면 KUBE-SVC-YYY로 전달

  3. KUBE-SVC-YYY

    랜덤으로 KUBE-SEP-ZZZ로 부하분산 처리

  4. KUBE-SEP-ZZZ

    Target IP / Port로 DNAT 처리

결론 : 내부에서 클러스터 IP로 접속 시, PREROUTE(nat) 에서 DNAT(3개 파드) 되고, POSTROUTE(nat) 에서 SNAT 되지 않고 나간다!

docker exec -it myk8s-control-plane bash
----------------------------------------

# 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!
iptables -t nat -nvL

iptables -v --numeric --table nat --list PREROUTING | column -t
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  778 46758 KUBE-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

iptables -v --numeric --table nat --list KUBE-SERVICES | column
# 바로 아래 룰(rule)에 의해서 서비스(ClusterIP)를 인지하고 처리를 합니다
Chain KUBE-SERVICES (2 references)
 pkts bytes target                     prot opt in     out     source               destination
   92  5520 KUBE-SVC-KBDEBIL6IU6WL7RF  tcp  --  *      *       0.0.0.0/0            10.105.114.73        /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000

iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF | column
watch -d 'iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF'

SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"

# SVC-### 에서 랜덤 확률(대략 33%)로 SEP(Service EndPoint)인 각각 파드 IP로 DNAT 됩니다!
## 첫번째 룰에 일치 확률은 33% 이고, 매칭되지 않을 경우 아래 2개 남을때는 룰 일치 확률은 50%가 됩니다. 이것도 매칭되지 않으면 마지막 룰로 100% 일치됩니다
Chain KUBE-SVC-KBDEBIL6IU6WL7RF (1 references)
 pkts bytes target                     prot opt in     out     source               destination
   38  2280 KUBE-SEP-6TM74ZFOWZXXYQW6  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ statistic mode random probability 0.33333333349
   29  1740 KUBE-SEP-354QUAZJTL5AR6RR  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ statistic mode random probability 0.50000000000
   25  1500 KUBE-SEP-PY4VJNJPBUZ3ATEL  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */

iptables -v --numeric --table nat --list KUBE-SEP-<각자 값 입력>
Chain KUBE-SEP-6TM74ZFOWZXXYQW6 (1 references)
 pkts bytes target     prot opt in     out     source               destination
   38  2280 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:172.16.158.3:80

iptables -v --numeric --table nat --list KUBE-SEP-354QUAZJTL5AR6RR | column -t
Chain KUBE-SEP-6TM74ZFOWZXXYQW6 (1 references)
 pkts bytes target     prot opt in     out     source               destination
   29  1500 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:172.16.184.3:80

iptables -v --numeric --table nat --list KUBE-SEP-PY4VJNJPBUZ3ATEL | column -t
Chain KUBE-SEP-6TM74ZFOWZXXYQW6 (1 references)
 pkts bytes target     prot opt in     out     source               destination
   25  1740 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:172.16.34.3:80

iptables -t nat --zero
iptables -v --numeric --table nat --list POSTROUTING | column; echo ; iptables -v --numeric --table nat --list KUBE-POSTROUTING | column
watch -d 'iptables -v --numeric --table nat --list POSTROUTING; echo ; iptables -v --numeric --table nat --list KUBE-POSTROUTING'
# POSTROUTE(nat) : 0x4000(2진수로 0100 0000 0000 0000, 10진수 16384) 마킹 되어 있지 않으니 RETURN 되고 그냥 빠져나가서 SNAT 되지 않는다!
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
  572 35232 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match ! 0x4000/0x4000
    0     0 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000
    0     0 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ random-fully

iptables -t nat -S | grep KUBE-POSTROUTING
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
...

SessionAffinity: ClientIP

클라이언트의 요청을 매번 동일한 목적지 파드로 전달되기 위한 설정


0d10-4ff4-aa55-8afb44fce245/b8a8001a-b1c7-4407-b3c8-003d7f55f363/image.png)

kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"**ClientIP**"}}'

# kubectl get svc svc-clusterip -o yaml
...
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
...

접근시 Client IP 세션 정보 기록하여 timeoutSeconds 안에 똑같은 클라이언트 호출시 동일 파드로 전달

서비스(ClusterIP) 부족한 점

  • 클러스터 외부에서는 서비스(ClusterIP)로 접속이 불가능 ⇒ NodePort 타입으로 외부에서 접속 가능!
  • IPtables 는 파드에 대한 헬스체크 기능이 없어서 문제 있는 파드에 연결 가능 ⇒ 서비스 사용, 파드에 Readiness Probe 설정으로 파드 문제 시 서비스의 엔드포인트에서 제거되게 하자! ← 이 정도면 충분한가? 혹시 부족한 점이 없을까?
  • 서비스에 연동된 파드 갯수 퍼센트(%)로 랜덤 분산 방식, 세션어피니티 이외에 다른 분산 방식 불가능IPVS 경우 다양한 분산 방식(알고리즘) 가능
    • 목적지 파드 다수가 있는 환경에서, 출발지 파드와 목적지 파드가 동일한 노드에 배치되어 있어도, 랜덤 분산으로 다른 노드에 목적지 파드로 연결 가능

NodePort

외부에서 클러스터의 NodePort로 접근 가능, 이후에는 Cluster IP 통신과 동일

cat <<EOT> echo-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: kans-websrv
        image: mendhak/http-https-echo
        ports:
        - containerPort: 8080
EOT

cat <<EOT> svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-nodeport
spec:
  ports:
    - name: svc-webport
      port: 9000        # 서비스 ClusterIP 에 접속 시 사용하는 포트 port 를 의미
      targetPort: 8080  # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
  selector:
    app: deploy-websrv
  type: NodePort
EOT

kubectl apply -f echo-deploy.yaml,svc-nodeport.yaml

⇒ ClusterIP와 다르게 32624 외부 포트가 열린 것을 확인

⇒ 현재 k8s 버전에서는 포트 Listen 되지 않고, iptables rules 처리

docker exec -it mypc curl -s $CNODE:$NPORT | jq

headers.host 의 경우 SNAT 처리 된 모습

iptables 정책 확인

  • iptables 정책 적용 순서
    PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-#(MARK) → KUBE-SVC-# → KUBE-SEP-# ⇒ KUBE-POSTROUTING (MASQUERADE)

KUBE-EXT-#(MARK) 의 경우 외부 → Node1 → Node2가 되는 경우에서 Node1으로 SNAT을 해야하기 때문에 적용 ⇒ return을 위함

1.PREROUTING

2.KUBE-SERVICE

3.KUBE-NODEPORT

4.KUBE-EXT-XXX

⇒ 마킹 및 점프!

5. KUBE-SVC-XXX

⇒ 3개의 파드로 전달

6. KUBE-SEP-XXX

⇒ DNAT 처리

7. POSTROUTING

마킹돼 있어, 출발지 IP를 접속한 노드의 IP로 SNAT 처리, 최초 출발지 Port는 랜덤 Port
⇒ 다른 노드의 Pod로 넘어가기 전에 SNAT 처리 해주는 것

externalTrafficPolicy 설정

외부 클라이언트 IP를 수집

externalTrafficPolicy: Local : NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속됨, 이때 SNAT 되지 않아서 외부 클라이언트 IP가 보존됨!

NodePort 부족한 점

  • 외부에서 노드의 IP와 포트로 직접 접속이 필요함 → 내부망이 외부에 공개(라우팅 가능)되어 보안에 취약함 ⇒ LoadBalancer 서비스 타입으로 외부 공개 최소화 가능!
  • 클라이언트 IP 보존을 위해서, externalTrafficPolicy: local 사용 시 파드가 없는 노드 IP로 NodePort 접속 시 실패 ⇒ LoadBalancer 서비스에서 헬스체크(Probe) 로 대응 가능!

0개의 댓글