1. 개요

쿠버네티스(Kubernetes) 오프라인 설치 테스트를 위해서는 외부 네트워크가 완전히 단절된 환경이 필요했다. 물론 별도 서버에 KVM이나 VirtualBox로 VM을 구성하고 네트워크를 단절시키는 방법도 있지만, 구성이 번거롭고 쿠버네티스 환경에 익숙해진 만큼 가급적 클러스터 내에서 모든 것을 해결하고 싶었다.

그러던 중 KubeVirt를 이용하면 쿠버네티스 클러스터에서 노드의 자원으로 VM을 생성하고 관리할 수 있다는 것을 발견했다. 또한, CiliumNetworkPolicy와 연계하면 오프라인 환경을 쉽게 구성할 수 있다고 판단했다.

2. KubeVirt란?

KubeVirt는 쿠버네티스에서 가상 머신(VM)을 관리할 수 있게 해주는 오픈소스 프로젝트다. 쿠버네티스의 기능을 확장하여, 기존의 VM 기반 워크로드를 컨테이너 기반 애플리케이션과 동일한 플랫폼에서 실행하고 관리할 수 있도록 지원한다.

KubeVirt는 사용자 정의 리소스 정의(CRD, Custom Resource Definitions)를 통해 쿠버네티스 API를 확장하는 방식으로 동작한다. 이를 통해 쿠버네티스는 파드(Pod)와 같은 네이티브 리소스처럼 VM 관련 객체를 이해하고 관리할 수 있게 된다. KubeVirt를 사용해 VM을 생성하면, VM은 사실상 KVM(Kernel-based Virtual Machine) 인스턴스를 포함하는 특별한 종류의 파드 내부에서 실행된다.

3. 사전 준비

VM을 생성하기 위해서는 먼저 하드웨어의 가상화 기능을 활성화해야 했다. 서버의 EFI(또는 BIOS) 설정에 진입하여 가상화 옵션(Intel VT-x, AMD-V 등)을 Enable로 변경했다. 이 옵션의 명칭은 CPU 및 메인보드 제조사에 따라 조금씩 다를 수 있다.

4. 설치 과정

KubeVirt를 클러스터에 설치하기 위해 필요한 컴포넌트는 다음과 같다.

  • kubevirt-operator
  • cdi-cr
  • cdi-operator
  • kubevirt-manager (선택사항)

모든 컴포넌트를 설치한 후, VM을 제어하기 위해 클라이언트 OS에 virtctl CLI 도구를 추가로 설치해주었다.

5. 기본 이미지 생성

VM 디스크를 구성하는 몇 가지 방법 중, 재사용성을 고려하여 기반이 되는 DataVolume을 미리 구성하는 방식을 선택했다. 그리고 dataVolumeTemplates를 정의하여 각 VM 인스턴스를 생성할 때마다 이 기반 DataVolume을 복제해서 사용하도록 했다.

아래는 NFS-CSI를 위한 StorageProfile과 Rocky Linux, Ubuntu 클라우드 이미지를 기반으로 DataVolume을 생성하는 매니페스트다.

apiVersion: cdi.kubevirt.io/v1beta1
kind: StorageProfile
metadata:
  name: nfs-csi
spec:
  claimPropertySets:
    - accessModes:
        - ReadWriteMany
      volumeMode: Filesystem
---
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: base-rocky
spec:
  source:
    http:
      url: https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
  contentType: kubevirt
  storage:
    resources:
      requests:
        storage: 30Gi
---
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: base-ubuntu-2404
spec:
  source:
    http:
      url: https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
  contentType: kubevirt
  storage:
    resources:
      requests:
        storage: 30Gi

6. VM 생성

기본 이미지가 준비된 후, VirtualMachine 리소스를 정의하여 VM을 생성했다. dataVolumeTemplates를 사용해 앞서 만든 base-rocky 이미지를 복제하여 master1-rootdisk라는 이름의 새 볼륨을 만들도록 설정했다.

NOTE

VM에 netpolicy: internal-egress-only 라벨을 추가했다. 이 라벨은 이후 네트워크 정책을 통해 외부 통신을 차단하는 데 사용된다.

아래는 VM 생성 매니페스트 예시다.

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: master1
spec:
  runStrategy: Manual
  dataVolumeTemplates:
    - metadata:
        name: master1-rootdisk
      spec:
        source:
          pvc:
            name: base-rocky
            namespace: default
        storage:
          resources:
            requests:
              storage: 100Gi
  template:
    metadata:
      labels:
        kubevirt.io/domain: master1
        # 외부 네트워크를 단절시키기 위한 라벨
        netpolicy: internal-egress-only
    spec:
      domain:
        devices:
          disks:
            - name: rootdisk
              disk:
                bus: virtio
            - name: cloudinitdisk
              disk:
                bus: virtio
        interfaces:
          - name: default
            bridge: {}
        resources:
          requests:
            cpu: 2
            memory: 4Gi
          limits:
            cpu: 2
            memory: 4Gi
      networks:
        - name: default
          pod: {}
      volumes:
        - name: rootdisk
          dataVolume:
            name: master1-rootdisk
        - name: cloudinitdisk
          cloudInitNoCloud:
            userData: |
              #cloud-config
              users:
                - name: user
                  groups: wheel
                  shell: /bin/bash
                  sudo: ALL=(ALL) NOPASSWD:ALL
                  chpasswd: { expire: False }
                  # openssl passwd -6 test
                  passwd: $6$vwoaX/.Bf7ZCxtzm$4Jkrpv3dEaDT8WoRmQ3tNi4.u7NfvU8TaEzMZsKVyNGotDTTnQloWQ72Qxu8qrJF1MqKd67g2baV9tQUpeIfV0
                  lock_passwd: false
                  ssh_pwauth: True

작성한 매니페스트를 클러스터에 적용한 뒤, virtctl start master1 명령어로 VM을 시작할 수 있다.

7. 네트워크 격리

CiliumNetworkPolicy를 활용하여 VM의 외부 네트워크 통신을 차단하고, 오프라인 환경을 구성했다. endpointSelector를 통해 netpolicy: internal-egress-only 라벨이 붙은 파드(VM)를 타겟으로 지정했다.

그 후, egress 규칙을 설정하여 클러스터 내부 대역(10.0.0.0/8)과 kube-apiserver로의 통신만 허용하고, egressDeny 규칙으로 그 외의 모든 외부 나가는 통신(0.0.0.0/0)을 차단했다.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: internal-egress-only
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      netpolicy: internal-egress-only
  egress:
    - toCIDR:
        - 10.0.0.0/8 # 클러스터 내부 IP 대역
    - toEntities:
        - kube-apiserver
        - cluster
  egressDeny:
    - toCIDR:
        - 0.0.0.0/0

8. 회고

이번 쿠버네티스 클러스터 구축 프로젝트에서 KubeVirt가 핵심 과업은 아니었기에, 더 깊게 탐구해보지 못한 몇 가지 아쉬운 점이 남았다. 이 사항들은 추후 여유가 될 때 다시 시도해 볼 계획이다.

  • GPU 할당 실패: 노드의 그래픽카드를 VM에 할당하고 싶었지만, 관련 지식 부족과 시간 제약으로 끝내 설정하지 못했다. 작업 시간이 조금 더 충분했다면 하는 아쉬움이 남는다.
  • 고정 IP 할당: VM의 IP 주소를 고정하기 위해 인터페이스를 설정하는 데 실패했다. Multus CNI를 활용하면 가능하다는 정보를 접했지만, 설정이 예상보다 복잡하여 이번에는 적용하지 못했다.

참고