[KANS3] LoadBalancer(MetalLB), IPVS

xgro·2024년 10월 4일
0

KANS3

목록 보기
5/9
post-thumbnail

📌 Notice

Kubernetes Advanced Networking Study (=KANS)
k8s 네트워크 장애 시, 네트워크 상세 동작 원리를 기반으로 원인을 찾고 해결하는 내용을 정리한 블로그입니다.

CloudNetaStudy 그룹에서 스터디를 진행하고 있습니다.

Gasida님께 다시한번 🙇 감사드립니다.

EKS 관련 이전 스터디 내용은 아래 링크를 통해 확인할 수 있습니다.



📌 Summary

  • Kubernetes에서 서비스를 외부로 노출하는 방법으로 ClusterIP, NodePort, LoadBalancer 타입의 동작 원리를 심층적으로 이해합니다.

  • ClusterIP와 NodePort, 그리고 LoadBalancer 서비스의 네트워크 흐름을 분석하고 iptables 및 IPVS의 규칙 설정 방식을 학습합니다.

  • MetalLB와 같은 로드 밸런서를 활용하여 Kubernetes에서 고가용성 네트워크를 구현하고 장애 상황에서의 대응 방법을 이해합니다.



📌 Study

👉 Step 00. LoadBalancer 소개

  • 외부 로드 밸런서를 사용하여 서비스를 외부에 노출합니다.

  • Kubernetes는 로드 밸런싱 구성 요소를 직접 제공하지 않습니다

  • you must provide one, or you can integrate your Kubernetes cluster with a cloud provider.

  • type: Loadbalancer의 서비스를 구현하기 위해 Kubernetes는 일반적으로 type: nodePort의 서비스를 요청하는 것과 동등한 내용을 변경하는 것으로 시작합니다. 그런 다음 클라우드 컨트롤러-관리자 구성 요소는 외부 로드 밸런서를 구성하여 할당된 노드 포트로 트래픽을 전달합니다.

  • 클라우드 제공업체 구현에서 이를 지원하는 경우 로드 밸런싱 서비스를 구성하여 노드 포트 할당을 생략할 수 있습니다.

김경보님께서 너무 깔끔하게 아키텍처를 그렸네요!! 출처 - https://kimalarm.tistory.com/102

✅ 01. 서비스(Service) LoadBalancer Type 통신 흐름

summary - 외부 클라이언트가 '로드밸런서' 접속 시 부하분산 되어 노드 도달 후 iptables 룰로 목적지 파드와 통신됨

외부에서 로드밸런서 (부하분산) 처리 후 → 노드(NodePort) 이후 기본 과정은 NodePort 과정과 동일합니다.

출처 - CloudNet@

노드는 외부에 공개되지 않고 로드밸런서만 외부에 공개되어, 외부 클라이언트는 로드밸런서에 접속을 할 뿐 내부 노드의 정보를 알 수 없습니다.

로드밸런서가 부하분산하여 파드가 존재하는 노드들에게 전달합니다. iptables 룰에서는 자신의 노드에 있는 파드만 연결한다. (externalTrafficPolicy: local)

DNAT 2번 동작하게 됩니다.

  • 첫번째(로드밸런서 접속 후 빠져 나갈때)
  • 두번째(노드의 iptables 룰에서 파드IP 전달 시)

쿠버네티스는 Service(LB Type) API 만 정의하고 실제 구현은 add-on 에 맡깁니다.

부하분산 최적화
노드에 파드가 없을 경우 '로드밸런서'에서 노드에 헬스 체크(상태 검사)가 실패하여 해당 노드로는 외부 요청 트래픽을 전달하지 않는다

출처 - CloudNet@


✅ 02. 서비스(LoadBalancer) 부족한 점

  • 서비스(LoadBalancer) 생성 시 마다 LB(예 AWS NLB)가 생성되어 자원 활용이 비효율적 ⇒ HTTP 경우 인그레스(Ingress) 를 통해 자원 활용 효율화 가능!

  • 서비스(LoadBalancer)는 HTTP/HTTPS 처리에 일부 부족함(TLS 종료, 도메인 기반 라우팅 등) ⇒ 인그레스(Ingress) 를 통해 기능 동작 가능!

  • (참고) 온프레미스 환경에서 제공 불가능??? ⇒ MetalLB 혹은 OpenELB(구 PorterLB) 를 통해서 온프레미스 환경에서 서비스(LoadBalancer) 기능 동작 가능!


👉 Step 01. MetalLB

✅ 00. MetalLB 요약

MetalLB Docs : MetalLB is a load-balancer implementation for bare metal Kubernetes clusters, using standard routing protocols - link

MetalLB - Layer2 모드

  • 리더 파드가 선출되고 해당 리더 파드가 생성된 노드로만 트래픽이 인입되어 해당 노드에서 iptables 분산되어 파드로 접속
  • 권장 사용 환경 : 테스트 및 소규모의 환경(동일 네트워크 1개 사용)에서 클라이언트 IP 보존이 필요 없을 때

출처 - CloudNet@

MetalLB - BGP 모드

  • speaker 파드가 BGP로 서비스 정보(EXTERNAL-IP)를 전파 후, 외부에서 라우터를 통해 ECMP 라우팅으로 부하 분산 접속
  • 권장 사용 환경 : 규모가 있고, 클라이언트 IP보존과 장애 시 빠른 절체가 필요하며, 네트워크 팀 협조가 가능할때
    출처 - CloudNet@

✅ 01. MetalLB 소개

MetalLB 는 온프레미스 환경(IDC)에서 사용할 수 있는 서비스(로드밸런서 타입)입니다.
(클라우드 환경의 서비스(로드밸런서 타입)와는 동작이 조금 다릅니다!)

MetalLB는 BareMetalLoadBalancer 약자!

출처 - CloudNet@

L2 mode (ARP)
출처 - CloudNet@

BGP mode
출처 - CloudNet@

  • 서비스(로드 밸런서)의 'External IP' 전파를 위해서 표준 프로토콜인 ARP(IPv4)/NDP(IPv6), BGP 를 사용합니다.
    • 데몬셋으로 speaker 파드를 생성하여 'External IP' 전파합니다
  • 일반적인 BGP 데몬과 다르게 BGP 업데이트(외부에서 광고하는 네트워크 대역)을 받지 않습니다.
  • 클라우드 플랫폼 호환 : 대부분의 클라우드 플랫폼(예 AWS, Azure, GCP 등)과 호환되지 않습니다 - 링크
  • CNI(네트워크 플러그인) 호환 : 일부 CNI와 연동에 이슈가 있습니다 - 링크

✅ 02. Layer 2 모드 (ARP/NDP)

Layer2 모드에서 서비스 접속

모드 소개
리더 파드가 선출되고 해당 리더 파드가 생성된 노드로만 트래픽이 인입되어 해당 노드에서 iptables 분산되어 파드로 접속

  • 서비스(로드밸런서) 'External IP' 생성 시 speaker 파드 중 1개가 리더가 되고, 리더 speaker 파드가 존재하는 노드로 서비스 접속 트래픽이 인입되게 됩니다
  • 데몬셋으로 배포된 speaker 파드는 호스트 네트워크를 사용합니다 ⇒ "NetworkMode": "host"
  • 리더는 ARP(GARP, Gratuitous APR)로 해당 'External IP' 에 대해서 자신이 소유(?)라며 동일 네트워크에 전파를 합니다.
  • 만약 리더(노드)가 장애 발생 시 자동으로 나머지 speaker 파드 중 1개가 리더가 됩니다.
    • 멤버 리스터 및 장애 발견은 hashicorp 의 memberlist를 사용 - Gossip based membership and failure detection
  • Layer 2에서 멤버 발견 및 자동 절체에 Keepalived(VRRP)도 있지만 사용하지 않은 이유

출처 - CloudNet@

제한 사항
single-node bottlenecking, potentially slow failover

  • single-node bottlenecking : 서비스 1개 생성 사용 시, 모든 서비스 접근 트래픽은 리더 파드가 존재하는 노드로만 인입되어서 부하가 집중
  • potentially slow failover : 리더(노드)가 장애 시 나머지 노드 리더가 선출되고, ARP 전파 및 갱신완료 전까지는 장애가 발생됨 (대략 10초~20초 정도)

✅ 03. BGP 모드

BGP 모드에서 서비스 접속

모드 소개
speaker 파드가 BGP로 서비스 정보(EXTERNAL-IP)를 전파 후, 외부에서 라우터를 통해 ECMP 라우팅으로 부하 분산 접속

  • speaker 파드에 BGP 가 동작하여 서비스 정보(EXTERNAL-IP)를 전파한다.
    • 기본은 IP주소(32bit)를 전파하며, 설정으로 축약된 네트워크 정보를 전파할 수 있다 → bgp-advertisements 에 aggregation-length 설정
    • BGP 커뮤니티, localpref 등 BGP 관련 설정을 할 수 있다.
    • IP 주소 마지막이 0 과 255 를 처리를 못하는 라우터 장비가 있을 경우 avoid-buggy-ips: true 옵션으로 할당되지 않게 할 수 있다.
  • 외부 클라이언트에서 SVC(서비스, EXTERNAL-IP)로 접속이 가능하며, 라우터에서 ECMP 라우팅을 통해 부하 분산 접속 할 수 있다.
    • 일반적으로 ECMP 는 5-tuple(프로토콜, 출발지IP, 목적지IP, 출발지Port, 목적지Port) 기준으로 동작합니다.
    • 물론 라우터 장비에 따라 다양한 라우팅(분산) 처리가 가능합니다
      출처 - CloudNet@

제한 사항
라우터에서 서비스로 인입이 되기 때문에, 라우터의 관련 설정이 중요한 만큼 네트워크팀과 협업을 적극 권장

  • 노드(speaker) 파드 장애 시 BGP Timer 설정 등 구성하고 있는 네트워크 환경에 맞게 최적화 작업이 필요
  • ECMP 부하 분산 접속 시 특정 파드에 몰리거나 혹은 세션 고정, flapping 등 다양한 환경에 대응이 필요
  • BGP 라우팅 설정 및 라우팅 전파 관련 최적화 설정이 필요

✅ 04. 실습 환경 소개

기본 정보 확인

# 파드와 서비스 사용 가능 네트워크 대역
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
                            "--service-cluster-ip-range=10.200.1.0/24",
                            "--cluster-cidr=10.10.0.0/16",

# kube-proxy 모드 확인 : iptables proxy 모드
kubectl describe  configmap -n kube-system kube-proxy | grep mode
mode: "iptables"

# iptables 정보 확인
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-control-plane  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker2 iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker3 iptables -t $i -S ; echo; done

파드 생성

cat <<EOF | kubectl apply -f -
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
EOF

파드 접속 확인

# 파드 정보 확인
kubectl get pod -owide

# 파드 IP주소를 변수에 지정
WPOD1=$(kubectl get pod webpod1 -o jsonpath="{.status.podIP}")
WPOD2=$(kubectl get pod webpod2 -o jsonpath="{.status.podIP}")
echo $WPOD1 $WPOD2

# 접속 확인
docker exec -it myk8s-control-plane  ping -i 1 -W 1 -c 1 $WPOD1
docker exec -it myk8s-control-plane  ping -i 1 -W 1 -c 1 $WPOD2
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD1 | grep Hostname
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD2 | grep Hostname
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD1 | egrep 'Hostname|RemoteAddr|Host:'
docker exec -it myk8s-control-plane  curl -s --connect-timeout 1 $WPOD2 | egrep 'Hostname|RemoteAddr|Host:'



👉 Step 02. MetalLB - Layer 2 모드

✅ 01. MetalLB 설치

설치 방법 지원 : Kubernetes manifests, using Kustomize, or using Helm

  • 참고 : kube-proxy 의 ipvs 모드 사용 시 'strictARP: true' 설정 필요

출처 - https://kschoi728.tistory.com/264

간단하게 manifests 로 설치 진행! - https://github.com/metallb/metallb/tree/main/config/manifests

# Kubernetes manifests 로 설치
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/refs/heads/main/config/manifests/metallb-native-prometheus.yaml
## 혹은 프로메테우스 미설치시 아래 manifests 로 설치
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/refs/heads/main/config/manifests/metallb-native.yaml

# metallb crd 확인
kubectl get crd | grep metallb
bfdprofiles.metallb.io                      2024-09-28T15:24:06Z
bgpadvertisements.metallb.io                2024-09-28T15:24:06Z
bgppeers.metallb.io                         2024-09-28T15:24:06Z
communities.metallb.io                      2024-09-28T15:24:06Z
ipaddresspools.metallb.io                   2024-09-28T15:24:06Z
l2advertisements.metallb.io                 2024-09-28T15:24:06Z
servicel2statuses.metallb.io                2024-09-28T15:24:06Z

# 생성된 리소스 확인 : metallb-system 네임스페이스 생성, 파드(컨트롤러, 스피커) 생성, RBAC(서비스/파드/컨피그맵 조회 등등 권한들), SA 등
kubectl get-all -n metallb-system # kubectl krew 플러그인 get-all 설치 후 사용 가능
kubectl get all,configmap,secret,ep -n metallb-system

# 파드 내에 kube-rbac-proxy 컨테이너는 프로메테우스 익스포터 역할 제공
kubectl get pods -n metallb-system -l app=metallb -o jsonpath="{range .items[*]}{.metadata.name}{':\n'}{range .spec.containers[*]}{'  '}{.name}{' -> '}{.image}{'\n'}{end}{end}"

## metallb 컨트롤러는 디플로이먼트로 배포됨
kubectl get ds,deploy -n metallb-system

## 데몬셋으로 배포되는 metallb 스피커 파드의 IP는 네트워크가 host 모드이므로 노드의 IP를 그대로 사용
kubectl get pod -n metallb-system -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP           NODE                  NOMINATED NODE   READINESS GATES
controller-679855f7d7-2dvbm   2/2     Running   0          9m17s   10.10.2.6    myk8s-worker3         <none>           <none>
speaker-jfvh9                 2/2     Running   0          9m17s   172.18.0.2   myk8s-worker          <none>           <none>
speaker-l2tdn                 2/2     Running   0          9m17s   172.18.0.5   myk8s-worker3         <none>           <none>
speaker-pzs8z                 2/2     Running   0          9m17s   172.18.0.3   myk8s-worker2         <none>           <none>
speaker-vfsdj                 2/2     Running   0          9m17s   172.18.0.4   myk8s-control-plane   <none>           <none>

## metallb 데몬셋으로 배포되는 스피커 파드는 hostNetwork 를 사용함
kubectl get ds -n metallb-system -o yaml
...
          ports:
          - containerPort: 7472
            name: monitoring
            protocol: TCP
          - containerPort: 7946
            name: memberlist-tcp
            protocol: TCP
          - containerPort: 7946
            name: memberlist-udp
            protocol: UDP
				...
        hostNetwork: true
...

## 스피커 파드의 Ports 정보 확인 : Host Ports 같이 확인
kubectl describe pod -n metallb-system -l component=speaker| grep Ports:
    Ports:         7472/TCP, 7946/TCP, 7946/UDP
    Host Ports:    7472/TCP, 7946/TCP, 7946/UDP

# (참고) 상세 정보 확인
kubectl get sa,cm,secret -n metallb-system
kubectl describe role -n metallb-system
kubectl describe deploy controller -n metallb-system
kubectl describe ds speaker -n metallb-system

컨피그맵 생성 : 모드 및 서비스 대역 지정
서비스(External-IP) 대역을 노드가 속한 eth0의 대역이 아니여도 상관없다! → 다만 이 경우 GW 역할의 라우터에서 노드들로 라우팅 경로 지정 필요

제 경우에는 브릿지의 네트워크 대역이 172.19.0.0/16으로 설정되어 그에 맞게 진행하였습니다.

# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.19.0.0/16 대역
docker network ls
docker inspect kind
# kind network 중 컨테이너(노드) IP(대역) 확인 : 172.19.0.2~ 부터 할당되며, control-plane 이 꼭 172.19.0.2가 안될 수 도 있음
docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'

# IPAddressPool 생성 : LoadBalancer External IP로 사용할 IP 대역
## MetalLB는 서비스를 위한 외부 IP 주소를 관리하고, 서비스가 생성될 때 해당 IP 주소를 동적으로 할당할 수 있습니다.
kubectl explain ipaddresspools.metallb.io

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: my-ippool
  namespace: metallb-system
spec:
  addresses:
  - 172.19.255.200-172.19.255.250
EOF

kubectl get ipaddresspools -n metallb-system
NAME        AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
my-ippool   true          false             ["172.18.255.200-172.18.255.250"]

# L2Advertisement 생성 : 설정한 IPpool을 기반으로 Layer2 모드로 LoadBalancer IP 사용 허용
## Kubernetes 클러스터 내의 서비스가 외부 네트워크에 IP 주소를 광고하는 방식을 정의

kubectl explain l2advertisements.metallb.io

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: my-l2-advertise
  namespace: metallb-system
spec:
  ipAddressPools:
  - my-ippool
EOF

kubectl get l2advertisements -n metallb-system
NAME              IPADDRESSPOOLS   IPADDRESSPOOL SELECTORS   INTERFACES
my-l2-advertise   ["my-ippool"]    


✅ 02. 서비스 생성 및 확인

서비스(LoadBalancer 타입) 생성

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: svc1
spec:
  ports:
    - name: svc1-webport
      port: 80
      targetPort: 80
  selector:
    app: webpod
  type: LoadBalancer  # 서비스 타입이 LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
  name: svc2
spec:
  ports:
    - name: svc2-webport
      port: 80
      targetPort: 80
  selector:
    app: webpod
  type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
  name: svc3
spec:
  ports:
    - name: svc3-webport
      port: 80
      targetPort: 80
  selector:
    app: webpod
  type: LoadBalancer
EOF

서비스 확인 및 리더 Speaker 파드 확인

# arp scan 해두기
docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet

# LoadBalancer 타입의 서비스 생성 확인 : EXTERNAL-IP가 서비스 마다 할당되며, 실습 환경에 따라 다를 수 있음
## LoadBalancer 타입의 서비스는 NodePort 와 ClusterIP 를 포함함 - 'allocateLoadBalancerNodePorts : true' 기본값
## ExternalIP 로 접속 시 사용하는 포트는 PORT(S) 의 앞에 있는 값을 사용 (아래의 경우는 TCP 80 임)
## 만약 노드의 IP에 NodePort 로 접속 시 사용하는 포트는 PORT(S) 의 뒤에 있는 값을 사용 (아래는 30485 임)
kubectl get service,ep
NAME                 TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
service/kubernetes   ClusterIP      10.200.1.1     <none>           443/TCP        121m
service/svc1         LoadBalancer   10.200.1.69    172.18.255.200   80:30485/TCP   3m37s
service/svc2         LoadBalancer   10.200.1.218   172.18.255.201   80:31046/TCP   3m37s
service/svc3         LoadBalancer   10.200.1.81    172.18.255.202   80:30459/TCP   3m37s

NAME                   ENDPOINTS                   AGE
endpoints/kubernetes   172.18.0.5:6443             31m
endpoints/svc1         10.10.1.6:80,10.10.3.6:80   8m4s
endpoints/svc2         10.10.1.6:80,10.10.3.6:80   8m4s
endpoints/svc3         10.10.1.6:80,10.10.3.6:80   8m4s

# LoadBalancer 타입은 기본적으로 NodePort를 포함 사용. NodePort는 ClusterIP를 포함 사용.
## 클라우드사업자 LB Type이나 온프레미스환경 HW LB Type 경우 LB 사용 시 NodePort 미사용 설정 가능
kubectl describe svc svc1

## 아래 처럼 LB VIP 별로 이던 speaker 배포된 노드가 리더 역할을 하는지 확인 가능
kubectl describe svc | grep Events: -A5
...
Events:
  Type    Reason        Age   From                Message
  ----    ------        ----  ----                -------
  Normal  IPAllocated   40m   metallb-controller  Assigned IP ["172.18.255.201"]
  Normal  nodeAssigned  40m   metallb-speaker     announcing from node "myk8s-worker" with protocol "layer2"
...

kubectl get svc svc1 -o json | jq
...
  "spec": {
    "allocateLoadBalancerNodePorts": true,
  ...
  "status": {
    "loadBalancer": {
      "ingress": [
        {
          "ip": "172.18.255.202",
          "ipMode": "VIP" # https://kubernetes.io/blog/2023/12/18/kubernetes-1-29-feature-loadbalancer-ip-mode-alpha/
        }                 # https://kubernetes.io/docs/concepts/services-networking/service/#load-balancer-ip-mode


# metallb CRD인 servicel2status 로 상태 정보 확인
kubectl explain servicel2status
kubectl get servicel2status -n metallb-system
kubectl describe servicel2status -n metallb-system
kubectl get servicel2status -n metallb-system -o json --watch # watch 모드


# 현재 SVC EXTERNAL-IP를 변수에 지정
SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC2EXIP=$(kubectl get svc svc2 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC3EXIP=$(kubectl get svc svc3 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $SVC1EXIP $SVC2EXIP $SVC3EXIP

# mypc/mypc2 에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾는법 : arping 툴 사용
## Unicast reply from 172.18.255.200: 해당 IP 주소에서 응답을 받았음을 의미합니다. 
## Sent 1 probes (1 broadcast(s)): 하나의 ARP 요청을 보냈고, 브로드캐스트 방식으로 요청을 전송했음을 나타냅니다.
## Received 1 response(s): 하나의 응답을 수신했음을 나타냅니다.
docker exec -it mypc2 arping -I eth0 -f -c 1 $SVC1EXIP
docker exec -it mypc2 arping -I eth0 -f -c 1 $SVC2EXIP
docker exec -it mypc2 arping -I eth0 -f -c 1 $SVC3EXIP

for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc arping -I eth0 -f -c 1 $i; done
docker exec -it mypc2 ip -c neigh


# icmp는 지원하지 않는다.
docker exec -it mypc2 ping -c 1 -w 1 -W 1 $SVC1EXIP
docker exec -it mypc2 ping -c 1 -w 1 -W 1 $SVC2EXIP
docker exec -it mypc2 ping -c 1 -w 1 -W 1 $SVC3EXIP

for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc2 ping -c 1 -w 1 -W 1 $i; done
for i in 172.19.0.2 172.19.0.3 172.19.0.4 172.19.0.5; do docker exec -it mypc2 ping -c 1 -w 1 -W 1 $i; done

# mypc/mypc2 에서 arp 테이블 정보 확인 >> SVC IP별로 리더 파드(스피커) 역할의 노드를 확인!
docker exec -it mypc2 ip -c neigh | sort
172.19.0.2 dev eth0 lladdr 02:42:ac:12:00:02 REACHABLE 
172.19.0.3 dev eth0 lladdr 02:42:ac:12:00:03 REACHABLE 
172.19.0.4 dev eth0 lladdr 02:42:ac:12:00:04 REACHABLE 
172.19.0.5 dev eth0 lladdr 02:42:ac:12:00:05 DELAY 
172.19.255.200 dev eth0 lladdr 02:42:ac:12:00:04 STALE 
172.19.255.201 dev eth0 lladdr 02:42:ac:12:00:04 STALE 
172.19.255.202 dev eth0 lladdr 02:42:ac:12:00:02 STALE 

# mac 주소에 매칭되는 IP(노드) 찾기
kubectl get node -owide 


# (옵션) 노드에서 ARP 패킷 캡쳐 확인
docker exec -it myk8s-control-plane tcpdump -i eth0 -nn arp
docker exec -it myk8s-worker        tcpdump -i eth0 -nn arp
docker exec -it myk8s-worker2       tcpdump -i eth0 -nn arp
docker exec -it myk8s-worker3       tcpdump -i eth0 -nn arp


# (옵션) metallb-speaker 파드 로그 확인
kubectl logs -n metallb-system -l app=metallb -f
kubectl logs -n metallb-system -l component=speaker --since 1h
kubectl logs -n metallb-system -l component=speaker -f

# (옵션) kubectl krew 플러그인 stern 설치 후 아래 명령 사용 가능
kubectl stern -n metallb-system -l app=metallb
kubectl stern -n metallb-system -l component=speaker --since 1h
kubectl stern -n metallb-system -l component=speaker # 기본 설정이 follow
kubectl stern -n metallb-system speaker  # 매칭 사용 가능

✅ 03. 서비스 접속 테스트

이제 드디어 k8s 클러스터 외부에서 노드의 IP를 감추고(?) VIP/Port 를 통해 k8s 클러스터 내부의 애플리케이션에 접속 할 수 있습니다!

클라이언트(mypc, mypc2) → 서비스(External-IP) 접속 테스트
출처 - CloudNet@

# 현재 SVC EXTERNAL-IP를 변수에 지정
SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC2EXIP=$(kubectl get svc svc2 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
SVC3EXIP=$(kubectl get svc svc3 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $SVC1EXIP $SVC2EXIP $SVC3EXIP

# mypc/mypc2 에서 접속 테스트
docker exec -it mypc2 curl -s $SVC1EXIP
docker exec -it mypc2 curl -s $SVC1EXIP | grep Hostname
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc2 curl -s $i | grep Hostname ; done
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do echo ">> Access Service External-IP : $i <<" ; docker exec -it mypc2 curl -s $i | grep Hostname ; echo ; done

## RemoteAddr 주소는 어떻게 나오나요? 왜 그럴까요?
##  NodePort 기본 동작과 동일하게 인입한 노드의 인터페이스로 SNAT 되어서 최종 파드로 전달됨
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do echo ">> Access Service External-IP : $i <<" ;docker exec -it mypc curl -s $i | egrep 'Hostname|RemoteAddr|Host:' ; echo ; done


# 부하분산 접속됨
docker exec -it mypc2 zsh -c "for i in {1..100}; do curl -s $SVC1EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc2 zsh -c "for i in {1..100}; do curl -s $SVC2EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
docker exec -it mypc2 zsh -c "for i in {1..100}; do curl -s $SVC3EXIP | grep Hostname; done | sort | uniq -c | sort -nr"

# 지속적으로 반복 접속
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC1EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC2EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC3EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"


# LoadBalancer Type은 기본값으로 NodePort 포함. NodePort 서비스는 ClusterIP 를 포함
# NodePort:PORT 및 CLUSTER-IP:PORT 로 접속 가능!
kubectl get svc svc1
NAME   TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)        AGE
svc1   LoadBalancer   10.200.1.82   172.18.255.202   80:30246/TCP   49m

# 컨트롤노드에서 각각 접속 확인 실행 해보자
docker exec -it myk8s-control-plane curl -s 127.0.0.0:30246 # NodePor Type
docker exec -it myk8s-control-plane curl -s 10.200.1.82     # ClusterIP Tpye

클라이언트 → 서비스(External-IP) 접속 시 : 리더 파드가 존재하는 노드 인입 후 Service 에 매칭된 iptables rules 에 따라 랜덤 부하 분산되어서(SNAT) 파드로 접속됩니다.


✅ 04. Failover 테스트

장애 발생 시 동작
출처 - CloudNet@


위 그림처럼 장애 발생 전 워커노드1의 스피커파드가 SVC1, SVC3 의 리더 스피커 파드 역할을 하고 있는 상태에서, 워커노드1에 장애가 발생하게 되면 남아있는 스피커 파드들은 워커노드1의 장애를 인지하게 됩니다. 이후 장애가 발생한 스피커파드가 소유한 서비스의 ExternalIP 에 대한 리더 파드를 다시 선출하게 됩니다. 예를 들면 워커노드2가 SVC1, SVC3에 대한 리더 스피커파드가 선출이 되게 되면, 해당 파드는 GARP 로 자신이 SVC1, SVC3 ExternalIP에 대한 소유라고 전파를 하게 됩니다.

다만 장애 발생 시 문제가 있다는 인지를 확인하는 시간과 ARP 정보가 전파되는 시간이 필요하며 대략 20초~1분 이내의 장애 지속 시간이 발생하게 됩니다.

워커노드 중 1대를 중지하여 장애를 발생시키고, 장애시간을 확인해보겠습니다.

MetalLB Layer2 모드의 경우 노드 장애나 MetalLB 파드에 장애가 발생 시 10초~1분 정도의 장애 지속 시간이 소요됩니다. 만약 실무에서 MetalLB Layer2 모드 사용 시, 해당 장애 지속 시간이 운영하고 있는 애플리케이션 동작에 문제가 없는 지 꼭 사전 장애 관련 테스트를 진행하셔서, 최적화 및 장애 대응 방안을 수립하시기를 권장합니다.


리더 Speaker 파드가 존재하는 노드를 재부팅!
→ curl 접속 테스트 시 10~20초 정도의 장애 시간이 발생하였다
→ 이후 자동 원복 되며, 원복 시 5초 정도 장애 시간 발생!

# 사전 준비
## 지속적으로 반복 접속
SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
docker exec -it mypc2 zsh -c "while true; do curl -s --connect-timeout 1 $SVC1EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"

## 상태 모니터링
watch -d kubectl get pod,svc,ep

## 실시간 로그 확인
kubectl logs -n metallb-system -l app=metallb -f
혹은
kubectl stern -n metallb-system -l app=metallb


# 장애 재연
## 리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)를 중지
docker stop <svc1 번 리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)> --signal 9
docker stop myk8s-worker --signal 9
혹은
docker stop <svc1 번 리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)> --signal 15
docker stop myk8s-worker --signal 15

docker ps -a
docker ps -a | grep worker$

## 지속적으로 반복 접속 상태 모니터링
### curl 연속 접속 시도 >> 대략 10초 이내에 정상 접근 되었지만, 20초까지는 불안정하게 접속이 되었다
### 실제로는 다른 노드의 speaker 파드가 리더가 되고, 이후 다시 노드(컨테이너)가 정상화되면, 다시 리더 speaker 가 됨
docker exec -it mypc zsh -c "while true; do curl -s --connect-timeout 1 $SVC1EXIP | egrep 'Hostname|RemoteAddr'; date '+%Y-%m-%d %H:%M:%S' ; echo ;  sleep 1; done"
Hostname: webpod1
RemoteAddr: 172.18.0.2:25432
2024-09-29 06:31:07

2024-09-29 06:31:09

Hostname: webpod2
RemoteAddr: 172.18.0.2:26011
2024-09-29 06:31:10

2024-09-29 06:31:12

2024-09-29 06:31:14
...

# 변경된 리더 Speaker 파드 확인
# mypc/mypc2 에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾기
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done

# mypc/mypc2 에서 arp 테이블 정보 확인 >> SVC IP별로 리더 파드(스피커) 역할의 노드를 확인!
docker exec -it mypc ip -c neigh | sort
kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기


# 장애 원복(노드 정상화)
## 노드(실제 컨테이너) 정상화 
docker start <svc1 번 리더 Speaker 파드가 존재하는 노드(실제는 컨테이너)>
docker start myk8s-worker

# 변경된 리더 Speaker 파드 확인
# mypc/mypc2 에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾기
for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done

# mypc/mypc2 에서 arp 테이블 정보 확인 >> SVC IP별로 리더 파드(스피커) 역할의 노드를 확인!
docker exec -it mypc ip -c neigh | sort
kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기

👉 Step 03. IPVS Proxy 모드

✅ 00. 실습 준비

실습을 위한 K8S 설치
실습 환경은 K8S v1.31.0 , CNI(Kindnet, Direct Routing mode) , IPVS proxy mode

  • 노드(실제로는 컨테이너) 네트워크 대역 : 172.18.0.0/16
  • 파드 사용 네트워크 대역 : 10.10.0.0/16 ⇒ 각각 10.10.1.0/24, 10.10.2.0/24, 10.10.3.0/24, 10.10.4.0/24
  • 서비스 사용 네트워크 대역 : 10.200.1.0/24
# 파일 작성
cat <<EOT> kind-svc-2w-ipvs.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
  "MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
    topology.kubernetes.io/zone: ap-northeast-2a
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  - containerPort: 30003
    hostPort: 30003
  - containerPort: 30004
    hostPort: 30004
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
    controllerManager:
      extraArgs:
        bind-address: 0.0.0.0
    etcd:
      local:
        extraArgs:
          listen-metrics-urls: http://0.0.0.0:2381
    scheduler:
      extraArgs:
        bind-address: 0.0.0.0
  - |
    kind: KubeProxyConfiguration
    metricsBindAddress: 0.0.0.0
    ipvs:
      strictARP: true
- role: worker
  labels:
    mynode: worker1
    topology.kubernetes.io/zone: ap-northeast-2a
- role: worker
  labels:
    mynode: worker2
    topology.kubernetes.io/zone: ap-northeast-2b
- role: worker
  labels:
    mynode: worker3
    topology.kubernetes.io/zone: ap-northeast-2c
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
  kubeProxyMode: "ipvs"        
EOT

# k8s 클러스터 설치
kind create cluster --config kind-svc-2w-ipvs.yaml --name myk8s --image kindest/node:v1.31.0
docker ps

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools dnsutils ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y'
for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools dnsutils ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done


# kube-proxy configmap 확인
kubectl describe cm -n kube-system kube-proxy
...
mode: ipvs
ipvs: # 아래 각각 옵션 의미 조사해보자!
  excludeCIDRs: null
  minSyncPeriod: 0s
  scheduler: ""
  strictARP: true  # MetalLB 동작을 위해서 true 설정 변경 필요
  syncPeriod: 0s
  tcpFinTimeout: 0s
  tcpTimeout: 0s
  udpTimeout: 0s
...

# strictARP: true는 ARP 패킷을 보다 엄격하게 처리하겠다는 설정입니다.
## IPVS 모드에서 strict ARP가 활성화되면, 노드의 인터페이스는 자신에게 할당된 IP 주소에 대해서만 ARP 응답을 보내게 됩니다. 
## 이는 IPVS로 로드밸런싱할 때 ARP 패킷이 잘못된 인터페이스로 전달되는 문제를 방지합니다.
## 이 설정은 특히 클러스터 내에서 여러 노드가 동일한 IP를 갖는 VIP(Virtual IP)를 사용하는 경우 중요합니다.


# 노드 별 네트워트 정보 확인 : kube-ipvs0 네트워크 인터페이스 확인
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c route; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -br -c addr show kube-ipvs0; echo; done
>> node myk8s-control-plane <<
kube-ipvs0       DOWN           10.200.1.1/32 10.200.1.10/32 

>> node myk8s-worker <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

>> node myk8s-worker2 <<
kube-ipvs0       DOWN           10.200.1.1/32 10.200.1.10/32 

>> node myk8s-worker3 <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -d -c addr show kube-ipvs0; echo; done
>> node myk8s-control-plane <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether 9e:1d:ca:21:c6:d1 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

>> node myk8s-worker <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether fa:21:0a:00:a7:6c brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

>> node myk8s-worker2 <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether ba:e9:75:56:db:00 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

>> node myk8s-worker3 <<
11: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default 
    link/ether a2:1d:9c:e6:ad:84 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0 
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536 
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

# kube-ipvs0 에 할당된 IP(기본 IP + 보조 IP들) 정보 확인 
kubectl get svc,ep -A
NAMESPACE     NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.200.1.1    <none>        443/TCP                  3m8s
kube-system   kube-dns     ClusterIP   10.200.1.10   <none>        53/UDP,53/TCP,9153/TCP   3m7s

# ipvsadm 툴로 부하분산 되는 정보 확인 : 서비스의 IP와 서비스에 연동되어 있는 파드의 IP 를 확인
## Service IP(VIP) 처리를 ipvs 에서 담당 -> 이를 통해 iptables 에 체인/정책이 상당 수준 줄어듬
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln ; echo; done

## IPSET 확인
docker exec -it myk8s-worker ipset -h
docker exec -it myk8s-worker ipset -L


# iptables 정보 확인 : 정책 갯수를 iptables proxy 모드와 비교해보자
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-control-plane  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker  iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker2 iptables -t $i -S ; echo; done
for i in filter nat mangle raw ; do echo ">> IPTables Type : $i <<"; docker exec -it myk8s-worker3 iptables -t $i -S ; echo; done

# 각 노드 bash 접속
docker exec -it myk8s-control-plane bash
docker exec -it myk8s-worker bash
docker exec -it myk8s-worker2 bash
docker exec -it myk8s-worker3 bash
----------------------------------------
exit
----------------------------------------


# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 직접 지정 혹은 IP 지정 없이 배포
docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity
혹은
docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity

docker ps

✅ 01. IPVS 소개

IPVS 소개(정보 수집)

  • IPVS 는 리눅스 커널에서 동작하는 소프트웨어 로드밸런서이다. 백엔드(플랫폼)으로 Netfilter 를 사용하며, TCP/UDP 요청을 처리 할 수 있다.
  • iptables 의 rule 기반 처리의 성능 한계와 분산 알고리즘이 없어서, 최근에는 대체로 IPVS 를 사용한다.

Netfilter Hook Function : 6개의 Netfilter Hook Function 을 사용 → Local Hook 사용 이유는 IPVS Dummy Interface 사용하기 때문
출처 - https://ssup2.github.io/theory_analysis/Linux_LVS_IPVS/

부하 분산 스케줄링 : kube-proxy 파라미터에 설정 적용 -ipvs-scheduler
출처 - https://net711.tistory.com/entry/lvs-리눅스-l4-만들기

IPSET


IPVS 정보 확인

# kube-proxy 로그 확인 :  기본값 부하분산 스케줄러(RoundRobin = RR)
kubectl stern -n kube-system -l k8s-app=kube-proxy --since 2h | egrep '(ipvs|IPVS)'
...
kube-proxy-2qmw7 kube-proxy I0929 07:41:23.512897       1 server_linux.go:230] "Using ipvs Proxier"
kube-proxy-2qmw7 kube-proxy I0929 07:41:23.514074       1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv4"
kube-proxy-2qmw7 kube-proxy I0929 07:41:23.514157       1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv6"
kube-proxy-v5lcv kube-proxy I0929 07:41:23.523480       1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv6"

# 기본 모드 정보 확인
kubectl get cm -n kube-system kube-proxy -o yaml | egrep 'mode|strictARP|scheduler'
      scheduler: ""
      strictARP: true
    mode: ipvs

# ipvsadm 툴로 부하분산 되는 정보 확인 : RR 부하분산 스케줄러 확인
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln ; echo; done
...
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.1:443 rr
  -> 172.18.0.2:6443              Masq    1      3          0    
...

# 커널 파라미터 확인
# (심화 옵션) strictARP - 링크 설정(유사한)이유
# --ipvs-strict-arp : Enable strict ARP by setting arp_ignore to 1 and arp_announce to 2
# arp_ignore : ARP request 를 받았을때 응답 여부 - 0(ARP 요청 도착시, any Interface 있으면 응답), 1(ARP 요청을 받은 Interface 가 해당 IP일때만 응답)
# arp_announce : ARP request 를 보낼 때 'ARP Sender IP 주소'에 지정 값 - 0(sender IP로 시스템의 any IP 가능), 2(sender IP로 실제 전송하는 Interface 에 IP를 사용)
docker exec -it myk8s-worker tree /proc/sys/net/ipv4/conf/kube-ipvs0
docker exec -it myk8s-worker cat /proc/sys/net/ipv4/conf/kube-ipvs0/arp_ignore
docker exec -it myk8s-worker cat /proc/sys/net/ipv4/conf/kube-ipvs0/arp_announce

# all 은 모든 인터페이스에 영항을 줌, 단 all 과 interface 값이 다를때 우선순위는 커널 파라미터 별로 다르다 - 링크
docker exec -it myk8s-worker sysctl net.ipv4.conf.all.arp_ignore
docker exec -it myk8s-worker sysctl net.ipv4.conf.all.arp_announce
docker exec -it myk8s-worker sysctl net.ipv4.conf.kube-ipvs0.arp_ignore
docker exec -it myk8s-worker sysctl net.ipv4.conf.kube-ipvs0.arp_announce
docker exec -it myk8s-worker sysctl -a | grep arp_ignore
docker exec -it myk8s-worker sysctl -a | grep arp_announce

# IPSET 확인
docker exec -it myk8s-worker ipset -h
docker exec -it myk8s-worker ipset -L


✅ 02. 실습 진행

목적지(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

실습 도식화
출처 - https://kschoi728.tistory.com/265

생성 및 확인 : IPVS Proxy 모드

# 생성
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
kubectl describe svc svc-clusterip
kubectl get endpoints svc-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip

# 노드 별 네트워트 정보 확인 : kube-ipvs0 네트워크 인터페이스 확인
## ClusterIP 생성 시 kube-ipvs0 인터페이스에 ClusterIP 가 할당되는 것을 확인
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -br -c addr show kube-ipvs0; echo; done
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -d -c addr show kube-ipvs0; echo; done

# 변수 지정
CIP=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.clusterIP}")
CPORT=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.ports[0].port}")
echo $CIP $CPORT

# ipvsadm 툴로 부하분산 되는 정보 확인
## 10.200.1.216(TCP 9000) 인입 시 3곳의 목적지로 라운드로빈(rr)로 부하분산하여 전달됨을 확인 : 모든 노드에서 동일한 IPVS 분산 설정 정보 확인
## 3곳의 목적지는 각각 서비스에 연동된 목적지 파드 3개이며, 전달 시 출발지 IP는 마스커레이딩 변환 처리
docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT ; echo; done

# ipvsadm 툴로 부하분산 되는 현재 연결 정보 확인 : 추가로 --rate 도 있음
docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --stats
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT --stats ; echo; done

docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --rate
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT --rate ; echo; done


# iptables 규칙 확인 : ipset list 를 활용
docker exec -it myk8s-control-plane iptables -t nat -S | grep KUBE-CLUSTER-IP

# ipset list 정보를 확인 : KUBE-CLUSTER-IP 이름은 아래 6개의 IP:Port 조합을 지칭
# 예를 들면 ipset list 를 사용하지 않을 경우 6개의 iptables 규칙이 필요하지만, ipset 사용 시 1개의 규칙으로 가능
docker exec -it myk8s-control-plane ipset list KUBE-CLUSTER-IP
Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 7
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x6343ff52
Size in memory: 456
References: 3
Number of entries: 5
Members:
10.200.1.1,tcp:443
10.200.1.10,tcp:53
10.200.1.10,udp:53
10.200.1.245,tcp:9000
10.200.1.10,tcp:9153

IPVS 정보 확인 및 서비스 접속 확인

#
for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT ; echo; done

# 변수 지정
CIP=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.clusterIP}")
CPORT=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.ports[0].port}")
echo $CIP $CPORT

# 컨트롤플레인 노드에서 ipvsadm 모니터링 실행 : ClusterIP 접속 시 아래 처럼 연결 정보 확인됨
watch -d "docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --stats; echo; docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --rate"

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

# 서비스 IP 변수 지정 : svc-clusterip 의 ClusterIP주소
SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
echo $SVC1

# TCP 80,9000 포트별 접속 확인 : 출력 정보 의미 확인
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) 부하분산 접속 확인 : 부하분산 비률 확인
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"

# 반복 접속
kubectl exec -it net-pod -- zsh -c "while true; do curl -s --connect-timeout 1 $SVC1:9000 | egrep 'Hostname|RemoteAddr|Host:'; date '+%Y-%m-%d %H:%M:%S' ; echo '--------------' ;  sleep 1; done"



📌 Conclusion

이번 스터디를 통해 Kubernetes에서 클러스터 내부 서비스를 외부로 노출하는 다양한 방법과 그 작동 원리에 대해 깊이 있게 이해할 수 있었습니다. 특히 ClusterIP, NodePort, LoadBalancer 서비스 타입을 중심으로 실제 실습을 통해 통신 흐름과 iptables, IPVS 규칙이 어떻게 구성되고 동작하는지 분석하였습니다.

ClusterIP 서비스를 통해 클러스터 내부에서만 접근 가능한 서비스를 생성하고, iptables의 규칙을 통해 파드로 트래픽이 분산되는 과정을 확인하였습니다. 또한 NodePort와 LoadBalancer 서비스를 사용하여 클러스터 외부에서의 접근 방식을 실습하고, externalTrafficPolicy 설정을 통해 클라이언트의 IP를 보존하는 방법을 학습하였습니다.

MetalLB를 활용하여 온프레미스 환경에서도 Kubernetes 로드 밸런서를 구현할 수 있었으며, 이를 통해 고가용성 네트워크를 구성하고 장애 대응 방법을 탐구하였습니다. 이러한 과정에서 Kubernetes의 네트워킹이 내부적으로 어떻게 트래픽을 처리하고 분산하는지 명확히 이해할 수 있었습니다.



🔗 Reference

profile
안녕하세요! DevOps 엔지니어 이재찬입니다. 블로그에 대한 피드백은 언제나 환영합니다! 기술, 개발, 운영에 관한 다양한 주제로 함께 나누며, 더 나은 협업과 효율적인 개발 환경을 만드는 과정에 대해 인사이트를 나누고 싶습니다. 함께 여행하는 기분으로, 즐겁게 읽어주시면 감사하겠습니다! 🚀

0개의 댓글

관련 채용 정보