개요
NodePort 타입으로 생성한 Service로 접속을 시도하면서, iptables 규칙과 부하 분산 동작을 확인
externalTrafficPolicy: ClientIP로 설정하고, 클라이언트의 IP가 보존되는 것을 직접 확인
실습 준비
외부 노드 생성
# 클러스터 외부에서의 접속을 가정하여 추가 노드 생성
# 클러스터는 kind라는 도커 브릿지 인터페이스를 이용
# 외부 노드도 이를 하도록 하고, 해당 대역의 IP를 직접 지정하여 생성
docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity
# 노드 생성 확인
docker ps | grep mypc
Shell
복사
목적지 Deployment 생성
# Deployment 정의한 manifest 생성
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
# manifest 적용
kubectl apply -f echo-deploy.yaml
Shell
복사
NodePort 타입 Service 생성
# NodePort 타입 manifest 생성
cat <<EOT> svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-nodeport
spec:
ports:
- name: svc-webport
port: 9000
targetPort: 8080
selector:
app: deploy-websrv
type: NodePort
EOT
# manifest 적용
kubectl apply -f svc-nodeport.yaml
Shell
복사
리소스 확인
# 생성 확인
watch -d 'kubectl get po -o wide ; echo ; kubectl get svc,ep svc-nodeport'
# Service가 사용하는 노드의 포트 확인
kubectl get svc svc-nodeport
Shell
복사
접속 시도
부하 분산
클라이언트 Pod를 이용하여 Service 접속 시 부하 분산되어 접속됨을 확인
# 노드의 포트를 확인
kubectl get svc svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}'
# 노드의 포트를 변수에 할당
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT
# 노드들의 IP를 확인
kubectl get no -o wide
# 노드들의 IP를 변수에 할당
CNODE=...
NODE1=...
NODE2=...
NODE3=...
Shell
복사
# 각 노드에 요청을 보내면, 해당 노드가 모두 요청을 받긴 했으나 요청 처리는 보두 고르게 분배 됨을 확인
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"
Shell
복사
ClusterIP
NodePort는 ClusterIP를 포함한다고 했었는데, 실제로 Control Plane에서 <cluster-ip>:<port>로 접속해보면 접속이 되는 것을 볼 수 있음
# <cluster-ip>:<port>에 해당하는 값들을 변수에 할당
CLUSTER_IP=$(kubectl get service svc-nodeport -o jsonpath="{.spec.clusterIP}")
SERVICE_PORT=$(kubectl get service svc-nodeport -o jsonpath="{.spec.ports[0].port}")
echo $CLUSTER_IP $SERVICE_PORT
Shell
복사
# 클러스터에 속한 Control Plane에서는 ClusterIP를 이용하여 접속 가능
docker exec -it myk8s-control-plane curl -s $CLUSTER_IP:$SERVICE_PORT | jq
Shell
복사
# 클러스터 외부 노드에서는 당연하게도 ClusterIP로는 접속 불가
docker exec -it mypc curl -s $CLUSTER_IP:$SERVICE_PORT
Shell
복사
sNAT
부하 분산을 확인하려던 위 요청을 로그를 켜둔 채로 요청하여 확인해보면, 외부 노드에서 요청했음에도 Control Plane의 IP를 출발지로 사용하는 것을 볼 수 있음에 따라 ClusterIP와는 달리 sNAT도 이용하는 것을 짐작 가능
# 좌측 터미널에는 로그를 출력
kubectl logs -f -l app=deploy-websrv
# 우측 터미널에는 외부 노드를 이용하여 Control Plane 노드로 트래픽을 요청
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
Shell
복사
iptables
ClusterIP를 지원하기 때문에 대체로 동작이 비슷하나, ClusterIP와 달리 sNAT도 포함하기 때문에 KUBE-NODEPORTS, KUBE-MARK-MASQ, KUBE-POSTROUTING이 동작
결론적으론 PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-XXX → KUBE-MARK-MASQ → KUBE-SVC-XXX → KUBE-SEP-(#POD1|#POD2|#POD3) → POSTROUTING → KUBE-POSTROUTING 으로 정책이 적용되면서 부하를 분산
# Control Plane 접속
docker exec -it myk8s-control-plane bash
Shell
복사
# PREROUTING 체인의 규칙을 확인
# KUBE-SERVICE 체인으로 점프하는 규칙이 존재
iptables -t nat -nvL PREROUTING | column -t
Shell
복사
# KUBE-SERVICES 체인의 규칙을 확인
# ClusterIP는 첫 번째 규칙에 매칭되어 KUBE-SVC-XXX로 전달
# 그 외 NodePort는 KUBE-NODEPORTS 체인으로 매칭되어 점프
iptables -t nat -nvL KUBE-SERVICES | column -t
Shell
복사
# KUBE-NODEPORTS 체인의 규칙을 확인
# 노드의 지정된 포트로 인입되는 경우 KUBE-EXT-XXX로 전달
iptables -t nat -nvL KUBE-NODEPORTS | column -t
Shell
복사
# KUBE-EXT-XXX 체인의 규칙을 확인
# 패킷에 마킹을 위한 KUBE-MARK-MASQ로 점프
iptables -t nat -nvL KUBE-EXT-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-MARK-MASQ 체인의 규칙을 확인
# 특별한 행동 없이 0x4000 마킹만 수행하고, KUBE-EXT-XXX에서 그 다음 규칙이었던 KUBE-SVC-XXX로 점프
iptables -t nat -nvL KUBE-MARK-MASQ | column -t
Shell
복사
# KUBE-SVC-XXX 체인의 규칙을 확인
# 여기서부터는 POSTROUTING 전까진 ClusterIP와 동일하게 동작하여 dNAT을 수행
iptables -t nat -nvL KUBE-SVC-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-SEP-(#POD1|#POD2|#POD3) 체인의 규칙을 확인
# 각 엔드포인트들은 dNAT 체인으로 이어지는 것을 볼 수 있음
iptables -t nat -nvL KUBE-SEP-XEXGJWEWSC2GPNPZ | column -t
iptables -t nat -nvL KUBE-SEP-R5FVJKGMGG2F5UME | column -t
iptables -t nat -nvL KUBE-SEP-GEQNJ6BO5AOHB6LH | column -t
Shell
복사
# sNAT도 수행하는 것을 확인
# POSTROUTING 체인을 확인하여 KUBE-POSTROUTING 체인이 존재함을 확인
# KUBE-POSTROUTING에는 mark match ! 0x4000/0x4000
# 마킹을 거쳐가면서 RETURN 구문을 건너뛰면서 MASQUERADE를 수행하고, source의 포트는 랜덤하게 교체
iptables -t nat -nvL POSTROUTING | column -t
iptables -t nat -nvL KUBE-POSTROUTING | column -t
Shell
복사
externalTrafficPolicy: Local
Service를 소개하는 글에서 externalTrafficPolicy에 대해 언급했었는데, 클라이언트 IP 주소의 수집이 필요한 경우 해당 설정을 이용하여 sNAT를 수행하지 않게 해서 이를 보존하는 것이 가능
초기화
# 연결 초기화 및 Service 재생성
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
# 노드의 포트를 확인
kubectl get svc svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}'
# 노드의 포트를 변수에 할당
NPORT=$(kubectl get service svc-nodeport -o jsonpath='{.spec.ports[0].nodePort}')
echo $NPORT
# 노드들의 IP를 확인
kubectl get no -o wide
# 노드들의 IP를 변수에 할당
CNODE=...
NODE1=...
NODE2=...
NODE3=...
Shell
복사
설정 확인
# externalTrafficPolicy 및 internalTrafficPolicy를 확인
kubectl get svc svc-nodeport -o json | grep TrafficPolicy
Shell
복사
설정 변경
# externalTrafficPolicy의 값을 변경
kubectl patch svc svc-nodeport -p '{"spec":{"externalTrafficPolicy": "Local"}}'
Shell
복사
# Pod를 하나 삭제
kubectl scale deployment deploy-echo --replicas=2
# Pod 조회
kubectl get po -o wide
Shell
복사
접속 확인
# 좌측 터미널에는 로그를 출력
kubectl logs -f -l app=deploy-websrv
# 우측 터미널에는 외부 노드를 이용하여 Control Plane 노드로 트래픽을 요청
# Pod가 존재하지 않는 Control Plane에서의 요청은 실패
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $CNODE:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
# 우측 터미널에는 외부 노드를 이용하여 Pod가 존재하는 그 외 노드로 트래픽을 요청
# Pod가 존재하는 노드에서의 요청은 도달하며, sNAT 없이 클라이언트의 IP가 보존
docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $NODE1:$NPORT | grep hostname; done | sort | uniq -c | sort -nr"
Shell
복사
iptables 확인
externalTrafficPolicy가 Cluster일 때와 달리 PREROUTING → KUBE-SERVICES → KUBE-NODEPORTS → KUBE-EXT-XXX → KUBE-SVL-XXX → KUBE-SEP-XXX 로 구성
KUBE-MARK-MASQ 과정과 POSTROUTING 이후 과정이 삭제되고, KUBE-SVC-XXX 대신 KUBE-SVL-XXX가 단일 KUBE-SEP-XXX과 연결되어 있음
# Pod가 존재하는 노드에 접속하여 확인
docker exec -it myk8s-worker bash
Shell
복사
Pod가 존재하지 않는 노드는 externalTrafficPolicy가 Cluster인 일반적인 iptables 체인과 유사한데, Pod가 존재하지 않기 때문에 KUBE-MARK-DROP 후 KUBE-FIREWALL로 차단되는 절차를 가짐
# PREROUTING 체인의 규칙을 확인
# KUBE-SERVICE 체인으로 점프하는 규칙이 존재
iptables -t nat -nvL PREROUTING | column -t
Shell
복사
# KUBE-SERVICES 체인의 규칙을 확인
# ClusterIP는 첫 번째 규칙에 매칭되어 KUBE-SVC-XXX로 전달
# 그 외 NodePort는 KUBE-NODEPORTS 체인으로 매칭되어 점프
iptables -t nat -nvL KUBE-SERVICES | column -t
Shell
복사
# KUBE-NODEPORTS 체인의 규칙을 확인
# 노드의 지정된 포트로 인입되는 경우 KUBE-EXT-XXX로 전달
iptables -t nat -nvL KUBE-NODEPORTS | column -t
Shell
복사
# KUBE-EXT-XXX 체인의 규칙을 확인
# 패킷 마킹 단계 없이 KUBE-SVL-XXX로 바로 점프
iptables -t nat -nvL KUBE-EXT-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-SVL-XXX 체인의 규칙을 확인
# 단일 KUBE-SEP-XXX 규칙만 존재
iptables -t nat -nvL KUBE-SVL-VTR7MTHHNMFZ3OFS | column -t
Shell
복사
# KUBE-SEP-XXX 체인의 규칙을 확인
# 엔드포인트는 dNAT 체인으로 이어지는 것을 볼 수 있음
iptables -t nat -nvL KUBE-SEP-R5FVJKGMGG2F5UME | column -t
Shell
복사
# dNAT만을 이용하는 것을 확인
# POSTROUTING 체인을 확인하여 KUBE-POSTROUTING의 체인이 존재함을 확인
# KUBE-POSTROUTING에는 mark match ! 0x4000/0x4000
# 마킹되어 있지 않은 상태임에 따라 RETURN 되어 sNAT은 수행되지 않음
iptables -t nat -nvL POSTROUTING | column -t
iptables -t nat -nvL KUBE-POSTROUTING | column -t
Shell
복사
리소스 삭제
kubectl delete svc,deploy --all
Shell
복사