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.)
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):
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:
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.
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]
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:
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:
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.
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]
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).
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.
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
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:
- The
Providermust advertise thevtpmfeature — set by hand once you've confirmed KMS + vTPM actually work end-to-end in that vCenter (never auto-discovered):
- The
VMClassrequests it withtpmEnabled: true— a class-level capability likefirmware, not a per-VM override:
- The
VMImagemust useinstallMode: deferred, not the defaultimmediate. Kairos'skcryptonly 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.deferredmode 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— theVMImageisn'treadyon any candidate provider (step 5).reason=NoProviderMatched— noProvidermatchesplacement.providerSelector, or none advertises the requested storage/network class or feature. Check theProvider.spec.capabilitiesagainst theVMClass.reason=TpmUnsupported—VMClass.spec.tpmEnabled: true, but no candidate failure domain'sProvideradvertises thevtpmfeature (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.