본문 바로가기

study/KANS 3기

KANS 3기 4주차

K8S Service에 대해서 알아보겠습니다.

1. ClusterIP

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

 

1.클러스터 내부에서만 'CLUSTER-IP' 로 접근 가능 ⇒ 서비스에 DNS(도메인) 접속도 가능!

2.서비스(ClusterIP 타입) 생성하게 되면, apiserver → (kubelet) → kube-proxy → iptables 에 rule(룰)이 생성

3.모드 노드(마스터 포함)에 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

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) 부하분산 접속 확인

# 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-control-plane 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대 혹은 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
----------------------------------
혹은
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 .

 

1번 worker노드에서 패킷 덤프 떠보기

 

ngrep 사용해보기

 

 

 

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 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
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
# 바로 아래 룰(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
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
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
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
watch -d 'iptables -v --numeric --table nat --list cali-POSTROUTING; echo ; iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; iptables -v --numeric --table nat --list cali-nat-outgoing'
# POSTROUTE(nat) : 0x4000 마킹 되어 있지 않으니 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
----------------------------------------

파드 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) 파드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-

# (방안2) 결과 확인 후 파드3번에 다시 레이블 생성
kubectl label pod webpod3 app=webpod

 

3번으로 통신이 안된다 -> proxy에 의해 룰이 업데이트(3번 파드 삭제) 되기 때문이다.

 

sessionAffinity: ClientIP

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

 

 

설정 및 파드 정보 확인

# 기본 정보 확인
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) 파드로 고정 접속

 

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

docker exec -it myk8s-control-plane bash
----------------------------------------
iptables -t nat -S
iptables -t nat -S | grep recent
# 아래 10800초(=180분=3시간) 클라이언트에서 접속된 DNAT(파드)를 연결 유지 관련 설정이 추가됨!
## service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 에 최대 세션 고정 시간 설정 변경 가능함
# iptables 'recent' 모듈(동적으로 IP 주소 목록을 생성하고 확인) , 'rcheck' (현재 ip list 에 해당 ip가 있는지 체크) , 'reap' (--seconds 와 함께 사용, 오래된 엔트리 삭제)
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-5VZC7FSJKRHQ2ZMK --mask 255.255.255.255 --rsource -j KUBE-SEP-5VZC7FSJKRHQ2ZMK
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-KDYAAWEZ3PX6YOF7 --mask 255.255.255.255 --rsource -j KUBE-SEP-KDYAAWEZ3PX6YOF7
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-IW6VVZBGJPZ433RS --mask 255.255.255.255 --rsource -j KUBE-SEP-IW6VVZBGJPZ433RS

-A KUBE-SEP-5VZC7FSJKRHQ2ZMK -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m recent --set --name KUBE-SEP-5VZC7FSJKRHQ2ZMK --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 172.16.197.7:80
-A KUBE-SEP-IW6VVZBGJPZ433RS -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m recent --set --name KUBE-SEP-IW6VVZBGJPZ433RS --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 172.16.46.4:80
-A KUBE-SEP-KDYAAWEZ3PX6YOF7 -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m recent --set --name KUBE-SEP-KDYAAWEZ3PX6YOF7 --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 172.16.228.69:80
----------------------------------------

 

 

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

 

 

서비스(ClusterIP) 부족한 점

- 클러스터 외부에서는 서비스(ClusterIP)로 접속이 불가능NodePort 타입으로 외부에서 접속 가능!

- IPtables 는 파드에 대한 헬스체크 기능이 없어서 문제 있는 파드에 연결 가능 ⇒ 서비스 사용, 파드에 Readiness Probe 설정으로 파드 문제 시 서비스의 엔드포인트에서 제거되게 하자! ← 이 정도면 충분한가? 혹시 부족한 점이 없을까?

- 서비스에 연동된 파드 갯수 퍼센트(%)로 랜덤 분산 방식, 세션어피니티 이외에 다른 분산 방식 불가능IPVS 경우 다양한 분산 방식(알고리즘) 가능

 

 

 

2. NodePort

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

 

실습

목적지(backend) 디플로이먼트(Pod) 파일 생성 : echo-deploy.yaml

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

 

 

서비스(NodePort) 파일 생성 : svc-nodeport.yaml

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

# 모니터링
watch -d 'kubectl get pod -owide;echo; kubectl get svc,ep svc-nodeport'

# 확인
kubectl get deploy,pod -o wide

# 아래 30158은 서비스(NodePort) 정보!
kubectl get svc svc-nodeport
NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
svc-nodeport   NodePort   10.111.1.238   <none>        9000:30158/TCP   3m3s

kubectl get endpoints svc-nodeport
NAME           ENDPOINTS                                              AGE
svc-nodeport   172.16.158.5:8080,172.16.184.3:8080,172.16.34.4:8080   4m18s

# Port , TargetPort , NodePort 각각의 차이점의 의미를 알자!
kubectl describe svc svc-nodeport

 

확인

 

 

 

 

서비스(NodePort) 접속 확인

외부 클라이언트(mypc 컨테이너)에서 접속 테스트 & 서비스(NodePort) 부하분산 접속 확인

# NodePort 확인
kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}'
30353

# NodePort 를 변수에 지정
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')

# 현재는 포트 Listen 되지 않고, iptables rules 처리됨
# 노드 포트 Listen 확인 : ss 옵션 -4(ipv4) -t(TCP) -l(Listen) -n(숫자로 출력) -p(프로세스)
# 모든 노드(마스터, 워커들)에서 노드 포트가 Listen(열림)됨!
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ss -tlnp; echo; done
root@k8s-m:~# ss -4tlnp | egrep "(Process|$NPORT)"
State     Recv-Q    Send-Q        Local Address:Port        Peer Address:Port   Process
LISTEN    0         4096                0.0.0.0:30466            0.0.0.0:*       users:(("kube-proxy",pid=8661,fd=10))

# 파드 로그 실시간 확인 (웹 파드에 접속자의 IP가 출력)
kubectl logs -l app=deploy-websrv -f

# 외부 클라이언트(mypc 컨테이너)
--------------------
# 노드의 IP와 NodePort를 변수에 지정
## CNODE=<컨트롤플레인노드의 IP주소>
## NODE1=<노드1의 IP주소>
## NODE2=<노드2의 IP주소>
## NODE3=<노드3의 IP주소>
CNODE=172.18.0.A
NODE1=172.18.0.B
NODE2=172.18.0.C
NODE3=172.18.0.D
CNODE=172.18.0.5
NODE1=172.18.0.2
NODE2=172.18.0.3
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"

# 아래 반복 접속 실행 해두자
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"


# NodePort 서비스는 ClusterIP 를 포함
# CLUSTER-IP:PORT 로 접속 가능! <- 컨트롤노드에서 아래 실행하자!
kubectl get svc svc-nodeport
NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
svc-nodeport   NodePort   10.111.1.238   <none>         9000:30158/TCP   3m3s

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


# (옵션) 노드에서 Network Connection
conntrack -E
conntrack -L --any-nat

# (옵션) 패킷 캡쳐 확인
tcpdump..

 

확인

 

 

확인해보면 모든 노드의 포트가 열려있다.

 

또한 노드포트는 클러스터 ip를 포함한다.

 

 

외부 클라이언트 → 서비스(NodePort) 접속 시 : 3개의 목적지(backend) 파드로 랜덤 부하 분산 접속됨

 

 

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

 

IPTABLES 정책 확인

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

컨트롤플레인 노드 - iptables 분석 << 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!

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

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

PREROUTING 정보 확인

iptables -t nat -S | grep PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
...

# 외부 클라이언트가 노드IP:NodePort 로 접속하기 때문에 --dst-type LOCAL 에 매칭되어서 -j KUBE-NODEPORTS 로 점프!
iptables -t nat -S | grep KUBE-SERVICES
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
...

# KUBE-NODEPORTS 에서 KUBE-EXT-# 로 점프!
## -m nfacct --nfacct-name localhost_nps_accepted_pkts 추가됨 : 패킷 flow 카운팅 - 카운트 이름 지정 
iptables -t nat -S | grep KUBE-NODEPORTS | grep <NodePort>
iptables -t nat -S | grep KUBE-NODEPORTS | grep 30898
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30898 -m nfacct --nfacct-name  localhost_nps_accepted_pkts -j KUBE-EXT-VTR7MTHHNMFZ3OFS
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30898 -j KUBE-EXT-VTR7MTHHNMFZ3OFS

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

# (old0 KUBE-NODEPORTS 에서 'KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000' 마킹 및 KUBE-SVC-# 로 점프!
watch -d 'iptables -v --numeric --table nat --list KUBE-NODEPORTS'
iptables -t nat -S | grep KUBE-NODEPORTS
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30158 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 30158 -j KUBE-SVC-VTR7MTHHNMFZ3OFS
...

## 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-EXT-VTR7MTHHNMFZ3OFS"
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "masquerade traffic for default/svc-nodeport:svc-webport external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -j 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'

 

externalTrafficPolicy 설정

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

 

 

이때 노드에 파드가 없으면 통신이 안 된다.

 

설정 및 파드 접속 확인

# 기본 정보 확인
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
  externalTrafficPolicy: Cluster
  internalTrafficPolicy: Cluster

# 기존 통신 연결 정보(conntrack) 제거 후 아래 실습 진행하자! : (모든 노드에서) conntrack -F
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i conntrack -F; echo; done
kubectl delete -f svc-nodeport.yaml
kubectl apply -f svc-nodeport.yaml

# externalTrafficPolicy: local 설정 변경
kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
kubectl get svc svc-nodeport -o json | grep 'TrafficPolicy"'
	"externalTrafficPolicy": "Local",
  "internalTrafficPolicy": "Cluster",

# 파드 3개를 2개로 줄임
kubectl scale deployment deploy-echo --replicas=2

# 파드 존재하는 노드 정보 확인
kubectl get pod -owide

# 파드 로그 실시간 확인 (웹 파드에 접속자의 IP가 출력)
kubectl logs -l app=deploy-websrv -f

# 외부 클라이언트(mypc)에서 실행
--------------------
# 노드의 IP와 NodePort를 변수에 지정
## CNODE=<컨트롤플레인노드의 IP주소>
## NODE1=<노드1의 IP주소>
## NODE2=<노드2의 IP주소>
## NODE3=<노드3의 IP주소>
CNODE=172.18.0.A
NODE1=172.18.0.B
NODE2=172.18.0.C
NODE3=172.18.0.D
CNODE=172.18.0.5
NODE1=172.18.0.2
NODE2=172.18.0.3
NODE3=172.18.0.4

## NodePort 를 변수에 지정
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT


# 서비스(NodePort) 부하분산 접속 확인 : 파드가 존재하지 않는 노드로는 접속 실패!, 파드가 존재하는 노드는 접속 성공 및 클라이언트 IP 확인!
docker exec -it mypc curl -s --connect-timeout 1 $CNODE:$NPORT | jq
for i in $CNODE $NODE1 $NODE2 $NODE3 ; do echo ">> node $i <<"; docker exec -it mypc curl -s --connect-timeout 1 $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"

# 아래 반복 접속 실행 해두자
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $NODE2:$NPORT | grep hostname; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"


# (옵션) 노드에서 Network Connection
conntrack -E
conntrack -L --any-nat
# 패킷 캡쳐 확인

 

-외부 클라이언트 → 각각 노드2,3 접속 시 : 각각 노드에 생성된 파드로만 접속됨!

 

-웹 파드에서 접속자의 IP 정보 확인(logs) 시 외부 클라이언트IP 가 그대로 확인됨

 

iptables 정책 적용 확인 : PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-XLB-### → KUBE-SEP-#<자신의 노드에 생성된 파드>

컨트롤플레인 노드 - iptables 분석 << 정책 확인 : 아래 정책 내용은 핵심적인 룰(rule)만 표시했습니다!
# (예시) 노드2에서 확인했습니다

docker exec -it myk8s-worker2 bash
---------------------------------------

iptables -t nat -S
iptables -t nat -S | grep <NodePort>
iptables -t nat -S | grep 31303
-A KUBE-NODEPORTS -d 127.0.0.0/8 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 31303 -m nfacct --nfacct-name  localhost_nps_accepted_pkts -j KUBE-EXT-VTR7MTHHNMFZ3OFS
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 31303 -j KUBE-EXT-VTR7MTHHNMFZ3OFS

iptables -t nat -S | grep 'A KUBE-EXT-VTR7MTHHNMFZ3OFS'
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -s 10.10.0.0/16 -m comment --comment "pod traffic for default/svc-nodeport:svc-webport external destinations" -j KUBE-SVC-VTR7MTHHNMFZ3OFS
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "masquerade LOCAL traffic for default/svc-nodeport:svc-webport external destinations" -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "route LOCAL traffic for default/svc-nodeport:svc-webport external destinations" -m addrtype --src-type LOCAL -j KUBE-SVC-VTR7MTHHNMFZ3OFS
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -j KUBE-SVL-VTR7MTHHNMFZ3OFS

# 실습 환경에서는 아래처럼 2개의 파드 중 자신의 노드에 생성된 파드 1개만 DNAT 연결됨
iptables -t nat -S | grep 'A KUBE-SVL-VTR7MTHHNMFZ3OFS'
-A KUBE-SVL-VTR7MTHHNMFZ3OFS -m comment --comment "default/svc-nodeport:svc-webport -> 10.10.2.7:8080" -j KUBE-SEP-FBJG45W6XHLV2NA6

iptables -t nat -S | grep 'A KUBE-SEP-FBJG45W6XHLV2NA6'
-A KUBE-SEP-FBJG45W6XHLV2NA6 -s 10.10.2.7/32 -m comment --comment "default/svc-nodeport:svc-webport" -j KUBE-MARK-MASQ
-A KUBE-SEP-FBJG45W6XHLV2NA6 -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp -j DNAT --to-destination 10.10.2.7:8080

---------------------------------------

 

노드 2번에는 룰이 적용되어 있다.

비스(NodePort) 부족한 점

-외부에서 노드의 IP와 포트로 직접 접속이 필요함 → 내부망이 외부에 공개(라우팅 가능)되어 보안에 취약함LoadBalancer 서비스 타입으로 외부 공개 최소화 가능!

-클라이언트 IP 보존을 위해서, externalTrafficPolicy: local 사용 시 파드가 없는 노드 IP로 NodePort 접속 시 실패LoadBalancer 서비스에서 헬스체크(Probe) 로 대응 가능!

 

 

Readiness Probe + Endpoints, EndpointSlice

서비스 사용 시 Readiness Probe 를 사용하여 헬스체크 실패 시 ‘Endpoints, EndpointSlice 출력 정보 차이’

Endpoint Slices - k8s_Docs

 

EndpointSlices

The EndpointSlice API is the mechanism that Kubernetes uses to let your Service scale to handle large numbers of backends, and allows the cluster to update its list of healthy backends efficiently.

kubernetes.io

 

엔드포인트 슬라이드는 각 서비스별 파드를 묶어서 관리하는 것 같습니다.

 

수퍼마리오 디플로이먼트에 Readiness Probe 설정 및 Service(Nodeport) 설정 배포

# 모니터링
watch kubectl get pod,svc,ep,endpointslice -owide

# 배포
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mario
  labels:
    app: mario
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mario
  template:
    metadata:
      labels:
        app: mario
    spec:
      tolerations:
      - key: "node-role.kubernetes.io/control-plane"
        effect: "NoSchedule"
      nodeSelector:
        node-role.kubernetes.io/control-plane: ""
      containers:
      - name: mario
        image: pengbai/docker-supermario
        readinessProbe:
          exec:
            command:
            - cat
            - healthcheck
---
apiVersion: v1
kind: Service
metadata:
  name: mario
spec:
  ports:
    - name: mario-webport
      port: 80
      targetPort: 8080
      nodePort: 30001
  selector:
    app: mario
  type: NodePort
  externalTrafficPolicy: Local
EOF

 

 

readiness가 실패해서 svc에 ep가 생기지 않습니다.

근데 endpointslice는 생성이되네요

 

헬스체크를 만들어주면 아래와 같이 생깁니다.

'study > KANS 3기' 카테고리의 다른 글

KANS 3기 5주차 첫번째  (1) 2024.10.04
KANS 3기 5주차 두번째  (0) 2024.10.04
KANS 3기 3주차 두번째  (0) 2024.09.18
KANS 3기 3주차 첫번째  (0) 2024.09.18
KANS 3기 3주차 실습환경 구축  (0) 2024.09.08