Post

Kube-vip 기반 고가용성 K3s 클러스터 구축

Kube-vip 기반 고가용성 K3s 클러스터 구축

이 글에서는 학교 내부망의 네트워크 제약을 Kube-vip로 극복하고 단일 가상 IP(VIP) 기반의 무중단 K3s HA 클러스터를 구축한 과정에 대해 설명합니다.

1. 개요

제가 운영하고 있는 코드플레이스는 원래 한 대의 물리 서버에서 Docker Swarm 기반으로 운영되고 있습니다. 하지만, 하나의 물리 서버가 장애가 발생할 경우 전체 서비스가 중단되는 문제가 있었습니다. 실제로 네트워크 장애 및 정전으로 인해 서비스가 중단된 사례가 발생하면서, 보다 안정적인 인프라를 구축할 필요성을 느끼게 되었습니다.

마침 사용 가능한 물리 서버가 3대로 늘어나면서 고가용성(High Availability, HA) 클러스터 구축을 결정했고, 경량화된 Kubernetes 배포판인 K3s를 선택했습니다. 이 과정에서 온프레미스 환경의 네트워크 제약사항을 해결하기 위해 Kube-vip를 도입했습니다.

이 글에서는 Kube-vip를 선택하게 된 배경과 실제 구축 과정, 그리고 동작 원리에 대해 정리하고자 합니다.

2. Kube-vip를 사용하게 된 배경

2-1. 온프레미스 네트워크 환경

코드플레이스 서버는 학교 내부망에 위치하고 있습니다. 따라서 외부에서 코드플레이스에 접근하기 위해서는 아래와 같이 공인 IP와 사설 IP를 이어주는 학교 NAT를 거치게 됩니다.

이 구조에서 핵심적인 제약사항은 다음과 같습니다.

  • 학교 NAT는 우리가 관리할 수 없는 영역입니다. 따라서 공인 IP를 추가로 할당받거나 NAT 레벨에서 로드밸런싱을 구성하는 것이 불가능했습니다.
  • 단일 사설 IP 주소로만 트래픽이 유입되기 때문에, 이 IP를 여러 노드가 공유하면서도 장애 시 자동으로 다른 노드로 넘길 수 있는 메커니즘이 필요했습니다.

2-2. 고려했던 다른 솔루션들

고가용성 클러스터 구축을 위해 여러 솔루션을 검토했습니다.

MetalLB

  • 온프레미스 환경에서 가장 널리 사용되는 로드밸런서
  • Layer 2 모드와 BGP 모드를 지원
  • 하지만 우리 환경에서는 상단 라우터(L3/L4 스위치)를 제어할 수 없어 BGP 설정이 불가능했고, Layer 2 모드만으로는 Control Plane의 고가용성까지 보장하기 어려웠습니다.

HAProxy + Keepalived

  • 검증된 고가용성 솔루션
  • 하지만 별도의 로드밸런서 인스턴스를 운영해야 하며, Keepalived 설정과 HAProxy 설정을 각각 관리해야 하는 운영 오버헤드가 발생합니다.
  • 쿠버네티스 클러스터 외부에서 추가 인프라를 관리해야 한다는 부담이 있었습니다.

2-3. Kube-vip를 선택한 이유

결국 저는 아래와 같은 Kube-vip의 장점들 때문에 이 솔루션을 선택하게 되었습니다.

  • 별도의 외부 인프라 불필요: 쿠버네티스 클러스터 내부의 파드로만 동작하여 추가 서버나 장비가 필요 없습니다.
  • Control Plane과 Service 모두 지원: API 서버의 고가용성(Control Plane VIP)과 서비스 로드밸런싱(Service VIP)을 모두 제공합니다.
  • 간단한 설정: YAML 매니페스트 파일만으로 설정이 완료되며, 복잡한 네트워크 설정이 필요 없습니다.
  • K3s와의 통합: K3s의 auto-deploying manifests 기능을 활용하여 클러스터 초기화 시점부터 Kube-vip를 자동으로 구동할 수 있습니다.

학교 NAT는 항상 고정된 하나의 VIP로만 트래픽을 보내면 되고, 내부적으로 어떤 노드가 다운되더라도 Kube-vip가 자동으로 살아있는 노드로 VIP를 이동시켜 줍니다. “외부 네트워크 장비의 지원 없이, 내부망에서 자체적으로 고가용성을 확보해야 한다”는 조건을 가장 심플하게 해결해 주는 방법이라고 생각했습니다.

3. Kube-vip란?

Kube-vip 공식 문서에 따르면, Kube-vip는 쿠버네티스 클러스터에서 가상 IP(VIP)를 생성하고 관리하는 오픈소스 프로젝트입니다.

Kube-vip는 다음과 같은 기능을 제공합니다.

  • 가상 IP(VIP) 관리: 클러스터 내에서 가상 IP를 생성하고, 이를 통해 트래픽을 분산시킬 수 있습니다.
  • 고가용성: 노드 장애 시에도 VIP가 살아있는 노드로 자동으로 이동하여 서비스 중단을 방지합니다.
  • 간편한 설치 및 구성: 쿠버네티스 클러스터 내에서 간단한 설정으로 사용할 수 있습니다.

3-1. Kube-vip VIP 동작 원리

Kube-vip는 ARP(Address Resolution Protocol)를 사용하여 VIP를 관리합니다. ARP는 네트워크에서 IP 주소를 MAC 주소로 변환하는 프로토콜로, Kube-vip는 이를 활용하여 VIP의 소유권을 동적으로 이동시킵니다.

Kube-vip의 VIP 동작 과정은 다음과 같습니다.

  • 리더 선출: Kube-vip 파드들은 쿠버네티스의 Lease 리소스를 사용하여 리더를 선출합니다. 이 과정은 etcd를 기반으로 하는 분산 합의 메커니즘을 활용합니다.
  • VIP 소유권 획득: 리더로 선출된 노드의 Kube-vip 파드가 VIP를 자신의 네트워크 인터페이스에 바인딩합니다.
  • Gratuitous ARP 전송: 리더 노드는 네트워크에 Gratuitous ARP(GARP) 메시지를 브로드캐스트합니다.
    • 이 메시지에는 “VIP X.X.X.X는 이제 MAC 주소 YY:YY:YY:YY:YY:YY로 연결되어 있습니다”라는 정보가 포함되어 있습니다.
  • ARP 캐시 업데이트: 같은 네트워크의 모든 장비(스위치, 라우터, 다른 서버)가 자신의 ARP 캐시를 업데이트하여, 이후 VIP로 보내는 모든 패킷이 새로운 리더 노드로 전달됩니다.
  • 장애 발생 시: 리더 노드가 다운되면 Lease가 만료되고, 남은 파드들이 새로운 리더를 선출합니다. 새 리더가 다시 GARP를 전송하여 VIP 소유권을 넘겨받습니다.

위 과정을 아주 간략하게 도식화하면 다음과 같습니다. 클러스터 내에서 리더를 선출하고, 리더가 VIP를 소유하며, GARP를 통해 네트워크에 이를 알리는 구조입니다.

이러한 ARP 기반의 VIP 관리를 통해 Kube-vip를 사용하여 하나의 고정된 IP 주소로 여러 대의 노드에 트래픽을 분산시킬 수 있습니다.

4. Kube-vip 설치 및 구성

이제 실제로 Kube-vip를 사용하여 K3s 클러스터를 구축하는 과정을 단계별로 설명하겠습니다. 첫번째 노드에서 Kube-vip 구성을 시작으로, 나머지 노드들을 클러스터에 조인하는 방식으로 진행됩니다.

K3s는 특정 디렉토리(/var/lib/rancher/k3s/server/manifests)에 yaml 파일을 넣어두면, 클러스터가 시작될 때 자동으로 이를 적용해 주는 기능을 제공하는데요, 저는 이 기능을 활용해 Kube-vip 매니페스트 파일을 미리 배치한 후 K3s를 설치하는 방식을 선택했습니다.

먼저 첫번째 노드에서 아래 단계를 따라 진행합니다.

4-1. 사전 준비 (환경변수 설정)

설치 과정에서 반복적으로 사용될 VIP 주소와 네트워크 인터페이스 이름을 변수로 미리 지정해 두면 편리합니다.

INTERFACE는 반드시 각 서버의 실제 네트워크 인터페이스 이름(예: eth0, ens33, eno1 등)과 일치해야 합니다. ip a 명령어로 확인해 주세요. 네트워크 인터페이스 이름이 다르면 VIP가 정상적으로 바인딩되지 않으므로, 반드시 모든 노드에서 동일하게 설정해 주세요. (제가 이걸로 한참 헤맸습니다…)

1
2
3
4
5
# 사용하려는 가상 IP (VIP)
export VIP=192.168.0.100

# 실제 물리 서버의 네트워크 인터페이스 이름
export INTERFACE=eno3

4-2. 매니페스트 파일 작성

Kube-vip 구동에 필요한 두 가지 매니페스트 파일(RBAC, DaemonSet)을 작성합니다.

권한 설정 (RBAC)

Kube-vip가 쿠버네티스 API와 통신하며 리더 선출 등을 수행할 수 있도록 권한을 부여합니다. 아래 내용을 kube-vip-rbac.yaml 파일로 저장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kube-vip
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  name: system:kube-vip-role
rules:
  - apiGroups: [""]
    resources: ["services/status"]
    verbs: ["update"]
  - apiGroups: [""]
    resources: ["services", "endpoints"]
    verbs: ["list", "get", "watch", "update"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["list", "get", "watch", "update", "patch"]
  - apiGroups: ["coordination.k8s.io"]
    resources: ["leases"]
    verbs: ["list", "get", "watch", "update", "create"] # 리더 선출을 위한 핵심 권한
  - apiGroups: ["discovery.k8s.io"]
    resources: ["endpointslices"]
    verbs: ["list", "get", "watch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["list"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: system:kube-vip-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-vip-role
subjects:
  - kind: ServiceAccount
    name: kube-vip
    namespace: kube-system

데몬셋 (DaemonSet) 템플릿

모든 마스터 노드에 Kube-vip 파드를 띄우기 위한 설정입니다. 아래 파일은 템플릿 파일로, 뒤이어 실행할 명령어를 통해 실제 설정값이 주입됩니다. kube-vip.yaml.tpl 파일로 저장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app.kubernetes.io/name: kube-vip-ds
    app.kubernetes.io/version: v1.0.2
  name: kube-vip-ds
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: kube-vip-ds
  template:
    metadata:
      labels:
        app.kubernetes.io/name: kube-vip-ds
        app.kubernetes.io/version: v1.0.2
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-role.kubernetes.io/master # 마스터 노드에 Kube-vip 파드가 배치되도록 설정 / 워커 노드에는 배치되지 않음
                    operator: Exists
              - matchExpressions:
                  - key: node-role.kubernetes.io/control-plane
                    operator: Exists
      containers:
        - args:
            - manager
          env:
            - name: vip_arp
              value: "true"
            - name: port
              value: "6443"
            - name: vip_interface
              value: ##VIP_INTERFACE## # 나중에 sed 명령어로 치환됨
            - name: vip_subnet
              value: "32"
            - name: dns_mode
              value: first
            - name: cp_enable
              value: "true"
            - name: cp_namespace
              value: kube-system
            - name: svc_enable
              value: "true"
            - name: vip_leaderelection
              value: "true"
            - name: vip_leasename
              value: plndr-cp-lock
            - name: vip_leaseduration
              value: "5"
            - name: vip_renewdeadline
              value: "3"
            - name: vip_retryperiod
              value: "1"
            - name: address
              value: ##VIP_ADDRESS## # 나중에 sed 명령어로 치환됨
            - name: prometheus_server
              value: :2112
          image: ghcr.io/kube-vip/kube-vip:v1.0.2
          imagePullPolicy: IfNotPresent
          name: kube-vip
          resources: {}
          securityContext:
            capabilities:
              add:
                - NET_ADMIN
                - NET_RAW
              drop:
                - ALL
      hostNetwork: true
      serviceAccountName: kube-vip
      tolerations:
        - effect: NoSchedule
          operator: Exists
        - effect: NoExecute
          operator: Exists
  updateStrategy: {}

4-3. 매니페스트 배치 및 K3s 설치

이제 작성한 파일들을 K3s가 인식할 수 있는 경로로 이동시키고, K3s 서버를 시작합니다.

1
2
3
4
5
6
7
8
9
10
# 1. K3s 매니페스트 디렉토리 생성 (아직 K3s 설치 전이므로 수동 생성)
sudo mkdir -p /var/lib/rancher/k3s/server/manifests/

# 2. RBAC 설정 파일 복사
sudo cp ./kube-vip-rbac.yaml /var/lib/rancher/k3s/server/manifests/kube-vip-rbac.yaml

# 3. 템플릿 복사 및 변수 치환 (VIP, INTERFACE 주입)
sudo cp ./kube-vip.yaml.tpl /var/lib/rancher/k3s/server/manifests/kube-vip.yaml
sudo sed -i "s/##VIP_ADDRESS##/$VIP/g" /var/lib/rancher/k3s/server/manifests/kube-vip.yaml
sudo sed -i "s/##VIP_INTERFACE##/$INTERFACE/g" /var/lib/rancher/k3s/server/manifests/kube-vip.yaml

파일 배치가 끝났다면, 아래 명령어로 K3s를 설치합니다.

1
2
3
4
5
curl -sfL https://get.k3s.io | sh -s - server \
    --cluster-init \
    --tls-san $VIP \
    --disable servicelb \
    --disable-cloud-controller
  • --cluster-init: 임베디드 etcd를 사용하여 클러스터를 초기화합니다. 이 옵션은 첫 번째 노드에서만 사용하며, 이후 노드는 –server 옵션으로 기존 클러스터에 조인합니다.
  • --tls-san $VIP: API 서버 TLS 인증서의 Subject Alternative Name(SAN)에 VIP를 추가합니다. 이를 통해 클라이언트(kubectl 등)가 VIP를 통해 API 서버에 접근할 때 인증서 검증 오류가 발생하지 않습니다.
  • --disable servicelb: K3s에 내장된 ServiceLB(Klipper LoadBalancer)를 비활성화합니다. Kube-vip가 LoadBalancer 역할을 대신하므로 중복을 피하기 위해 끕니다.
  • --disable-cloud-controller: 온프레미스 환경에서는 클라우드 컨트롤러가 필요 없으므로 비활성화합니다.

--disable servicelb 옵션을 빼먹으면 노드 조인 시 네트워크 연결이 끊어질 수 있습니다. ServiceLB와 Kube-vip가 동시에 동작하면서 동일한 VIP에 대해 서로 다른 ARP 응답을 보내 ARP 충돌이 발생하기 때문입니다. 이로 인해 네트워크 스위치의 ARP 테이블이 불안정해지고, 결과적으로 해당 IP로의 트래픽이 올바르게 라우팅되지 않아 네트워크 단절이 발생합니다. (이것때문에 또 한참 헤맸습니다…)

4-4. 나머지 노드 추가 (Cluster Join)

첫 번째 노드가 정상적으로 구동되었다면, 나머지 2대의 노드를 클러스터에 합류시켜 고가용성 환경을 완성합니다. 새로운 노드를 클러스터에 추가하기 위해서는 토큰이 필요하므로, 첫 번째 노드에서 아래 명령어를 입력해 토큰 값을 확인합니다.

1
2
export TOKEN=$(sudo cat /var/lib/rancher/k3s/server/node-token)
echo "클러스터 조인 토큰: $TOKEN"

새로 추가할 노드(2번, 3번 서버)에서 아래 명령어를 실행합니다. $VIP와 $TOKEN 값은 위에서 확인한 값을 넣어주세요.

1
2
3
4
5
6
7
8
9
10
11
# 변수 설정
export VIP="192.168.0.100"      # 첫 번째 노드에서 설정한 VIP와 동일해야 함
export TOKEN="<확인한_토큰_값>"

# K3s 설치 및 클러스터 조인
curl -sfL https://get.k3s.io | sh -s - server \
    --server https://$VIP:6443 \
    --token ${TOKEN} \
    --tls-san $VIP \
    --disable servicelb \
    --disable-cloud-controller

클러스터에 조인이 안되는 경우, UFW 방화벽 설정을 확인해 주세요. K3s가 사용하는 기본 포트(6443, 8472 등)가 열려 있어야 합니다.

5. 설치 확인 및 테스트

5-1. Kube-vip 파드 상태 확인

먼저 Kube-vip가 정상적으로 구동되고 있는지 확인합니다.

1
2
3
4
5
6
7
kubectl get pods -n kube-system -l app.kubernetes.io/name=kube-vip-ds

# 출력 예시 (3대의 마스터 노드에 각각 1개의 Kube-vip 파드가 Running 상태로 표시됨)
NAME                      READY   STATUS    RESTARTS   AGE
kube-vip-ds-abcde         1/1     Running   0          10m
kube-vip-ds-fghij         1/1     Running   0          10m
kube-vip-ds-klmno         1/1     Running   0          9m

5-2. 노드 상태 확인

모든 노드가 Ready 상태인지 확인합니다.

1
2
3
4
5
6
7
kubectl get nodes

# 출력 예시 (Ready 상태의 노드 3대 확인)
NAME                                 STATUS   ROLES                       AGE   VERSION
xxx-xxxx-xxx-xxx                     Ready    control-plane,etcd,master   11d   v1.33.6+k3s1
xxxx-xxxxxxxx-xxxxxxxxx-xxxxx-xxxx   Ready    control-plane,etcd,master   11d   v1.33.6+k3s1
xxxxx-x                              Ready    control-plane,etcd,master   9d    v1.33.6+k3s1

5-3. VIP 동작 테스트

마지막으로, VIP가 정상적으로 동작하는지 테스트합니다.

먼저, 리더 선출에 사용되는 Lease 리소스를 확인합니다.

1
2
3
4
5
6
7
8
9
kubectl get lease -n kube-system plndr-cp-lock -o yaml

# 출력 예시 (spec.holderIdentity 필드에 현재 리더 노드 이름이 표시됨)
spec:
  acquireTime: "2025-12-10T11:19:08.269641Z"
  holderIdentity: <리더_노드_이름>
  leaseDurationSeconds: 5
  leaseTransitions: 3
  renewTime: "2025-12-13T12:41:45.037759Z"

이제 VIP가 할당된 노드에 접속하여, 해당 노드의 네트워크 인터페이스에 VIP가 바인딩되어 있는지 확인합니다.

1
2
3
4
5
6
7
8
9
ip a show $INTERFACE | grep $VIP

# 출력 예시 (VIP가 할당된 노드에서 확인)
2: eno3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
    inet <사설_IP_주소> brd <브로드캐스팅 IP> scope global noprefixroute eno3
       valid_lft forever preferred_lft forever
    inet <VIP_주소> scope global deprecated eno3 # 기존 사설 IP 외에 VIP가 추가로 할당되어 있는지 확인
       valid_lft forever preferred_lft forever

5-4. 장애 조치(HA) 테스트

리더 노드를 강제로 종료하여 VIP가 다른 노드로 이동하는지 확인합니다.

1
2
3
4
# 1. 리더 노드 확인
kubectl get lease -n kube-system plndr-cp-lock -o yaml

# spec.holderIdentity 필드에서 리더 노드 이름 확인
1
2
# 2. 리더 노드에서 K3s 서비스 중지 (예: systemctl 사용)
sudo systemctl stop k3s
1
2
3
4
# 3. 몇 초 후, 다른 노드에서 Lease 리소스 확인
kubectl get lease -n kube-system plndr-cp-lock -o yaml

# spec.holderIdentity 필드가 새로운 리더 노드 이름으로 변경되었는지 확인
1
2
3
4
# 4. 새로운 리더 노드에서 VIP가 바인딩되었는지 확인
ip a show $INTERFACE | grep $VIP

# VIP가 새로운 리더 노드에 바인딩되어 있는지 확인

6. 마무리

이상으로 Kube-vip를 사용한 이유와 고가용성 K3s 클러스터를 구축하는 방법에 대해 정리해보았는데요, Kube-vip는 외부 로드밸런서나 상단 라우터의 지원 없이도 VIP 기반의 고가용성을 구현할 수 있는 유용한 솔루션임을 다시 한 번 느꼈습니다.

코드플레이스에서 Kube-vip를 도입함으로써 하나의 고정 IP 주소로 여러 대의 마스터 노드에 트래픽을 안정적으로 분산시킬 수 있게 되었고, 장애 발생 시에도 자동으로 다른 노드로 VIP가 이동하여 서비스 중단 없이 운영할 수 있게 되었습니다.

이 글을 통해 비슷환 환경에서 고가용성 클러스터를 구축하려는 분들께 도움이 되었으면 합니다.

References

This post is licensed under CC BY 4.0 by the author.