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, andkonnectivityunder<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:
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.