Skip to content

Guide: vSphere Provider

This guide takes you from a cluster that already runs the core controller to a scheduled VirtualMachine on vCenter — installing the vSphere provider, registering a vCenter, and applying every VMClass, VMImage, and VirtualMachine along the way. Everything uses the released image ghcr.io/firestoned/banlieue:v0.1.0; nothing is built locally and there is no simulator. (For vcsim/local development, see Developer → Local Development.)

flowchart LR sec[Secret: vsphere-creds] --> prov[Provider: prod-vsphere] prov -->|provider controller logs in| vc[(vCenter)] vmc[VMClass: db-prod-large] --> vm[VirtualMachine: db-prod-01] vmi[VMImage: ubuntu-22.04-cloudinit] --> vm prov --> vm vm -->|controller schedules| vsm[VSphereMachine]

Prerequisites

  • The core controller installed (CRDs + controller running in banlieue-system).
  • A reachable vCenter and credentials with read access to inventory (datacenters, clusters, datastores, networks, and the VM templates you'll reference).
  • A VM template present in vCenter for the image you'll register (this guide uses one named ubuntu-22.04-cloudinit).
  • The repo checked out at the release tag (for the provider manifests):

    git clone --branch v0.1.0 --depth 1 https://github.com/firestoned/banlieue
    cd banlieue
    

1. Install the vSphere provider

The provider is the same banlieue image run with the provider vsphere subcommand. It needs its own ServiceAccount/RBAC, a ConfigMap, and a Deployment (all in banlieue-system, reusing the namespace from the controller guide).

kubectl apply -R -f deploy/provider-vsphere/rbac/
kubectl apply -f deploy/provider-vsphere/configmap.yaml
kubectl apply -f deploy/provider-vsphere/deployment.yaml
kubectl apply -f deploy/provider-vsphere/service.yaml

Operator-managed installs are the norm

Applying a Provider against a cluster installed with banlieue bootstrap operator spawns a dedicated provider workload automatically — see Provider lifecycle. This static install is the standalone shape: it serves every Provider in banlieue-system (its watch and its credential reads are scoped to the install namespace, security review 2026-07-31).

The Deployment (excerpt) — single binary, role-selected by args, pinned to the release:

# deploy/provider-vsphere/deployment.yaml (excerpt)
containers:
  - name: provider
    image: ghcr.io/firestoned/banlieue:v0.1.0
    args: ["provider", "vsphere", "--namespace", "banlieue-system"]
    envFrom:
      - configMapRef: { name: banlieue-provider-vsphere-config }

The provider's ClusterRole is read-only on providers (it only patches their status) and reconciles vmimages/status and vspheremachines. Credentials and CA bundles are read by name through a namespaced Role in banlieue-system (rbac/role.yaml) — no provider identity holds cluster-wide Secret access (security review 2026-07-31 CHAIN-002). Wait for it:

kubectl -n banlieue-system rollout status deploy/banlieue-provider-vsphere --timeout=120s

2. Create the vCenter credentials Secret

The provider never embeds credentials in the Provider CR — it reads a Secret referenced by spec.connection.credentialsRef. vSphere needs username and password keys:

kubectl -n banlieue-system create secret generic vsphere-creds \
  --from-literal=username='administrator@vsphere.local' \
  --from-literal=password='REPLACE-ME'

Already use govc?

If your shell has GOVC_URL / GOVC_USERNAME / GOVC_PASSWORD set, you can derive the Secret and Provider straight from them — see the mapping table in Developer → Local Development.

3. Register the Provider

A Provider declares one vCenter and the storage/network classes it exposes. The capabilities block is the explicit contract the scheduler matches a VMClass against — every class a workload requests must be listed here.

provider.yaml
apiVersion: banlieue.io/v1alpha1
kind: Provider
metadata:
  name: prod-vsphere
  namespace: banlieue-system
  labels:
    dc: dc1
    env: prod
spec:
  providerClassRef:
    name: vsphere
  connection:
    endpoint: https://vcenter.example.com/sdk
    credentialsRef:
      name: vsphere-creds
    # insecureSkipTLSVerify: true     # self-signed only; prefer caBundle below
    # Validate against a private CA — set EXACTLY ONE of inline/configMapRef/secretRef
    # (key defaults to ca.crt). See ADR-0008.
    # caBundle:
    #   configMapRef:
    #     name: corp-ca-bundle
    #   # secretRef: { name: vcenter-ca }
    #   # inline: |
    #   #   -----BEGIN CERTIFICATE-----
    #   #   ...
  capabilities:
    storageClasses:
      - name: gold
        target: { datastore: ds-fast-01 }
    networkClasses:
      - name: prod
        target: { portGroup: vmnet-prod }
    features: [hotAddCPU, hotAddMemory, efiSecureBoot]
kubectl apply -f provider.yaml

Within a few seconds the provider controller logs into vCenter, walks the inventory, and populates status.failureDomains[] (one per datacenter/cluster):

kubectl -n banlieue-system get provider prod-vsphere
# NAME           CLASS     READY
# prod-vsphere   vsphere   True

kubectl -n banlieue-system get provider prod-vsphere -o yaml | yq '.status.failureDomains'

If READY is not True, jump to Troubleshooting.

Simpler failure-domain names (ADR-0023)

The auto-computed name is <provider>-<datacenter>-<cluster>, hashed when too long — real enterprise cluster names routinely produce something like prod-vsphere-dc-east-compute-cluster-a1b2c3d4. If you already have a simpler convention for these zones, override it per (datacenter, cluster) pair:

spec:
  failureDomainNameOverrides:
    - datacenter: DC-East
      cluster: Compute-Cluster-A
      name: cluster-01

Opt-in only — any (datacenter, cluster) pair not listed here still gets the auto-computed name. Two overrides resolving to the same name fail the whole Provider reconcile (Ready=False), since that name is now also a vCenter template folder segment (ADR-0020) and a Job name.

The resolved name (override or auto-computed) is also mirrored into status.failureDomains[].labels.name, so a VirtualMachine can target this exact zone by its friendly name instead of the raw, backend-reported cluster label:

placement:
  failureDomainSelector:
    matchLabels:
      name: cluster-01

4. Define a VMClass

A VMClass is the reusable hardware "shape" plus the abstract classes/features a backend must satisfy. It is cluster-scoped (no namespace). The classes named here (gold, prod) and the features must be advertised by a candidate Provider.

vmclass.yaml
apiVersion: banlieue.io/v1alpha1
kind: VMClass
metadata:
  name: db-prod-large
spec:
  hardware:
    cpus: 8
    memoryMiB: 32768
    disks:
      - { name: root, sizeGiB: 80,  storageClass: gold, provisioning: thin }
      - { name: data, sizeGiB: 500, storageClass: gold, provisioning: eagerZeroed }
  network:
    interfaces:
      - name: eth0
        networkClass: prod
        ipam: {}                   # omit static/pool for DHCP — see the API reference
  firmware: efi-secure
  features: [hotAddCPU, hotAddMemory, efiSecureBoot]
kubectl apply -f vmclass.yaml

5. Register a VMImage

A VMImage maps a backend-agnostic OS name to a per-backend source. For vSphere the source kind: Template and ref is the template name in vCenter (the provider verifies it exists in every failure-domain datacenter).

vmimage.yaml
apiVersion: banlieue.io/v1alpha1
kind: VMImage
metadata:
  name: ubuntu-22.04-cloudinit
spec:
  osFamily: linux
  osDistribution: ubuntu
  osVersion: "22.04"
  architecture: amd64
  guestAgent: cloud-init
  sources:
    - providerClass: vsphere
      kind: Template
      ref: ubuntu-22.04-cloudinit      # must exist as a template in vCenter
kubectl apply -f vmimage.yaml

# The provider flips per-provider readiness once it finds the template:
kubectl get vmimage ubuntu-22.04-cloudinit -o yaml | yq '.status.perProvider'

ready: true for prod-vsphere is the gate the scheduler waits on. If it stays false, check the reason (TemplateNotFound, ConnectFailed, …).

6. Create a VirtualMachine

This is the only resource an end user writes. It references the class and image by name and (optionally) constrains placement.

virtualmachine.yaml
apiVersion: banlieue.io/v1alpha1
kind: VirtualMachine
metadata:
  name: db-prod-01
  namespace: banlieue-system
  labels: { app: db-prod }
spec:
  classRef: { name: db-prod-large }
  imageRef: { name: ubuntu-22.04-cloudinit }
  placement:
    providerSelector:
      matchLabels: { dc: dc1, env: prod }    # matches the Provider's labels
  desiredPowerState: PoweredOn
kubectl apply -f virtualmachine.yaml

7. Verify the end-to-end flow

kubectl -n banlieue-system get virtualmachine db-prod-01
# NAME         CLASS           IMAGE                    PROVIDER       POWER       READY
# db-prod-01   db-prod-large   ubuntu-22.04-cloudinit   prod-vsphere   PoweredOn   ...

# The controller created a backend infra CR, owned by the VM:
kubectl -n banlieue-system get vspheremachines -l app.kubernetes.io/name=banlieue
kubectl -n banlieue-system get vm db-prod-01 -o yaml | yq '.status.scheduled, .status.conditions'

A successful schedule populates status.scheduled (provider + failure domain + resolved storage/network) and creates a VSphereMachine in the same namespace.

8. (Optional) TPM-sealed disk encryption

If your vCenter has a KMS (Key Management Server) registered under Configure → Key Providers, banlieue can attach a virtual TPM (vTPM) to a VM so Kairos's kcrypt seals its LUKS encryption key to that VM's own TPM at install time — no remote unlock server needed (ADR-0039). This needs three things wired together; get any one wrong and encryption silently doesn't happen:

  1. The Provider must advertise the vtpm feature — set by hand once you've confirmed KMS + vTPM actually work end-to-end in that vCenter (never auto-discovered):
spec:
  capabilities:
    features: [hotAddCPU, hotAddMemory, efiSecureBoot, vtpm]
  1. The VMClass requests it with tpmEnabled: true — a class-level capability like firmware, not a per-VM override:
spec:
  firmware: efi
  features: [vtpm]
  tpmEnabled: true
  1. The VMImage must use installMode: deferred, not the default immediate. Kairos's kcrypt only ever seals a key to a TPM present during install — it cannot encrypt an already-installed disk later. banlieue's normal pipeline installs Kairos once into a golden template and clones it for every VM, so a vTPM attached to the clone would have nothing to seal against if the template disk was already installed. deferred mode leaves the template un-installed (ISO still attached, no power-on at build time); each clone then runs Kairos's installer itself, at its own first boot, with its own freshly-attached vTPM already present (ADR-0040):
spec:
  template:
    firmware: efi
    installMode: deferred
  cloudConfigs:
    - secretRef: { name: kairos-encrypted-install-cloud-config }

The cloud-config for a deferred-mode image is the opposite contract of a normal template build: install.reboot: true / poweroff: false (the VM keeps running as the production workload after install, not power itself off for templating) and no after-install-chroot identity-wipe stage — each clone installs fresh and gets its own real machine-id/SSH host keys.

Full worked examples: 12-vmclass-tpm-encrypted.yaml, 13-vmimage-kairos-deferred-install-tpm.yaml.

Provisioning time and readiness

A deferred-mode VM's VirtualMachine/VSphereMachine reports provisioned=true/Ready the instant the clone powers on — which for this mode means "the unattended install just started," not "the VM is ready" (a documented gap, ADR-0034/ADR-0040). Expect the full unattended-install window (typically 8-12 minutes) before the VM is actually usable, every time, for every VM — not just once at template-build time like a normal immediate-mode image.

Confirmed working end-to-end

Validated live against a real vCenter: a tpmEnabled: true VM's disk (COS_PERSISTENT) came up as crypto_LUKS, mounted and auto-unlocked via its own vTPM on first boot, with no manual intervention.

Troubleshooting

Provider not Ready — read the condition reason:

Reason Meaning Look at
SecretMissing credentialsRef Secret doesn't exist kubectl -n banlieue-system get secret vsphere-creds
SecretInvalid Secret missing username/password kubectl get secret … -o yaml
ConnectFailed bad creds / endpoint / TLS provider logs
InventoryFailed login OK, inventory walk failed provider logs

VMImage per-provider not ready: TemplateNotFound (template absent in a datacenter), ConnectFailed, LookupFailed, NoVSphereSource.

VirtualMachine stuck Scheduled=False:

  • reason=ImageNotReady — the VMImage isn't ready on any candidate provider (step 5).
  • reason=NoProviderMatched — no Provider matches placement.providerSelector, or none advertises the requested storage/network class or feature. Check the Provider.spec.capabilities against the VMClass.
  • reason=TpmUnsupportedVMClass.spec.tpmEnabled: true, but no candidate failure domain's Provider advertises the vtpm feature (step 8 above).
kubectl -n banlieue-system logs deploy/banlieue-provider-vsphere
kubectl -n banlieue-system logs deploy/banlieue-controller
kubectl -n banlieue-system describe virtualmachine db-prod-01   # Events

Phase 1B scope

This release ships capability introspection (failureDomains) and the VMImage template check. The VSphereMachine VM-lifecycle reconciler (clone → power-on → status mirror) lands in a later iteration, so the VirtualMachine schedules and a VSphereMachine is created, but the VM is not yet powered on in vCenter.

Full schema reference

Every field of every CRD: API Reference.