서비스의 종류
September 2024 (3263 Words, 19 Minutes)
기존과 동일하게 클러스터를 프로비저닝합니다.
제 개인 git에 virtualbox - vagrant 프로비저닝 파일을 업로드해두었습니다.
클러스터 컴포넌트의 외부노출 발전단계
-
파드 생성 : K8S 클러스터 내부에서만 접속
- 서비스(Cluster Type) 연결 : K8S 클러스터 내부에서만 접속
- 동일한 애플리케이션의 다수의 파드의 접속을 용이하게 하기 위한 서비스에 접속
- 고정 접속(호출) 방법을 제공 : 흔히 말하는 ‘고정 VirtualIP’ 와 ‘Domain주소’ 생성
- 서비스(NodePort Type) 연결 : 외부 클라이언트가 서비스를 통해서 클러스터 내부의 파드로 접속
- 서비스(NodePort Type)의 일부 단점을 보완한 서비스(LoadBalancer Type) 도 있습니다!
서비스 종류
ClusterIP
타입
NodePort
타입
LoadBalancer
타입 (기본 모드) : NLB 인스턴스 유형
Service (LoadBalancer Controller
) : AWS Load Balancer Controller + NLB IP 모드 동작 with AWS VPC CNI
Cluster IP
통신 흐름
클라이언트(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 에 의해서 분산 접속이 됨
실습
테스트용 파드 세개를 생성합니다
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
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에 모든 노드에서 규칙이 동일하게 생성되는것을 확인 할 수 있습니다.
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"
tcpdump -i eth0 tcp port 80 -nnq
IPtables 정책 확인
iptables 정책 적용 순서
PREROUTING → KUBE-SERVICES → KUBE-SVC-### → KUBE-SEP-#<파드1> , KUBE-SEP-#<파드2> , KUBE-SEP-#<파드3>파드3>파드2>파드1>
결론 : 내부에서 클러스터 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가 조회됩니다.
SVC-### 에서 랜덤 확률(대략 33%)로 SEP(Service EndPoint)인 각각 파드 IP로 DNAT 됩니다!
첫번째 룰에 일치 확률은 33% 이고, 매칭되지 않을 경우 아래 2개 남을때는 룰 일치 확률은 50%가 됩니다.
이것도 매칭되지 않으면 마지막 룰로 100% 일치됩니다
어떤 기준으로 랜덤 분산을 정의하는가
Kube Proxy의 IPTABLES는 어떤기준으로 % 랜덤 분산을 설정할까?
해당 함수에서 kubeproxy가 iptables 모드로 동작할때
- 서비스가 사용하는 특정 포트에 트래픽이 들어올 때, 이를 적절한 엔드포인트(즉, 파드)로 전달하는 iptables 규칙을 생성합니다.
- 부하 분산을 위해 각 엔드포인트(파드)에 대해 확률적인 분배를 사용합니다.
probability
함수는 특정 엔드포인트에 트래픽이 분산될 확률을 계산하는데, 이는 엔드포인트의 개수에 따라 달라집니다.statistic
모듈은 확률적으로 각 엔드포인트로 트래픽을 전달하며, 마지막 엔드포인트는 무조건 남은 트래픽을 수신하도록 설정됩니다.
추가적으로 SessionAffinity 설정에 따라
클라이언트 IP 기반 세션 어피니티를 사용할 경우, recent
모듈을 통해 클라이언트의 연결을 추적하여 동일한 클라이언트가 항상 동일한 파드로 연결되도록 설정할 수 있습니다
SessionAffinity: ClientIP
sessionAffinity: ClientIP
: 클라이언트가 접속한 목적지(파드)에 고정적인 접속을 지원 - k8s_Docs
-
설정 및 파드 접속 확인
# 기본 정보 확인 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 -Siptables -t nat -S | grep recent
아래 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
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 되어 목적지 파드와 통신 후 리턴 트래픽은 최초 인입 노드를 경유해서 외부로 되돌아감
NodePort(노드포트)는 모든 노드(마스터 포함)에 Listen 됨!
외부 클라이언트의 출발지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
가상머신에서 마스터노드에 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
- 웹 파드에서 접속자의 IP 정보 확인(logs) 시 외부 클라이언트IP 가 아닌, 노드의 IP로 SNAT 되어서 확인됨
iptables 정책 적용 순서
: PREROUTING → KUBEs-SERVICES → KUBE-NODEPORTS(MARK) → KUBE-SVC-# → KUBE-SEP-# ⇒ KUBE-POSTROUTING (MASQUERADE) ← 규칙 과정 일부 업데이트됨
ExternalTrafficPolicy
externalTrafficPolicy: Local
: NodePort 로 접속 시 해당 노드에 배치된 파드로만 접속됨, 이때 SNAT 되지 않아 외부 클라이언트 IP가 보존됨!
Node1:NodePort 접속시 Node1에 생성된 파드(Pod1)로만 접속됨
Node3에 파드가 없을 경우에 접속 시 연결 실패됨!
-
외부 클라이언트의 IP 주소(아래 출발지IP: 50.1.1.1)가 노드의 IP로 SNAT 되지 않고 서비스(backend) 파드까지 전달됨!
직접 확인해보기
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'
NodePort의 부족한 점
- 외부에서 노드의 IP와 포트로 직접 접속이 필요함 → 내부망이 외부에 공개(라우팅 가능)되어 보안에 취약함 ⇒ LoadBalancer 서비스 타입으로 외부 공개 최소화 가능!
- 클라이언트 IP 보존을 위해서,
externalTrafficPolicy: local
사용 시 파드가 없는 노드 IP로 NodePort 접속 시 실패 ⇒ LoadBalancer 서비스에서 헬스체크(Probe) 로 대응 가능!
도대체 왜 이렇게 복잡하게 동작하는지 궁금하시리라 생각합니다. 이해를 해보자면, 개발자가 직접 관리할 수 있는 서버만 가지고도 부하분산을 편리하게 사용하다 보니 이렇게 복잡하고 비효율적인 구조로 >동작할수 밖에 없었다고 생각합니다!