Skip to content

Multiple hosted clusters on one VLAN

One isolated Layer-2 network can host several HyperShift hosted clusters. A single oooi stack serves all of them: one DHCP server, one split-horizon DNS server, and one Envoy proxy.

The model has two resource types:

Resource Scope Owns
Infra One VLAN CIDR, NAD, gateway, DHCP pool, DNS server IP, proxy server IP, upstream resolvers
InfraClusterAttachment One hosted cluster Cluster domain, control-plane namespace, optional external Service and apps ingress

Each attachment contributes its DNS records and SNI backends to the shared children. The Infra reconciler is the only writer of the shared DHCPServer, DNSServer, and ProxyServer.

Example

Two clusters share one VLAN with documentation-only names and addresses. Replace every value before applying anything.

apiVersion: hostedcluster.densityops.com/v1alpha1
kind: Infra
metadata:
  name: tenant-vlan100
  namespace: clusters
spec:
  networkConfig:
    cidr: 192.0.2.0/24
    gateway: 192.0.2.1
    networkAttachmentDefinition: vlan100
    networkAttachmentNamespace: default
    dnsServers:
      - 198.51.100.53
  infraComponents:
    dhcp:
      serverIP: 192.0.2.2
      rangeStart: 192.0.2.100
      rangeEnd: 192.0.2.199
    dns:
      serverIP: 192.0.2.3
    proxy:
      serverIP: 192.0.2.4
      internalProxyService: tenant-vlan100-proxy.clusters.svc.cluster.local
---
apiVersion: hostedcluster.densityops.com/v1alpha1
kind: InfraClusterAttachment
metadata:
  name: example-hcp-a
  namespace: clusters
spec:
  infraRef:
    name: tenant-vlan100
  hostedClusterRef:
    name: example-hcp-a
    namespace: clusters
  dns:
    clusterName: example-hcp-a
    baseDomain: clusters.example.com
---
apiVersion: hostedcluster.densityops.com/v1alpha1
kind: InfraClusterAttachment
metadata:
  name: example-hcp-b
  namespace: clusters
spec:
  infraRef:
    name: tenant-vlan100
  hostedClusterRef:
    name: example-hcp-b
    namespace: clusters
  dns:
    clusterName: example-hcp-b
    baseDomain: clusters.example.com

The generated records per attachment:

  • api, api-int, oauth, ignition, and konnectivity under <clusterName>.<baseDomain> resolve to the shared proxy on the VLAN, and to the shared proxy ClusterIP from management pods.
  • Envoy routes each fully qualified SNI name to that cluster's control-plane namespace, which defaults to <hostedClusterRef.namespace>-<hostedClusterRef.name>.

Kubernetes service aliases

The shared DNS server publishes these aliases once, to the shared VLAN proxy address, when at least one KubeVirt attachment has a discovered worker source address:

kubernetes
kubernetes.default
kubernetes.default.svc
kubernetes.default.svc.cluster.local

Envoy disambiguates the aliases with each attachment's worker source /32 ranges. The hostname-SNI path uses port 443; the IP-based Kubernetes Service path uses port 6443 without SNI. The controller watches HyperShift NodePool objects for membership and CAPI Machine objects for lifecycle and address changes. It filters Machine.status.addresses to the Infra.spec.networkConfig.cidr. The controller consumes the addresses exposed by CAPK; validate which interfaces CAPK publishes and how quickly it refreshes them on the target release. If the Machine status is not populated yet, both alias backends are omitted and the fully qualified routes remain available while reconciliation retries.

The source range is a routing condition, not authentication. Enforce source anti-spoofing in the CNI or switching layer when different tenants share a VLAN.

Apps ingress per cluster

Enable spec.appsIngress on each attachment independently. Every attachment gets its own MetalLB address pool, and the ranges must be disjoint:

spec:
  appsIngress:
    enabled: true
    metallb:
      addressPoolName: example-hcp-a-apps
      ipAddressPoolRange: 192.0.2.200-192.0.2.209

Each attachment's wildcard VIP is reported on its own status, so one cluster's ingress can be Pending while another is Ready.

Public OAuth per cluster

When an attachment needs public OAuth, enable its externalService. This creates one hosting-cluster LoadBalancer and one MetalLB allocation for that attachment; other attachments on the same shared proxy get independent Services and VIPs:

spec:
  externalService:
    enabled: true
    addressPoolName: example-hcp-a-oauth
    annotations:
      external-dns.alpha.kubernetes.io/hostname: "oauth.example-hcp-a.clusters.example.com."
    labels:
      external-dns.example.com/publish: "yes"

The generated Service is named <attachment>-proxy-external, exposes port 443 only, and selects the shared Envoy pods. Configure ExternalDNS on the hosting cluster to watch these Services. The annotation above is per cluster, so no multi-hostname merge or shared OAuth VIP is required.

Public API records are separate from OAuth. Each HostedCluster's kube-apiserver LoadBalancer Service lives in its own control-plane namespace, so a management-cluster ExternalDNS instance must watch all of those Services. Each api.<cluster>.<baseDomain> record therefore follows its own API VIP; there is no shared API VIP. See Public DNS and OAuth publishing for the ownership and label-filter requirements.

Failure modes

Symptom Reason Resolution
Infra Ready=False, reason DuplicateHostname Two attachments declare the same domain Remove or rename one attachment's dns values
Infra Ready=False, reason DuplicateHostedCluster Two attachments reference the same HostedCluster Keep one attachment per HostedCluster
Attachment stuck without Ready Its control-plane namespace does not exist yet Create the HostedCluster first; HyperShift creates <ns>-<name>
ProxyServer has no routes after edits A conflict excluded every attachment; stale shared routing is removed Fix the conflict listed in the Infra condition message

Fully qualified SNI names

Every attachment contributes fully qualified SNI names such as api.example-hcp-a.clusters.example.com. Those names do not depend on source address discovery and are the recommended bootstrap path. The unqualified aliases above are the exception: they are source-scoped and are not emitted until the worker addresses are known.

There is no implicit single-cluster binding. To serve a cluster, create an InfraClusterAttachment, even when the Infra has only one attachment.