중소기업 쿠버네티스 구축 – 04. 스토리지와 로드밸런서: 트래픽 진입점 (local-path·MetalLB L2)

2026.07.15

·

guniq 인사이트 썸네일 — 중소기업 쿠버네티스 구축 04. 스토리지와 로드밸런서

시리즈 안내
이 글은 “중소기업 쿠버네티스 구축” 시리즈의 4편입니다. 3편에서 K3s 클러스터의 뼈대를 세웠습니다. 하지만 지금의 클러스터는 데이터를 영구 저장할 수도, 외부에서 접근할 수도 없습니다. 이번 편에서는 그 두 가지 토대 — 스토리지(local-path)와 로드밸런서(MetalLB) — 를 놓습니다.


들어가며

3편을 끝낸 클러스터는 비유하자면 전기는 들어왔지만 수도와 현관문이 없는 집입니다.

  • 수도(스토리지): Pod가 데이터를 저장해도, 그 Pod가 죽으면 데이터가 사라집니다. 영구히 보관할 “저장 공간”을 연결해야 합니다.
  • 현관문(로드밸런서): 외부에서 클러스터 안의 서비스로 들어올 “입구 IP”가 없습니다. 이걸 만들어야 합니다.

이번 편에서 이 둘을 해결합니다. 순서가 중요한데, 둘 다 이후 모든 것(Ingress, ArgoCD, MinIO)의 전제 조건이라 지금 깔아둡니다.


1. 스토리지: 왜 필요하고, 무엇을 쓸 것인가

쿠버네티스의 저장소 개념

쿠버네티스에서 Pod는 언제든 죽고 다시 태어나는 일회용 존재입니다. Pod 내부에 저장한 데이터는 Pod가 사라지면 같이 사라집니다. 그래서 영구 데이터는 Pod 바깥의 볼륨(Volume)에 저장해야 합니다.

이때 등장하는 개념이 둘입니다.

  • PV (PersistentVolume): 실제 저장 공간 그 자체 (디스크의 한 영역)
  • PVC (PersistentVolumeClaim): “이만큼의 저장 공간을 주세요”라는 요청서

Pod는 PVC로 저장 공간을 요청하고, 쿠버네티스가 적절한 PV를 찾아 연결해줍니다. 매번 PV를 손으로 만들기 번거로우니, 요청(PVC)이 오면 자동으로 PV를 만들어주는 도구를 두는데, 이게 프로비저너(provisioner)입니다.

local-path-provisioner

K3s는 local-path-provisioner를 기본 내장합니다. 이름 그대로, 노드의 로컬 디스크 경로에 저장 공간을 만들어주는 프로비저너입니다.

  • 장점: 별도 설치 없이 바로 쓸 수 있고, 로컬 디스크라 빠릅니다.
  • 한계: 데이터가 특정 노드에 묶입니다(노드가 죽으면 그 데이터 접근 불가). 분산 저장이 필요하면 Longhorn 같은 걸 쓰지만, 중소기업 규모에서는 local-path로 충분한 경우가 많습니다. 정말 중요한 데이터(오브젝트 스토리지 등)는 나중에 NAS(NFS)에 따로 둘 겁니다(08편).

저장 경로를 /data로 변경

local-path는 기본적으로 /var/lib/rancher/k3s/storage에 데이터를 저장합니다. 그런데 1편에서 우리는 /data를 별도 디스크로 분리해뒀고, 2편에서 /data/k3s-storage 디렉토리를 미리 만들어뒀습니다. 저장 경로를 이쪽으로 바꿔, 영속 데이터가 OS 디스크가 아닌 전용 데이터 디스크에 쌓이게 합니다.

local-path의 설정은 ConfigMap에 들어 있습니다. 이를 수정합니다.

kubectl edit configmap local-path-config -n kube-system

에디터가 열리면 config.json 부분의 경로를 /data/k3s-storage로 바꿉니다.

{
  "nodePathMap": [
    {
      "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
      "paths": ["/data/k3s-storage"]
    }
  ]
}

저장 후 프로비저너를 재시작해 변경을 반영합니다.

kubectl rollout restart deployment local-path-provisioner -n kube-system

동작 검증

PVC를 하나 만들어 PV가 자동 생성되는지 확인합니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
EOF

상태를 봅니다.

kubectl get pvc test-pvc

local-path는 “실제로 Pod가 이 PVC를 쓸 때” PV를 만드는 지연 바인딩(WaitForFirstConsumer) 방식이라, 처음엔 Pending으로 보일 수 있습니다. 이는 정상입니다. 테스트가 끝났으면 정리합니다.

kubectl delete pvc test-pvc

2. 로드밸런서: MetalLB가 푸는 문제

베어메탈의 딜레마

클라우드(AWS·GCP 등)에서 쿠버네티스를 쓰면, LoadBalancer 타입 서비스를 만들 때 클라우드가 알아서 외부 IP를 붙은 로드밸런서를 만들어줍니다. 그런데 우리처럼 자체 VM/베어메탈 환경에는 그런 게 없습니다. LoadBalancer 서비스를 만들어도 외부 IP가 <pending>인 채로 영영 멈춥니다.

MetalLB가 이 문제를 해결합니다. 베어메탈 환경에서 LoadBalancer 서비스에 우리 네트워크 대역의 실제 IP를 발급해주는 도구입니다.

L2 모드란

MetalLB는 두 가지 모드가 있는데, 우리는 L2 모드를 씁니다.

  • L2 모드: 클러스터 노드 중 하나가 특정 IP를 “내가 이 IP 담당”이라고 ARP로 광고합니다. 외부에서 그 IP로 오는 트래픽이 해당 노드로 들어오고, 거기서 서비스로 전달됩니다. 설정이 간단하고 별도 네트워크 장비 설정이 필요 없어 중소 규모에 적합합니다.
  • BGP 모드: 라우터와 BGP로 연동하는 방식. 대규모·고가용성에 유리하지만 라우터 설정이 필요해 복잡합니다.

우리 규모에는 L2 모드가 정답입니다.

MetalLB 설치

매니페스트로 설치합니다.

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml

설치 후 Pod가 모두 뜰 때까지 기다립니다.

kubectl get pods -n metallb-system -w

controllerspeaker Pod들이 Running이 되면 준비 완료입니다. (Ctrl+C로 빠져나옵니다.)


3. MetalLB IP 풀 설정

이제 MetalLB에게 “어떤 IP 대역을 나눠줘도 되는지” 알려줍니다. 1편 IP 정책에서 .200-220을 MetalLB 풀로 예약해뒀습니다.

두 가지 리소스를 만듭니다.

  • IPAddressPool: 발급 가능한 IP 범위
  • L2Advertisement: 그 풀을 L2 모드로 광고하겠다는 선언
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: prod-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.0.12.200-10.0.12.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: prod-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - prod-pool
EOF

이제 클러스터에서 LoadBalancer 타입 서비스를 만들면, MetalLB가 10.0.12.200~220 중 하나를 자동 할당합니다.


4. 동작 검증

실제로 IP가 발급되는지 테스트 서비스로 확인합니다.

# 테스트용 디플로이먼트
kubectl create deployment test-nginx --image=nginx

# LoadBalancer 서비스로 노출
kubectl expose deployment test-nginx --type=LoadBalancer --port=80

서비스를 확인합니다.

kubectl get svc test-nginx

EXTERNAL-IP 칼럼에 10.0.12.200 같은 실제 IP가 붙으면 성공입니다. <pending>이 아니라 IP가 나왔다는 건 MetalLB가 제대로 동작한다는 뜻입니다.

확인했으면 테스트 리소스를 정리합니다.

kubectl delete svc test-nginx
kubectl delete deployment test-nginx

5. 지금까지의 그림

이번 편을 마치면 클러스터는 이런 상태가 됩니다.

        [외부 네트워크 10.0.12.0/24]
                   │
                   ▼
     ┌──────────────────────────────┐
     │  MetalLB 풀: .200 ~ .220       │  ← 현관문(외부 진입 IP)
     │  LoadBalancer 서비스에 IP 발급  │
     └──────────────────────────────┘
                   │
                   ▼
            [K3s 클러스터]
                   │
     ┌──────────────────────────────┐
     │  local-path-provisioner        │  ← 수도(영속 저장소)
     │  PVC 요청 → /data/k3s-storage  │
     └──────────────────────────────┘

수도와 현관문이 생겼습니다. 하지만 아직 현관문(MetalLB IP)이 하나의 서비스에만 연결됩니다. 서비스가 수십 개가 되면 IP를 수십 개 발급받아야 할까요? 그렇지 않습니다.

다음 편에서 Ingress가 등장합니다. MetalLB IP 하나를 Ingress가 받고, 그 뒤에서 도메인(argocd.prd..., s3.prd... 등)에 따라 알맞은 서비스로 갈래를 나눠줍니다. 현관문 하나로 모든 방을 안내하는 셈입니다.


마치며

4편에서는 클러스터에 두 가지 필수 토대를 놓았습니다.

  • 스토리지(local-path): PV/PVC 개념을 이해하고, 저장 경로를 전용 데이터 디스크(/data/k3s-storage)로 바꿨습니다. 중소 규모엔 로컬 스토리지로 충분하고, 중요한 데이터는 나중에 NFS로 따로 둡니다.
  • 로드밸런서(MetalLB L2): 베어메탈에서 LoadBalancer 서비스에 실제 IP를 발급하는 문제를 해결했습니다. .200-220 풀을 L2 모드로 광고하도록 설정했습니다.

이 둘은 앞으로 등장할 모든 것의 받침대입니다. 다음 편에서는 그 위에 Ingress와 TLS를 올려, 외부에서 도메인으로 서비스에 접근하는 경로를 완성합니다. 05편 — Ingress-NGINX와 OPNsense Caddy 연동으로 이어집니다.


이전 글: 중소기업 쿠버네티스 구축 – 03. K3s 부트스트랩: 마스터와 워커 조인
다음 글: 중소기업 쿠버네티스 구축 – 05. Ingress와 TLS 연동: 외부 노출 완성 (Ingress-NGINX·OPNsense Caddy·와일드카드)


MORE POSTS

다른 글 보기

Photo by Unsplash (unsplash.com) — Free to use
뉴스

API를 바꾸는 AI와 ‘의도 이해’ 기반 웹 — SOA 2.0의 부상

2026.07.24
guniq 인사이트 썸네일 — IT 프로젝트 관리 도구 완전 해부 – 20. PSTA의 장단점과 한계 — 그리고 시리즈를 마치며
프로젝트 관리

IT 프로젝트 관리 도구 완전 해부 – 20. PSTA의 장단점과 한계 — 그리고 시리즈를 마치며

2026.07.23
guniq 인사이트 썸네일 — 중소기업 쿠버네티스 구축 – 06. 웹 대시보드: 클러스터 시각화 (Headlamp·ServiceAccount)
테크 랩

중소기업 쿠버네티스 구축 – 06. 웹 대시보드: 클러스터 시각화 (Headlamp·ServiceAccount)

2026.07.22

프로젝트 문의 환영합니다

기획부터 개발, 운영까지 함께 만들어 드립니다.