Skip to content

Guide: Core Controller

This guide installs the banlieue main controller on a real Kubernetes cluster from the released image ghcr.io/firestoned/banlieue:v0.4.0. The controller watches VirtualMachine resources, schedules them onto a Provider, creates the backend-specific infrastructure CR (e.g. VSphereMachine), and mirrors status back. It is the foundation every provider builds on — install it first, then add a provider with the vSphere Provider guide.

What you get after this guide

The CRDs, the controller Deployment (running, leader-elected), least-privilege RBAC, and optional admission hardening. The controller alone will accept VirtualMachines and try to schedule them, but nothing provisions until a Provider and its provider controller exist.

Prerequisites

  • A Kubernetes cluster, 1.30+ (1.30 is required only for the ValidatingAdmissionPolicies in step 6; the controller itself runs on 1.27+).
  • kubectl with cluster-admin (you are installing CRDs, ClusterRoles, and cluster-scoped admission policies).
  • The released manifests. Pin the repo to the tag so the YAML matches the image:

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

1. Install the CRDs

The CRDs are generated from the Rust types and committed at the release tag.

kubectl apply -f deploy/crds/
customresourcedefinition.apiextensions.k8s.io/providers.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/providerclasses.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/virtualmachines.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/vmclasses.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/vmimages.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/vsphereclusters.infrastructure.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/vspheremachines.infrastructure.banlieue.io created
customresourcedefinition.apiextensions.k8s.io/vspheremachinetemplates.infrastructure.banlieue.io created

Verify the API is registered:

kubectl explain virtualmachine.spec
kubectl api-resources --api-group=banlieue.io

2. Create the namespace

All banlieue workloads run in banlieue-system under the Pod Security restricted profile.

kubectl apply -f deploy/controller/namespace.yaml
# deploy/controller/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: banlieue-system
  labels:
    app.kubernetes.io/name: banlieue
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

3. Apply RBAC (least privilege)

The controller needs a ServiceAccount, a ClusterRole, and a binding. The role is scoped tightly:

  • banlieue.io — full reconcile on providers, virtualmachines, vmclasses, vmimages (+ their status and the finalizers subresources it writes).
  • infrastructure.banlieue.io — full reconcile on vspheremachines / vspheremachinetemplates, but only watch + status on vsphereclusters (CAPI/the operator owns their lifecycle — the controller never creates or deletes them).
  • events — create/patch (state transitions visible in kubectl describe).
  • coordination.k8s.io/leases — leader election.

No secrets access at all (security review 2026-07-31 SEC-008): providers read their own credentials through their per-instance Roles, and user-data travels inside the infrastructure CRDs — the controller never reads a Secret.

kubectl apply -R -f deploy/controller/rbac/

This ClusterRole is not aggregated into Cluster API's manager. That is a separate, smaller role, banlieue-capi-infrastructure (deploy/capi/clusterrole-aggregate.yaml), which grants CAPI only the infrastructure kinds (ADR-0096). See Cluster API.

4. Apply configuration

Runtime configuration is a ConfigMap consumed via envFrom:

kubectl apply -f deploy/controller/configmap.yaml
# deploy/controller/configmap.yaml (data)
RUST_LOG: "info,kube=warn,hyper=warn,tower=warn"
RUST_LOG_FORMAT: "json"            # structured logs for in-cluster
BANLIEUE_METRICS_PORT: "8080"
BANLIEUE_HEALTH_PORT: "8081"

5. Deploy the controller

kubectl apply -f deploy/controller/deployment.yaml
kubectl apply -f deploy/controller/service.yaml

The Deployment runs the single banlieue binary with the controller subcommand (ADR-0004), pinned to the released tag and hardened for the restricted PSS profile:

# deploy/controller/deployment.yaml (excerpt)
spec:
  template:
    spec:
      serviceAccountName: banlieue-controller
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: controller
          image: ghcr.io/firestoned/banlieue:v0.4.0   # pinned, never :latest
          args: ["controller"]                          # role selector
          envFrom:
            - configMapRef: { name: banlieue-controller-config }
          ports:
            - { name: metrics, containerPort: 8080 }
            - { name: health,  containerPort: 8081 }
          livenessProbe:  { httpGet: { path: /livez,  port: health } }
          readinessProbe: { httpGet: { path: /readyz, port: health } }
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }

Wait for it to become ready:

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

banlieue ships optional ValidatingAdmissionPolicies that enforce invariants the CRD schema can't express — in the API server, via CEL, with no webhook to run or certificates to rotate (Kubernetes 1.30+):

Policy Enforces
banlieue-virtualmachine-immutable-refs VirtualMachine.spec.classRef / imageRef are immutable after creation (changing class/image is a delete-and-recreate).
banlieue-provider-immutable-class Provider.spec.providerClassRef.name is immutable.
banlieue-provider-cabundle-source Provider.spec.connection.caBundle names exactly one of inline / configMapRef / secretRef.
banlieue-provider-connection Provider.spec.connection.endpoint is https://, has no embedded userinfo, and insecureSkipTLSVerify requires an explicit opt-in annotation (CHAIN-001).
banlieue-provider-credentialsref-authorization The principal creating/updating a Provider must itself be authorized to get the Secret named by spec.connection.credentialsRef (CHAIN-001).
banlieue-providerclass-guardrails Bounds what a ProviderClass writer can hand a provider Deployment via spec.additionalRules / spec.workloadNamespace (SEC-006).
banlieue-vmimage-import-source VMImage.spec.sources[].importFrom must be digest-pinned (@sha256:) and from an allow-listed registry (CHAIN-003).
kubectl apply -f deploy/admission/

Each file pairs a ValidatingAdmissionPolicy with a binding whose validationActions: ["Deny"] enforces it. To roll out in report-only mode first, edit the binding to ["Warn", "Audit"], observe, then switch to Deny.

Confirm a violation is rejected (after you have a VM — see the provider guide):

kubectl patch virtualmachine db-prod-01 --type=merge -p '{"spec":{"imageRef":{"name":"something-else"}}}'
# Error from server: ... spec.imageRef.name is immutable (was ubuntu-22.04-cloudinit); delete and recreate ...

7. Verify

kubectl -n banlieue-system get deploy,pods
kubectl -n banlieue-system logs deploy/banlieue-controller | head

# Leader election lease acquired:
kubectl -n banlieue-system get lease banlieue-controller

# Metrics / health are reachable in-cluster:
kubectl -n banlieue-system port-forward deploy/banlieue-controller 8081:8081 8080:8080 &
curl -s localhost:8081/readyz   # -> leader (or standby on a second replica)
curl -s localhost:8080/metrics | grep banlieue_leader

A healthy controller logs leader acquisition and a reconcile loop that idles until you create resources. What each endpoint reports, and how to scrape the metrics, is in the Observability guide.

What's next

The controller is running but has no backend to schedule onto. Add one:

  • Cloud Hypervisor Provider, the reference backend: prepare a bare-metal KVM host and connect it, then run VMs on it.
  • vSphere Provider: take a VirtualMachine all the way to a running VM on vCenter.
  • libvirt Provider: register a libvirt/KVM host over mutual TLS and run VMs on it.
  • Proxmox VE Provider: register a Proxmox node with a least-privilege API token and run VMs as template clones.

To uninstall, delete in reverse order (deploy/admission/, deploy/controller/, then deploy/crds/ — deleting the CRDs removes all banlieue objects).