[KANS 3기] LoadBalancer(MetalLB), IPVS
들어가며
이번 주에는 LoadBalancer 서비스와 MetalLB, 그리고 kube-proxy의 모드중 하나인 IPVS에 대해 알아보겠습니다. KANS 3기 5주차 스터디를 시작하겠습니다.
LoadBalancer 서비스
LoadBalancer란?
- LoadBalancer는 Kubernetes의 Service 유형의 하나로, 클러스터 외부에서 클러스터 내부의 서비스에 접근할 수 있도록 서비스를 노출시키는 역할을 합니다.
- Kubernetes에서는 자체적으로 LoadBalancer를 제공하지 않고, 클라우드 서비스 제공업체의 LoadBalancer(AWS의 ALB, NLB), LoadBalancer 하드웨어 장비(Citrix, F5 networks), 또는 오픈소스 LoadBalancer (MetalLB 등)를 사용합니다.
- 기본적으로 LoadBalancer를 사용하면 NodePort를 먼저 생성한 다음 LoadBalancer와 연결해야 하지만 (NodePort 접근 방식), 구성에 따라 NodePort 없이 바로 LoadBalancer를 생성할 수도 (Pod Direct 접근 방식) 있습니다.
환경별 LoadBalancer
환경별 LoadBalancer 비교
클라우드 서비스 제공업체의 LoadBalancer
- 클라우드 서비스 제공업체의 LoadBalancer는 클라우드 서비스 제공업체가 제공하는 서비스로, 클라우드 서비스 제공업체의 LoadBalancer를 사용하면 클라우드 서비스 제공업체의 LoadBalancer를 통해 클러스터 외부에서 클러스터 내부의 서비스에 접근할 수 있습니다.
- 하지만 클라우드 서비스 제공업체마다 동작 방식과 기능이 다르기 때문에 각 클라우드 서비스 제공업체의 LoadBalancer를 사용할 때는 해당 클라우드 서비스 제공업체의 LoadBalancer의 동작 방식과 기능을 확인해야 합니다.
- 대표적인 클라우드 서비스 제공업체인 Amazon Web Service는 다음의 LoadBalancer를 제공합니다.
- Classic Load Balancer (CLB) : 가장 오래된 로드밸런서로 NLB, ALB보다 기능이 적습니다.
- Network Load Balancer (NLB) : Layer 4 계층의 네트워크 로드밸런서로 TCP/UDP/TLS 트래픽을 지원합니다. CLB/ALB에 비해서 처리속도가 빠릅니다. (Application Load Balancer (ALB)는 Layer 7 계층의 애플리케이션 로드밸런서로 http/https/gRPC 트래픽을 지원합니다. ALB는 Ingress시 생성됩니다.)
클라우드 서비스 제공업체의 LoadBalancer 서비스 동작 방식
- NodePort 접근 방식
- 외부 클라이언트는 LoadBalancer의 IP 주소로 요청을 보내면 LoadBalancer는 요청을 받아서 노드들의 NodePort로 부하를 분산하여 전달합니다.
- 이때 NodePort로 인입 후에 iptables를 통해 파드로 랜덤 부하분산을 통해 전달합니다.
- 이 과정에서 DNAT를 통한 부하 분산과정이 두번 수행됩니다. (LoadBalancer에서 NodePort로 전달될때, 노드의 iptables 룰로 파드 IP로 전달될때)
- Pod Direct 접근 방식
- LoadBalancer에서 파드의 IP로 직접 부하분산해서 전달합니다.
- LoadBalancer가 파드의 IP 정보를 알기 위해서, 별도의 LoadBalancer Controller를 구성하고 LoadBalancer Controller가 LoadBalancer에게 파드의 IP를 동적으로 전달합니다.
- 이 과정에서 부하 분산과정이 한번 수행되며 NodePort 방식 보다 효율 적입니다.
클라우드의 LoadBalancer 제공 방식 비교
온프레미스 환경에서의 LoadBalancer
하드웨어 장비 기반 LoadBalancer 서비스 동작 방식
- 하드웨어 장비 기반 LoadBalancer는 AWS LoadBalancer 서비스와 거의 동일하게 별도의 장비로 접속 후 노드에 NodePort 혹은 파드로 직접 전달하여 통신할 수 있습니다.
- 대표적으로 Citrix, F5 Networks의 제품 등이 있습니다.
- 예시) Citrix ADC for K8S - 링크 & Citrix ADC(Ingress/Service) with k8s - 링크
소프트웨어 기반 LoadBalancer 서비스 동작 방식
- 소프트웨어 기반 LoadBalancer는 별도의 네트워크 장비 없이 소프트웨어로 동작합니다.
- 대표적으로 MetalLB, OpenELB, PubeLB, kube-vip, LoxiLB 등이 있습니다.
- MetalLB에 대해서는 좀 더 자세히 알아보겠습니다.
MetalLB
- MetalLB는 BareMetalLoadBalancer의 약자로, 온프레미스 환경에서 사용할 수 있는 오픈소스 LoadBalancer입니다.
- 쿠버네티스는 DaemonSet으로 Speaker 파드를 생성하여 External IP를 전파합니다. External IP는 노드의 IP 대신 외부에서 접속할 수 있는 IP 입니다.
- 이를 통해 노드의 IP를 외부에 노출하지 않을 수 있어서 보안성을 높일 수 있습니다.
- Speaker 파드는 External IP 전파를 위해 표준 프로토콜인 ARP(Address Resolution Protocol) 혹은 BGP(Border Gateway Protocol)를 사용합니다.
- MetalLB는 일부 퍼블릭 클라우드 플랫폼 환경에서 동작하지 않습니다. 이유는 가상서버 IP에 매칭되는 MAC 주소가 아닌 IP에 대한 ARP 요청을 차단하기 때문입니다.
- 또한 일부 CNI에서의 동작에 이슈가 있습니다. Calico의 IPIP 모드에서 BGP 사용시 MetalLB의 BGP와 충돌이 생겨 문제가 발생하곤 합니다.
- 실무에서 사용시에는 이슈나 제약사항을 확인하고, 사전 테스트 진행후 사용할 필요가 있습니다.
Layer2 모드
- Layer2 모드는 ARP(Address Resolution Protocol)를 통해서 External IP를 전파합니다.
- ARP란?
ARP 동작 모식도 (출처)
- 동일 네트워크 내부에서 통신을 위해서는 상대방의 MAC(Media Access Control) 주소를 알아야 합니다.
- 이때 IP 주소를 전송하면서 이 IP의 주인의 MAC 주소를 알려달라는 패킷을 보내면, 해당 IP 주소를 가진 호스트에서 자신의 MAC 주소를 응답합니다.
- 이것이 ARP의 동작 방식이며, ARP 테이블에 IP와 MAC 주소를 저장하고, 이후 통신시 ARP 테이블을 참조하여 통신을 합니다.
- ARP에 대해서 알아보았으니 Layer2 동작에 대해 다시 알아보겠습니다.
MetalLB Layer2 동작 (출처: 추가예정)
- 위의 그림에서 호스트 NS/파드 NS의 NS는 네임스페이스를 의미하며, 여기서의 네임스페이스는 첫주차 컨테이너 격리에서 배웠던 Linux OS 차원의 네임스페이스를 의미합니다.
- 흐름을 파악해보면 아래와 같습니다.
- LoadBalancer 서비스 리소스 생성시 MetalLB 스피커 파드중에 리더(Leader) 스피커 파드가 선택됩니다. 리더 스피커 파드는
해당 LoadBalancer 서비스의 External IP를 가지고 ARP 응답을 합니다. 또한 GARP(Gratuitous ARP)를 통해 네트워크 내의 모든 호스트에게
해당 External IP의 MAC 주소를 전파합니다.
- 데몬셋으로 배포된 speaker 파드는
NetworkMode: host
로 호스트 네임스페이스를 공유하며, 호스트 네임스페이스에서 ARP 응답을 합니다. - 만약 리더 스피커 파드에 장애가 발생하면, 다른 스피커 파드가 리더 스피커 파드로 선출됩니다.
- 멤버 리스터 및 자애 발견은 hashicorp의 memberlist를 사용합니다.
- 데몬셋으로 배포된 speaker 파드는
- 클라이언트1이 SVC1의 External IP로 접속을 시도하면, 해당 트래픽은 SVC1의 External IP 정보를 전파하는 리더 스피커파드가 있는 노드1으로 전달됩니다. 또한 클라이언트2는 SVC2의 External IP로 접속을 시도하면, 해당 트래픽은 SVC2의 External IP 정보를 전파하는 리더 스피커파드가 있는 노드3로 전달됩니다.
- 노드에 도착한 트래픽은 해당 노드의 iptables를 통해 ClusterIP와 동일하게 해당 서비스에 연동된 엔드포인트 파드들로 (4) 랜덤 부하분산 되어 전달됩니다.
- LoadBalancer 서비스 리소스 생성시 MetalLB 스피커 파드중에 리더(Leader) 스피커 파드가 선택됩니다. 리더 스피커 파드는
해당 LoadBalancer 서비스의 External IP를 가지고 ARP 응답을 합니다. 또한 GARP(Gratuitous ARP)를 통해 네트워크 내의 모든 호스트에게
해당 External IP의 MAC 주소를 전파합니다.
- Layer2 모드의 단점
- single-node bottlenecking : 리더 스피커 파드가 있는 노드에만 트래픽이 인입되어 부하가 집중 됩니다.
- potentially slow failover : 리더 스피커 파드에 장애가 발생하면, 나머지 노드 리더가 선출되고, ARP 전파 및 갱신 완료전까지는 장애가 발생됩니다. (대략 10초~20초 소요)
BGP 모드
- BGP 모드는 Routing 프로토콜인 BGP(Border Gateway Protocol)를 통해서 External IP를 전파합니다.
- 기본은 IP주소(32bit)를 전파하며, 설정으로 축약된 네트워크 정보를 전파할 수 있습니다. (bgp-advertisements에 aggregation-length 설정)
- BGP 커뮤니티, localpref 등 다양한 BGP 속성을 사용할 수 있습니다.
- IP 주소의 마지막이 0과 255로 끝나는 IP를 처리 못하는 라우터 장비가 있는 경우
avoid-buggy-ips: true
설정을 통해 IP가 0과 255로 끝나는 IP를 사용하지 않도록 설정할 수 있습니다.
- 외부에서 라우터를 통해 ECMP(Equal Cost Multi Path) 라우팅을 통해 부하 분산을 지원합니다.
- 일반적으로 ECMP는 5-tuple(프로토콜, 출발지 IP, 목적지 IP, 출발지 포트, 목적지 포트)을 기반으로 동작합니다.
- 라우터 장비에 따라 다양한 라우팅(분산) 처리가 가능합니다.
- BGP 모드의 제한사항
- 라우터에서 서비스로 인입이 되기 때문에, 라우터 설정이 중요하며 네트워크 팀과 협업이 권장됩니다.
- Speaker 노드 파드 장애시 BGP Timer 설정 등, 구성하고 있는 네트워크 환경에 맞게 최적화 작업이 필요합니다.
- ECMP 부하 분산 접속시 특정 파드에 부하가 집중되거나, 세션 고정, flapping 등 다양한 환경에 대응이 필요합니다.
- BGP 라우팅 설정 및 라우팅 전파 관련 최적화 설정이 필요합니다.
MetalLB 실습
실습환경 준비
- 이번에도 KIND를 통해 실습을 진행해보겠습니다.
KIND 클러스터 구성
# kind 클러스터 설정 파일 작성
$ cat <<EOT> kind-svc-2w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
"InPlacePodVerticalScaling": true #실행 중인 파드의 리소스 요청 및 제한을 변경할 수 있게 합니다.
"MultiCIDRServiceAllocator": true #서비스에 대해 여러 CIDR 블록을 사용할 수 있게 합니다.
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: #API 서버에 추가 인수를 제공
runtime-config: api/all=true #모든 API 버전을 활성화
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
- 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 #파드 IP를 위한 CIDR 범위를 정의합니다. 파드는 이 범위에서 IP를 할당받습니다.
serviceSubnet: 10.200.1.0/24 #서비스 IP를 위한 CIDR 범위를 정의합니다. 서비스는 이 범위에서 IP를 할당받습니다.
EOT
# k8s 클러스터 설치
$ kind create cluster --config kind-svc-2w.yaml --name myk8s --image kindest/node:v1.31.0
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 83661e652fb1 kindest/node:v1.31.0 "/usr/local/bin/entr…" 39 seconds ago Up 34 seconds 0.0.0.0:30000-30004->30000-30004/tcp, 127.0.0.1:59215->6443/tcp myk8s-control-plane
# 242777ad8f3c kindest/node:v1.31.0 "/usr/local/bin/entr…" 39 seconds ago Up 34 seconds myk8s-worker
# f8022585c864 kindest/node:v1.31.0 "/usr/local/bin/entr…" 39 seconds ago Up 34 seconds myk8s-worker2
# 80988133cdfc kindest/node:v1.31.0 "/usr/local/bin/entr…" 39 seconds ago Up 34 seconds myk8s-worker3
# 노드에 기본 툴 설치
$ 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
# k8s v1.31.0 버전 확인
$ kubectl get node
# => NAME STATUS ROLES AGE VERSION
# myk8s-control-plane Ready control-plane 110s v1.31.0
# myk8s-worker Ready <none> 100s v1.31.0
# myk8s-worker2 Ready <none> 100s v1.31.0
# myk8s-worker3 Ready <none> 100s v1.31.0
# 노드 labels 확인
$ kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | jq
# => {
# "kubernetes.io/hostname": "myk8s-control-plane",
# "mynode": "control-plane",
# ...
# }
# {
# "kubernetes.io/hostname": "myk8s-worker",
# "mynode": "worker1",
# ...
# }
# {
# "kubernetes.io/hostname": "myk8s-worker2",
# "mynode": "worker2",
# ...
# }
# {
# "kubernetes.io/hostname": "myk8s-worker3",
# "mynode": "worker3",
# ...
# }
# kind network 중 컨테이너(노드) IP(대역) 확인
$ docker ps -q | xargs docker inspect --format ' '
# => /myk8s-control-plane 172.20.0.5
# /myk8s-worker 172.20.0.4
# /myk8s-worker2 172.20.0.2
# /myk8s-worker3 172.20.0.3
# 파드CIDR 과 Service 대역 확인 : CNI는 kindnet 사용
$ kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet
# => podSubnet: 10.10.0.0/16
# serviceSubnet: 10.200.1.0/24
$ 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",
# MultiCIDRServiceAllocator : https://kubernetes.io/docs/tasks/network/extend-service-ip-ranges/
$ kubectl get servicecidr
# => NAME CIDRS AGE
# kubernetes 10.200.1.0/24 4m59s
# 노드마다 할당된 dedicated subnet (podCIDR) 확인
$ kubectl get nodes -o jsonpath="{.items[*].spec.podCIDR}"
# => 10.10.0.0/24 10.10.3.0/24 10.10.2.0/24 10.10.1.0/24
# kube-proxy configmap 확인
$ kubectl describe cm -n kube-system kube-proxy
# => ...
# mode: iptables
# iptables:
# localhostNodePorts: null
# masqueradeAll: false
# masqueradeBit: null
# minSyncPeriod: 1s
# syncPeriod: 0s
# ...
# 노드 별 네트워트 정보 확인 : CNI는 kindnet 사용
$ for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i cat /etc/cni/net.d/10-kindnet.conflist; echo; done
# => >> node myk8s-control-plane <<
# {
# "cniVersion": "0.3.1",
# "name": "kindnet",
# "plugins": [
# {
# "type": "ptp",
# "ipMasq": false,
# "ipam": {
# "type": "host-local",
# "dataDir": "/run/cni-ipam-state",
# "routes": [
# { "dst": "0.0.0.0/0" }
# ],
# "ranges": [
# [ { "subnet": "10.10.0.0/24" } ]
# ]
# },
# "mtu": 1500
# },
# ...
# ]
# }
#
# >> node myk8s-worker <<
# ...
$ for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c route; echo; done
# => >> node myk8s-control-plane <<
# default via <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.0.2 </span>dev <span style="color:teal;">veth545bb56e </span>scope host
# <span style="color:purple;">10.10.0.3 </span>dev <span style="color:teal;">veth184fcd53 </span>scope host
# <span style="color:purple;">10.10.0.4 </span>dev <span style="color:teal;">vethc5dfe430 </span>scope host
# <span style="color:purple;">10.10.1.0/24 </span>via <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.2.0/24 </span>via <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.3.0/24 </span>via <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">172.20.0.0/16 </span>dev <span style="color:teal;">eth0 </span>proto kernel scope link src <span style="color:purple;">172.20.0.5 </span>
#
# >> node myk8s-worker <<
# default via <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.0.0/24 </span>via <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.1.0/24 </span>via <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.2.0/24 </span>via <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">172.20.0.0/16 </span>dev <span style="color:teal;">eth0 </span>proto kernel scope link src <span style="color:purple;">172.20.0.4 </span>
#
# >> node myk8s-worker2 <<
# default via <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.0.0/24 </span>via <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.1.0/24 </span>via <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.3.0/24 </span>via <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">172.20.0.0/16 </span>dev <span style="color:teal;">eth0 </span>proto kernel scope link src <span style="color:purple;">172.20.0.2 </span>
#
# >> node myk8s-worker3 <<
# default via <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.0.0/24 </span>via <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.2.0/24 </span>via <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.3.0/24 </span>via <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">172.20.0.0/16 </span>dev <span style="color:teal;">eth0 </span>proto kernel scope link src <span style="color:purple;">172.20.0.3 </span>
$ for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
# => >> node myk8s-control-plane <<
# 1: <span style="color:teal;">lo: </span><LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback <span style="color:olive;">00:00:00:00:00:00</span> brd <span style="color:olive;">00:00:00:00:00:00</span>
# inet <span style="color:purple;">127.0.0.1</span>/8 scope host lo
# valid_lft forever preferred_lft forever
# 2: <span style="color:teal;">tunl0@NONE: </span><NOARP> mtu 1480 qdisc noop state <span style="color:red;">DOWN </span>group default qlen 1000
# link/ipip <span style="color:olive;">0.0.0.0</span> brd <span style="color:olive;">0.0.0.0</span>
# 4: <span style="color:teal;">veth545bb56e@if4: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">0e:8b:3c:4f:43:43</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netns cni-98b7b37a-bb7a-ea56-47c9-ce3a0b1fb08a
# inet <span style="color:purple;">10.10.0.1</span>/32 scope global veth545bb56e
# valid_lft forever preferred_lft forever
# 5: <span style="color:teal;">vethc5dfe430@if4: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">8a:70:dd:42:02:96</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netns cni-79ddddfd-6177-bbd6-5fdc-3f7f6bf07fdc
# inet <span style="color:purple;">10.10.0.1</span>/32 scope global vethc5dfe430
# valid_lft forever preferred_lft forever
# 6: <span style="color:teal;">veth184fcd53@if4: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">8a:92:74:11:f9:d9</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netns cni-5e4ecb7e-2120-f372-76cc-9a467c85159b
# inet <span style="color:purple;">10.10.0.1</span>/32 scope global veth184fcd53
# valid_lft forever preferred_lft forever
# 28: <span style="color:teal;">eth0@if29: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">02:42:ac:14:00:05</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netnsid 0
# inet <span style="color:purple;">172.20.0.5</span>/16 brd <span style="color:purple;">172.20.255.255 </span>scope global eth0
# valid_lft forever preferred_lft forever
#
# >> node myk8s-worker <<
# 1: <span style="color:teal;">lo: </span><LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback <span style="color:olive;">00:00:00:00:00:00</span> brd <span style="color:olive;">00:00:00:00:00:00</span>
# inet <span style="color:purple;">127.0.0.1</span>/8 scope host lo
# valid_lft forever preferred_lft forever
# 2: <span style="color:teal;">tunl0@NONE: </span><NOARP> mtu 1480 qdisc noop state <span style="color:red;">DOWN </span>group default qlen 1000
# link/ipip <span style="color:olive;">0.0.0.0</span> brd <span style="color:olive;">0.0.0.0</span>
# 26: <span style="color:teal;">eth0@if27: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">02:42:ac:14:00:04</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netnsid 0
# inet <span style="color:purple;">172.20.0.4</span>/16 brd <span style="color:purple;">172.20.255.255 </span>scope global eth0
# valid_lft forever preferred_lft forever
# ...
# 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
# 각 노드 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
# ----------------------------------------
# kind 설치 시 kind 이름의 도커 브리지가 생성된다 : 172.20.0.0/16 대역
$ docker network ls
# => NETWORK ID NAME DRIVER SCOPE
# a8d530305515 bridge bridge local
# 8204a0851463 host host local
# 3bbcc6aa8f38 kind bridge local
$ docker inspect kind
# => [
# {
# "Name": "kind",
# "Id": "3bbcc6aa8f388f86f02478f41de1e4dd917e5812b6cf6257972e4af0bedf5021",
# "Created": "2020-01-01T11:37:09.195259833Z",
# "Scope": "local",
# "Driver": "bridge",
# "IPAM": {
# "Driver": "default",
# "Options": {},
# "Config": [
# {
# "Subnet": "172.20.0.0/16",
# "Gateway": "172.20.0.1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {
# "242777ad8f3c7009963155c3d7c4551e1407570d6986d9ef6346e6d33990e538": {
# "Name": "myk8s-worker",
# "EndpointID": "f6fb304fa38125ed1075d9c71b83559cff5066e71630c272e94311258021144e",
# "MacAddress": "02:42:ac:14:00:04",
# "IPv4Address": "172.20.0.4/16",
# },
# "80988133cdfcfaafe520b35cec924b9fa87f26ea474102b833e92d7ca693fb2b": {
# "Name": "myk8s-worker3",
# "EndpointID": "42aec973b496fdc7b8ede07c11fd94fe35631216d3ecd54d2ab794849b834787",
# "MacAddress": "02:42:ac:14:00:03",
# "IPv4Address": "172.20.0.3/16",
# },
# "83661e652fb1d34542b760209f670f330e25b1c51c8c0404e69d47eb9c79f407": {
# "Name": "myk8s-control-plane",
# "EndpointID": "d1e1efb2d90b7d8e9ce16b6274a62e7799d923681739dc8826c36c8b122d09c0",
# "MacAddress": "02:42:ac:14:00:05",
# "IPv4Address": "172.20.0.5/16",
# },
# "f8022585c864bd53b31b84e22e2b4381da6c5b7a2ada1583f18136e7f8c6b3b9": {
# "Name": "myk8s-worker2",
# "EndpointID": "0866892fb0c2c1c8c2021d92a665a130a20aae5b76fbdc1549138da642a60883",
# "MacAddress": "02:42:ac:14:00:02",
# "IPv4Address": "172.20.0.2/16",
# }
# },
# "Options": {
# "com.docker.network.bridge.enable_ip_masquerade": "true",
# "com.docker.network.driver.mtu": "1500"
# },
# "Labels": {}
# }
# ]
# arp scan 해두기
$ docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet
# => Interface: eth0, type: EN10MB, MAC: 02:42:ac:14:00:05, IPv4: 172.20.0.5
# Starting arp-scan 1.10.0 with 65536 hosts (https://github.com/royhills/arp-scan)
# 172.20.0.1 02:42:a0:b9:45:0f (Unknown: locally administered)
# 172.20.0.2 02:42:ac:14:00:02 (Unknown: locally administered)
# 172.20.0.3 02:42:ac:14:00:03 (Unknown: locally administered)
# 172.20.0.4 02:42:ac:14:00:04 (Unknown: locally administered)
# mypc 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 지정 없이 혹은 지정 해서 사용
$ docker run -d --rm --name mypc --network kind --ip 172.20.0.100 nicolaka/netshoot sleep infinity # IP 지정 실행 시
# => docker: Error response from daemon: Invalid address 172.20.0.100: It does not belong to any of this network's subnets.
# IP 지정 실행 시 에러 발생 시 아래 처럼 IP 지정 없이 실행
$ docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity # IP 지정 없이 실행 시
# => 5863ee53a7334a4a524c8c965b2505237c43037ff33f435340b6c167e3484eb6
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 5863ee53a733 nicolaka/netshoot "sleep infinity" 15 seconds ago Up 14 seconds mypc
# ...
# mypc2 컨테이너 기동 : kind 도커 브리지를 사용하고, 컨테이너 IP를 지정 없이 혹은 지정 해서 사용
$ docker run -d --rm --name mypc2 --network kind --ip 172.20.0.200 nicolaka/netshoot sleep infinity # IP 지정 실행 시
# => docker: Error response from daemon: Invalid address 172.20.0.200: It does not belong to any of this network's subnets.
# IP 지정 실행 시 에러 발생 시 아래 처럼 IP 지정 없이 실행
$ docker run -d --rm --name mypc2 --network kind nicolaka/netshoot sleep infinity # IP 지정 없이 실행 시
# => 0d1d3bc32161bafcf5e188e4788553c88cd278d0a2e8dac02d42216e80a9985c
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 0d1d3bc32161 nicolaka/netshoot "sleep infinity" 9 seconds ago Up 7 seconds mypc2
# 5863ee53a733 nicolaka/netshoot "sleep infinity" About a minute ago Up About a minute mypc
# ...
# kind network 중 컨테이너(노드) IP(대역) 확인
$ docker ps -q | xargs docker inspect --format ' '
# => /myk8s-control-plane 172.20.0.5
# /myk8s-worker 172.20.0.4
# /myk8s-worker2 172.20.0.2
# /myk8s-worker3 172.20.0.3
# /mypc 172.20.0.6
# /mypc2 172.20.0.7
# kube-ops-view 설치
$ helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
$ helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=NodePort,service.main.ports.http.nodePort=30000 --set env.TZ="Asia/Seoul" --namespace kube-system
# => NAME: kube-ops-view
# LAST DEPLOYED: Sun Jan 1 15:57:45 2020
# NAMESPACE: kube-system
# STATUS: deployed
# REVISION: 1
# TEST SUITE: None
# NOTES:
# 1. Get the application URL by running these commands:
# export NODE_PORT=$(kubectl get --namespace kube-system -o jsonpath="{.spec.ports[0].nodePort}" services kube-ops-view)
# export NODE_IP=$(kubectl get nodes --namespace kube-system -o jsonpath="{.items[0].status.addresses[0].address}")
# echo http://$NODE_IP:$NODE_PORT
# myk8s-control-plane 배치하기 위해서 nodeSelector, tolerations 설정
$ kubectl -n kube-system edit deploy kube-ops-view
# => ---
# spec:
# ...
# template:
# ...
# spec:
# nodeSelector:
# mynode: control-plane
# tolerations:
# - key: "node-role.kubernetes.io/control-plane"
# operator: "Equal"
# effect: "NoSchedule"
# ---
# 설치 확인
$ kubectl -n kube-system get pod -o wide -l app.kubernetes.io/instance=kube-ops-view
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# kube-ops-view-58f96c464d-kp8l8 0/1 ContainerCreating 0 5s <none> <span style="color: red;">myk8s-control-plane</span> <none> <none>
# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : macOS 사용자
$ echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5"
# => KUBE-OPS-VIEW URL = http://localhost:30000/#scale=1.5
$ echo -e "KUBE-OPS-VIEW URL = http://localhost:30000/#scale=2"
# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : Windows 사용자
$ echo -e "KUBE-OPS-VIEW URL = http://192.168.50.10:30000/#scale=1.5"
$ echo -e "KUBE-OPS-VIEW URL = http://192.168.50.10:30000/#scale=2"
# kube-ops-view 접속 URL 확인 (1.5 , 2 배율) : AWS_EC2 사용자
$ echo -e "KUBE-OPS-VIEW URL = http://$(curl -s ipinfo.io/ip):30000/#scale=1.5"
$ echo -e "KUBE-OPS-VIEW URL = http://$(curl -s ipinfo.io/ip):30000/#scale=2"
실습환경이 구축 완료된 kube-ops-view 화면
프로메테우스 스택 설치
#
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# => "prometheus-community" has been added to your repositories
# 파라미터 파일 생성
$ cat <<EOT > monitor-values.yaml
prometheus:
service:
type: NodePort
nodePort: 30001
prometheusSpec:
podMonitorSelectorNilUsesHelmValues: false
serviceMonitorSelectorNilUsesHelmValues: false
nodeSelector:
mynode: control-plane
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Equal"
effect: "NoSchedule"
grafana:
defaultDashboardsTimezone: Asia/Seoul
adminPassword: kans1234
service:
type: NodePort
nodePort: 30002
nodeSelector:
mynode: control-plane
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Equal"
effect: "NoSchedule"
# sidecar:
# dashboards:
# enabled: true
# dashboards:
# default:
# custom-dashboard:
# gnetId: 20162 # MetalLB 대시보드 ID
# datasource: Prometheus # 사용할 데이터소스 이름을 명시
# revision: 1 # 대시보드의 버전
defaultRules:
create: false
alertmanager:
enabled: false
EOT
# 배포
$ kubectl create ns monitoring
# => namespace/monitoring created
$ helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 62.3.0 -f monitor-values.yaml --namespace monitoring
# => NAME: kube-prometheus-stack
# LAST DEPLOYED: Sun Jan 1 16:16:32 2020
# NAMESPACE: monitoring
# STATUS: deployed
# REVISION: 1
# NOTES:
# kube-prometheus-stack has been installed. Check its status by running:
# kubectl --namespace monitoring get pods -l "release=kube-prometheus-stack"
#
# Visit https://github.com/prometheus-operator/kube-prometheus for instructions on how to create & configure Alertmanager and Prometheus instances using the Operator.
# 확인
$ helm list -n monitoring
# => NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
# kube-prometheus-stack monitoring 1 2020-01-01 16:16:32.988771 +0900 KST deployed kube-prometheus-stack-62.3.0 v0.76.0
# Grafana 접속 계정 : admin / kans1234 : macOS 사용자
$ echo -e "Prometheus URL = http://localhost:30001"
# => Prometheus URL = http://localhost:30001
$ echo -e "Grafana URL = http://localhost:30002"
# => Grafana URL = http://localhost:30002
# Grafana 접속 계정 : admin / kans1234 : Windows 사용자
$ echo -e "Prometheus URL = http://192.168.50.10:30001"
$ echo -e "Grafana URL = http://192.168.50.10:30002"
# Grafana 접속 계정 : admin / kans1234 : AWS_EC2 사용자
$ echo -e "Prometheus URL = http://$(curl -s ipinfo.io/ip):30001"
$ echo -e "Grafana URL = http://$(curl -s ipinfo.io/ip):30002"
# (참고) helm 삭제
$ helm uninstall -n monitoring kube-prometheus-stack
- 그라파나 접속 후 MetalLB 대시보드 import
- Dashboards > Manage > Import
- GnetId : 20162
- Datasource : Prometheus
- Import 버튼 클릭
- 그라파나 대시보드 확인
- Home > MetalLB 대시보드 선택
- Home > MetalLB 대시보드 선택
파드 생성
$ 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
# => pod/webpod1 created
# pod/webpod2 created
# 파드 정보 확인
$ kubectl get pod -owide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# webpod1 1/1 Running 0 38s 10.10.3.2 myk8s-worker <none> <none>
# webpod2 1/1 Running 0 38s 10.10.2.3 myk8s-worker2 <none> <none>
# 파드 IP주소를 변수에 지정
$ WPOD1=$(kubectl get pod webpod1 -o jsonpath="{.status.podIP}")
$ WPOD2=$(kubectl get pod webpod2 -o jsonpath="{.status.podIP}")
$ echo $WPOD1 $WPOD2
# => 10.10.3.2 10.10.2.3
# 접속 확인
$ docker exec -it myk8s-control-plane ping -i 1 -W 1 -c 1 $WPOD1
# => PING 10.10.3.2 (10.10.3.2) 56(84) bytes of data.
# 64 bytes from 10.10.3.2: icmp_seq=1 ttl=63 time=0.082 ms
#
# --- 10.10.3.2 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 0.082/0.082/0.082/0.000 ms
$ 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
# => Hostname: webpod1
$ 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:'
# => Hostname: webpod1
# RemoteAddr: 172.20.0.5:41896
# Host: 10.10.3.2
$ docker exec -it myk8s-control-plane curl -s --connect-timeout 1 $WPOD2 | egrep 'Hostname|RemoteAddr|Host:'
MetalLB - Layer2 모드 실습
MetalLB 설치
- 링크 : https://metallb.universe.tf/installation/
- 설치 방법 : Kubernetes manifests, Kustomize, using Helm
- kube-proxy가 ipvs 모드 사용시
strictARP: true
설정 필요
- kube-proxy가 ipvs 모드 사용시
- 간단하게 manifests로 설치하겠습니다. - 링크
# Kubernetes manifests 로 설치
# kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/refs/heads/main/config/manifests/metallb-native-prometheus.yaml
# => namespace/metallb-system created
# ...
# serviceaccount/speaker created
# ...
# daemonset.apps/speaker created
# servicemonitor.monitoring.coreos.com/controller-monitor created
# servicemonitor.monitoring.coreos.com/speaker-monitor created
# validatingwebhookconfiguration.admissionregistration.k8s.io/metallb-webhook-configuration created
# metallb crd 확인
$ kubectl get crd | grep metallb
# => bfdprofiles.metallb.io 2020-01-01T07:31:16Z
# bgpadvertisements.metallb.io 2020-01-01T07:31:16Z
# bgppeers.metallb.io 2020-01-01T07:31:16Z
# communities.metallb.io 2020-01-01T07:31:16Z
# ipaddresspools.metallb.io 2020-01-01T07:31:17Z
# l2advertisements.metallb.io 2020-01-01T07:31:17Z
# servicel2statuses.metallb.io 2020-01-01T07:31:17Z
# 생성된 리소스 확인 : metallb-system 네임스페이스 생성, 파드(컨트롤러, 스피커) 생성, RBAC(서비스/파드/컨피그맵 조회 등등 권한들), SA 등
$ kubectl get-all -n metallb-system # kubectl krew 플러그인 get-all 설치 후 사용 가능
$ kubectl get all,configmap,secret,ep -n metallb-system
# => NAME READY STATUS RESTARTS AGE
# pod/controller-679855f7d7-m8spp 2/2 Running 0 5m36s
# pod/speaker-dm26z 2/2 Running 4 (91s ago) 5m36s
# pod/speaker-dr8kh 2/2 Running 0 5m36s
# pod/speaker-pctt7 2/2 Running 3 (90s ago) 5m36s
# pod/speaker-w69v6 2/2 Running 4 (91s ago) 5m36s
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/controller-monitor-service ClusterIP None <none> 9120/TCP 5m36s
# service/metallb-webhook-service ClusterIP 10.200.1.191 <none> 443/TCP 5m36s
# service/speaker-monitor-service ClusterIP None <none> 9120/TCP 5m36s
#
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
# daemonset.apps/speaker 4 4 4 4 4 kubernetes.io/os=linux 5m36s
#
# NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/controller 1/1 1 1 5m36s
#
# NAME DESIRED CURRENT READY AGE
# replicaset.apps/controller-679855f7d7 1 1 1 5m36s
#
# NAME DATA AGE
# configmap/kube-root-ca.crt 1 5m37s
# configmap/metallb-excludel2 1 5m36s
#
# NAME TYPE DATA AGE
# secret/memberlist Opaque 1 5m18s
# secret/metallb-webhook-cert Opaque 4 5m36s
#
# NAME ENDPOINTS AGE
# endpoints/controller-monitor-service 10.10.1.3:9120 5m36s
# endpoints/metallb-webhook-service 10.10.1.3:9443 5m36s
# endpoints/speaker-monitor-service 172.20.0.2:9120,172.20.0.3:9120,172.20.0.4:9120 + 1 more... 5m36s
# 파드 내에 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}"
# => controller-679855f7d7-m8spp:
# kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
# controller -> quay.io/metallb/controller:main
# speaker-dm26z:
# kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
# speaker -> quay.io/metallb/speaker:main
# speaker-dr8kh:
# kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
# speaker -> quay.io/metallb/speaker:main
# speaker-pctt7:
# kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
# speaker -> quay.io/metallb/speaker:main
# speaker-w69v6:
# kube-rbac-proxy -> gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0
# speaker -> quay.io/metallb/speaker:main
## metallb 컨트롤러는 디플로이먼트로 배포됨
$ kubectl get ds,deploy -n metallb-system
# => NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
# daemonset.apps/speaker 4 4 4 4 4 kubernetes.io/os=linux 6m26s
#
# NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/controller 1/1 1 1 6m26s
## 데몬셋으로 배포되는 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-rg9pw 2/2 Running 0 22m 10.10.1.3 myk8s-worker3 <none> <none>
# speaker-9njww 2/2 Running 0 22m 172.20.0.3 myk8s-worker3 <none> <none>
# speaker-lk9wt 2/2 Running 0 22m 172.20.0.2 myk8s-worker2 <none> <none>
# speaker-wz9w5 2/2 Running 0 22m 172.20.0.5 myk8s-control-plane <none> <none>
# speaker-zbwdq 2/2 Running 0 22m 172.20.0.4 myk8s-worker <none> <none>
# (참고) 상세 정보 확인
$ 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 역할의 라우터에서 노드들로 라우팅 경로 지정 필요합니다.
# kind 설치 시 kind 이름의 도커 브리지가 생성됩니다 : 172.20.0.0/16 대역
$ docker network ls
# => NETWORK ID NAME DRIVER SCOPE
# a8d530305515 bridge bridge local
# 8204a0851463 host host local
# 3bbcc6aa8f38 kind bridge local
$ docker inspect kind
# kind network 중 컨테이너(노드) IP(대역) 확인 : 172.20.0.2~ 부터 할당되며, control-plane 이 꼭 172.20.0.2가 안될 수 도 있음
$ docker ps -q | xargs docker inspect --format ' '
# => /mypc2 172.20.0.7
# /mypc 172.20.0.6
# /myk8s-worker 172.20.0.4
# /myk8s-control-plane 172.20.0.5
# /myk8s-worker2 172.20.0.2
# /myk8s-worker3 172.20.0.3
# 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.20.255.200-172.20.255.250
EOF
# => ipaddresspool.metallb.io/my-ippool unchanged
$ kubectl get ipaddresspools -n metallb-system
# => NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
# my-ippool true false ["172.20.255.200-172.20.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
# => l2advertisement.metallb.io/my-l2-advertise created
$ kubectl get l2advertisements -n metallb-system
# => NAME IPADDRESSPOOLS IPADDRESSPOOL SELECTORS INTERFACES
# my-l2-advertise ["my-ippool"]
- 로그 확인
# (옵션) 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 # 매칭 사용 가능
서비스 생성 및 확인
$ 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
# => service/svc1 created
# service/svc2 created
# service/svc3 created
서비스 확인 및 리더 Speaker 파드 확인
# arp scan 해두기
$ docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet
# => Interface: eth0, type: EN10MB, MAC: 02:42:ac:14:00:05, IPv4: 172.20.0.5
# Starting arp-scan 1.10.0 with 65536 hosts (https://github.com/royhills/arp-scan)
# 172.20.0.1 02:42:1f:41:79:66 (Unknown: locally administered)
# 172.20.0.2 02:42:ac:14:00:02 (Unknown: locally administered)
# 172.20.0.3 02:42:ac:14:00:03 (Unknown: locally administered)
# 172.20.0.4 02:42:ac:14:00:04 (Unknown: locally administered)
# 172.20.0.6 02:42:ac:14:00:06 (Unknown: locally administered)
# 172.20.0.7 02:42:ac:14:00:07 (Unknown: locally administered)
# 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 3h55m
# service/svc1 LoadBalancer 10.200.1.213 172.20.255.200 80:32145/TCP 128m
# service/svc2 LoadBalancer 10.200.1.59 172.20.255.201 80:32238/TCP 128m
# service/svc3 LoadBalancer 10.200.1.201 172.20.255.202 80:31593/TCP 128m
#
# NAME ENDPOINTS AGE
# endpoints/kubernetes 172.20.0.5:6443 3h55m
# endpoints/svc1 10.10.2.3:80,10.10.3.2:80 128m
# endpoints/svc2 10.10.2.3:80,10.10.3.2:80 128m
# endpoints/svc3 10.10.2.3:80,10.10.3.2:80 128m
# LoadBalancer 타입은 기본적으로 NodePort를 포함 사용. NodePort는 ClusterIP를 포함 사용.
## 클라우드사업자 LB Type이나 온프레미스환경 HW LB Type 경우 LB 사용 시 NodePort 미사용 설정 가능
$ kubectl describe svc svc1
# => Name: svc1
# ...
# Annotations: metallb.io/ip-allocated-from-pool: my-ippool
# Selector: app=webpod
# Type: LoadBalancer
# IP Family Policy: SingleStack
# IP Families: IPv4
# IP: 10.200.1.213
# IPs: 10.200.1.213
# LoadBalancer Ingress: 172.20.255.200 (VIP)
# Port: svc1-webport 80/TCP
# TargetPort: 80/TCP
# NodePort: svc1-webport 32145/TCP
# Endpoints: 10.10.3.2:80,10.10.2.3:80
# Session Affinity: None
# External Traffic Policy: Cluster
# Internal Traffic Policy: Cluster
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Normal IPAllocated 3m19s metallb-controller Assigned IP ["172.20.255.200"]
# Normal nodeAssigned 5m55s (x2 over 5m55s) metallb-speaker announcing from node "myk8s-worker" with protocol "layer2"
## 아래 처럼 LB VIP 별로 이던 speaker 배포된 노드가 리더 역할을 하는지 확인 가능
$ kubectl describe svc | grep Events: -A5
# => Events: <none>
#
# Name: svc1
# Namespace: default
# Labels: <none>
# --
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Normal IPAllocated 4m24s metallb-controller Assigned IP ["172.20.255.200"]
# Normal nodeAssigned 6m42s (x2 over 6m42s) 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.20.255.200",
# "ipMode": "VIP"
# ...
# metallb CRD인 servicel2status 로 상태 정보 확인
$ kubectl explain servicel2status
$ kubectl get servicel2status -n metallb-system
# => NAME ALLOCATED NODE SERVICE NAME SERVICE NAMESPACE
# l2-cm8sw myk8s-worker svc2 default
# l2-j6w4k myk8s-worker svc1 default
# l2-k5cdm myk8s-worker3 svc3 default
$ 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
# => 172.20.255.200 172.20.255.201 172.20.255.202
# mypc/mypc2 에서 현재 SVC EXTERNAL-IP를 담당하는 리더 Speaker 파드 찾는법 : arping 툴 사용
## Unicast reply from 172.20.255.200: 해당 IP 주소에서 응답을 받았음을 의미합니다.
## Sent 1 probes (1 broadcast(s)): 하나의 ARP 요청을 보냈고, 브로드캐스트 방식으로 요청을 전송했음을 나타냅니다.
## Received 1 response(s): 하나의 응답을 수신했음을 나타냅니다.
$ docker exec -it mypc arping -I eth0 -f -c 1 $SVC1EXIP
# => ARPING 172.20.255.200 from 172.20.0.6 eth0
# Unicast reply from 172.20.255.200 [02:42:AC:14:00:03] 1.139ms
# Sent 1 probes (1 broadcast(s))
# Received 1 response(s)
$ docker exec -it mypc arping -I eth0 -f -c 1 $SVC2EXIP
# => ARPING 172.20.255.201 from 172.20.0.6 eth0
# Unicast reply from 172.20.255.201 [02:42:AC:14:00:02] 1.827ms
# ...
$ docker exec -it mypc arping -I eth0 -f -c 1 $SVC3EXIP
# => ARPING 172.20.255.202 from 172.20.0.6 eth0
# Unicast reply from 172.20.255.202 [02:42:AC:14:00:04] 0.982ms
# ...
$ for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc arping -I eth0 -f -c 1 $i; done
# => ARPING 172.20.255.200 from 172.20.0.6 eth0
# Unicast reply from 172.20.255.200 [02:42:AC:14:00:03] 1.016ms
# Sent 1 probes (1 broadcast(s))
# Received 1 response(s)
# ARPING 172.20.255.201 from 172.20.0.6 eth0
# Unicast reply from 172.20.255.201 [02:42:AC:14:00:02] 1.965ms
# ...
# ARPING 172.20.255.202 from 172.20.0.6 eth0
# Unicast reply from 172.20.255.202 [02:42:AC:14:00:04] 1.789ms
# ...
$ docker exec -it mypc ip -c neigh
# => <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:1f:41:79:66 </span>STALE
# <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>STALE
# <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:05 </span>STALE
# <span style="color:purple;">172.20.255.200 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>REACHABLE
# <span style="color:purple;">172.20.255.201 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>REACHABLE
# <span style="color:purple;">172.20.255.202 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>REACHABLE
# <span style="color: green;">ping은 모두 패킷 100% 로스되면서 실패합니다.</span>
# <span style="color: green;">서비스 port로만 열려있기때문에 ping은 실패하는것입니다.</span>
# <span style="color: green;">여기서 ping을 하는 이유는 arp table을 생성하기 위함입니다.</span>
$ docker exec -it mypc ping -c 1 -w 1 -W 1 $SVC1EXIP
# => PING 172.20.255.200 (172.20.255.200) 56(84) bytes of data.
#
# --- 172.20.255.200 ping statistics ---
# 1 packets transmitted, 0 received, 100% packet loss, time 0ms
$ docker exec -it mypc ping -c 1 -w 1 -W 1 $SVC2EXIP
$ docker exec -it mypc ping -c 1 -w 1 -W 1 $SVC3EXIP
$ for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc ping -c 1 -w 1 -W 1 $i; done
$ for i in 172.20.0.2 172.20.0.3 172.20.0.4 172.20.0.5; 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
# => <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:1f:41:79:66 </span>STALE
# <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>REACHABLE
# <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>REACHABLE
# <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>REACHABLE
# <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:05 </span>REACHABLE
# <span style="color:purple;">172.20.255.200 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>REACHABLE
# <span style="color:purple;">172.20.255.201 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>REACHABLE
# <span style="color:purple;">172.20.255.202 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>REACHABLE
$ kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기
# => NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# myk8s-control-plane Ready control-plane 4h35m v1.31.0 172.20.0.5 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker Ready <none> 4h34m v1.31.0 172.20.0.4 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker2 Ready <none> 4h34m v1.31.0 172.20.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker3 Ready <none> 4h34m v1.31.0 172.20.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# (옵션) 노드에서 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 # 매칭 사용 가능
서비스 접속 테스트
- 클러스터 외부에서 external ip와 port를 통해 k8s 클러스터 내부의 서비스에 접속해보겠습니다.
# 현재 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
# => 172.20.255.200 172.20.255.201 172.20.255.202
# mypc/mypc2 에서 접속 테스트
$ docker exec -it mypc curl -s $SVC1EXIP
# => Hostname: webpod2
# IP: 127.0.0.1
# IP: 10.10.2.3
# RemoteAddr: 172.20.0.3:40816
# GET / HTTP/1.1
# Host: 172.20.255.200
# User-Agent: curl/8.7.1
# Accept: */*
$ docker exec -it mypc curl -s $SVC1EXIP | grep Hostname
# => Hostname: webpod2
$ for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do docker exec -it mypc curl -s $i | grep Hostname ; done
# => Hostname: webpod1
# Hostname: webpod2
# Hostname: webpod2
$ for i in $SVC1EXIP $SVC2EXIP $SVC3EXIP; do echo ">> Access Service External-IP : $i <<" ; docker exec -it mypc curl -s $i | grep Hostname ; echo ; done
# => >> Access Service External-IP : 172.20.255.200 <<
# Hostname: webpod1
#
# >> Access Service External-IP : 172.20.255.201 <<
# Hostname: webpod2
#
# >> Access Service External-IP : 172.20.255.202 <<
# Hostname: webpod1
## 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
# => >> Access Service External-IP : 172.20.255.200 <<
# Hostname: webpod2
# RemoteAddr: 172.20.0.3:23163
# Host: 172.20.255.200
#
# >> Access Service External-IP : 172.20.255.201 <<
# Hostname: webpod2
# RemoteAddr: 10.10.2.1:15401
# Host: 172.20.255.201
#
# >> Access Service External-IP : 172.20.255.202 <<
# Hostname: webpod2
# RemoteAddr: 172.20.0.4:12711
# Host: 172.20.255.202
# 부하분산 접속됨
$ docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $SVC1EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
# => 54 Hostname: webpod2
# 46 Hostname: webpod1
$ docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $SVC2EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
# => 56 Hostname: webpod1
# 44 Hostname: webpod2
$ docker exec -it mypc zsh -c "for i in {1..100}; do curl -s $SVC3EXIP | grep Hostname; done | sort | uniq -c | sort -nr"
# => 53 Hostname: webpod1
# 47 Hostname: webpod2
# 지속적으로 반복 접속
$ 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.20.0.3:39516
# 2024-01-01 11:22:10
#
# Hostname: webpod1
# RemoteAddr: 172.20.0.3:20966
# 2024-01-01 11:22:11
#
# Hostname: webpod2
# RemoteAddr: 172.20.0.3:3638
# 2024-01-01 11:22:12
$ 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.89 172.20.255.200 80:30613/TCP 22m
# 컨트롤노드에서 각각 접속 확인 실행 해보자
$ docker exec -it myk8s-control-plane curl -s 127.0.0.1:30613 # NodePort Type
# => Hostname: webpod2
# IP: 127.0.0.1
# IP: 10.10.2.3
# RemoteAddr: 172.20.0.5:44387
# GET / HTTP/1.1
# Host: 127.0.0.1:30613
# User-Agent: curl/7.88.1
# Accept: */*
$ docker exec -it myk8s-control-plane curl -s 10.200.1.89 # ClusterIP Tpye
# => Hostname: webpod2
# IP: 127.0.0.1
# IP: 10.10.2.3
# RemoteAddr: 172.20.0.5:28647
# GET / HTTP/1.1
# Host: 10.200.1.89
# User-Agent: curl/7.88.1
# Accept: */*
Failover 테스트
- 위의 그림처럼 장애 발생전에 워커노드 1의 스피커 파드가 SVC1, SVC2 서비스의 리더 역할을 하고 있는 상태에서, 워커노드 1에 장애가 발생하면, 남아있는 스피커 파드들이 워커노드 1의 장애 상황을 인지하게 됩니다.
- 이후 장애가 발생한 스피커 파드가 소유한 ExternalIP에 대해 리더파드를 다시 선출하고 GARP로 새로 선출된 리더파드의 MAC 주소를 전파합니다.
- 다만 장애 발생으로 문제를 인식하는 시간과 ARP 정보가 전파되는 시간, 그리고 클라이언트의 ARP 캐시 갱신 시간 등을 고려하면 20초~1분 이내의 장애 지속시간이 발생할 수 있습니다.
- 현재 실습에서 SVC1 => worker node 3, SVC2 => worker node 2, SVC3 => worker node 1 에 배포되어 있는 상태에서 워커노드 중 1대를 중지하여 장애를 발생시키고, 장애시간을 확인해보겠습니다.
# 사전 준비
## 지속적으로 반복 접속
$ SVC1EXIP=$(kubectl get svc svc1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ 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"
## 상태 모니터링
$ 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$
# => 242777ad8f3c kindest/node:v1.31.0 "/usr/local/bin/entr…" 6 hours ago Exited (130) 7 minutes ago myk8s-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: webpod2
# RemoteAddr: 172.20.0.3:25432
# 2024-10-05 12:04:30
#
# 2024-10-05 12:04:32
#
# 2024-10-05 12:04:34
#
# Hostname: webpod2
# RemoteAddr: 172.20.0.3:18511
# 2024-10-05 12:04:35
#
# 2024-10-05 12:04:37
#
# 2024-10-05 12:04:39
# ...
# 변경된 리더 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
# => <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:1f:41:79:66 </span>STALE
# <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>STALE
# <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>STALE
# <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>STALE
# <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:05 </span>STALE
# <span style="color:purple;">172.20.255.200 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>REACHABLE
# <span style="color:purple;">172.20.255.201 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>REACHABLE
# <span style="color:purple;">172.20.255.202 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>REACHABLE
$ kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기
# => NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# myk8s-control-plane Ready control-plane 5h47m v1.31.0 172.20.0.5 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker NotReady <none> 5h47m v1.31.0 172.20.0.4 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker2 Ready <none> 5h47m v1.31.0 172.20.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# myk8s-worker3 Ready <none> 5h47m v1.31.0 172.20.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.10.76-linuxkit containerd://1.7.18
# <span style="color: green;">원래 리더 Speaker 파드가 존재했던 myk8s-worker 노드가 아닌</span>
# <span style="color: green;">myk8s-worker3 노드가 리더 Speaker 파드를 가지고 있음을 확인할 수 있습니다.</span>
# 장애 원복(노드 정상화)
## 노드(실제 컨테이너) 정상화
# $ 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
# => <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:1f:41:79:66 </span>STALE
# <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>STALE
# <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>STALE
# <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>STALE
# <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:05 </span>STALE
# <span style="color:purple;">172.20.255.200 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:03 </span>REACHABLE
# <span style="color:purple;">172.20.255.201 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:02 </span>REACHABLE
# <span style="color:purple;">172.20.255.202 </span>dev <span style="color:teal;">eth0 </span>lladdr <span style="color:olive;">02:42:ac:14:00:04 </span>REACHABLE
$ kubectl get node -owide # mac 주소에 매칭되는 IP(노드) 찾기
# <span style="color: green;">myk8s-worker를 복구하니 SVC3의 리더 스피커 파드가 다시 myk8s-worker가 되었습니다.</span>
장애발생후 복구 되기까지 실습 화면
(옵션) externalTrafficPolicy: Local
- LoadBalancer도 NodePort와 마찬가지로 externalTrafficPolicy 옵션을 사용할 수 있습니다.
- 설정 방법
$ kubectl patch svc svc1 -p '{"spec":{"externalTrafficPolicy": "Local"}}'
$ kubectl patch svc svc2 -p '{"spec":{"externalTrafficPolicy": "Local"}}'
$ kubectl patch svc svc3 -p '{"spec":{"externalTrafficPolicy": "Local"}}'
- 클라이언트에서 서비스의 External IP로 접속시, 리더 스피커 노드에 위치한 애플리케이션 파드로만 접속이 되며, 클라이언트 IP가 보존됩니다.
- 단점
- 부하분산이 되지 않아 비효율적입니다.
- 리더 노드에 애플리케이션 파드가 없을 경우 서비스 접속이 불가능합니다.
- 따라서 MetalLB에서는 externalTrafficPolicy: Local 옵션을 사용하지 않는 것을 권장합니다.
MetalLB - BGP 모드
- 현재 실습환경이 KIND 여서 BGP 모드는 실습을 못해보는것 같습니다.
- 향후에 baremetal이나 VM으로 구성된 클러스터에서 BGP 모드를 실습해 보고 이번에는 이론만 살펴보겠습니다.
- BGP 모드에서는 ARP를 사용하지 않고, BGP 데몬을 사용하여, 클러스터 외부의 라우터에 External IP를 전파합니다.
- ARP 모드는 스피커 리더가 있는 노드로만 트래픽이 전달되었지만, BGP 모드에서는 ECMP를 지원하여 여러 노드에 서비스를 분산시킬 수 있습니다.
- BGP 패킷을 캡쳐해보면 아래와 같이 Service의 External IP를 전파하는 것을 확인할 수 있습니다.
- 이때는
externalTrafficPolicy: Local
의 사용을 적극 권장합니다. - 또한 Failover가 매우 빠르며 거의 무중단으로 서비스가 가능합니다.
BGP 모드 설정
-
MetalLB의 BGP 모드 설정은 ConfigMap을 통해 설정합니다.
$ cat <<EOF | kubectl replace --force -f - apiVersion: v1 kind: ConfigMap metadata: namespace: metallb-system name: config data: config: | peers: - peer-address: 192.168.10.254 peer-asn: 64513 my-asn: 64512 address-pools: - name: default protocol: bgp avoid-buggy-ips: true addresses: - 172.20.1.0/24 EOF
-
리눅스 라우터에 BGP 설정 예시
router bgp 64513 bgp router-id 192.168.10.254 maximum-paths 4 network 10.1.1.0/24 neighbor 192.168.10.10 remote-as 64512 neighbor 192.168.10.101 remote-as 64512 neighbor 192.168.10.102 remote-as 64512 ...
ExternalIP 서비스
- ExternalIP 서비스는 NodePort 서비스와 유사하게 외부 IP를 제공하는 서비스입니다.
- ExternalIP 서비스는 특정 노드IP로 인입한 트래픽을 해당 노드의 파드로 전달해서 외부에서 접속할 수 있게 합니다.
- 단 사용을 권장하고 있지는 않으며, 특별한 이유가 없다면 NodePort를 사용하는것이 좋습니다.
설정 항목 | 설명 |
---|---|
spec.externalIPs | 노드 IP 주소(ExternalIP) |
spec.ports[].port | ExternalIP 와 ClusterIP 에서 수신할 포트 번호 |
spec.ports[].targetPort | 목적지 컨테이너 포트 번호 |
- 실습
$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo
spec:
replicas: 2
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: ndks-websrv
image: k8s.gcr.io/echoserver:1.5
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: svc-externalip
spec:
type: ClusterIP
externalIPs:
- 192.168.10.101
- 192.168.10.102
ports:
- name: svc-webport
port: 9000
targetPort: 8080
selector:
app: deploy-websrv
EOF
# => deployment.apps/deploy-echo created
# service/svc-externalip created
# 확인 : ExternalIP 도 결국 ClusterIP를 사용(포함)
$ kubectl get svc svc-externalip
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# svc-externalip ClusterIP 10.200.1.42 192.168.10.101,192.168.10.102 9000/TCP 14s
$ kubectl describe svc svc-externalip
# => Name: svc-externalip
# Namespace: default
# Labels: <none>
# Annotations: <none>
# Selector: app=deploy-websrv
# Type: ClusterIP
# IP Family Policy: SingleStack
# IP Families: IPv4
# IP: 10.200.1.42
# IPs: 10.200.1.42
# External IPs: 192.168.10.101,192.168.10.102
# Port: svc-webport 9000/TCP
# TargetPort: 8080/TCP
# Endpoints: 10.10.1.3:8080,10.10.3.2:8080
# Session Affinity: None
# External Traffic Policy: Cluster
# Internal Traffic Policy: Cluster
# Events: <none>
# ExternalTrafficPolicy 설정이 없음
$ kubectl get svc svc-externalip -o yaml
# => apiVersion: v1
# kind: Service
# metadata:
# creationTimestamp: "2024-10-05T12:45:06Z"
# name: svc-externalip
# namespace: default
# resourceVersion: "38088"
# uid: b64588f5-1589-4b9b-9652-b5979f8872a1
# spec:
# clusterIP: 10.200.1.42
# clusterIPs:
# - 10.200.1.42
# externalIPs:
# - 192.168.10.101
# - 192.168.10.102
# externalTrafficPolicy: Cluster
# internalTrafficPolicy: Cluster
# ipFamilies:
# - IPv4
# ipFamilyPolicy: SingleStack
# ports:
# - name: svc-webport
# port: 9000
# protocol: TCP
# targetPort: 8080
# selector:
# app: deploy-websrv
# sessionAffinity: None
# type: ClusterIP
# status:
# loadBalancer: {}
IPVS Proxy 모드
IPVS Proxy 모드 소개
- IPVS Proxy 모드는 지난주에 살펴보았던 kube-proxy의 모드중 하나로, 리눅스 커널의 IPVS 기능을 사용하여 로드밸런싱을 수행합니다.
- IPVS는 L4 레이어에서 동작하며, kube-proxy의 iptables 모드보다 성능이 우수하고, 대규모 클러스터에서 더 효율적으로 동작합니다.
- iptables이 비해 좀 더 높은 성능을 보여주며, 규칙 갯수도 줄일 수 있습니다.
- 부하분산 알고리즘도 다음과 같이 다양하게 지원합니다.
- 라운드 로빈 (Round Robin) : 우선순위를 두지 않고 요청을 순차적으로 전달합니다.
- 가중치 라운드 로빈 (Weighted Round Robin) : 서버에 가중치를 부여하여 요청을 전달합니다.
- 최소 연결 (Least Connection) : 현재 연결 수가 가장 적은 서버로 요청을 전달합니다.
- 가중치 최소 연결 (Weighted Least Connection) : 서버에 가중치를 부여하여 연결 수가 가장 적은 서버로 요청을 전달합니다.
- 지역성 기반 최소 연결 (Locality-Based Least Connection) : 클라이언트와 가까우면서 요청이 적은 서버로 요청을 전달합니다.
- 목적지 해싱 (Destination Hashing) : 요청의 목적지 IP 주소를 해싱하여 서버를 선택합니다.
- 출발지 해싱 (Source Hashing) : 요청의 출발지 IP 주소를 해싱하여 서버를 선택합니다.
- 최단 지연 (Shortest Expected Delay) : 서버의 응답 지연 시간을 고려하여 서버를 선택합니다.
- 큐잉 방지 (Never Queue) : 연결이 없는 서버에 우선적으로 트래픽을 보내고, 모든 서버에 트래픽이 있으면 최단 지연 방식으로 트래픽을 보냅니다.
- 라운드 로빈 (Round Robin) : 우선순위를 두지 않고 요청을 순차적으로 전달합니다.
IPVS Proxy 모드 실습
실습환경 설정
- 먼저 기존 실습 환경을 삭제합니다.
$ kind delete cluster --name myk8s
# => Deleting cluster "myk8s" ...
# Deleted nodes: ["myk8s-worker" "myk8s-control-plane" "myk8s-worker2" "myk8s-worker3"]
- 실습환경은 KIND를 사용하며, KIND 클러스터에 IPVS Proxy 모드를 적용해보겠습니다.
- 실습 환경 : K8S v1.31.0, CNI(Kindnet / Direct Routing mode), IPVS proxy mode
- 노드(실제로는 컨테이너) 네트워크 대역 : 172.20.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
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# aa2f031f0959 kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes myk8s-worker3
# 0a67b245f7ee kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes 0.0.0.0:30000-30004->30000-30004/tcp, 127.0.0.1:49623->6443/tcp myk8s-control-plane
# 4525aac3b06f kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes myk8s-worker2
# 0087fe52e3d2 kindest/node:v1.31.0 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes myk8s-worker
# 노드에 기본 툴 설치
$ 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 # IPVS에서 제외할 CIDR을 지정합니다. IPVS 룰을 정리할때 제외할 대역을 지정합니다.
# minSyncPeriod: 0s # IPVS 룰을 동기화할 최소 주기를 지정합니다. 이 옵션을 사용하면 IPVS 룰을 동기화하는 주기를 제한할 수 있습니다.
# scheduler: "" # IPVS 스케줄러는 IPVS가 사용할 로드밸런싱 알고리즘을 지정합니다.
# strictARP: true # MetalLB 동작을 위해서 true 설정 변경 필요
# syncPeriod: 0s # IPVS 룰을 동기화할 주기를 지정합니다. 이 옵션을 사용하면 IPVS 룰을 동기화하는 주기를 지정할 수 있습니다.
# tcpFinTimeout: 0s # IPVS에서 TCP 연결이 종료된 후 FIN 상태를 유지하는 시간을 지정합니다.
# tcpTimeout: 0s # IPVS에서 TCP 패킷을 처리하는 시간을 지정합니다.
# udpTimeout: 0s # IPVS에서 UDP 패킷을 처리하는 시간을 지정합니다.
# ...
# 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
# => >> node myk8s-control-plane <<
# default via <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.0.2 </span>dev <span style="color:teal;">vethc61550c2 </span>scope host
# <span style="color:purple;">10.10.0.3 </span>dev <span style="color:teal;">veth85b53091 </span>scope host
# <span style="color:purple;">10.10.0.4 </span>dev <span style="color:teal;">veth8ad445fd </span>scope host
# <span style="color:purple;">10.10.1.0/24 </span>via <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.2.0/24 </span>via <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.3.0/24 </span>via <span style="color:purple;">172.20.0.4 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">172.20.0.0/16 </span>dev <span style="color:teal;">eth0 </span>proto kernel scope link src <span style="color:purple;">172.20.0.5 </span>
#
# >> node myk8s-worker <<
# default via <span style="color:purple;">172.20.0.1 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.0.0/24 </span>via <span style="color:purple;">172.20.0.5 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.1.0/24 </span>via <span style="color:purple;">172.20.0.3 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">10.10.2.0/24 </span>via <span style="color:purple;">172.20.0.2 </span>dev <span style="color:teal;">eth0 </span>
# <span style="color:purple;">172.20.0.0/16 </span>dev <span style="color:teal;">eth0 </span>proto kernel scope link src <span style="color:purple;">172.20.0.4 </span>
# ...
$ for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -c addr; echo; done
# => >> node myk8s-control-plane <<
# 1: <span style="color:teal;">lo: </span><LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback <span style="color:olive;">00:00:00:00:00:00</span> brd <span style="color:olive;">00:00:00:00:00:00</span>
# inet <span style="color:purple;">127.0.0.1</span>/8 scope host lo
# valid_lft forever preferred_lft forever
# 2: <span style="color:teal;">tunl0@NONE: </span><NOARP> mtu 1480 qdisc noop state <span style="color:red;">DOWN </span>group default qlen 1000
# link/ipip <span style="color:olive;">0.0.0.0</span> brd <span style="color:olive;">0.0.0.0</span>
# 4: <span style="color:teal;">kube-ipvs0: </span><BROADCAST,NOARP> mtu 1500 qdisc noop state <span style="color:red;">DOWN </span>group default
# link/ether <span style="color:olive;">0a:91:66:a1:e6:bb</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span>
# inet <span style="color:purple;">10.200.1.1</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# inet <span style="color:purple;">10.200.1.10</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# 5: <span style="color:teal;">vethc61550c2@if4: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">de:a6:86:25:bb:08</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netns cni-4751a95b-cc8c-ff23-9dd5-35e7f1a2223b
# inet <span style="color:purple;">10.10.0.1</span>/32 scope global vethc61550c2
# valid_lft forever preferred_lft forever
# 6: <span style="color:teal;">veth85b53091@if4: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">ea:36:d9:10:fc:2f</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netns cni-db21fe8b-f48c-f3fe-6c6a-b2f204eea0e5
# inet <span style="color:purple;">10.10.0.1</span>/32 scope global veth85b53091
# valid_lft forever preferred_lft forever
# 7: <span style="color:teal;">veth8ad445fd@if4: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">b6:bb:1b:44:13:eb</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netns cni-26749ed0-4863-eef5-640b-96f804e871ae
# inet <span style="color:purple;">10.10.0.1</span>/32 scope global veth8ad445fd
# valid_lft forever preferred_lft forever
# 46: <span style="color:teal;">eth0@if47: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">02:42:ac:14:00:05</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netnsid 0
# inet <span style="color:purple;">172.20.0.5</span>/16 brd <span style="color:purple;">172.20.255.255 </span>scope global eth0
# valid_lft forever preferred_lft forever
#
# >> node myk8s-worker <<
# 1: <span style="color:teal;">lo: </span><LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback <span style="color:olive;">00:00:00:00:00:00</span> brd <span style="color:olive;">00:00:00:00:00:00</span>
# inet <span style="color:purple;">127.0.0.1</span>/8 scope host lo
# valid_lft forever preferred_lft forever
# 2: <span style="color:teal;">tunl0@NONE: </span><NOARP> mtu 1480 qdisc noop state <span style="color:red;">DOWN </span>group default qlen 1000
# link/ipip <span style="color:olive;">0.0.0.0</span> brd <span style="color:olive;">0.0.0.0</span>
# 4: <span style="color:teal;">kube-ipvs0: </span><BROADCAST,NOARP> mtu 1500 qdisc noop state <span style="color:red;">DOWN </span>group default
# link/ether <span style="color:olive;">fe:2c:45:76:c8:8d</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span>
# inet <span style="color:purple;">10.200.1.1</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# inet <span style="color:purple;">10.200.1.10</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# 44: <span style="color:teal;">eth0@if45: </span><BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state <span style="color:green;">UP </span>group default
# link/ether <span style="color:olive;">02:42:ac:14:00:04</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> link-netnsid 0
# inet <span style="color:purple;">172.20.0.4</span>/16 brd <span style="color:purple;">172.20.255.255 </span>scope global eth0
# valid_lft forever preferred_lft forever
# ...
# <span style="color: green;">👉 노드별로 kube-ipvs0 인터페이스가 생성되었으며, IP 주소가 할당되어 있습니다.</span>
$ 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 <<
# <span style="color:teal;">kube-ipvs0 </span><span style="color:red;">DOWN </span><span style="color:purple;">10.200.1.1</span>/32 <span style="color:purple;">10.200.1.10</span>/32
#
# >> node myk8s-worker <<
# <span style="color:teal;">kube-ipvs0 </span><span style="color:red;">DOWN </span><span style="color:purple;">10.200.1.1</span>/32 <span style="color:purple;">10.200.1.10</span>/32
#
# >> node myk8s-worker2 <<
# <span style="color:teal;">kube-ipvs0 </span><span style="color:red;">DOWN </span><span style="color:purple;">10.200.1.1</span>/32 <span style="color:purple;">10.200.1.10</span>/32
#
# >> node myk8s-worker3 <<
# <span style="color:teal;">kube-ipvs0 </span><span style="color:red;">DOWN </span><span style="color:purple;">10.200.1.1</span>/32 <span style="color:purple;">10.200.1.10</span>/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 <<
# 4: <span style="color:teal;">kube-ipvs0: </span><BROADCAST,NOARP> mtu 1500 qdisc noop state <span style="color:red;">DOWN </span>group default
# link/ether <span style="color:olive;">0a:91:66:a1:e6:bb</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> promiscuity 0 minmtu 0 maxmtu 0
# dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# inet <span style="color:purple;">10.200.1.1</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# inet <span style="color:purple;">10.200.1.10</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
#
# >> node myk8s-worker <<
# 4: <span style="color:teal;">kube-ipvs0: </span><BROADCAST,NOARP> mtu 1500 qdisc noop state <span style="color:red;">DOWN </span>group default
# link/ether <span style="color:olive;">fe:2c:45:76:c8:8d</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> promiscuity 0 minmtu 0 maxmtu 0
# dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# inet <span style="color:purple;">10.200.1.1</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# inet <span style="color:purple;">10.200.1.10</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
#
# >> node myk8s-worker2 <<
# 4: <span style="color:teal;">kube-ipvs0: </span><BROADCAST,NOARP> mtu 1500 qdisc noop state <span style="color:red;">DOWN </span>group default
# link/ether <span style="color:olive;">5a:26:49:54:18:21</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> promiscuity 0 minmtu 0 maxmtu 0
# dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# inet <span style="color:purple;">10.200.1.1</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# inet <span style="color:purple;">10.200.1.10</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
#
# >> node myk8s-worker3 <<
# 4: <span style="color:teal;">kube-ipvs0: </span><BROADCAST,NOARP> mtu 1500 qdisc noop state <span style="color:red;">DOWN </span>group default
# link/ether <span style="color:olive;">fa:0c:2d:44:52:23</span> brd <span style="color:olive;">ff:ff:ff:ff:ff:ff</span> promiscuity 0 minmtu 0 maxmtu 0
# dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
# inet <span style="color:purple;">10.200.1.1</span>/32 scope global kube-ipvs0
# valid_lft forever preferred_lft forever
# inet <span style="color:purple;">10.200.1.10</span>/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 service/kubernetes ClusterIP 10.200.1.1 <none> 443/TCP 31m
# kube-system service/kube-dns ClusterIP 10.200.1.10 <none> 53/UDP,53/TCP,9153/TCP 31m
#
# NAMESPACE NAME ENDPOINTS AGE
# default endpoints/kubernetes 172.20.0.5:6443 31m
# kube-system endpoints/kube-dns 10.10.0.3:53,10.10.0.4:53,10.10.0.3:53 + 3 more... 31m
# 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
# => >> node myk8s-control-plane <<
# 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.20.0.5:6443 Masq 1 3 0
# TCP 10.200.1.10:53 rr
# -> 10.10.0.3:53 Masq 1 0 0
# -> 10.10.0.4:53 Masq 1 0 0
# TCP 10.200.1.10:9153 rr
# -> 10.10.0.3:9153 Masq 1 0 0
# -> 10.10.0.4:9153 Masq 1 0 0
# UDP 10.200.1.10:53 rr
# -> 10.10.0.3:53 Masq 1 0 0
# -> 10.10.0.4:53 Masq 1 0 0
#
# >> node myk8s-worker <<
# 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.20.0.5:6443 Masq 1 0 0
# TCP 10.200.1.10:53 rr
# -> 10.10.0.3:53 Masq 1 0 0
# -> 10.10.0.4:53 Masq 1 0 0
# TCP 10.200.1.10:9153 rr
# -> 10.10.0.3:9153 Masq 1 0 0
# -> 10.10.0.4:9153 Masq 1 0 0
# UDP 10.200.1.10:53 rr
# -> 10.10.0.3:53 Masq 1 0 0
# -> 10.10.0.4:53 Masq 1 0 0
# ...
## 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.20.0.100 nicolaka/netshoot sleep infinity
# 혹은
$ docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity
$ docker ps
# => CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 16b541ee953e nicolaka/netshoot "sleep infinity" 31 seconds ago Up 31 seconds mypc
# aa2f031f0959 kindest/node:v1.31.0 "/usr/local/bin/entr…" 34 minutes ago Up 34 minutes myk8s-worker3
# 0a67b245f7ee kindest/node:v1.31.0 "/usr/local/bin/entr…" 34 minutes ago Up 34 minutes 0.0.0.0:30000-30004->30000-30004/tcp, 127.0.0.1:49623->6443/tcp myk8s-control-plane
# 4525aac3b06f kindest/node:v1.31.0 "/usr/local/bin/entr…" 34 minutes ago Up 34 minutes myk8s-worker2
# 0087fe52e3d2 kindest/node:v1.31.0 "/usr/local/bin/entr…" 34 minutes ago Up 34 minutes myk8s-worker
IPVS 정보 확인
# kube-proxy 로그 확인 : 기본값 부하분산 스케줄러(RoundRobin = RR)
$ kubectl stern -n kube-system -l k8s-app=kube-proxy --since 2h | egrep '(ipvs|IPVS)'
# => ...
# kube-proxy-24z49 kube-proxy I1005 15:10:04.041490 1 server_linux.go:230] "Using ipvs Proxier"
# kube-proxy-24z49 kube-proxy I1005 15:10:04.048394 1 proxier.go:364] "IPVS scheduler not specified, use rr by default" ipFamily="IPv4"
# kube-proxy-24z49 kube-proxy I1005 15:10:04.048529 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
# => >> node myk8s-control-plane <<
# 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.20.0.5: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
# => /proc/sys/net/ipv4/conf/kube-ipvs0
# |-- ...
# |-- arp_accept
# |-- arp_announce
# `-- ...
$ docker exec -it myk8s-worker cat /proc/sys/net/ipv4/conf/kube-ipvs0/arp_ignore
# => 0
$ docker exec -it myk8s-worker cat /proc/sys/net/ipv4/conf/kube-ipvs0/arp_announce
# => 0
# all 은 모든 인터페이스에 영항을 줌, 단 all 과 interface 값이 다를때 우선순위는 커널 파라미터 별로 다르다 - 링크
$ docker exec -it myk8s-worker sysctl net.ipv4.conf.all.arp_ignore
# => net.ipv4.conf.all.arp_ignore = 1
$ docker exec -it myk8s-worker sysctl net.ipv4.conf.all.arp_announce
# => net.ipv4.conf.all.arp_announce = 2
$ docker exec -it myk8s-worker sysctl net.ipv4.conf.kube-ipvs0.arp_ignore
# => net.ipv4.conf.kube-ipvs0.arp_ignore = 0
$ docker exec -it myk8s-worker sysctl net.ipv4.conf.kube-ipvs0.arp_announce
# => net.ipv4.conf.kube-ipvs0.arp_announce = 0
$ docker exec -it myk8s-worker sysctl -a | grep arp_ignore
# => net.ipv4.conf.all.arp_ignore = 1
# net.ipv4.conf.default.arp_ignore = 0
# net.ipv4.conf.eth0.arp_ignore = 0
# net.ipv4.conf.ip6tnl0.arp_ignore = 0
# net.ipv4.conf.kube-ipvs0.arp_ignore = 0
# net.ipv4.conf.lo.arp_ignore = 0
# net.ipv4.conf.tunl0.arp_ignore = 0
$ docker exec -it myk8s-worker sysctl -a | grep arp_announce
# => net.ipv4.conf.all.arp_announce = 2
# net.ipv4.conf.default.arp_announce = 0
# net.ipv4.conf.eth0.arp_announce = 0
# net.ipv4.conf.ip6tnl0.arp_announce = 0
# net.ipv4.conf.kube-ipvs0.arp_announce = 0
# net.ipv4.conf.lo.arp_announce = 0
# net.ipv4.conf.tunl0.arp_announce = 0
# IPSET 확인
$ docker exec -it myk8s-worker ipset -h
$ docker exec -it myk8s-worker ipset -L
목적지(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
생성 및 확인 : IPVS Proxy 모드
# 생성
$ kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml
# => pod/webpod1 created
# pod/webpod2 created
# pod/webpod3 created
# pod/net-pod created
# service/svc-clusterip created
# 파드와 서비스 사용 네트워크 대역 정보 확인
$ 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",
# 확인
$ kubectl get pod -owide
# => NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# net-pod 1/1 Running 0 36s 10.10.0.5 myk8s-control-plane <none> <none>
# webpod1 1/1 Running 0 36s 10.10.3.2 myk8s-worker <none> <none>
# webpod2 1/1 Running 0 36s 10.10.1.2 myk8s-worker2 <none> <none>
# webpod3 1/1 Running 0 36s 10.10.2.2 myk8s-worker3 <none> <none>
$ kubectl get svc svc-clusterip
# => NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# svc-clusterip ClusterIP 10.200.1.17 <none> 9000/TCP 44s
$ kubectl describe svc svc-clusterip
$ kubectl get endpoints svc-clusterip
# => NAME ENDPOINTS AGE
# svc-clusterip 10.10.1.2:80,10.10.2.2:80,10.10.3.2:80 55s
$ kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip
# => NAME ADDRESSTYPE PORTS ENDPOINTS AGE
# svc-clusterip-scf9k IPv4 80 10.10.2.2,10.10.1.2,10.10.3.2 63s
# 노드 별 네트워트 정보 확인 : 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
# => 10.200.1.17 9000
# ipvsadm 툴로 부하분산 되는 정보 확인
## 10.200.1.216(TCP 9000) 인입 시 3곳의 목적지로 라운드로빈(rr)로 부하분산하여 전달됨을 확인 : 모든 노드에서 동일한 IPVS 분산 설정 정보 확인
## 3곳의 목적지는 각각 서비스에 연동된 목적지 파드 3개이며, 전달 시 출발지 IP는 마스커레이딩 변환 처리
$ docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT
# => Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.200.1.17:9000 rr
# -> 10.10.1.2:80 Masq 1 0 0
# -> 10.10.2.2:80 Masq 1 0 0
# -> 10.10.3.2:80 Masq 1 0 0
$ 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
# => >> node myk8s-control-plane <<
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.200.1.17:9000 rr
# -> 10.10.1.2:80 Masq 1 0 0
# -> 10.10.2.2:80 Masq 1 0 0
# -> 10.10.3.2:80 Masq 1 0 0
#
# >> node myk8s-worker <<
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.200.1.17:9000 rr
# -> 10.10.1.2:80 Masq 1 0 0
# -> 10.10.2.2:80 Masq 1 0 0
# -> 10.10.3.2:80 Masq 1 0 0
# ...
# ipvsadm 툴로 부하분산 되는 현재 연결 정보 확인 : 추가로 --rate 도 있음
$ docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --stats
# => Prot LocalAddress:Port Conns InPkts OutPkts InBytes OutBytes
# -> RemoteAddress:Port
# TCP 10.200.1.17:9000 0 0 0 0 0
# -> 10.10.1.2:80 0 0 0 0 0
# -> 10.10.2.2:80 0 0 0 0 0
# -> 10.10.3.2:80 0 0 0 0 0
$ 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
# => >> node myk8s-control-plane <<
# Prot LocalAddress:Port Conns InPkts OutPkts InBytes OutBytes
# -> RemoteAddress:Port
# TCP 10.200.1.17:9000 0 0 0 0 0
# -> 10.10.1.2:80 0 0 0 0 0
# -> 10.10.2.2:80 0 0 0 0 0
# -> 10.10.3.2:80 0 0 0 0 0
#
# >> node myk8s-worker <<
# Prot LocalAddress:Port Conns InPkts OutPkts InBytes OutBytes
# -> RemoteAddress:Port
# TCP 10.200.1.17:9000 0 0 0 0 0
# -> 10.10.1.2:80 0 0 0 0 0
# -> 10.10.2.2:80 0 0 0 0 0
# -> 10.10.3.2:80 0 0 0 0 0
# ...
$ docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --rate
# => Prot LocalAddress:Port CPS InPPS OutPPS InBPS OutBPS
# -> RemoteAddress:Port
# TCP 10.200.1.17:9000 0 0 0 0 0
# -> 10.10.1.2:80 0 0 0 0 0
# -> 10.10.2.2:80 0 0 0 0 0
# -> 10.10.3.2:80 0 0 0 0 0
$ 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
# => >> node myk8s-control-plane <<
# Prot LocalAddress:Port CPS InPPS OutPPS InBPS OutBPS
# -> RemoteAddress:Port
# TCP 10.200.1.17:9000 0 0 0 0 0
# -> 10.10.1.2:80 0 0 0 0 0
# -> 10.10.2.2:80 0 0 0 0 0
# -> 10.10.3.2:80 0 0 0 0 0
#
# >> node myk8s-worker <<
# Prot LocalAddress:Port CPS InPPS OutPPS InBPS OutBPS
# -> RemoteAddress:Port
# TCP 10.200.1.17:9000 0 0 0 0 0
# -> 10.10.1.2:80 0 0 0 0 0
# -> 10.10.2.2:80 0 0 0 0 0
# -> 10.10.3.2:80 0 0 0 0 0
# ...
# iptables 규칙 확인 : ipset list 를 활용
$ docker exec -it myk8s-control-plane iptables -t nat -S | grep KUBE-CLUSTER-IP
# => -A KUBE-SERVICES ! -s 10.10.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
# -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT
# 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: 5
# Header: family inet hashsize 1024 maxelem 65536
# Size in memory: 512
# References: 3
# Number of entries: 5
# Members:
# 10.200.1.1,tcp:443
# 10.200.1.10,tcp:9153
# 10.200.1.10,tcp:53
# 10.200.1.17,tcp:9000
# 10.200.1.10,udp:53
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
# => >> node myk8s-control-plane <<
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.200.1.17:9000 rr
# -> 10.10.1.2:80 Masq 1 0 0
# -> 10.10.2.2:80 Masq 1 0 0
# -> 10.10.3.2:80 Masq 1 0 0
#
# >> node myk8s-worker <<
# Prot LocalAddress:Port Scheduler Flags
# -> RemoteAddress:Port Forward Weight ActiveConn InActConn
# TCP 10.200.1.17:9000 rr
# -> 10.10.1.2:80 Masq 1 0 0
# -> 10.10.2.2:80 Masq 1 0 0
# -> 10.10.3.2:80 Masq 1 0 0
# ...
# 변수 지정
$ 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
# => 10.200.1.17 9000
# 컨트롤플레인 노드에서 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
# => 10.200.1.17
# TCP 80,9000 포트별 접속 확인 : 출력 정보 의미 확인
$ kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000
# => Hostname: webpod2
# IP: 127.0.0.1
# IP: ::1
# IP: 10.10.1.2
# IP: fe80::3009:36ff:fe8f:d5a
# RemoteAddr: 10.10.0.5:58980
# GET / HTTP/1.1
# Host: 10.200.1.17:9000
# User-Agent: curl/8.7.1
# Accept: */*
$ kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
# => Hostname: webpod3
$ kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
# => Hostname: webpod1
# 서비스(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"
# => 4 Hostname: webpod3
# 3 Hostname: webpod2
# 3 Hostname: webpod1
$ 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"
# => 34 Hostname: webpod1
# 33 Hostname: webpod3
# 33 Hostname: webpod2
$ 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"
# => 334 Hostname: webpod2
# 333 Hostname: webpod3
# 333 Hostname: webpod1
# 혹은
$ 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"
IPVS Proxy 모드 : 부하분산 확인
- IPVS는 기존의 iptables의 부하분산보다 더 균등하게 부하분산을 수행함을 확인 할 수 있었습니다.
마치며
이번 주에는 LoadBalancer, LoadBalancer를 온프레미스에서 사용하기 위한 MetalLB, ClusterIP, IPVS Proxy 모드에 대해 알아보았습니다. 온프레미스 K8S에서 서비스 유형을 LoadBalancer로 했을때 ExternalIP가 할당 되지 않은 이유를 이제야 알았습니다. 단순히 쓰기만 해왔던 기술의 원리와 이유를 알게되니 뿌듯합니다. 아직 알아야 할 것이 산더미이고 지금 이순간에도 새로운 기술들이 개발된다니 또다시 첩첩산중이라는것을 느낍니다.
IPVS는 아직 모르는 부분이 많지만, 실무에 적용해보고 싶은 기술입니다. 네트워크 부하때문에 CPU가 높아지는 경우가 많은데, 이를 해결할 수 있는 방법인것 같아 유용할것 같습니다.
정말 매운맛의 스터디이지만 많은 것을 배우고 있습니다. 다음 주에는 드디어 기다리던 GatewayAPI를 스터디 합니다. 기대가 됩니다. :)