Kubernetes Cluster에서 MetalLB를 사용하다 보면, IP 트래픽을 처리하는 방식이 궁금할 때가 많다.
특히 Baremetal 환경에서 서비스를 운영한 개발자, 운영자라면 특히나 MetalLB의 동작 방식이나 원리가 이해가 안 되고, 감(느낌)도 안 잡힌다.
어디에 MetalLB 설계서 같은 문서가 있으면 좋겠지만, 그런게 없으니까 일단 궁금한 점이 떠오를 때마다 테스트하고 기록하는 수 밖에 ~~~
Q) Kubernetes Service 리소스의 External IP 항목에 보여지는 IP Address는 실제로 어디에 있는가?
실제로 LoadBalancer Type으로 서비스 리소스를 설정하고, External IP Address를 할당받아서 테스트를 해보니까,
External IP Address는 Master Node 또는 Worker Node에 있는 특정한 Network Port(예: eth2)의 Mac Address를 공유해서 쓰고 있었다.
물론 Network Port(예: eth2)는 External IP Address와 같은 Network 대역이라서 라우팅이 가능한 Port가 된다.
(아마 MetalLB Operator가 동일한 Network 대역을 찾아서 할당해주는 것 같다)
참고: Layer 2 Mode로 MetalLB를 구성하고 테스트한 결과임
그렇다면, Master Node와 Worker Node가 10개가 있을 때, 어떤 Master Node와 Worker Node에 External IP Address가 있게 되는걸까?
이 External IP Address와 관련있는 Pod의 위치(Pod가 구동된 Master Node 또는 Worker Node)와는 전혀 관련이 없다.
MetalLB는 External IP Address를 Worker Node의 특정 Ethernet Port에 할당할 때, 최대한 분산되도록 스케쥴링한다.
만약 MetalLB가 External IP를 worker-a에 이미 할당할 것이 있다면, 그 다음 External IP를 worker-a가 아닌 다른 Node에 위치하도록 구성한다. 즉, 이 External IP가 한쪽 Kubernetes Node에 몰려서 외부 트래픽이 한개의 Node에 집중되는 것을 막으려는 노력을 하는 것이다.
각 term이 의미하는 것을 지금 이해해도, 몇 개월 뒤에 다시 service resource에 대한 manifest를 작성하려고 보면 또 헷갈려서 다시 문서를 뒤적거리게 된다.
Port
Service Object 자체의 Port. 즉, 여러 Pod를 묶어서 이 Port 값으로 노출시킨다.
한 kubernetes cluster 내에서 다른 pod가 내 pod에게 Layer 4 메시지를 전송할 때 바라보는 port number.
만약 MetalLB, NGINX 같은 Ingress Gateway를 사용하는 경우라면, 이 'port' 값이 Cluster 외부에서 LB(즉, Ingress Gateway)를 통해 들어오는 Port number가 된다. 예를 들어, http://{EXTERNAL-IP}:{SERVICE-PORT} 이런 형태가 된다.
TargetPort
내 pod 안에 있는 container가 listening하고 있는 port number.
container(즉, app)이 어떤 port를 listening하고 있는지 정확한 값을 알고 설정해야 한다. (HTTPD 설정시 기본 값을 이용했다면, 대부분 80이지 않을까?)
NodePort
kubernetes 밖으로 노출시킬 port number.
Ingress Gateway 또는 Istio를 사용하는 경우에는 딱히 설정할 필요없는 설정 항목.
Example
만약 2개의 TCP Port를 Service로 오픈하고 싶다면, 아래와 같이 Service Resource와 Pod Resource를 설정한다.
##
## Service Manifest Example
##
kind: Service
metadata:
... 중간 생략 ...
spec:
ports:
- name: metrics
port: 24231
protocol: TCP
targetPort: metrics
- name: logfile-metrics
port: 2112
protocol: TCP
targetPort: logfile-metrics
... 중간 생략 ...
##
## Pod Manifest Example
##
kind: Pod
metadata:
... 중간 생략 ...
spec:
containers:
name: my-container
ports:
- containerPort: 24231
name: metrics
protocol: TCP
- containerPort: 2112
name: logfile-metrics
protocol: TCP
... 중간 생략 ...
MetalLB의 버전에 따라 설치 결과가 조금씩 달라서 MetalLB를 설치한 날짜를 밝힌다.
아래 절차를 따라하면 MetalLB 설치, 그리고 예제 앱 테스트 모두 잘 동작한다.
MetalLB 설치에 관한 공식 문서는 아래 Web Docs를 참고 https://metallb.universe.tf/installation/
위 Web Docs는 여러 도구를 이용한 설치 방법을 설명하지만, 나는Manifest 방식을 이용하여 MetalLB를 설치했다.
위 Web docs를 보면서 따라해보면, 막힘없이 쭉쭉~ 잘 설치되었다.
설치 완료한 이후에 LB 테스트를 해봤는데 잘 동작했다.
내가 설치한 환경은 이렇다.
Kubernetes: v1.25.3 CNI: Calico v3.24.5
내가 Web docs를 보면서 수행했던 명령 및 설정 파일 편집한 내역만 추려보면 이렇다.
strict ARP mode 활성화하기
아래 예제처럼 kube-proxy configmap을 수정한다.
(strictARP항목을false에서true로 변경)
## 아래 명령을 수행
$ kubectl edit configmap -n kube-system kube-proxy
... 중간 생략 ...
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true ## 이 부분을 수정한다. false -> true
... 중간 생략 ...
Manifest 이용하여 Metal LB 설치하기
아래 명령을 따라서 수행한다.
$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml
##
## MetalLB가 잘 설치되었다면,
## 아래와 같이 metallb-controller와 metallb-speaker pod가 Running 상태로 보일 것이다.
##
$ kubectl get -n metallb-system pod
NAME READY STATUS RESTARTS AGE
controller-84d6d4db45-g94mh 1/1 Running 0 31s
speaker-6jqzw 1/1 Running 0 31s
speaker-hlrg7 1/1 Running 0 31s
speaker-sq94q 1/1 Running 0 31s
## 참고: speaker가 running 상태가 되려면 대략 25~30초 정도 걸린다.
## Pod의 상태가 ConfigErr로 보인다고 당황해하지 않아도 된다.
## 30초 후에 Running 상태로 변경될거다.
주의할 점: 위와 같이 L2Advertisement, IPAddressPool을 변경하고 적용해도 실제로 Service resource가 생성될 때, 과거의 IPAddressPool을 이용한다. 아마 MetalLB 버그 같은데, 과감하게 metallb-system 네임스페이스의 controller pod를 종료시키고 재기동하면 새로운 IPAddressPool이 적용된다. (아래 명령을 참고)
$ kubectl get -n metallb-system pod -l component=controller
NAME READY STATUS RESTARTS AGE
controller-84d6d4db45-vgt9r 1/1 Running 0 6m12s
$ kubectl delete -n metallb-system pod controller-84d6d4db45-vgt9r
## 20초 정도 후에 Pod가 재기동되고, 그 뒤로 새로운 IPAddressPool이 정상적으로 반영된다.
Example App을 이용하여 MetalLB 동작 확인
$ cat >> my-example.yaml <<-EOF
apiVersion: v1
kind: Pod
metadata:
name: almighty
labels:
app: almighty
spec:
terminationGracePeriodSeconds: 3
containers:
- name: almighty
image: docker.io/andrewloyolajeong/almighty:0.2.4
---
apiVersion: v1
kind: Service
metadata:
name: almighty
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: almighty
ports:
- name: myweb
protocol: TCP
port: 8080
targetPort: 8080
- name: yourweb
protocol: TCP
port: 80
targetPort: 80
EOF
$
$ kubectl create ns ns1
$ kubectl apply -n ns1 -f my-example.yaml
$ kubectl get -n ns1 svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
almighty LoadBalancer 10.104.156.217 10.1.4.150 8080:30859/TCP,80:31917/TCP 116s
$
##
## 위 결과에서 EXTERNAL-IP에 적절한 IP Address가 출력되면, 잘 되는 것이다.
## 그리고 아래와 같이 실제로 MetalLB가 할당해준 10.1.4.150 주소를 통해 Networking이 되는지 확인한다.
##
$ curl 10.1.4.150:8080
Hello from example application. (written by Andrew)
$ curl 10.1.4.150:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...
...
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
$
올레~~~ 잘 동작한다 ^^
참고: ping 명령으로 EXTERNAL-IP 통신 여부를 확인하지 말 것 !!!
MetalLB 설치하고, 많은 사용자들이 실수하는 것이 아래처럼 ping 명령으로 External-IP로 ICMP 패킷이 전달되는지 체크한다.
$ ping 10.10.12.43
MetalLB가 만든 External-IP 주소는 K8S Cluster의 특정 Worker Node의 Netfilter에만 존재하는 Chain Rule일 뿐이다.
즉, OS Kernel이 물린 Network Port의 MAC Address에 Binding한 IP Address가 아니다보니, OS Kernel이 ICMP Echo Request에 대해 반응할리 없다. 단, K8s LoadBalancer 구현체(즉, MetalLB)의 구현에 따라서 MetalLB가 직접 ICMP Echo Response를 만들 수는 있지만, 이것은 구현체의 버전에 따라 다를 수 있으니까 ICMP Echo(즉, Ping)에 대한 테스트를 시도하지 않는 것이 좋다.
아래와 같이 CURL을 이용하여 테스트하는 것이 정확한 테스트 방법이다.
$ curl https://10.10.12.43:8443
다시 한번 강조한다. 절대 ping 명령으로 MetalLB의 External-IP 동작 유무를 체크하지 말자 !!!
참고: Address 설정은 모든 대역의 주소가 다 가능한다.
아래 설정 예제처럼 특정 addresses 대역을 사용하겠다고 지정하면, MetalLB Speaker가 알아서 동일한 Broadcast Domain에 해당하는 Network Port로 GARP(Gratuitous ARP)를 Broadcasting한다.
$ vi metallb/values.yaml
... 중간 생략 ...
configInline:
address-pools:
- name: default
protocol: layer2
addresses:
- 10.10.12.40/29
... 중간 생략 ...
따라서 Worker Node에 Network Port가 여러 개 있다면, 위 address-pools에 설정된 ip address 대역 중에서 broadcast domain이 일치하는 Network Port로 GARP 패킷이 흘러나간다.
참고: 특정 Service에 특정 External-IP(Public IP)를 고정해서 사용하기
External-IP는 Kubernetes Cluster 밖에 있는 Client App이 접근하기 위한 주소이기 때문에 고정하는게 운영상 편하다.
그런데 MetalLB는 IP Address Range를 설정하도록 되어 있기 때문에 K8s Service 생성시 어떤 External-IP가 K8s Service에 할당될지 알 수 없다.
이럴 때 아래와 같이 K8s Service에 설정을 추가하면 특정 External-IP(즉, Public IP Address)를 고정해서 사용할 수 있다.
$ cat my-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: mysvc
(... 중간 생략 ...)
spec:
type: LoadBalancer
## MetalLB의 IP Address Range가 211.10.3.160 ~ 190 이라고 가정하고
## 이 mysvc에 211.10.3.165 주소를 고정해서 사용하고 싶아면 아래와 같이 사용한다.
loadBalancerIP: 211.10.3.165
(... 중간 생략 ...)