End-to-End Guide: Deploying Envoy Gateway on Bare-Metal Kubernetes

End-to-End Guide: Deploying Envoy Gateway on Bare-Metal Kubernetes
Photo by Omar Flores / Unsplash

This guide walks through deploying Envoy Gateway on a self-managed, bare-metal Kubernetes cluster (like RKE2 on VMware) and routing external traffic to an application using nip.io.

Because this is a bare-metal environment without a cloud provider, we must manually patch the Envoy Gateway service to mimic a cloud LoadBalancer provisioning an IP.

Step 1: Install the Gateway API CRDs

Envoy Gateway requires the modern Kubernetes Gateway API definitions to function. These must be installed into the cluster first.

kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml

Step 2: Install Envoy Gateway

We use Helm to deploy the Envoy Gateway controller. We include --skip-crds because we manually installed them in the previous step.

# Create the namespace
kubectl create namespace envoy-gateway-system

# Install Envoy Gateway via Helm
helm install otterscale-envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
  --version v1.7.4 -n envoy-gateway-system --skip-crds

Wait a few moments and verify the controller is running:

kubectl get pods,svc -n envoy-gateway-system

Step 3: Create the GatewayClass

The GatewayClass links any Gateway you create to the specific Envoy Gateway controller we just installed. Without this, your Gateways will stay in a <pending> state.

Save as gatewayclass.yaml and apply:

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
kubectl apply -f gatewayclass.yaml

Step 4: Deploy the Target Application (Podinfo)

Deploy a sample application and expose it internally via a ClusterIP Service.

Save as podinfo.yaml and apply:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: podinfo
  template:
    metadata:
      labels:
        app: podinfo
    spec:
      containers:
      - name: podinfo
        image: stefanprodan/podinfo
        ports:
        - containerPort: 9898
---
apiVersion: v1
kind: Service
metadata:
  name: podinfo
spec:
  selector:
    app: podinfo
  ports:
  - port: 9898
    targetPort: 9898
kubectl apply -f podinfo.yaml

Step 5: Configure the Gateway and HTTPRoute

Now we define the physical entry point (Gateway) and the routing rules (HTTPRoute).

  • The Gateway listens on port 80.
  • The HTTPRoute looks for the hostname podinfo.<YOUR_IP>.nip.io and sends traffic to the internal podinfo service.

(Replace 10.0.50.10 with the actual IP address of your bare-metal server).

Save as routing.yaml and apply:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: standard-web-gateway
  namespace: envoy-gateway-system
spec:
  gatewayClassName: envoy
  listeners:
  - name: web-listener
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: podinfo-route
  namespace: default
spec:
  parentRefs:
  - name: standard-web-gateway
    namespace: envoy-gateway-system
  hostnames:
  - "podinfo.10.0.50.10.nip.io"
  rules:
  - backendRefs:
    - name: podinfo
      port: 9898
kubectl apply -f routing.yaml

Step 6: The Bare-Metal Patch

At this point, the Envoy controller has created a LoadBalancer service for your Gateway, but it is stuck in <pending> because there is no cloud provider to assign it an IP.

We must manually patch the service with the bare-metal server's IP address to unblock the controller.

Run these commands, substituting your actual IP address:

# 1. Store the auto-generated service name in a variable
SVC=$(kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=standard-web-gateway -o name)

# 2. Patch the service with your server IP (Replace 10.0.50.10)
kubectl patch $SVC \
  -n envoy-gateway-system \
  --subresource=status \
  --type=merge \
  -p '{"status":{"loadBalancer":{"ingress":[{"ip":"10.0.50.10"}]}}}'

Step 7: Verify and Test

Check that the Gateway is now showing as Programmed: True and has an address assigned:

kubectl get gateway standard-web-gateway -n envoy-gateway-system

Finally, test the connection using the wildcard DNS:

curl http://podinfo.10.0.50.10.nip.io/

You should successfully see the Podinfo response!

Subscribe to Experiment Lab

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe