Back to Devoured - September 04, 2026
DEVOURED
Metal3 meets KubeVirtBMC: Provisioning KubeVirt VMs like bare metal

Metal3 meets KubeVirtBMC: Provisioning KubeVirt VMs like bare metal

DevOps CNCF
Pairing Metal3 with KubeVirtBMC allows developers to provision virtual machines using the same bare-metal tooling and Redfish workflows used for physical servers.
What: Anish Bista and Zespre Chang detail how to use the KubeVirtBMC operator to create virtual BMC interfaces for KubeVirt VMs. Metal3 interacts with these virtual BMCs via Redfish, enabling PXE-less provisioning of VMs using ISO images without needing a specialized provisioning network.
Why it matters: This approach standardizes infrastructure management by treating virtualized workloads as physical hardware, allowing unified CI/CD pipelines to deploy across both VM and bare-metal environments.
Deep dive
  • KubeVirtBMC creates virtual BMC pods linked to VMs.
  • Metal3 (Bare Metal Operator) manages lifecycle via Ironic.
  • Redfish virtual media is used to boot ISOs, bypassing DHCP/PXE complexity.
  • Demo requires cert-manager, KubeVirt, and the Ironic Standalone Operator.
  • Configures custom Redfish virtual media for streamlined VM booting.
Decoder
  • BMC (Baseboard Management Controller): A specialized microcontroller embedded on a server motherboard that allows remote management of hardware regardless of the CPU or OS state.
  • Redfish: A standard API for managing hardware platforms and components, replacing older protocols like IPMI.
  • Metal3 (Metal Kubed): A project that manages bare-metal servers through Kubernetes by using Custom Resources.
Original article

In the previous post, we introduced KubeVirtBMC and showed how it provides virtual BMC endpoints for KubeVirt VMs. We tested it with raw IPMI and Redfish commands. That was fun, but the real power of KubeVirtBMC shines when you pair it with actual bare-metal provisioning tools.

In this post, we’ll walk through a complete end-to-end demo: using Metal3 to manage and provision KubeVirt VMs through KubeVirtBMC, just as if they were physical servers. You’ll be able to follow along and replicate every step.

Why Metal3?

Metal3 (Metal Kubed) is a CNCF Incubating project that brings bare-metal host management into the Kubernetes ecosystem. Under the hood, it uses OpenStack Ironic to handle the heavy lifting, inspecting hardware, setting boot devices, and writing OS images to disks.

The core abstraction is the BareMetalHost custom resource. You declare what you want (BMC address, credentials, desired image), and Metal3 takes care of the rest. The typical BareMetalHost lifecycle goes like this:

Metal3 expects to talk to a BMC via IPMI or Redfish. Physical servers have these built in. KubeVirt VMs don’t—unless you give them one with KubeVirtBMC.

What We’re Building

Here’s a high-level overview of the demo environment:

Everything runs in a single Kubernetes cluster. Metal3 manages BareMetalHost resources that point to the virtual BMC endpoints created by KubeVirtBMC. When Metal3 tells Ironic to power on a host or attach a boot image, Ironic sends Redfish requests to the BMC pod. The BMC pod translates these into Kubernetes API calls to control the KubeVirt VM. Metal3 doesn’t know (or care) that it’s talking to a VM.

Prerequisites

Before we start, make sure you have:

  • A Kubernetes cluster with virtualization support (nested virtualization or bare metal)
  • KubeVirt installed and functional
  • A storage provider (for VM disks)
  • kubectl, helm, and kustomize installed locally

Step 1: Install cert-manager

Both KubeVirtBMC and Metal3 components require cert-manager for webhook certificates.

helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
    --namespace=cert-manager \
    --create-namespace \
    --set=crds.enabled=true

Step 2: Install KubeVirtBMC

helm upgrade --install kubevirtbmc kubevirtbmc \
    --repo=https://charts.kubevirtbmc.io \
    --namespace=kubevirtbmc-system \
    --create-namespace

Step 3: Create a KubeVirt VM with BMC

We’ll create a VM that simulates a bare-metal server. It needs a disk, a network interface, and it should start in a powered-off state so that Metal3 can manage its lifecycle from scratch.

Create a PVC for the VM’s root disk:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: metal3-demo-vm-disk
  namespace: default
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
EOF

Create the VirtualMachine with runStrategy: Halted so it stays powered off:

cat <<EOF | kubectl apply -f -
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: metal3-demo-vm
  namespace: default
spec:
  runStrategy: Halted
  template:
    spec:
      domain:
        cpu:
          cores: 4
        memory:
          guest: 8Gi
        devices:
          disks:
          - name: rootdisk
            disk:
              bus: virtio
          - name: cdrom
            cdrom:
              bus: sata
          interfaces:
          - name: default
            macAddress: "02:00:00:00:00:01"
            bridge: {}
        firmware:
          bootloader:
            efi:
              secureBoot: false
      networks:
      - name: default
        pod: {}
      volumes:
      - name: rootdisk
        persistentVolumeClaim:
          claimName: metal3-demo-vm-disk
EOF

Now create the VirtualMachineBMC and its credential Secret:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: demo-bmc-secret
  namespace: default
stringData:
  username: admin
  password: password
---
apiVersion: bmc.kubevirt.io/v1beta1
kind: VirtualMachineBMC
metadata:
  name: demo-bmc
  namespace: default
spec:
  virtualMachineRef:
    name: metal3-demo-vm
  authSecretRef:
    name: demo-bmc-secret
EOF

Step 4: Install the Metal3 Stack

Install the Ironic Standalone Operator

git clone https://github.com/metal3-io/ironic-standalone-operator.git
cd ironic-standalone-operator
make install deploy

kubectl -n ironic-standalone-operator-system wait --for=condition=Available \
    deploy/ironic-standalone-operator-controller-manager \
    --timeout=120s

Deploy Ironic

First, create the namespace and the required TLS certificates:

kubectl create ns baremetal-operator-system
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: selfsigned-issuer
  namespace: baremetal-operator-system
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ironic-cacert
  namespace: baremetal-operator-system
spec:
  commonName: ironic-ca
  isCA: true
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: ironic-cacert
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: ca-issuer
  namespace: baremetal-operator-system
spec:
  ca:
    secretName: ironic-cacert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ironic-cert
  namespace: baremetal-operator-system
spec:
  secretTemplate:
    labels:
      environment.metal3.io/ironic-standalone-operator: "true"
  dnsNames:
  - ironic.baremetal-operator-system.svc
  - ironic.baremetal-operator-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: ca-issuer
  secretName: ironic-cert
EOF
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  labels:
    environment.metal3.io/ironic-standalone-operator: "true"
  name: ironic-credentials
  namespace: baremetal-operator-system
type: kubernetes.io/basic-auth
stringData:
  username: ironic
  password: supersecret
EOF
cat <<EOF | kubectl apply -f -
apiVersion: ironic.metal3.io/v1alpha1
kind: Ironic
metadata:
  name: ironic
  namespace: baremetal-operator-system
spec:
  apiCredentialsName: ironic-credentials
  networking:
    disableHostNetwork: true
  tls:
    certificateName: ironic-cert
    disableVirtualMediaTLS: true
  version: "37.0"
EOF

Install the Bare Metal Operator

git clone https://github.com/metal3-io/baremetal-operator.git
cd baremetal-operator
mkdir -p config/overlays/kubevirtbmc-demo

cat > config/overlays/kubevirtbmc-demo/kustomization.yaml <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: baremetal-operator-system
resources:
- ../../default
components:
- ../../components/basic-auth
- ../../components/tls
EOF

cat > config/default/ironic.env <<EOF
DEPLOY_KERNEL_URL=
DEPLOY_RAMDISK_URL=
IRONIC_ENDPOINT=https://ironic.baremetal-operator-system.svc
IRONIC_CACERT_FILE=/opt/metal3/certs/ca/tls.crt
IRONIC_INSECURE=false
EOF
kustomize build config/overlays/kubevirtbmc-demo | kubectl apply -f -

Step 5: Register the VM as a BareMetalHost

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: metal3-demo-vm-bmc-secret
  namespace: default
stringData:
  username: admin
  password: password
---
apiVersion: metal3.io/v1alpha1
kind: BareMetalHost
metadata:
  name: metal3-demo-vm
  namespace: default
spec:
  online: true
  bootMACAddress: "02:00:00:00:00:01"
  bmc:
    address: redfish-virtualmedia+http://metal3-demo-vm-virtbmc.default.svc.cluster.local:80/redfish/v1/Systems/1
    credentialsName: metal3-demo-vm-bmc-secret
    disableCertificateVerification: true
  rootDeviceHints:
    deviceName: /dev/vda
EOF

Step 6: Provision the Host with an OS Image

kubectl patch bmh metal3-demo-vm --type=merge -p '
{
  "spec": {
    "image": {
      "url": "https://releases.ubuntu.com/resolute/ubuntu-26.04-live-server-amd64.iso",
      "format": "live-iso"
    }
  }
}'

Step 7: Verify and Clean Up

To deprovision (wipe the host and return it to the available pool):

kubectl patch bmh metal3-demo-vm --type=json \
   -p '[{"op": "remove", "path": "/spec/image"}]'

Gotchas and Tips

  • MAC address mismatch. The bootMACAddress in BareMetalHost must match an actual interface on the VM.
  • HTTP vs HTTPS. KubeVirtBMC serves Redfish over plain HTTP by default. Use redfish-virtualmedia+http://.
  • Virtual media vs network boot. The redfish-virtualmedia driver boots the IPA ramdisk and provisioning images via virtual media (ISO attachment), which means no PXE, no DHCP, and no provisioning network is required.
  • Ironic host networking. In this article, we disable host networking solely for demonstration purposes, allowing us to rely on in-cluster DNS.
  • Cross-cluster scenarios. If Metal3 runs in a different cluster than KubeVirt, you can expose the KubeVirtBMC Services externally using Ingress or NodePort.

Why This Matters

  • CI/CD for bare-metal tools. Metal3, Ironic, and similar projects can use KubeVirtBMC to run their integration tests on KubeVirt VMs instead of maintaining a fleet of physical servers.
  • Developer inner loop. If you’re developing bare-metal provisioning features, you can iterate much faster with VMs that spin up in seconds.
  • Training and demos. Showcasing Metal3 no longer requires a rack of servers. A single Kubernetes cluster with KubeVirt is enough.

What’s Next

The KubeVirtBMC project is actively evolving. There is more work underway to improve Redfish compatibility and extend the BMC feature set. If you want to follow the progress or contribute, head over to the GitHub repository.

Explore all 57 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi