🚀 요약

SUMMARY

KubeVirt VM에 붙인 secondary IP가 재기동할 때마다 바뀌는 게 문제였다. Pod 애노테이션으로 IP를 고정하려 했지만 KubeVirt가 VM을 launcher 파드로 넘길 때 ips 필드를 지워버려서 먹히지 않았다. 결국 VXLAN 브리지로 노드 간 L2를 확장하고, 그 위에 Multus NAD를 VM별로 하나씩 두면서 Static IPAM으로 주소를 NAD에 박아두는 쪽으로 우회했다. IP가 파드가 아니라 선언(NAD)에 묶이니, VM을 껐다 켜거나 다른 노드로 옮겨도 같은 주소를 유지한다.

💡 개요

앞서 KubeVirt로 오프라인 테스트 환경 만들기 글 끝에 “고정 IP 할당은 실패했다, Multus 쓰면 된다는데 복잡해서 접었다”고 적어뒀다. 그 접어둔 걸 다시 폈다.

KubeVirt VM은 실제로는 특수한 파드(virt-launcher) 안에서 돌아간다. 그래서 기본 네트워크는 파드 네트워크를 그대로 받는데, 파드 IP는 파드가 다시 뜨면 바뀐다. VM 입장에서 virtctl stopstart를 하면 launcher 파드가 새로 뜨고, 그때마다 secondary IP도 딸려 바뀌었다. (재부팅은 좀 다르다. VM 안에서 init 6로 게스트만 재부팅하면 파드는 그대로라 IP도 안 바뀐다. 문제가 되는 건 파드 자체가 재생성되는 경우다.)

VM을 다른 VM이나 외부에서 고정된 주소로 계속 찾아야 하는 용도라면 이건 곤란하다. IP가 바뀔 때마다 붙는 쪽 설정을 고쳐야 하니까. 그래서 “재기동을 하든, VM이 다른 노드로 스케줄되든 항상 같은 IP”라는 조건을 만족시키고 싶었다.

📋 선정 배경

요구사항은 세 줄로 정리됐다.

  • VM을 껐다 켜도(=launcher 파드 재생성) secondary IP가 유지될 것
  • VM이 어느 노드로 스케줄되든 같은 IP가 그대로 붙을 것 (노드 위치 무관)
  • 구성이 선언적이라 IaC로 그대로 재현될 것

제약도 있었다. 클러스터 CNI는 Cilium이고, secondary 네트워크는 Multus로 얹는 구조다. Cilium 위에 Multus를 태우려면 Cilium이 CNI를 독점하지 않도록 cni.exclusive: false를 꺼줘야 하고, NAD의 type으로 쓰는 플러그인 바이너리가 각 노드의 /opt/cni/bin에 실제로 있어야 한다.

NOTE

cni.exclusive를 안 끄면 Cilium이 /etc/cni/net.d를 자기 것만 남기고 정리해버려서 Multus 체인이 통째로 무시된다. Multus를 얹는데 secondary 인터페이스가 아예 안 생긴다면 여기부터 의심하는 게 빠르다. 그리고 bridge, static 같은 플러그인은 containernetworking/plugins에서 받아 /opt/cni/bin에 깔아둬야 한다.

📊 비교

고정 IP를 만드는 방법을 순서대로 시도했다. 결과부터 표로.

시도방식결과
기본 IPAM (host-local/DHCP)NAD에 IPAM만 걸고 IP는 자동 할당재기동마다 IP 변동. 애초에 고정이 목적이 아님
Pod 애노테이션 ips 요청k8s.v1.cni.cncf.io/networks에 원하는 IP를 명시KubeVirt가 launcher 파드로 넘길 때 ips 필드를 제거 → 무시됨
NAD에 Static IPAM주소를 NAD 안에 직접 박고, VM별로 NAD를 하나씩고정 IP 유지. 채택

가운데 줄이 이 글의 핵심 삽질이다. Multus로 secondary 네트워크에 특정 IP를 요청하는 정석은, 파드(여기선 VM 템플릿)에 이런 애노테이션을 다는 것이다.

# 정석대로라면 이렇게 networkName과 ips를 함께 요청한다.
# 하지만 KubeVirt VM에서는 ips가 무시된다.
annotations:
  k8s.v1.cni.cncf.io/networks: |
    [{ "name": "vm-fixed", "namespace": "vm", "ips": ["10.20.30.11/24"] }]

일반 파드였으면 이걸로 끝이다. 그런데 VM에 걸면 요청한 IP가 안 잡혔다. 한참 헤매다 확인한 건, KubeVirt가 VM 스펙을 실제 launcher 파드 스펙으로 변환하면서 이 네트워크 요청의 ips 필드를 떼어낸다는 점이다. VM의 네트워크는 KubeVirt가 자체적으로 관리하는 영역이라, 파드 레벨에서 IP를 지정해 넣는 이 경로를 그대로 통과시켜주지 않는다. 업스트림 동작이 그래서, 내 설정이 틀린 게 아니라 이 경로 자체가 VM에는 막혀 있던 거였다. (설정을 스무 번쯤 고쳐본 뒤에야 “내 문제가 아니구나”를 받아들였다.)

그러면 IP를 파드가 아니라 NAD 자체에 박아두면 된다. 요청하는 주체가 사라지니 떼일 필드도 없다. Static IPAM이 딱 그 용도다.

✅ 선정 사유

정리된 구성은 이렇다. 노드 위에 VXLAN 브리지를 깔아 L2를 확장하고, 그 브리지를 쓰는 Multus NAD를 VM마다 하나씩 만들되 IPAM은 static으로 고정한다.

1. VXLAN 브리지로 노드 간 L2 확장

노드 위치와 무관하게 같은 IP가 붙으려면, 그 IP가 사는 L2 세그먼트가 노드 하나에 갇혀 있으면 안 된다. 그래서 각 노드에 VXLAN 인터페이스를 만들고 리눅스 브리지(br-vxlan0)에 물려, 모든 노드가 같은 오버레이 L2를 공유하게 했다. VM이 어느 노드에 스케줄되든 이 브리지에 붙기만 하면 같은 대역 안에서 통신한다. (이 부분은 노드 프로비저닝 단계에서 한 번 깔아두는 밑작업이다.)

2. VM별 NAD에 Static IPAM

브리지가 준비됐으면 그 위에 NAD를 얹는다. typebridge로 이 VXLAN 브리지를 가리키고, ipamstatic으로 줄 주소를 직접 적는다. 고정하고 싶은 VM마다 이런 NAD를 하나씩 둔다.

# VM 한 대에 대응하는 고정 IP용 NAD.
# bridge 플러그인이 노드의 br-vxlan0(=VXLAN 오버레이)에 인터페이스를 붙이고,
# static IPAM이 이 주소를 그대로 부여한다. IP가 파드가 아니라 이 선언에 묶인다.
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: vm-fixed-master-01
  namespace: vm
spec:
  config: |
    {
      "cniVersion": "0.3.1",
      "type": "bridge",
      "bridge": "br-vxlan0",
      "ipam": {
        "type": "static",
        "addresses": [
          { "address": "10.20.30.11/24", "gateway": "10.20.30.1" }
        ]
      }
    }

Static IPAM은 NAD 하나당 주소가 고정이라, VM별로 다른 IP를 주려면 NAD도 그만큼 만들어야 한다. 파드 오토스케일 같은 데는 안 맞지만, “정해진 몇 대의 VM에 정해진 주소”라는 이 상황엔 오히려 단순해서 좋았다. NAD가 곧 IP 대장 역할을 한다.

3. VM에 secondary로 붙이기

VM 템플릿에서는 기본 파드 네트워크는 그대로 두고(외부 egress·클러스터 통신용), 위 NAD를 secondary 인터페이스로 추가한다.

# 기본(default)은 파드 네트워크, fixed가 위에서 만든 고정 IP용 secondary.
spec:
  template:
    spec:
      domain:
        devices:
          interfaces:
            - name: default
              masquerade: {}
            - name: fixed
              bridge: {}
      networks:
        - name: default
          pod: {}
        - name: fixed
          multus:
            networkName: vm/vm-fixed-master-01

이렇게 하면 IP를 결정하는 게 launcher 파드의 수명이 아니라 NAD라는 선언이 된다. virtctl stop/start로 파드가 새로 떠도, 스케줄러가 VM을 다른 노드에 올려도, 붙는 NAD가 같으면 IP도 같다. 요구했던 세 조건이 다 여기서 나온다. 처음엔 그럴듯해 보였던 Pod 애노테이션 경로를 버리고 NAD로 옮긴 게 IaC와도 잘 맞았다. VM 정의와 NAD 정의를 같이 커밋해두면 클러스터를 다시 세워도 주소 배치가 그대로 재현된다.

한계도 분명하다. VM 대수만큼 NAD가 늘고, 주소를 사람이 직접 관리해야 한다. 대수가 크게 늘면 Whereabouts 같은 IPAM으로 넘어가는 게 맞겠지만, 지금 규모에선 눈에 보이는 Static이 관리하기 편했다.

🔗 참고