Lesson 31 of 66 · 13 min read · Updated
Installing Envoy Gateway and Exposing Your First .NET API
On this page
In the previous article you saw why Ingress is being left behind and audited what a migration has to cover. The audit ended with an empty result: on a fresh minikube cluster, kubectl get crd | grep gateway.networking.k8s.io printed nothing. Gateway API is not built into Kubernetes, so the first job is to install it.
You do not install Gateway API on its own. You install an implementation, which brings the CRDs, a controller that watches them, and the proxies that actually carry traffic. This series uses Envoy Gateway, the Envoy project's own Gateway API controller. Everything you write in the Gateway API resources would work the same on any other conformant implementation.
By the end of this article a request to storefront.local will travel through an Envoy proxy and land on one of the three kiosk-api Pods, without a single annotation.
We will cover the following topics:
- How Envoy Gateway is structured: a control plane and per-Gateway data planes
- Installing Envoy Gateway v1.9.2 with Helm, and what the chart installs
- Creating the
egGatewayClass - Creating the
storefront-gatewayGateway with an HTTP listener - Writing your first HTTPRoute for kiosk-api
- Reaching the Gateway with
kubectl port-forwardand withminikube tunnel - Reading the status conditions that tell you whether everything was accepted
Technical Requirements
- A minikube cluster and
kubectl. - Helm 3.8 or later, because the chart is published as an OCI artifact.
- The
kiosk-api:1.0image, Deployment, and Service in thestorefrontnamespace from the previous article. Step 1 recreates them if needed. curlfor testing.
Core Concepts
Control Plane and Data Plane
Envoy Gateway has two layers, and it helps to keep them apart from the start.
The control plane is a single Deployment called envoy-gateway in the envoy-gateway-system namespace. It watches GatewayClasses, Gateways, and Routes, translates them into Envoy configuration, and pushes that configuration to the proxies. It never handles a user request.
The data plane is made of Envoy proxies. For every Gateway you create, Envoy Gateway creates a Deployment of Envoy Pods and a Service in front of them. Those Pods are what your users actually connect to.
| Layer | What it is | Created by | Carries traffic? |
|---|---|---|---|
| Control plane | envoy-gateway Deployment | Helm install | No |
| Data plane | Envoy Deployment and Service per Gateway | Envoy Gateway, when you create a Gateway | Yes |
So creating a Gateway is not just configuration. It provisions real infrastructure: Pods, a Service, and on a cloud cluster a load balancer.
What the Helm Chart Installs
The default Helm install applies three things:
- The Gateway API CRDs: GatewayClass, Gateway, HTTPRoute, GRPCRoute, ReferenceGrant, and others.
- The Envoy Gateway CRDs for its own extensions, such as
EnvoyProxy,ClientTrafficPolicy,BackendTrafficPolicy, andSecurityPolicy. - The control plane Deployment, its RBAC, and its webhooks.
The bundled Gateway API CRDs come from the experimental channel, which is a superset of the Standard channel. That lets Envoy Gateway offer alpha kinds such as TCPRoute. Everything this series uses is in the Standard channel, so your manifests stay portable.
If your platform already manages the Gateway API CRDs, for example a managed cloud cluster, you can tell the chart not to install them with --set crds.gatewayAPI.enabled=false. On minikube, let the chart do it.
The Three Resources You Will Create
GatewayClass is cluster-scoped. It names a controller. When you create a GatewayClass whose controllerName is gateway.envoyproxy.io/gatewayclass-controller, Envoy Gateway claims it and marks it Accepted. Any Gateway that references this class is then handled by Envoy Gateway.
Gateway is namespaced. It describes a point where traffic enters: which ports to listen on, with which protocol, for which hostnames. Each entry is a listener. A Gateway with one HTTP listener on port 80 is the simplest useful Gateway.
HTTPRoute is namespaced too. It attaches to a Gateway through parentRefs, declares which hostnames it serves, and contains rules. Each rule has matches (path, headers, query parameters, method) and backendRefs (the Services that receive the request).
| Resource | Scope | Owned by | Answers the question |
|---|---|---|---|
GatewayClass eg | Cluster | Platform/infrastructure | Which software implements my Gateways? |
Gateway storefront-gateway | Namespace | Platform team | Where does traffic enter, on which ports and hostnames? |
HTTPRoute kiosk-api | Namespace | Application team | Which requests go to which Service? |
How a Request Flows
Once everything is in place, a request follows this path:
- The client connects to the Envoy Service created for
storefront-gateway. - The Service forwards to one of the Envoy Pods.
- Envoy matches the Host header against the listener and the HTTPRoute hostnames, then the path against the rule matches.
- Envoy picks one of the kiosk-api Pod IPs directly. It reads the Service's EndpointSlices and load balances itself. It does not go through the kiosk-api ClusterIP.
- A kiosk-api Pod answers.
Step 4 surprises people. The backendRefs field names a Service, but Envoy uses the Service only to discover endpoints. That is why a Pod that fails its readiness probe disappears from Envoy's rotation just as it does from the Service.
Reaching the Gateway on minikube
The Envoy Service is of type LoadBalancer. On a cloud cluster it gets an external IP and the Gateway reports that IP in status.addresses. On minikube nothing provisions load balancers, so the address stays pending. There are two ways around that:
| Option | How | When to use |
|---|---|---|
kubectl port-forward | Forward a local port to the Envoy Service | Quick tests. Dies with the command. |
minikube tunnel | Run in a separate terminal. It assigns an IP to every LoadBalancer Service. | When you want the Gateway to get a real address in its status. |
Both are shown in the Step-by-Step section.
On Docker Desktop's built-in kind cluster the situation is different. Docker Desktop provisions LoadBalancer Services itself: the Envoy Service gets an address from Docker's 172.18.x.x network, the Gateway becomes Programmed=True without any tunnel, and every listener port is published on localhost. That address is not reachable from the host, so use http://localhost/ instead of the Gateway address. There is one catch: only one LoadBalancer Service can own a given localhost port. If another one already holds port 80, such as an Ingress NGINX controller from the previous article, the Envoy Service stays <pending> and the Gateway reports Programmed=False (AddressNotAssigned). The port-forward works in every case. On a plain kind cluster, run cloud-provider-kind to get LoadBalancer addresses.
Status Conditions
Every Gateway API resource reports what the controller thought of it. This is one of the biggest improvements over Ingress, and you should get into the habit of reading it.
| Resource | Condition | Meaning when True |
|---|---|---|
| GatewayClass | Accepted | A controller claimed this class. |
| Gateway | Accepted | The controller understood the Gateway spec. |
| Gateway | Programmed | The proxies are configured and ready to receive traffic. |
| HTTPRoute (per parent) | Accepted | The route attached to that Gateway. |
| HTTPRoute (per parent) | ResolvedRefs | Every backendRef points to something that exists and is allowed. |
The next article looks at these conditions in depth, including the reasons you see when they are False.
Hands-On: Kubernetes Commands
Install Envoy Gateway with Helm
Installs the CRDs and the control plane into envoy-gateway-system.
Wait for the control plane
Blocks until the envoy-gateway Deployment is available.
List GatewayClasses with their acceptance
The ACCEPTED column should say True.
List Gateways with their address and readiness
PROGRAMMED tells you whether the proxies are ready.
List HTTPRoutes
Shows each route and the hostnames it serves.
Find the Envoy Service of a Gateway
Envoy Gateway labels the Services it creates with the Gateway's name and namespace.
Print a route's status per parent
The quickest way to see why a route does not work.
Uninstall Envoy Gateway
Removes the control plane. Delete your Gateways first so their proxies are cleaned up.
Step-by-Step Example
The Scenario
The storefront platform team has picked Envoy Gateway to replace Ingress NGINX. Your job is to install it, give the cluster a GatewayClass and a shared Gateway, and expose kiosk-api on storefront.local with an HTTPRoute that the kiosk team owns.
Step 1 — Make Sure kiosk-api Is Running
You need the storefront namespace, the kiosk-api Deployment with three replicas, and the kiosk-api ClusterIP Service on port 80. If you followed the previous article they are already there:
If not, follow Step 1 of the previous article. The app is the .NET 10 minimal API from the Services series. It returns the name and IP of the Pod that served the request, which makes load balancing visible.
Step 2 — Install Envoy Gateway
Check that the Gateway API CRDs now exist:
The exact list depends on the CRD version bundled with the chart. What matters is that GatewayClass, Gateway, and HTTPRoute are served as v1.
Step 3 — Create the GatewayClass
Save as 3-gatewayclass.yaml. This is usually done once per cluster by the platform team.
ACCEPTED: True means Envoy Gateway recognised its own controller name and claimed the class. If you mistype the controller name, the class stays unaccepted forever, because no controller claims it.
Step 4 — Create the Gateway
Save as 4-storefront-gateway.yaml. One listener, plain HTTP on port 80.
PROGRAMMED: False with an empty address is expected on minikube. The proxies are being created, and the LoadBalancer Service has no external IP. Look at what Envoy Gateway provisioned for you:
You never wrote a Deployment or a Service for Envoy. Creating the Gateway was enough. The names include the Gateway's namespace, its name, and a hash.
Step 5 — Create the HTTPRoute
Save as 5-kiosk-api-route.yaml. This is the object the kiosk team owns.
Compare it with the Ingress from the previous article. Host, path, and backend are all still there, now as typed fields. The IngressClass is gone, replaced by a reference to a specific Gateway.
Step 6 — Check the Route Status
Before sending traffic, ask whether the route was accepted:
Accepted means the route attached to storefront-gateway. ResolvedRefs means the kiosk-api Service exists. If either were False, the reason would tell you why. You will break both on purpose in the next article.
Step 7 — Send Traffic with port-forward
Find the Envoy Service by its labels and forward local port 8888 to its port 80:
Send a few requests with the right Host header:
All three Pods answer. Envoy load balances across the endpoints of the kiosk-api Service.
Step 8 — Prove That the Hostname Matters
Send the same request without the Host header, or with a different host:
The listener accepts any hostname, but the only route attached to it serves storefront.local. Envoy finds no matching route and answers 404 itself. Your Pods never see these requests.
Stop the port-forward when you are done:
Step 9 — Give the Gateway a Real Address with minikube tunnel
In a second terminal, start the tunnel and leave it running. It may ask for your password, because it changes the host's routing table.
Back in the first terminal, the Gateway now has an address and is fully programmed:
Read the address from the Gateway status and call it directly:
On Docker Desktop's kind cluster you need neither the tunnel nor the address: curl -s -H "Host: storefront.local" http://localhost/ reaches the Gateway as soon as it is Programmed.
On a cloud cluster this is the normal state: the address is the cloud load balancer, and you point the DNS record for storefront.local at it.
Step 10 — Clean Up
Delete the route and the Gateway. Deleting the Gateway also deletes the Envoy Deployment and Service that were created for it. Keep the GatewayClass, Envoy Gateway, and kiosk-api, because the next article builds on them.
Stop minikube tunnel with Ctrl+C.
Summary
- Gateway API is installed by an implementation. This series uses Envoy Gateway v1.9.2, installed with one Helm command that also brings the Gateway API CRDs.
- Envoy Gateway has a control plane (
envoy-gateway) and creates a data plane of Envoy Pods and a Service for every Gateway. - A GatewayClass names the controller.
egwithgateway.envoyproxy.io/gatewayclass-controlleris claimed by Envoy Gateway. - A Gateway declares listeners. One HTTP listener on port 80 is enough to start.
- An HTTPRoute attaches to a Gateway with
parentRefsand sends matching requests tobackendRefs. Envoy load balances directly across the Service's Pod endpoints. - On minikube, reach the Gateway with
kubectl port-forwardor give it an address withminikube tunnel. - Always read status conditions:
AcceptedandProgrammedon the Gateway,AcceptedandResolvedRefson each route parent.
In the next article you will take the resource model apart: listeners, allowedRoutes, sectionName, hostname matching, and what each status condition says when something goes wrong.