hereticles

hereticles

heresy, ticles, and

02 Jun 2026

Getting Envoy Gateway working with kind without cloud-provider-kind

EXTERNAL-IP: <pending>. The official workaround is cloud-provider-kind, a binary that watches the cluster and assigns real IPs from the Docker bridge network.

The problem with cloud-provider-kind:

  • It must run as a persistent background process alongside the cluster
  • It’s not portable (no asdf plugin, no standard package)
  • On systems without systemd (OpenRC, etc.) there’s no clean way to manage it as a service

I wanted to have a simple to bootstrap dev environment for henge , a platform to pushes config to edge devices.

The solution

Envoy Gateway provides an EnvoyProxy custom resource that lets you configure how it creates the proxy service. Switching the service type to NodePort and fixing the nodePort to a known value means kind’s extraPortMappings can map it to a host port — no background process required.

1. kind config

Map the fixed NodePort to a host port in infra/kind/kind-config.yaml :

1
2
3
4
5
6
7
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 30080
        hostPort: 9876

containerPort is the port on the kind node (Docker container). hostPort is the port on your machine. Pick a hostPort that isn’t already in use.

2. EnvoyProxy resource

Create infra/kind/envoy-proxy.yaml :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: henge
  namespace: default
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: NodePort
        patch:
          type: StrategicMerge
          value:
            spec:
              ports:
                - port: 80
                  nodePort: 30080

The StrategicMerge patch type uses Kubernetes’s merge key mechanism — for Service ports the merge key is port, so this merges with the existing port 80 entry rather than replacing all ports. The value field expects a YAML object, not a JSON string (that was a gotcha we hit).

3. GatewayClass

Create infra/kind/gateway-class.yaml :

1
2
3
4
5
6
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller

4. Gateway

Reference the EnvoyProxy resource from your Gateway via infrastructure.parametersRef:

deploy/gateway.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: henge
  namespace: default
spec:
  gatewayClassName: eg
  infrastructure:
    parametersRef:
      group: gateway.envoyproxy.io
      kind: EnvoyProxy
      name: henge
  listeners:
    - name: http
      protocol: HTTP
      port: 80

5. HTTPRoute

For SSE connections, set timeouts.request: 0s to prevent the proxy closing long-lived connections.

Henge uses long running SSE connections without heartbeats to deploy config updates. Without this setting, it could look like the connection has closed when it hasn’t.

deploy/gateway.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: henge
  namespace: default
spec:
  parentRefs:
    - name: henge
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      timeouts:
        request: 0s
      backendRefs:
        - name: henge
          port: 80

The port chain

localhost:9876
  → kind node port 30080       (kind extraPortMappings)
  → Envoy Gateway NodePort     (fixed via EnvoyProxy StrategicMerge patch)
  → Service port 80            (internal cluster routing)
  → Pod port 8080              (application)

Gotchas

install.yaml does not create a GatewayClass. Envoy Gateway v1.8.0’s install manifest sets up the controller but does not create a GatewayClass. Without it, the Gateway stays in Waiting for controller indefinitely. Create it separately and apply it after the controller is running.

The GatewayClass takes a few seconds to reach Accepted: True. Apply it and wait before applying the Gateway. If the Gateway is applied while the GatewayClass is still Unknown, it will be accepted once the class is ready — but worth checking kubectl get gatewayclass before proceeding.

extraPortMappings cannot be changed without recreating the cluster. kind nodes are Docker containers, and Docker container port mappings are fixed at creation time — there’s no way to add or change them on a running container. If you change the containerPort in kind config, you must run kind delete cluster and recreate it. This is why fixing the nodePort matters — a random NodePort would require tearing down and recreating the cluster every time it changed.