home..

서비스의 종류

mont-kim Docker Container Pod Container Kubernetes Service

기존과 동일하게 클러스터를 프로비저닝합니다.

제 개인 git에 virtualbox - vagrant 프로비저닝 파일을 업로드해두었습니다.

클러스터 컴포넌트의 외부노출 발전단계

  1. 파드 생성 : K8S 클러스터 내부에서만 접속

    images/KANS/week4/part1/Untitled.png

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

    images/KANS/week4/part1/Untitled1.png

  3. 서비스(NodePort Type) 연결 : 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속
    • 서비스(NodePort Type)의 일부 단점을 보완한 서비스(LoadBalancer Type) 도 있습니다!

    images/KANS/week4/part1/Untitled2.png

서비스 종류

ClusterIP 타입

images/KANS/week4/part1/Untitled3.png

NodePort 타입

images/KANS/week4/part1/Untitled4.png

LoadBalancer 타입 (기본 모드) : NLB 인스턴스 유형

images/KANS/week4/part1/Untitled5.png

Service (LoadBalancer Controller) : AWS Load Balancer Controller + NLB IP 모드 동작 with AWS VPC CNI

images/KANS/week4/part1/Untitled6.png

Cluster IP

통신 흐름

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

iptables 분산룰(정책)은 모든 노드에 자동으로 설정됨

iptables 분산룰(정책)은 모든 노드에 자동으로 설정됨

10.96.0.1 은 CLUSTER-IP 주소

10.96.0.1 은 CLUSTER-IP 주소

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

실습

테스트용 파드 세개를 생성합니다

cat <<EOT> 3pod.yaml
EOT

for i in 1 2 3; do
  cat <<EOT>> 3pod.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod$i
  labels:
    app: webpod
spec:
  nodeName: myk8s-worker$i
  containers:
  - name: container
    image: traefik/whoami
  terminationGracePeriodSeconds: 0
EOT
done

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

그리고 클라이언트용 test pod을 같이 생성합니다.

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

image.png


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

# 서비스 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-control-plane iptables -t nat -S | grep $SVC1; echo; done

서비스의 IP를 통해 생성되는 규칙입니다.

kube-proxy에 의해 IPTABLES에 모든 노드에서 규칙이 동일하게 생성되는것을 확인 할 수 있습니다.

image.png

Service (clusterip) 부하분산 확인

그래도 해당 서비스 주소로 호출하게될경우 비슷한 응답이 오는것을 확인 할 수 있습니다


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"

image.png

tcpdump -i eth0 tcp port 80 -nnq

image.png

IPtables 정책 확인

iptables 정책 적용 순서

PREROUTING → KUBE-SERVICES → KUBE-SVC-### → KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>

CleanShot 2024-09-22 at 14.24.46.png

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

iptables -t nat -nvL

iptables -v --numeric --table nat --list PREROUTING 

iptables -v --numeric --table nat --list KUBE-SERVICES
# 바로 아래 (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
   svc-clusterip:svc-webport cluster IP */ tcp dpt:9000

iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF

순서대로 조회를 할경우 해당 service에 할당된 serviceendpoint가 조회됩니다.

image.png

SVC-### 에서 랜덤 확률(대략 33%)로 SEP(Service EndPoint)인 각각 파드 IP로 DNAT 됩니다!

첫번째 룰에 일치 확률은 33% 이고, 매칭되지 않을 경우 아래 2개 남을때는 룰 일치 확률은 50%가 됩니다.

이것도 매칭되지 않으면 마지막 룰로 100% 일치됩니다

어떤 기준으로 랜덤 분산을 정의하는가

Kube Proxy의 IPTABLES는 어떤기준으로 % 랜덤 분산을 설정할까?

proxier.go

해당 함수에서 kubeproxy가 iptables 모드로 동작할때

  • 서비스가 사용하는 특정 포트에 트래픽이 들어올 때, 이를 적절한 엔드포인트(즉, 파드)로 전달하는 iptables 규칙을 생성합니다.
  • 부하 분산을 위해 각 엔드포인트(파드)에 대해 확률적인 분배를 사용합니다.
  • probability 함수는 특정 엔드포인트에 트래픽이 분산될 확률을 계산하는데, 이는 엔드포인트의 개수에 따라 달라집니다.
  • statistic 모듈은 확률적으로 각 엔드포인트로 트래픽을 전달하며, 마지막 엔드포인트는 무조건 남은 트래픽을 수신하도록 설정됩니다.

추가적으로 SessionAffinity 설정에 따라 클라이언트 IP 기반 세션 어피니티를 사용할 경우, recent 모듈을 통해 클라이언트의 연결을 추적하여 동일한 클라이언트가 항상 동일한 파드로 연결되도록 설정할 수 있습니다

SessionAffinity: ClientIP

sessionAffinity: ClientIP : 클라이언트가 접속한 목적지(파드)에 고정적인 접속을 지원 - k8s_Docs

CleanShot 2024-09-22 at 15.21.08.png

images/KANS/week4/part1/Untitled9.png

  • 설정 및 파드 접속 확인

      # 기본 정보 확인
      kubectl get svc svc-clusterip -o yaml
      kubectl get svc svc-clusterip -o yaml | grep sessionAffinity
        
      # 반복 접속
      kubectl exec -it net-pod -- zsh -c "while true; do curl -s --connect-timeout 1 $SVC1:9000 | egrep 'Hostname|IP: 10|Remote'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
        
      # sessionAffinity: ClientIP 설정 변경
      kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"ClientIP"}}'
      혹은
      kubectl get svc svc-clusterip -o yaml | sed -e "s/sessionAffinity: None/sessionAffinity: ClientIP/" | kubectl apply -f -
        
      #
      kubectl get svc svc-clusterip -o yaml
      ...
        sessionAffinity: ClientIP
        sessionAffinityConfig:
          clientIP:
            timeoutSeconds: 10800
      ...
        
      # 클라이언트(TestPod) Shell 실행
      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"
    
  • 클라이언트(TestPod) → 서비스(ClusterIP) 접속 시 : 1개의 목적지(backend) 파드로 고정 접속

    _2021-07-18__4.01.12.png

iptables 정책 적용 확인 : 기존 룰에 고정 연결 관련 추가됨

docker exec -it myk8s-control-plane bash
----------------------------------------
iptables -t nat -Siptables -t nat -S | grep recent

image.png

아래 10800초(3시간)동안 클라이언트에서 접속된 DNAT(파드)를 연결 유지 관련 설정이 추가 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds로 최대 세션 고정 시간 설정 변경 가능 iptables ‘recent’ 모듈(동적으로 IP 주소 목록을 생성하고 확인) ,

‘rcheck’ (현재 ip list 에 해당 ip가 있는지 체크) ,

‘reap’ (–seconds 와 함께 사용, 오래된 엔트리 삭제)

[https://en.wikipedia.org/wiki/Netfilter#/media/File:Netfilter-components.svg](https://en.wikipedia.org/wiki/Netfilter#/media/File:Netfilter-components.svg)

https://en.wikipedia.org/wiki/Netfilter#/media/File:Netfilter-components.svg

docker exec -it myk8s-control-plane bash
----------------------------------------
# 도움말
conntrack -h

# List conntrack or expectation table
conntrack -L
conntrack -L --any-nat # List conntrack - source or destination NAT ip
conntrack -L --src-nat # List conntrack - source NAT ip
conntrack -L --dst-nat # List conntrack - destination NAT ip

# 입력 예시 >> 위 그림에 (1), (3) 정보가 출력된다
conntrack -L --dst-nat <DNAT 되어서 접속된 목적지 파드의 IP>
conntrack -L --dst-nat 10.10.X.Y
  • 다음 실습을 위해 오브젝트 삭제 kubectl delete svc,pods --all

Service 의 부족한 점

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

NodePort

통신 흐름

요약 : 외부 클라이언트가 ‘노드IP:NodePort’ 접속 시 해당 노드의 iptables 룰에 의해서 SNAT/DNAT 되어 목적지 파드와 통신 후 리턴 트래픽은 최초 인입 노드를 경유해서 외부로 되돌아감

CleanShot 2024-09-22 at 15.42.49.png

NodePort(노드포트)는 모든 노드(마스터 포함)에 Listen 됨!

NodePort(노드포트)는 모든 노드(마스터 포함)에 Listen 됨!

외부 클라이언트의 출발지IP도 SNAT 되어서 목적지 파드에 도착함!, 물론 DNAT 동작 포함!

외부 클라이언트의 출발지IP도 SNAT 되어서 목적지 파드에 도착함!, 물론 DNAT 동작 포함!

  • 외부에서 클러스터의 ‘서비스(NodePort)’ 로 접근 가능 → 이후에는 Cluster IP 통신과 동일!
  • 모드 노드(마스터 포함)에 iptables rule 이 설정되므로, 모든 노드에 NodePort 로 접속 시 iptables rule 에 의해서 분산 접속이 됨
  • Node 의 모든 Loca IP(Local host Interface IP : loopback 포함) 사용 가능 & Local IP를 지정 가능
  • 쿠버네티스 NodePort 할당 범위 기본(30000-32767) & 변경하기 - 링크

실습

목적지 파드 생성

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

images/KANS/week4/part1/Untitled

images/KANS/week4/part1/Untitled

가상머신에서 마스터노드에 nodeport로 접속을 시도합니다.


CNODE=172.18.0.3
NODE1=172.18.0.5
NODE2=172.18.0.2
NODE3=172.18.0.4*
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT

# 서비스(NodePort) 부하분산 접속 확인
docker exec -it mypc curl -s $CNODE:$NPORT | jq # headers.host 주소는 왜 그런거죠?
for i in $CNODE $NODE1 $NODE2 $NODE3 ; do echo ">> node $i <<"; docker exec -it mypc curl -s $i:$NPORT; echo; done

# 컨트롤플레인 노드에는 목적지 파드가 없는데도, 접속을 받아준다! 이유는?
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE2:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE3:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"

CIP=$(kubectl get service svc-nodeport -o jsonpath="{.spec.clusterIP}")
CIPPORT=$(kubectl get service svc-nodeport -o jsonpath="{.spec.ports[0].port}")
echo $CIP $CIPPORT
docker exec -it myk8s-control-plane curl -s $CIP:$CIPPORT | jq

# mypc에서 CLUSTER-IP:PORT 로 접속 가능할까?
docker exec -it mypc curl -s $CIP:$CIPPORT

image.png

image.png

  • 웹 파드에서 접속자의 IP 정보 확인(logs) 시 외부 클라이언트IP 가 아닌, 노드의 IPSNAT 되어서 확인됨

image.png

iptables 정책 적용 순서 : PREROUTING → KUBEs-SERVICES → KUBE-NODEPORTS(MARK) → KUBE-SVC-# → KUBE-SEP-# ⇒ KUBE-POSTROUTING (MASQUERADE) ← 규칙 과정 일부 업데이트됨

CleanShot 2024-09-22 at 16.30.38.png

CleanShot 2024-09-22 at 16.30.58.png

CleanShot 2024-09-22 at 16.31.19.png

CleanShot 2024-09-22 at 16.31.26.png

images/KANS/week4/part1/Untitled

ExternalTrafficPolicy

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

Node1:NodePort 접속시 Node1에 생성된 파드(Pod1)로만 접속됨

Node1:NodePort 접속시 Node1에 생성된 파드(Pod1)로만 접속됨

CleanShot 2024-09-22 at 17.09.17.png

Node3에 파드가 없을 경우에 접속 시 연결 실패됨!

Node3에 파드가 없을 경우에 접속 시 연결 실패됨!

  • 외부 클라이언트의 IP 주소(아래 출발지IP: 50.1.1.1)가 노드의 IP로 SNAT 되지 않고 서비스(backend) 파드까지 전달됨!

    images/KANS/week4/part1/Untitled18.png

    CleanShot 2024-09-22 at 17.09.40.png

직접 확인해보기

Controlplane 노드의 iptables 분석


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

# 패킷 카운트 초기화
iptables -t nat --zero

PREROUTING 정보 확인

iptables -t nat --zero
iptables -t nat -S | grep PREROUTING

# 외부 클라이언트가 노드IP:NodePort 로 접속하기 때문에 --dst-type LOCAL 에 매칭되어서 -j KUBE-NODEPORTS 로 점프!
iptables -t nat -S | grep KUBE-SERVICES | grep nodeport

# KUBE-NODEPORTS 에서 KUBE-EXT-# 로 점프!
## -m nfacct --nfacct-name localhost_nps_accepted_pkts 추가됨 : 패킷 flow 카운팅 - 카운트 이름 지정 
iptables -t nat -S | grep KUBE-NODEPORTS | grep 30

## nfacct 확인
## nfacct flush # 초기화
nfacct list

## KUBE-EXT-# 에서 'KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000' 마킹 및 KUBE-SVC-# 로 점프!
# docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $CNODE:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done" 반복 접속 후 아래 확인
watch -d 'iptables -v --numeric --table nat --list KUBE-EXT-VTR7MTHHNMFZ3OFS'
iptables -t nat -S | grep "A KUBE-SVC-VTR7MTHHNMFZ3OFS"

# KUBE-SVC# 이후 과정은 Cluster-IP 와 동일! : 3개의 파드로 DNAT 되어서 전달
iptables -t nat -S | grep "A KUBE-SVC-VTR7MTHHNMFZ3OFS -"
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-Q5ZOWRTVDPKGFLOL
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-MMWCMKTGOFHFMRIZ
-A KUBE-SVC-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport" -j KUBE-SEP-CQTAHW4MAKGGR6M2

# 외부가 아니라 SVC 에 Endpoint 에서 접근 시에는 아래 포함된 Rule 에서 MARK 되어서 Hairpin NAT 처리됨
iptables -t nat -S | grep KUBE-SEP-Q5ZOWRTVDPKGFLOL
iptables -t nat -S | grep KUBE-SEP-MMWCMKTGOFHFMRIZ
iptables -t nat -S | grep KUBE-SEP-CQTAHW4MAKGGR6M2

POSTROUTING 정보 확인
# 마킹되어 있어서 출발지IP를 접속한 노드의 IP 로 SNAT(MASQUERADE) 처리함! , 최초 출발지Port는 랜덤Port 로 변경
iptables -t nat -S | grep "A KUBE-POSTROUTING"
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
...

# docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $CNODE:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done" 반복 접속 후 아래 확인
watch -d 'iptables -v --numeric --table nat --list KUBE-POSTROUTING;echo;iptables -v --numeric --table nat --list POSTROUTING'

image.png

image.png

NodePort의 부족한 점

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

도대체 왜 이렇게 복잡하게 동작하는지 궁금하시리라 생각합니다. 이해를 해보자면, 개발자가 직접 관리할 수 있는 서버만 가지고도 부하분산을 편리하게 사용하다 보니 이렇게 복잡하고 비효율적인 구조로 >동작할수 밖에 없었다고 생각합니다!

© 2024 mont kim   •  Powered by Soopr   •  Theme  Moonwalk