Kubernetes

[ Kans 3 Study - 4w ] 1. Service - ClusterIP

su''@ 2024. 9. 22. 18:53
CloudNetaStudy - Kubernets Networtk 3기 실습 스터디 게시글입니다.

 

[  서비스 타입  ]

  • Cluster Type : 서비스(Cluster Type) 연결 : K8S 클러스터 내부에서만 접속
    • 동일한 애플리케이션의 다수의 파드의 접속을 용이하게 하기 위한 서비스에 접속
    • 고정 접속(호출) 방법을 제공 : 흔히 말하는 ‘고정 VirtualIP’ 와 ‘Domain주소’ 생성

출처 : https://kim-dragon.tistory.com/52

 

  • NodePort Type : 서비스(NodePort Type) 연결 : 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속
    • 서비스(NodePort Type)의 일부 단점을 보완한 서비스(LoadBalancer Type) 도 있다!
      출처 : https://kim-dragon.tistory.com/52
  • LoadBalancer Type : Nodeport 타입의 확장판이라고 할 수 있으며 서비스를 외부에 노출 할 수 있다.
    • NodePort타입 앞단에 Loadbalancer가 붙어서 살아있는 노드를 체크하여 트래픽을 전달 할 수 있는 장점이 있다.
    • 클라우드 공급업체(AWS, GCP 등)에서 지원하는 기능

 

더보기
  • 약자 소개
    • S.IP : Source IP , 출발지(소스) IP
    • D.IP : Destination IP, 도착치(목적지) IP
    • S.Port : Source Port , 출발지(소스) 포트
    • D.Port : Destination Port , 도착지(목적지) 포트
    • NAT : Network Address Translation , 네트워크 주소 변환
    • SNAT : Source IP 를 NAT 처리, 일반적으로 출발지 IP를 변환
    • DNAT : Destination IP 를 NAT 처리, 일반적으로 목적지 IP와 목적지 포트를 변환

 


 

ClusterIP

 

[  ClusterIP 통신 흐름 ]

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

 

iptables 분산룰(정책)은 모든 노드에 자동으로 설정됨 / 10.96.0.1 은 CLUSTER-IP 주소

 

 

  • 클러스터 내부에서만 'CLUSTER-IP' 로 접근 가능 ⇒ 서비스에 DNS(도메인) 접속도 가능!
  • 서비스(ClusterIP 타입) 생성하게 되면, apiserver → (kubelet) → kube-proxy → iptables 에 rule(룰)이 생성됨.
  • 모든 노드(마스터 포함)에 iptables rule 이 설정되므로, 파드에서 접속 시 해당 노드에 존재하는 iptables rule 에 의해서 분산 접속이 됨.

 

[ 실습 구성 ]

  • 목적지(backend) 파드(pod) 생성 : 3pod.yaml
    cat <<EOT> 3pod.yaml
    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
    EOT
  • 클라이언트(TestPod) 생성 : netpod.yaml
    cat <<EOT> netpod.yaml
    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
    EOT
  • 서비스(ClusterIP) 생성
    svc-clusterip.yaml ← spec.ports.port 와 spec.ports.targetPort 가 어떤 의미인지 꼭 이해하자!
    cat <<EOT> svc-clusterip.yaml
    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       # 서비스 타입
    EOT
  • 생성 및 확인
    # 모니터링
    watch -d 'kubectl get pod -owide ;echo; kubectl get svc,ep svc-clusterip'
    
    # 생성
    kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml
    
    # 파드와 서비스 사용 네트워크 대역 정보 확인 
    kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
    
    # 확인
    kubectl get pod -owide
    kubectl get svc svc-clusterip
    
    # spec.ports.port 와 spec.ports.targetPort 가 어떤 의미인지 꼭 이해하자!
    kubectl describe svc svc-clusterip
    
    # 서비스 생성 시 엔드포인트를 자동으로 생성, 물론 수동으로 설정 생성도 가능
    kubectl get endpoints svc-clusterip
    kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip

 

[ 서비스(ClusterIP) 접속 확인 ] 

  • 클라이언트(TestPod) Shell 에서 접속 테스트 & 서비스(ClusterIP) 부하분산 접속 확인
    클라이언트 파드 (10.10.0.5)  →  service (10.200.1.139)  →  web pod 1 (10.10.1.2)
                                                                                              →  web pod 2 (10.10.2.3)
                                                                                              →  web pod 3 (10.10.3.2)
    # webpod 파드의 IP 를 출력
    kubectl get pod -l app=webpod -o jsonpath="{.items[*].status.podIP}"
    
    # webpod 파드의 IP를 변수에 지정
    WEBPOD1=$(kubectl get pod webpod1 -o jsonpath={.status.podIP})
    WEBPOD2=$(kubectl get pod webpod2 -o jsonpath={.status.podIP})
    WEBPOD3=$(kubectl get pod webpod3 -o jsonpath={.status.podIP})
    echo $WEBPOD1 $WEBPOD2 $WEBPOD3
    
    # net-pod 파드에서 webpod 파드의 IP로 직접 curl 로 반복 접속
    for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod; done
    for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | grep Hostname; done
    for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | grep Host; done
    for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | egrep 'Host|RemoteAddr'; done
    
    # 서비스 IP 변수 지정 : svc-clusterip 의 ClusterIP주소
    SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
    echo $SVC1
    
    # 위 서비스 생성 시 kube-proxy 에 의해서 iptables 규칙이 모든 노드에 추가됨 
    docker exec -it myk8s-control-plane iptables -t nat -S | grep $SVC1
    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
    -A KUBE-SERVICES -d 10.200.1.52/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
    
    ## (참고) ss 툴로 tcp listen 정보에는 없음 , 별도 /32 host 라우팅 추가 없음 -> 즉, iptables rule 에 의해서 처리됨을 확인
    docker exec -it myk8s-control-plane ss -tnlp
    docker exec -it myk8s-control-plane ip -c route
    
    # TCP 80,9000 포트별 접속 확인 : 출력 정보 의미 확인
    kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1
    kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000
    kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
    kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
    
    # 서비스(ClusterIP) 부하분산 접속 확인
    ## for 문을 이용하여 SVC1 IP 로 100번 접속을 시도 후 출력되는 내용 중 반복되는 내용의 갯수 출력
    ## 반복해서 실행을 해보면, SVC1 IP로 curl 접속 시 3개의 파드로 대략 33% 정도로 부하분산 접속됨을 확인
    kubectl exec -it net-pod -- zsh -c "for i in {1..10};   do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
    kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
    kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
    혹은
    kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"
    kubectl exec -it net-pod -- zsh -c "for i in {1..100};   do curl -s $SVC1:9000 | grep Hostname; sleep 0.1; done"
    kubectl exec -it net-pod -- zsh -c "for i in {1..10000}; do curl -s $SVC1:9000 | grep Hostname; sleep 0.01; done"
    
    
    # conntrack 확인
    docker exec -it myk8s-control-plane bash
    ----------------------------------------
    conntrack -h
    conntrack -E
    conntrack -C
    conntrack -S
    conntrack -L --src 10.10.0.6 # net-pod IP
    conntrack -L --dst $SVC1     # service ClusterIP
    exit
    ----------------------------------------
    
    # (참고) Link layer 에서 동작하는 ebtables
    ebtables -L

  •  각 워커노드에서 패킷 덤프 확인
    # 방안1 : 1대 혹은 3대 bash 진입 후 tcpdump 해둘 것
    docker exec -it myk8s-worker bash
    docker exec -it myk8s-worker2 bash
    docker exec -it myk8s-worker3 bash
    ----------------------------------
    # nic 정보 확인
    ip -c link
    ip -c route
    ip -c addr
    
    # tcpdump/ngrep : eth0 >> tcp 9000 포트 트래픽은 왜 없을까? iptables rule 동작 그림을 한번 더 확인하고 이해해보자
    ## ngrep 네트워크 패킷 분석기 활용해보기 : 특정 url 호출에 대해서만 필터 등 깔끔하게 볼 수 있음 - 링크
    tcpdump -i eth0 tcp port 80 -nnq
    tcpdump -i eth0 tcp port 80 -w /root/svc1-1.pcap
    tcpdump -i eth0 tcp port 9000 -nnq
    ngrep -tW byline -d eth0 '' 'tcp port 80'
    
    # tcpdump/ngrep : vethX
    VETH1=<각자 자신의 veth 이름>
    tcpdump -i $VETH1 tcp port 80 -nn
    tcpdump -i $VETH1 tcp port 80 -w /root/svc1-2.pcap
    tcpdump -i $VETH1 tcp port 9000 -nn
    ngrep -tW byline -d $VETH1 '' 'tcp port 80'
    
    exit
    ----------------------------------
    
    # 방안2 : 노드(?) 컨테이너 bash 직접 접속하지 않고 호스트에서 tcpdump 하기
    docker exec -it myk8s-worker tcpdump -i eth0 tcp port 80 -nnq
    VETH1=<각자 자신의 veth 이름> # docker exec -it myk8s-worker ip -c route
    docker exec -it myk8s-worker tcpdump -i $VETH1 tcp port 80 -nnq
    
    # 호스트PC에 pcap 파일 복사 >> wireshark 에서 분석
    docker cp myk8s-worker:/root/svc1-1.pcap .
    docker cp myk8s-worker:/root/svc1-2.pcap .
  • 클라이언트(TestPod) → 서비스(ClusterIP) 접속 시 : 3개의 목적지(backend) 파드로 랜덤 부하 분산 접속됨

 

 

[  IPTABLES 정책 확인 ] 

  • iptables 정책 적용 순서 : PREROUTING → KUBE-SERVICES → KUBE-SVC-### → KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>
  • 결론 : 내부에서 클러스터 IP로 접속 시, PREROUTE(nat) 에서 DNAT(3개 파드) 되고, POSTROUTE(nat) 에서 SNAT 되지 않고 나간다!
    # 컨트롤플레인에서 확인 : 너무 복잡해서 리턴 트래픽에 대해서는 상세히 분석 정리하지 않습니다.
    docker exec -it myk8s-control-plane bash
    ----------------------------------------
    
    # iptables 확인
    iptables -t filter -S
    iptables -t nat -S
    iptables -t nat -S | wc -l
    iptables -t mangle -S
    
    # iptables 상세 확인 - 매칭 패킷 카운트, 인터페이스 정보 등 포함
    iptables -nvL -t filter
    iptables -nvL -t nat
    iptables -nvL -t mangle
    
    # rule 갯수 확인
    iptables -nvL -t filter | wc -l
    iptables -nvL -t nat | wc -l
    
    # 규칙 패킷 바이트 카운트 초기화
    iptables -t filter --zero; iptables -t nat --zero; iptables -t mangle --zero
    
    # 정책 확인 : 아래 정책 내용은 핵심적인 룰(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
    ...
    
    exit
    ----------------------------------------
    
    # 위 서비스 생성 시 kube-proxy 에 의해서 iptables 규칙이 모든 노드에 추가됨을 한번 더 확이
    docker exec -it myk8s-control-plane iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF
    ...
    
    for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF; echo; done
    ...

 

[ 파드 1개 장애 발생 시 동작 확인 ]

  • 동작 확인을 위한 모니터링
    # 터미널1 >> ENDPOINTS 변화를 잘 확인해보자!
    watch -d 'kubectl get pod -owide;echo; kubectl get svc,ep svc-clusterip;echo; kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip'
    
    # 터미널2
    SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
    kubectl exec -it net-pod -- zsh -c "while true; do curl -s --connect-timeout 1 $SVC1:9000 | egrep 'Hostname|IP: 10'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
    혹은
    kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
  • 파드 1개 삭제 후 확인
    # (방안1) 파드3번 삭제 >> 서비스의 엔드포인트가 어떻게 변경되는지 확인 하자!, 지속적인 curl 접속 결과 확인!, for 문 실행 시 결과 확인!, 절체 시간(순단) 확인!
    kubectl delete pod webpod3
    
    # (방안1) 결과 확인 후 다시 파드 3번 생성 >> 서비스 디스커버리!
    kubectl apply -f 3pod.yaml
    
    ---------------------------------
    # (방안2) 파드3번에 레이블 삭제
    kubectl get pod --show-labels
    
    ## 레이블(라벨)의 키값 바로 뒤에 하이픈(-) 입력 시 해당 레이블 삭제됨! >> 레이블과 셀렉터는 쿠버네티스 환경에서 매우 많이 활용된다!
    kubectl label pod webpod3 app-
    kubectl get pod --show-labels
    
    # (방안2) 결과 확인 후 파드3번에 다시 레이블 생성
    kubectl label pod webpod3 app=webpod
쿠버네티스의 Endpoint Controller 는 지속적으로 엔드포인트를 Watch 하고 List 에 추가, 삭제를 합니다!

 

  • k8s 클러스터 외부(mypc)에서는 서비스(ClusterIP)로 접속이 불가능
    docker ps
    docker exec -it mypc ping -c 1 172.18.0.2
    docker exec -it mypc curl <SVC1_IP>:9000
    docker exec -it mypc curl <Pod IP>:80

 

[ sessionAffinity: ClientIP ]

  • sessionAffinity: ClientIP : 클라이언트가 접속한 목적지(파드)에 고정적인 접속을 지원 - k8s_Docs
  • 설정 및 파드 접속 확인
  • 클라이언트(TestPod) → 서비스(ClusterIP) 접속 시 : 1개의 목적지(backend) 파드로 고정 접속
  • iptables 정책 적용 확인 : 기존 룰에 고정 연결 관련 추가됨
  • 다음 실습을 위해 오브젝트 삭제 kubectl delete svc,pods --all

 

[ 서비스(ClusterIP) 부족한 점 ]

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