Gateway and routing¶
Overview¶
The gateway-provider Pulumi stack is responsible for installing the networking foundation
that all model-serving workloads depend on. It performs three distinct jobs:
- Install CRDs — Gateway API CRDs and Gateway API Inference Extension (GIE) CRDs must be present in the cluster before any Gateway or InferencePool resource can be created.
- Install Istio — Deploys
istio-baseandistiodvia Helm, enabling Istio as the Gateway API implementation. - Create shared Gateway — When
infraStackRef(cloud) orgatewayHostname(on-prem) is configured, creates ashared-gatewayresource ingateway-systembacked by a cloud-managed or Istio/MetalLB load balancer.app_shaideand other stacks attachHTTPRouteresources to this gateway.
Why a shared Gateway instead of a LoadBalancer Service per workload?¶
Without a shared gateway, each workload (e.g. app_shaide) would need its own LoadBalancer
Service. On GCP this means a separate L4 Network Load Balancer per service — each with its own
external IP, its own GCP LB resource, and its own cost.
The shared gateway consolidates all external traffic behind a single entry point:
| LoadBalancer per Service | Shared Gateway | |
|---|---|---|
| GCP LB resources | One per service | One for all services |
| External IPs | One per service | One static IP |
| TLS certificate | One per service | One GKE-managed cert |
| Cost | Scales with number of services | Fixed |
On-prem the same principle applies — one MetalLB IP and one Istio proxy instead of one per
service. The goal on on-prem is to mirror the cloud infrastructure as closely as possible,
which is why shared-gateway is deployed on RKE2 as well.
CRD Installation¶
CRDs are installed via kustomize.NewDirectory — the kustomize rendering engine is embedded
in the Pulumi Kubernetes provider. Each CRD is tracked as an individual Pulumi resource,
enabling drift detection and eliminating the kubectl binary dependency.
SSA field-manager conflicts (GCP)¶
GKE clusters pre-install Gateway API CRDs via kube-addon-manager, which holds ownership of
fields such as metadata.annotations.gateway.networking.k8s.io/bundle-version and
spec.versions. Pulumi's Server-Side Apply would conflict with this ownership.
A patchForceCRDs transform is applied to both kustomize directories. It injects the
pulumi.com/patchForce: "true" annotation on every CRD resource, equivalent to
kubectl apply --force-conflicts. Pulumi takes ownership of the conflicting fields without
an error. On RKE2, where no prior owner exists, the annotation has no effect.
kustomize.NewDirectory
└── transform: patchForceCRDs
└── sets metadata.annotations["pulumi.com/patchForce"] = "true"
on every kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition
Traffic Flows¶
GCP¶
Client
│
▼
GKE L7 LB (203.0.113.10)
│
▼
shared-gateway [gateway-system] ← gateway-provider
│
└── HTTPRoute ← app_shaide
└──▶ shaide-server (ClusterIP) [app-shaide ns]
│
└──▶ infra-<model>-inference-gateway-istio (in-cluster FQDN)
│
▼
per-model Gateway (istio, ClusterIP) ← app_serving (llm-d-infra)
│
└── HTTPRoute ← app_serving
└──▶ InferencePool
└──▶ model pods
On-prem (RKE2)¶
Client
│
▼
MetalLB L2 (DNS: shaide.internal.lan)
│
▼
shared-gateway-istio Service (LoadBalancer) [gateway-system]
│
▼
Istio Envoy proxy
│
▼
shared-gateway [gateway-system] ← gateway-provider
│
└── HTTPRoute ← app_shaide
└──▶ shaide-server (ClusterIP) [app-shaide ns]
│
└──▶ infra-<model>-inference-gateway-istio (in-cluster FQDN)
│
▼
per-model Gateway (istio, ClusterIP) ← app_serving (llm-d-infra)
│
└── HTTPRoute ← app_serving
└──▶ InferencePool
└──▶ model pods
Azure — AGC¶
Client
│
▼
AGC frontend (*.alb.azure.com)
│
▼
shared-gateway [gateway-system] ← gateway-provider
│
└── HTTPRoute ← app_shaide
└──▶ shaide-server (ClusterIP) [app-shaide ns]
│
└──▶ infra-<model>-inference-gateway-istio (in-cluster FQDN)
│
▼
per-model Gateway (istio, ClusterIP) ← app_serving (llm-d-infra)
│
└── HTTPRoute ← app_serving
└──▶ InferencePool
└──▶ model pods
- Azure-managed L7 load balancer — Microsoft runs the infrastructure outside the cluster
- AKS provides the
azure-alb-externalGatewayClass built-in - AGC assigns its own stable hostname (
*.alb.azure.com); DNS uses CNAME, not A record - No static IP needed — the hostname is stable
- Requires
Microsoft.ServiceNetworking/trafficControllers - The
ApplicationLoadBalancerCR + delegated ALB subnet (snet-alb-<stack>) connect AGC to the cluster - Zero-ops: Microsoft manages scaling, patching, and availability
Azure — Istio¶
Not supported. AGC is the only supported gateway implementation on AKS. Istio remains installed for inference routing (the per-model gateways above), but it is not used as the shared ingress gateway on Azure.
Downstream stacks (app_shaide, app_serving) are unaffected — the Gateway and HTTPRoute
resources are identical regardless of which implementation backs them.
Same Gateway API spec, different infrastructure behind it.
GKE's gateway controller provisions a Google Cloud L7 load balancer externally — no in-cluster Service is created. The IP is a GCP resource, not a Kubernetes Service.
Istio's gateway controller creates a physical Deployment + LoadBalancer Service in-cluster.
MetalLB handles L2 ARP to advertise the IP on the LAN.
# GCP — no in-cluster Service:
$ kubectl get svc -n gateway-system
No resources found in gateway-system namespace.
$ kubectl get gateway -n gateway-system
NAME CLASS ADDRESS PROGRAMMED
shared-gateway gke-l7-regional-external-managed 203.0.113.10 True
# On-prem — Istio creates a backing Service:
$ kubectl --kubeconfig ~/.kube/rke2-cluster.yaml get svc -n gateway-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
shared-gateway-istio LoadBalancer 10.43.100.50 10.0.10.50 15021:31238/TCP,80:30418/TCP
$ kubectl --kubeconfig ~/.kube/rke2-cluster.yaml get gateway -n gateway-system
NAME CLASS ADDRESS PROGRAMMED
shared-gateway istio 10.0.10.50 True
Azure AGC vs Azure Istio — Operational Differences¶
How traffic reaches the cluster¶
AGC operates entirely outside the AKS cluster. Microsoft provisions an Azure-managed L7 load
balancer (the Application Gateway for Containers resource) in response to the ApplicationLoadBalancer
CR. The ALB Controller pod runs inside the cluster and watches for Gateway and HTTPRoute
resources, then programs the external AGC resource over the Azure control plane. No Envoy pod is
created; traffic never touches an in-cluster proxy before reaching the backend pods.
Istio runs the data plane inside the cluster. The istiod control plane and an Envoy
(shared-gateway-istio) pod are both deployed into gateway-system. An Azure Standard Load
Balancer (L4) forwards external traffic to the Envoy pod's NodePort, and Envoy applies the
Gateway / HTTPRoute routing rules before passing requests to the backend.
IP address and DNS¶
AGC does not expose a routable public IP. Instead it assigns a stable Azure-internal hostname
of the form <hash>.fz52.alb.azure.com. DNS must use a CNAME record pointing to that hostname.
The hostname is stable across Gateway updates, cluster upgrades, and node replacements.
Istio exposes a public IP through the shared-gateway-istio LoadBalancer Service. To prevent
the IP from changing when the Service or cluster is recreated, a static PublicIPAddress resource
is pre-allocated in the AKS node resource group (mc-aks-<stack>) by the 03_cluster stack and
bound to the Gateway via spec.addresses (type: IPAddress). DNS uses an A record pointing
to that static IP.
Failure domain¶
With AGC, a misconfigured Gateway or HTTPRoute affects only the Azure-side programming.
The in-cluster workloads continue running; only external routing is disrupted.
With Istio, the Envoy pod is in the request path. If shared-gateway-istio crashes or is
evicted, all external traffic stops. The Deployment has a single replica by default — consider
scaling it for production workloads.
Health probes¶
AGC manages its own health checks against the backend pods and handles probe configuration automatically.
Istio relies on the Azure Load Balancer to health-check the Envoy pod. Azure defaults to an
HTTP probe on port 80 with path /, which Envoy answers with 404 (no matching route). This is
worked around by propagating the annotation
service.beta.kubernetes.io/port_80_health-probe_port: "15021" via spec.infrastructure.annotations
on the Gateway resource — Azure then probes Istio's dedicated status endpoint
(15021/healthz/ready) instead.
Resource consumption¶
| AGC | Istio | |
|---|---|---|
| In-cluster pods | ALB Controller only | istiod + Envoy (shared-gateway-istio) |
| Azure resources | AGC frontend, ALB subnet | Standard LB, Public IP |
| Cluster CPU/memory | Minimal | ~200m CPU, ~256Mi per istiod + Envoy |
| Required subnet | Delegated /24 (snet-alb-<stack>) |
None |
Gateway implementation by target¶
| Target | GatewayClass | Backed by |
|---|---|---|
| GKE | gke-l7-regional-external-managed |
Google Cloud L7 load balancer |
| AKS | azure-alb-external |
Application Gateway for Containers |
| On-prem | istio |
Istio Envoy behind MetalLB |
On AKS, AGC is the only supported option. It offloads L7 routing to Azure, keeps in-cluster resource usage down, and needs no health-probe workaround.