
쿠버네티스에 대해서 공부하다보면 매니페스트 중에 네트워크 연결을 도와주는 Service라는 종류가 있다. Service 매니페스트는 크게 3가지로, 내부에서 IP를 통해서 연결할 수 있게 해주는 ClusterIP와 NodePort 그리고 LoadBalancer 이렇게 3가지가 있다.
클라우드에서 제공하는 쿠버네티스를 사용하게 되면, LoadBalancer 타입의 Service를 사용할 때 GCP나 AWS같은 IaaS 플랫폼에서 제공하는 로드 밸런서를 사용한다. 하지만 아무것도 없는 온프레미스에서 로드 밸런서 타입을 사용하면 계속 Pending으로 뜨게 된다. 이 때 metalLB와 같은 로드밸런서를 통해서 IP 대역을 할당받아야 한다. 이러한 아무것도 없는 온프레미스인 베어메탈에서 로드밸런서 타입의 서비스 매니페스트를 쓰기 위해서 도와주는 툴이 MetalLB이다.
온프레미스에 구성되어있는 쿠버네티스 클러스터를 관리하기 위해서 Nginx Proxy Manager와 MetalLB 등을 관리하게 되면서, 이렇게 OSI 레이어에서 작동하는 로드 밸런서들을 정리하기 위해서 공부한 내용을 다루며, 직접 온프레미스에서 구현한 아키텍처에 대해서 정리할 예정이다.
MetalLB는 Controller&Speaker 구조로 되어있으며, L2 레이어 모드로 사용할 수도 있고, BGP 모드로 사용할 수도 있다. 주로 L4 로드밸런서와 같이 사용하면서 L2,L3 역할로 사용한다고 한다. 이렇게 다양한 역할로 사용할 수 있는 MetalLB에 대해서 하나씩 살펴보자. 다양한 레퍼런스를 참고했지만, 아무래도 영상 자료가 가장 좋은 것 같아서 혹시나 kubectl 기반으로 터미널 기반으로 이해하는게 좋을 것 같은 사람에게는 다음의 영상을 추천한다.
- [악분 일상] MetalLB L2모드와 디버깅 : https://www.youtube.com/watch?v=RnjKMYD8SKc
MetalLB란 무엇인가?
먼저 MetalLB를 아주 짧게 정의하면, 클라우드가 아닌 베어메탈/온프레 클러스터에서 Service: LoadBalancer를 쓸 수 있게 해주는 소프트웨어 로드밸런서다. 클라우드 환경에서는 AWS ALB/NLB, GCP LB 같은 managed L4 로드밸런서가 자동으로 생성된다. 하지만 베어메탈에서는 그런 매니지드 로드밸런서가 없어서 EXTERNAL-IP가 계속 <pending> 상태로 남는다.
MetalLB는 이 빈자리를 채우기 위해, 쿠버네티스 안에 설치되는 Controller + Speaker로 구성된 컴포넌트로, ( Controller와 Speaker는 이후에 다룬다.) 외부에서 접근 가능한 IP(VIP, Vitual IP)를 쿠버네티스 서비스에 할당하고, 그 IP를 네트워크에 광고해주는 역할을 한다. MetalLB는 미리 정해둔 IP 풀에서 하나를 골라서 EXTERNAL-IP로 붙여준다. 그리고 그 IP로 들어오는 트래픽이 클러스터 노드 -> Service -> Pod로 자연스럽게 흘러가도록 L2/L3 레벨에서 처리해준다.
왜 굳이 MetalLB를 사용할까?
쿠버네티스 Service 타입을 다시 한 번 짚고 가면 다음과 같다.
- ClusterIP : 클러스터 내부 전용 가상 IP
- NodePort : 각 노트의 특정 포트를 열어서 외부 접근, 3만번대 포트여서 실서비스용으로는 다루기 어려움
- LoadBalancer : 외부에서 접근 가능한 IP를 정식으로 달아주는 타입, 이 IP 앞단에 L4 로드밸런서와 함께 사용한다.
클라우드에서는 이 LoadBalancer 타입이 클라우드 벤더의 로드밸런서를 트리거하는 역할을 하는데, 반대로 베어메탈에서는 이 부분을 우리가 직접 구현해야한다. kubernetes 공식 문서에서도 다음과 같이 쓰여있다.
On cloud providers which support external load balancers, setting the type field to LoadBalancer provisions a load balancer for your Service.
- [kubernetes] Service : https://kubernetes.io/docs/concepts/services-networking/service/
Service
Expose an application running in your cluster behind a single outward-facing endpoint, even when the workload is split across multiple backends.
kubernetes.io
Kubernetes의 Loadbalancer 매니페스트
직접 매니페스트로 보자면 다음과 같다. ClusterIP는 내부 서비스끼리의 통신을 위한 것이기 때문에, nodePort와 LoadBalancer만 놓고 보면 다음과 같다. 이는 쿠버네티스의 Service에 대한 공식 문서에서 가져온 스크립트이다.
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app.kubernetes.io/name: MyApp
ports:
- port: 80
# By default and for convenience, the `targetPort` is set to
# the same value as the `port` field.
targetPort: 80
# Optional field
# By default and for convenience, the Kubernetes control plane
# will allocate a port from a range (default: 30000-32767)
nodePort: 30007
이러한 nodePort는 쿠버네티스의 30000번대 포트를 흘려주는 역할을 하기 때문에 쓰기 어렵다. 반면에 LoadBalancer는 어떨까?
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 192.0.2.127
loadBalancer는 이렇게 IP를 지정해서 노출시킬 수 있도록 한다. 이러한 필요 때문에 LoadBalancer 타입을 쓰게 되며, 온프레에서는 MetalLB를 통해서 구현할 수 있다.
MetalLB의 demo 스크립트
간단하게 nginx를 사용하는 예시를 통해서 external-ip가 어떻게 잡히는 지 보자. 아래의 metallb-demo.yaml을 실행한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: metallb-demo
labels:
app: metallb-demo
spec:
replicas: 1
selector:
matchLabels:
app: metallb-demo
template:
metadata:
labels:
app: metallb-demo
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: metallb-demo
labels:
app: metallb-demo
spec:
type: LoadBalancer
selector:
app: metallb-demo
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
그리고 직접 svc 명령어를 통해서 확인해보면 다음과 같이 확인할 수 있다.
➜ kube k get svc metallb-demo -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
metallb-demo LoadBalancer 10.96.17.184 <pending> 80:32399/TCP 18s app=metallb-demo
역시 External-IP가 pending으로 잡혀있다. 이제 metallb를 본격적으로 설치하여 EXTERNAL-IP를 뚫어줄 수 있도록 해보자. 아래와 같이 metallb를 설치하여 직접 Loadbalaner 타입의 서비스 매니페스트가 정상적으로 동작하도록 해보자.
MetalLB 설치
다음으로는 metallb 공식 문서에 따라서 설치를 진행할 수 있다. 자세한 내용은 아래에서 살펴볼 수 있다.
- [metallb] installation : https://metallb.io/installation/
Installation :: MetalLB, bare metal load-balancer for Kubernetes
Before starting with installation, make sure you meet all the requirements. In particular, you should pay attention to network addon compatibility. If you’re trying to run MetalLB on a cloud platform, you should also look at the cloud compatibility page
metallb.io
아래의 순서대로 설치할 수 있다. 먼저 설정을 통해서 strictAPR 기능을 true로 바꿔준다.
kubectl edit configmap -n kube-system kube-proxy
다음과 같다.
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true // false라면 true로 바꿔준다.
설정을 적용하고 나서 아래의 명령어를 통해 설치한다.
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml
위 명령어를 통해서 metallb를 성공적으로 설치할 수 있다. 하지만 다시 기존의 metallb-demo를 확인해서 IP 할당이 성공적으로 되었는지 살펴보면, 여전히 pending 상태인 것을 살펴볼 수 있다. 심지어 metallb쪽 네임스페이스에 있는 리소스들을 확인해도 정상적으로 떠있는 것을 확인할 수 있다.
➜ kube k get all -n metallb-system
NAME READY STATUS RESTARTS AGE
pod/controller-78fb49f59-dwvrf 1/1 Running 0 3m55s
pod/speaker-pgdh2 1/1 Running 0 3m55s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/metallb-webhook-service ClusterIP 10.99.106.87 <none> 443/TCP 3m55s
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/speaker 1 1 1 1 1 kubernetes.io/os=linux 3m55s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/controller 1/1 1 1 3m55s
NAME DESIRED CURRENT READY AGE
replicaset.apps/controller-78fb49f59 1 1 1 3m55s
이러한 설정에서 끝나는 것이 아니라, 다음으로는 metallb 설정을 추가해줘야한다. 이 포스팅에서는 간단하게 데모를 보이기 위함으로써 가장 기본이 되는 L2 모드를 통해서 설정한다.
이 포스팅의 앞부분에서 설명한 내용에 따르면, ClusterIP와 NodePort와 다르게 IP를 직접 지정해서 외부로 노출시키기 위해서 사용하는 것이 바로 이 LoadBalancer 타입이다. 따라서 metallb 설정에 따라서 설정하는 것을 통해서 IP 풀을 할당받을 수 있으며, 해당하는 IP로 외부에 노출시키는 것이 가능하다.
- [metallb] configuration layer 2 : https://metallb.io/configuration/#layer-2-configuration
Configuration :: MetalLB, bare metal load-balancer for Kubernetes
MetalLB remains idle until configured. This is accomplished by creating and deploying various resources into the same namespace (metallb-system) MetalLB is deployed into. There are various examples of the configuration CRs in configsamples. Also, the API i
metallb.io
위에 나온 내용에 따라서 아래와 같은 설정을 추가해줘야한다. IP Pool을 다음과 같이 설정할 수 있다. 필요에 따라 추가하고자 하는 풀을 추가한다. 나는 아래와 같이 기본 설정에 이름을 metallb-ip-address.yaml로 추가했다.
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 192.168.1.240-192.168.1.250
다음으로는 이러한 IP 풀을 광고하기 위한 Advertisement가 필요하다. 아래와 같이 IP 풀을 붙여줄 수 있으며 나는 metallb-l2.yaml이라는 이름으로 추가했다.
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
이제, 추가한 두 파일을 kubectl apply로 적용하고 나면, 다음과 같이 정상적으로 LoadBalancer 타입에 대한 IP가 할당된 것을 확인할 수 있다. metallb의 config에 따라서 설정한 IP 대역을 받은 것을 알 수 있다.
➜ kube k get svc metallb-demo -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
metallb-demo LoadBalancer 10.96.17.184 192.168.1.240 80:32399/TCP 18m app=metallb-demo
이렇게 metallb를 L2 레이어로 설정하는 간단한 실습은 끝난다. 중간에 metallb가 배포되어있는 네임스페이스에서 리소스를 살펴보면 Controller나 Speaker라는 개념이 나오고, 중간에 Advertisement라는 개념이 나온다. 이제 이론적으로 해당 부분을 짚고 넘어가보자.
MetalLB의 구성 요소 : Controller와 Speaker
MetalLB는 크게 두 가지 컴포넌트로 나뉜다.
- Controller (컨트롤러, Deployment)
쿠버네티스 API를 watch하고 있다가, type: LoadBalancer 서비스가 생성/변경되는 것을 감시한다. 이를 감지하면 미리 정의한 IPAddressPool 안에서 사용 가능한 IP를 하나 선택해서 서비스에 할당한다.
- Speaker (스피커, DaemonSet)
각 노드마다 하나씩 떠 있는 Pod으로 Controller에서 결정한 할당 정보를 보고, Layer2 모드에서는 ARP/NDP 응답을 통해서 VIP를 광고하고, BGP 모드에서는 라우터와 BGP 세션을 맺고 VIP를 라우팅 테이블에 광고한다. 참고로 호스트 네트워크를 사용한다.
MetalLB의 L2 모드
metallb에서 설정이 BGP에 비해 비교적 간단한 편인 L2 모드에 대해서 그림으로 나타내보면 다음과 같다.

리더가 된 speaker가 외부의 트래픽을 받아서 ARP를 통해서 트래픽을 보내주는 방식을 사용한다. 앞서 advertisement라는 스크립트를 작성했었는데, 그것은 특정 IP 풀을 리더가 된 스피커 인스턴스가 관리하고 있다는 것을 알려준다. external-ip의 소유를 리더 인스턴스라고 전파하는 역할을 하며, 트래픽이 리더 스피커로 오면 iptables를 통해서 트래픽을 부하분산 해준다.
여기서 말하는 특정 IP 풀을 리더 스피커가 관리하고 있다는 것을 알려주는 것을 전문 용어로 ARP라고 한다. 클라이언트가 이 IP를 가진 MAC 주소는 뭐야? 라고 물으면 리더 스피커가 그것에 응답하는 구조이다.
정리하면 L2 모드는, L2Advertisement 리소스로 "어떤 IP 풀을 L2로 광고할지"를 정해두고, 그 풀에서 할당하는 VIP에 대해 speaker 하나가 대표로 ARP 응답을 하는 구조이다. 장애가 나면 리더가 다른 노드로 넘어가고, 새 노드가 다시 ARP를 뿌리면서 VIP를 인계받는다.
MetalLB의 BGP 모드
BGP 모드는 VIP를 한 노드에서 독점해서 들지 않고, 각 노드의 speaker가 라우터와 BGP 세션을 맺고 VIP 경로를 직접 광고한다.

controller가 loadBalancer라는 서비스 매니페스트를 보고서 똑같이 IPAddressPool에서 EXTERNAL-IP(VIP)를 골라 Service에 붙인다. 그리고 L2와 다르게 BGPPeer/BGPAdvertisement 설정에 따라 각 노드의 speaker가 라우터와 BGP 피어링을 맺는다. 간단하게 설명하면 speaker는 라우터에게 "이 VIP(예: 203.0.113.10/32)는 나를 통해 갈 수 있어"라는 BGP advertisement를 보낸다.
그림을 보면, router가 가운데 있고 각 노드의 speaker와 연결되어 있는데 이 구조 덕분에 라우터 입장에서는 "이 VIP로 가는 경로가 node1, node2, node3에 모두 있다"라고 보이게 되고, 보통 ECMP(동일 비용 다중 경로)로 트래픽을 여러 노드에 나눠 보낼 수 있다.
외부 클라이언트 입장에서는 VIP로 요청 보내면, 먼저 라우터까지 가게 되고, 라우터가 BGP 경로를 보고 node1/node2/node3 중 하나로 트래픽을 분산한다. 각 노드에 도착한 후에는 L2 모드와 똑같이, kube-proxy가 Service 정보를 보고 실제 Pod로 L4 로드밸런싱을 한다.
정리하면, BGP 모드는 BGPAdvertisement 리소스로 "어떤 IP 풀을 BGP로 광고할지"를 정해두고, 각 노드의 speaker가 라우터와 동등한 BGP 피어가 되어 VIP 경로로 광고하는 구조이다. 라우터 레벨에서 여러 노드로 트래픽이 분산되기 때문에, L2 모드처럼 "한 노드에만 트래픽이 몰리는 병목"을 피할 수 있고, 노드 장애 시에도 BGP 세션 종료를 통해 비교적 빠르게 라우트를 갱신할 수 있다.
마치며
참고로, Metallb같은 경우에는 라우팅 프로토콜을 사용하기 때문에, 사용하기 전에 네트워크 엔지니어와 상의가 필요하다고 한다. 혹시 문제가 나도 metallb가 원인이라는 것을 빠르게 파악할 수 있다나. 베어메탈에서 쿠버네티스를 쓰는 경우에는 정말 예상치 못한 일들이 마구마구 일어난다..
참고
- [velog] K8s에서의 MetalLB : https://velog.io/@youwins/MetalLB
- [youtube] MetalLB L2모드 동작원리와 디버깅: https://www.youtube.com/watch?v=RnjKMYD8SKc&t=209s
감사합니다.
'DevOps > kubernetes' 카테고리의 다른 글
| 학교 공지사항 스크래핑 & 푸시알림 서버 만들기 (0) | 2025.09.18 |
|---|---|
| [CKA] 기출 문제 정리 (0) | 2025.01.12 |
| 쿠버네티스의 Kustomize에 대해서 (1) | 2024.12.20 |
| ETCD Leader Election이란? (1) | 2024.12.18 |
| Kubernetes의 Ingress와 Service 차이점에 대한 고찰 (0) | 2024.12.17 |