3. The fleet
3.1 Cluster types: stateless and stateful
Requirements: R1, R5, R17, R18, R19, R20, R21, R23, R24.
Every environment has two kinds of clusters. The split follows one rule: a cluster that holds no data can be taken out of traffic, upgraded or even rebuilt at any time; a cluster that holds data cannot, so it is made zone-redundant and changed as rarely as possible.
Where does a workload run
Data does not belong in Kubernetes by default. Every workload is placed by going down this list and stopping at the first option that fits:
- Stateless cluster – all application code. Its state lives in the options below.
- Azure PaaS service in the backend network (PostgreSQL, MySQL, Managed Redis, Cosmos DB for MongoDB, Event Hubs with Kafka, Blob Storage, …) reached through private endpoints from the stateless clusters. Zone redundancy, backups, patching and upgrades are Microsoft’s job; the immutable copies in the paired region are the platform’s (section 6.3). Services with no equivalent in the other clouds (Azure SQL, Service Bus, Cosmos DB’s NoSQL API) are allowed only as a documented cloud-pinning exception at onboarding (section 1.3); where this document names them, it describes the Azure implementation of such an exception.
- Stateful cluster – last option, only when
- no Azure PaaS service provides the capability (e.g. a product that is only shipped as a container or operator), or
- a special use case requires it (e.g. a licence, a latency requirement to data that only the cluster can meet, a protocol the PaaS service does not offer).
A workload in the stateful cluster needs a documented exception (why 1 and 2 do not fit, owner, exit plan) approved by
the platform team, and the stateful cluster itself is only built when the first exception is approved
(optional); the exception is the label platform/stateful-exception=<ticket> on its namespace, required by a
policy. The exceptions are reviewed when new PaaS services become available. The fewer workloads the
stateful cluster runs, the smaller its blast radius and upgrade risk.
Comparison
Stateless (aks-<env>-sl-az<N>) |
Stateful (aks-<env>-sf) |
|
|---|---|---|
| Runs | Frontends, APIs, workers – anything that can be killed and recreated; state in Azure PaaS | Only approved exceptions: workloads that own data on disks (StatefulSets, operators) for which no PaaS service fits |
| Count per environment | Per exposure (int, ext) one per availability zone: sl-az1, sl-az2 in dev and acc; sl-az1, sl-az2, sl-az3 in prd (section 3.2) |
Zero or one per exposure – built only when the first approved workload of that exposure needs it |
| Availability zones | All nodes of a cluster in its AZ (every pool --zones <N>); the cluster is the unit of failure |
Every isolation zone has one node pool per AZ (intfiz1, intfiz2, intfiz3, …); system pool spread over AZ 1–3 |
| Nodes | Classic node pools per zone, capacity type and architecture with the cluster autoscaler, from zero (section 2.3) | Classic AKS node pools with a fixed VM size and node count |
| Network | Frontend spoke vnet-<env>-<exposure>-sl-az<N> in the exposure’s subscription, peered to the exposure’s hub |
Backend spoke vnet-<env>-<exposure>-sf in the exposure’s subscription, peered to the exposure’s hub, together with the PaaS private endpoints |
| Internet | Egress only via Azure Firewall FQDN allow-list (outbound type userDefinedRouting); inbound only via the traffic layer |
None. Network isolated cluster (outbound type none, bootstrap artifacts from the private ACR cache), no public IPs, UDR 0.0.0.0/0 → Firewall which denies all Internet for backend prefixes |
| CNI | Azure CNI Pod Subnet with dynamic IP allocation, powered by Cilium - every pod with its zone’s address (section 2.1) | The same |
| Incoming HTTP(S) | Gateway API, the implementation chosen per cluster (Traefik), one Gateway per isolation zone, HTTPS only; the Kubernetes Ingress API is not used |
No Gateway API implementation and no Ingress. Applications are published with internal LoadBalancer Services (L4) |
| TLS | Terminated by the zone’s gateway with Let’s Encrypt certificates for internal DNS names; applications serve plain HTTP inside the cluster | Terminated by the application itself – a hard onboarding requirement |
| Node-to-node encryption | Azure Virtual Network encryption – no service mesh, no mTLS | Application TLS (VNet encryption may be enabled as defence in depth but is not relied on) |
| Kubernetes version | Standard support, two speeds: sl-az1 on the second latest GA minor (N-1), sl-az2 (and sl-az3) one minor behind (N-2), never older than 1.36 (update policy) |
Long Term Support (--tier premium --k8s-support-plan AKSLongTermSupport): created on the second latest GA minor (N-1), 1.37 at the earliest (the first minor whose claims report when they were last used), then kept on that minor until 6 months before its LTS ends |
| Tier | Standard | Premium (required for LTS; billed at the Premium rate only once the minor has left community support - AKS, April 2026) |
| Scaling | ≥ 2 replicas per app, HPA / KEDA on load, VPA resizes pods in place, the cluster autoscaler adds and removes nodes; each cluster sized to carry 100 % of the load alone (the zone pools’ maximum) | ≥ 3 replicas per StatefulSet, one per AZ; no autoscaling – fixed node count per pool, sized when an exception is onboarded and changed as a planned IaC change |
| Storage | None – admission policy rejects PersistentVolumeClaims; ephemeral OS disks | Azure Disk Premium_ZRS / StandardSSD_ZRS, Azure Files ZRS (over NFS with encryption in transit, mounted with the namespace’s workload identity instead of a storage account key - both GA in the Azure Files CSI driver since August 2026; the alternatives are weighed per exception), through per-zone StorageClasses encrypted with the zone’s own key (section 4.2); prefer PaaS for databases. Every claim carries Kubernetes’ Unused condition (1.37): the storage agent proposes what to do with volumes no pod has used for longer than the zone’s retention, and nothing deletes them without the owning team |
| Upgrade model | Drain from traffic, upgrade or rebuild, return (picture 3.5) | In place, one AZ at a time, PDB-protected (picture 3.5) |
Isolation zones are kept in both cluster types. Each stateless and stateful cluster has the nodes, subnets, Key
Vaults and policies of the zones of its exposure – int-fi and int-se in an internal cluster, ext-fi and ext-se
in an external one – exactly as in pictures 2.1–2.6; the data in the stateful cluster is what needs the country
separation most. Its applications are reachable only from the same zone of the stateless clusters.
Internal and external: separate subscriptions and clusters
Internal and external workloads never share a cluster or a subscription (the security team’s decision). Countries are separated one level lower, inside a cluster, by node pool and network:
| Separation | Internal ↔ external (exposure) | Country ↔ country (fi, se, …) |
|---|---|---|
| Boundary | Subscription and cluster: sub-aks-<env>-int and sub-aks-<env>-ext, each with its own cells and optional stateful cluster |
Isolation zone inside the clusters of one exposure |
| Control plane, system pool, admission, Flux, the gateway control plane | Separate | Shared by the exposure’s zones |
| Nodes and network | Separate spoke VNets peered to separate hubs with an Azure Firewall each, no peering and no route between them | Own node pools, own address space, subnets, NSGs and route tables |
| Key Vaults | Separate platform vaults (kv-<env>-int-platform, kv-<env>-ext-platform: certificates, the clusters’ keys) and zone vaults |
Own zone vault (kv-<env>-<zone>) |
| Azure RBAC, Azure Policy, budget, quota | Per subscription | Per zone where it matters (namespaces, ABAC, policy mutations) |
| ACR | One per exposure subscription, fed by the same promotion pipeline | Shared |
| Entry points | External: Traffic Manager and public load balancers, only in the external subscription. Internal: hub Private DNS and internal load balancers, only in the internal one | One profile / Private DNS zone per zone |
So an external cluster – the one the Internet reaches – has no internal zone, no internal identity, no role on an internal vault and no route to an internal subnet at all: its hub has neither a peering to the internal hub nor a gateway to the corporate network; a compromise of its control plane, nodes, subscription or Firewall does not reach the internal workloads. Traffic between the two exposures is like traffic between two zones (R4): denied. An internal application that must call an external one goes to its public entry point like any Internet client (an FQDN rule on the internal Firewall); an external application has no path to an internal one, and giving it one is a design change, not a Firewall rule. What the two keep in common is the source – the same IaC modules with the exposure as a parameter, the same fleet repository with a directory per cluster – and the management VNet, Entra ID, GitHub and the environment’s Grafana Cloud stack.
Incoming traffic: from outside and from inside
Clients never connect to a pod or node directly, and the stateful cluster is never reached from outside the platform. The two kinds of zones use the cells differently, because their availability requirements differ:
- External zones (
ext-*): every application runs in every cell, active-active. A traffic layer (Azure Traffic Manager, the cell router) outside the cells answers each client’s DNS query with one cell, chosen by the client’s network, and leaves a failed or drained cell out of its answers. - Internal zones (
int-*): every application runs in one cell, its home cell. The application team chooses the cell; its DNS name points straight to the zone’s gateway in that cell, maintained by ExternalDNS. A migration workflow moves an application to another cell without downtime – on request, around cluster upgrades, and after a cell failure. A cell failure therefore costs the internal applications of that cell a short outage until they are migrated (recovery time target TBD, minutes) – accepted because internal applications have lower availability requirements than external ones. There is no WAF and no load balancer in front of internal zones.
| Source | Entry point | Path to the cells |
|---|---|---|
Internet (external zones ext-*) |
Azure Traffic Manager: public DNS name of the application → CNAME to the zone’s Traffic Manager profile, which answers with one cell | The public load balancer of the zone’s gateway in every cell: one public IP per external zone and cell, HTTPS (443) only |
Corporate network (internal zones int-*) |
The zone’s gateway in the application’s home cell: <app>.int-fi.prd.example.com resolves in the hub Private DNS to the gateway’s internal load balancer IP of zone int-fi in that cell |
On-premises → ExpressRoute/VPN → the internal hub’s Firewall → the gateway’s internal load balancer of the zone in the home cell |
| Other Azure workloads (other spokes) | Same as the corporate network | Same as above; a Firewall rule per source |
| Applications in the same zone and cell | The application’s DNS name, like any client; never its Service or pods, which only its own namespace and the zone’s gateway reach (R22) |
The hub’s Firewall (a rule in the caller’s rule file) → the zone’s gateway in the cell; never through the traffic layer or to another cell |
| Applications in the same internal zone, other cell | The application’s DNS name, like any internal client | The internal hub’s Firewall → the zone’s gateway in the other cell (one Firewall rule per zone: zone prefix → zone prefix) |
| Applications in another zone | Never directly (R4). If a business flow needs it, the caller is treated like any other client: the application’s DNS name, resolved to the target zone’s gateway, with an application rule in the caller’s rule file (section 2.1) | The hub’s Firewall (which proxies the flow from its own address) → the target zone’s gateway in the home cell |
No component in front of a cell keeps state about the client: sessions and caches belong in Azure PaaS (e.g. Azure Cache for Redis) or in the token, so a client can be moved to another cell at any time without losing anything. What the cell choice controls is which application version a client sees while a release moves through the cells one by one (releases cell by cell).
External zones: active-active with DNS affinity
The traffic layer is DNS: the client connects straight to the cell Traffic Manager names, and its TLS session ends at the zone’s gateway there - nothing in between decrypts it. Every client is mapped to one cell by its network, so it keeps using the same cell across requests, TLS sessions and reconnects – also clients that do not support cookies (API clients, mobile apps, other systems):
- The affinity exists for consistent versions, not for state. It makes sure that a client sees one application version at a time during a release.
- A failed or drained cell is left out of the answers, and its clients move to another cell as soon as their cached DNS answer expires (the record’s TTL, 30 s); they come back when it returns.
- All cells always prove that they work. A cluster that only receives traffic in a failure is a cold standby with cold caches and untested capacity. With active-active, a cell failure only affects the clients of that cell (half or a third).
- Long-lived connections (WebSockets, Server-Sent Events, gRPC streaming) stay on one cell for the life of the connection – DNS is only asked again when the client reconnects. Applications that use them must reconnect with back-off, because draining a cell closes them after the drain timeout.
How Traffic Manager maps a client to a cell (Traffic Manager routing methods):
per zone and environment one parent profile with the Subnet routing method maps the address space of the
clients’ resolvers, in blocks assigned by measured traffic, to one child profile per cell with the Priority
method, which lists the cell’s own gateway first and the other cells after it in a fixed order, so the clients of a
failed cell all move to the same cell. The health probe is HTTPS GET /healthz of an application of the zone through
each cell’s gateway every 30 s, a cell is left out after 2 failures, and the TTL is 30 s, so a drain or a failover
reaches most clients within a minute; a cell is taken out by disabling its endpoints and returned a quarter of its
blocks at a time. The platform answers with A records only.
Limits of DNS affinity, accepted in this design: clients behind a large shared resolver land on the same cell, so the load is less even than per-request routing (the cells are sized N+1, the blocks are rebalanced outside releases, and an alert fires when a cell carries more than its N+1 share for an hour); a resolver that ignores the TTL keeps using a drained cell longer, which the drain timeout allows for; a mobile client that changes networks may change cells and meet the other version, which the compatibility rule covers. There is no WAF in the traffic layer and none in the gateway: the design launches with DDoS protection, the 443-only NSG, the gateway’s TLS termination, Cilium ingress rules and the applications’ own input validation as the controls.
Real client IP: the zone’s gateway Service uses externalTrafficPolicy: Local (set through the Gateway’s
parametersRef ConfigMap), so the gateway’s Envoy sees the client’s own
address; there is no proxy whose X-Forwarded-For it would have to trust.
Every external application also has a per-cell host name (e.g. <app>-<cluster>.ext-fi.prd.example.com, an A
record of that cell’s gateway kept by ExternalDNS). The release and upgrade pipelines run their smoke and synthetic
tests on it, through the cell’s real TLS path, before the cell gets user traffic
(releases, section 3.5);
the same names are the Traffic Manager endpoints.
The public addresses of the platform are the external zones’ gateways - one Azure Standard public load balancer IP
per external zone and cell, created by the gateway’s Service in <zone>-gateway. Besides them only the platform
gateway of a cell that takes Flux’s GitHub webhook (one listener for the receiver, the platform’s
one HTTP/1.1 path); every cell’s Flux also polls Git every minute. An internal cell has no public load
balancer at all - one cannot answer behind the hub firewall. All public IPs are protected by one Azure DDoS Network Protection plan for the whole
tenant, attached to every cell VNet. The NSGs allow TCP 443 from
the Internet on the system subnet (where every zone’s gateway pods run) and on an external zone’s node subnet, and
nothing else from outside the VNet. This traffic does not pass the hub Firewall; the NSG, the zone’s gateway and the
application’s Cilium ingress (only from the zone’s gateway) are its controls.
Internal zones: one home cell per application, moved by migration
Placement. Every internal application has exactly one home cell per environment; the default is the slow-speed
cell sl-az2, which runs the more proven Kubernetes version (update policy).
The team chooses it in Git (placement.yaml, e.g. cell: sl-az2), which every cell reads from main through a
Flux Kustomization of its own (path placement/, interval 1 min) – environment state, not part of the release train
(section 3.3). It creates the application’s
Flux Kustomization only in its home cell – and during a migration also in the target cell; the application’s
manifests themselves still come from the cell’s branch.
DNS by ExternalDNS. Each internal zone has a Private DNS zone (int-fi.prd.example.com) in the exposure’s
environment resource group, linked to every cell VNet and to the hub, so the hub firewall’s DNS proxy - and the
corporate network behind it - resolves it. Every cell runs ExternalDNS in the platform namespace external-dns,
one instance per exposure of names:
external-dns-int: providerazure-private-dns, a workload identity with Private DNS Zone Contributor on the internal zones’ Private DNS zones only,management.azure.comon the Firewall allow-list; sourcegateway-httproutelimited to Gateways labelledplatform/exposure=int, so the A record of each hostname of anHTTPRoutepoints to the zone’s gateway load balancer in that cell;external-dns-ext(external cells): providerazureon the public zone, Gateways labelledplatform/exposure=ext, the per-cell host names and the global names that Traffic Manager fronts;- the home cell: every internal route carries the annotation
platform/home-cell: <cluster>, and each cell’sexternal-dns-intpublishes only the routes that name it (--annotation-filter=platform/home-cell=<cluster>), so exactly one cell publishes each name. The value comes from the team’s placement through Flux substitution, the same in every cell; Azure Policy rejects the annotation set by hand in application manifests; - one shared owner ID per environment with
upsert-only, so the target cell of a migration can take over a record from a source cell that is gone; record TTL 60 s, sync interval 1 min.
Migration workflow – a pipeline in the runbooks repository; the same steps serve every reason to move:
- Deploy to the target: the placement adds the target cell without
platform/dns-publish; Flux deploys the application there (the autoscaler adds nodes if needed). Wait until its Kustomization isReady. - Test: smoke tests against the target cell’s gateway IP with the application’s real host name
(
curl --resolve), over the real TLS path. - Switch DNS: the placement moves
platform/dns-publishto the target cell. Its ExternalDNS overwrites the A record within a minute. - Drain the source: wait for the TTL and until the source cell’s gateway sees no more requests for the application (or a maximum, e.g. 15 min, for clients that cache DNS too long); long-lived connections are closed and reconnect to the target.
- Remove from the source: the placement drops the source cell; Flux prunes the application there.
Until step 5, the migration is rolled back by moving platform/dns-publish back. Planned migrations are
zero-downtime. Teams start one by a pull request that changes their placement.yaml.
Cell failure. An alert on the cell’s health (gateway health routes probed by Grafana Cloud synthetic monitoring, AKS and VM health) dispatches the failover: the migration workflow for all internal applications of the failed cell, with step 4 skipped and step 5 deferred until the cell is back (a safe action, section 6.1). Their recovery time is alert + Flux reconcile + node provisioning and pod start in the target + ExternalDNS sync + TTL. The failed-over applications stay in their new cell; the team or the platform moves them back by a planned migration.
Consequences:
- TLS ends at the zone’s gateway. Internal clients connect to it directly with the zone’s certificate (section 4.1); there is no WAF in front of internal zones.
- Real client IP: the gateway sees it directly, because its
ServiceusesexternalTrafficPolicy: Localand the internal hub’s Firewall allows the flow with network rules (no SNAT to private destinations). - One version at a time. An internal application runs in one cell, so its clients see one version, apart from the few minutes of a rolling update, which the compatibility rule covers anyway.
- Capacity: every cell must still be able to take all internal applications of the other cell(s) – for upgrades and failures – so the N+1 sizing does not change. The zone pools’ maximum is the same in every cell.
- Firewall: corporate network and the pipeline agents → the zone’s prefixes (its node and load balancer subnets in every cell, one IP group per zone) on TCP 443, so a migration never needs a Firewall change.
VNets and address plan
One spoke VNet per cluster, one address space per cell, one slot per isolation zone - the slot layout of
section 2.1, stated per cell in the environment’s shared configuration: an address space for
nodes and load balancers (/24 slots by the zone’s index) and a pod address space (a /16 of the RFC 6598 range
100.64.0.0/10 with a /20 per slot index; every node pool needs a pod subnet, the system pool’s is
snet-system-pods). An example cell with zone int-se at index 2:
| Cell | Address space | snet-system |
A zone’s slot | snet-<zone>-nodes |
snet-<zone>-ilb |
Pod subnets |
|---|---|---|---|---|---|---|
dev-int-stateless-az1 |
10.100.0.0/16 (snet-system 10.100.0.0/24, int-se 10.100.2.0/24, its load balancers 10.100.2.128/27) |
/24 |
/24 |
/25 - about 120 nodes |
/27 |
Pod address space 100.64.0.0/16, a /20 per slot index: snet-system-pods 100.64.0.0/20, snet-int-fi-pods 100.64.16.0/20, snet-int-se-pods 100.64.32.0/20 |
Because a zone has the same index in every cell, the zone is two prefixes per cell, its node slot and its pod slot,
and the Firewall’s IP group for the zone lists them (int-se in the example: 10.100.2.0/24, 100.64.32.0/20): a
rule written for a zone does not change when a cell is added or rebuilt, and a rebuilt cell takes its own address
spaces again (drained first, so the old VNet is gone before the new one exists). The Firewall’s SNAT private ranges
include 100.64.0.0/10 with the RFC 1918 ranges, so no pod address ever leaves the landing zone.
The private endpoints are not in the cells. The zone Key Vaults, the platform Key Vault, the registry (prd), the
Terraform state and the AI model endpoint have their private endpoints in one subnet (snet-shared-pe) of the
environment’s shared network (vnet-aks-<env>), registered in the hub’s privatelink.* zones; a cell reaches them
through the hub Firewall, whose rule for the vaults takes the cells’ system subnets - where the External Secrets
Operators run - as its source. Zone separation of vault access is RBAC and ABAC plus that source group, not a
subnet per zone. The same shared network holds the stateful cluster when one is built.
Why not one common VNet for all cells?
| One VNet per cell (chosen) | One VNet per environment | One frontend VNet for all stateless clusters + backend VNet | |
|---|---|---|---|
| Frontend/backend separation (R19) | Separate VNets, only reachable through the exposure’s hub Firewall | Lost – the Internet-less backend shares a VNet with the frontend, separated only by NSG/UDR | Kept |
| Change one cluster at a time (R26) | Yes – a VNet change (address space, DNS servers, VNet encryption, peering) affects one cluster, and a stateless cluster can be rebuilt with a fresh VNet | No – every VNet change hits all clusters at once | No for the stateless clusters – a VNet change hits all of them at once |
| VNet settings per cluster type | VNet encryption on the cells only, other DNS/egress settings for the backend | One setting for all | Yes |
| One prefix per zone | One per zone and cell, with the same slot index everywhere; the Firewall’s IP group per zone joins them | Yes | Yes |
| Extra cost | One hub peering per cell; a new zone is one IP group entry per cell | – | – |
Sizing. /24 slots give every zone about 120 nodes, enough for a small environment; a production cell takes
larger slots (a /16 cut into sixteen gives each slot a /20), which is a per-cell value, so cells of different
sizes coexist and a cell is resized by a rebuild. The corporate IPAM therefore reserves one prefix per cell, not
one per zone across cells. If the corporate plan wants one
summarisable prefix per country or zone instead, the same indices give such a plan (a /12 per environment, a /16
per country, a /17 per zone) without changing the cells.
IPv6: the platform is IPv4 only and plans no IPv6.
Adding a country online
A new country is two new zones, one per exposure. A zone is a slot of every cell’s address space, so nothing is re-addressed, no peering changes and no route is added; it is added in the order dev → acc → prd, and the existing zones only get new Firewall rules:
- Declare the zone in the environment’s shared configuration (its exposure and its index, a free slot in every cell of the exposure, which the IaC checks), its Key Vault name and, for an internal zone, its Private DNS zone; the environment module creates the vault with its private endpoint and the DNS zone.
- Subnets and NSGs (
snet-<zone>-nodes,snet-<zone>-ilb) appear in every cell of the exposure from the slot, andsnet-<zone>-podsfrom the pod address space’s slot), and the cluster identity gets Network Contributor on them and on the zone’s route table (rt-<cell>-<zone>: the default route, the cell’s address spaces to the Firewall - its own load balancer subnet included -, the zone’s own node subnet and pod slot kept in the VNet); the other tables, the system subnets’ included, already route its slot, because they route the whole address space, so none of them changes. The Firewall’s Kubernetes-flow rules name the zone’s IP group. - Firewall: the zone’s IP group (its node and pod slots in every cell), its allow-list and its application rule collections; the existing rules do not change.
- Certificates and identities: the zone’s gateway certificate in the platform Key Vault (the renewal job), the zone’s External Secrets reader identities and role assignments; in the stateful cluster also the zone’s persistent volume keys, disk encryption sets, storage accounts and StorageClasses (section 4.2).
- Nodes: in the stateless cells, the zone’s four pools (from zero, section 2.3); in the
stateful cluster, node pools
<zone>z1–z3(fixed node count). Neither restarts existing nodes. - Policies and Git: the zone’s Azure Policy mutation and validation definitions (one per zone), and the zone’s
Gateway, External Secrets Operator, Cilium baseline and namespaces through Flux (the platform repository’sinfrastructure/zones, applied once per zone aszone-<zone>) and the stateful pipeline. - Entry points: ExternalDNS’s role on the internal zone’s Private DNS zone; the external zone’s Traffic Manager profiles and the public DNS CNAMEs of its applications. Existing zones’ entry points do not change.
A country can be switched off in a cell without giving up its slot: its zone stays defined (vault, DNS zone, certificate) and runs no nodes there.
Flows between frontend and backend
| Source | Destination | Port | Rule |
|---|---|---|---|
| Stateless zone X pods (their own addresses) | Stateful zone X application internal LBs | Application TLS port (e.g. TCP 443) | Same isolation zone only (int-fi → int-fi), via Firewall; TLS terminated by the application |
| Stateless zone X pods | Zone X PaaS private endpoints | TCP 443, 1433, 5432, 5671 | Same isolation zone only, via Firewall; TLS enforced by the PaaS service |
| All nodes of the exposure | The exposure’s ACR private endpoint | TCP 443 | Images, Helm charts |
| Internal clusters | External clusters, and the reverse | – | Blocked: no peering and no route between the two hubs. An internal caller reaches an external application through its public entry point like any Internet client; the reverse has no path |
| Stateful cluster | Frontend networks | – | Blocked – the backend never initiates connections to the frontend |
| Stateful cluster | Internet | – | Blocked – no route, no outbound IP, Firewall deny-all |
| Management VNet pipeline agents | Stateful API server (Private Link) | TCP 443 | Deployments to the stateful cluster (section 3.3) |
| All nodes of the environment (Grafana Alloy) | Grafana Cloud private endpoint in the hub | TCP 443 | Telemetry through Azure Private Link, so the stateful cluster still has no Internet (section 6.2) |
| Stateful nodes / pods | Microsoft Entra ID (AzureActiveDirectory service tag) |
TCP 443 | Exception, needed for workload identity token exchange because Entra ID has no Private Link for sign-in |
| Stateful nodes (Azure Policy add-on) | data.policy.core.windows.net, store.policy.core.windows.net, dc.services.visualstudio.com |
TCP 443 | Exception, Firewall application rules for these FQDNs only; the add-on has no Private Link (section 2.7) |
Telemetry goes to Grafana Cloud through Private Link; the AKS monitoring add-on is not enabled (section 6.2). Other AKS add-ons that need public Azure endpoints are not enabled in the stateful cluster; the Azure Policy add-on is the one accepted exception (section 2.7). These two destinations (Entra ID, Azure Policy) are the whole list of public endpoints the stateful cluster may reach; the Firewall application rules are the inventory, and the isolation tests prove from inside the cluster that nothing else answers.
Shared tier failure domains
Cells isolate compute and releases. The shared tier - Traffic Manager, the hub Firewalls, Private DNS, Entra ID, the Azure Policy endpoints, Key Vault, the registry, the PaaS data services, the stateful cluster, Grafana Cloud and GitHub - is not split into cells, so each of its parts is a failure domain of its own: zone-redundant where Azure offers it, and with a known effect when it is lost anyway. What each failure does to running traffic, and how it recovers, is the fault table of section 0.3, which the resilience tests inject one by one.