Replacing Ingress-NGINX with Envoy Gateway in My Personal Cluster

Published on November 23, 2025, 17:00 UTC 7 minutes New!

With the retirement of ingress-nginx, many users are looking for alternatives for ingress controllers. Envoy gateway looked like a promising option, so I decided to give it a try in my personal Kubernetes cluster. I'll describe my experience deploying Envoy Gateway and how I was able to replicate my previous ingress-nginx setup. This blog post covers my personal cluster and the migration would have been harder in a production environment with more complex requirements.

Deploying Envoy Gateway

I chose to deploy Envoy Gateway using Helm and Envoy Gateway's official Helm chart. The deployment was straightforward, and I was able to get Envoy Gateway up and running quickly. Here are the values I used for the Helm chart:

config:
envoyGateway:
    extensionApis:
        enableBackend: true
    logging:
        level:
          default: info
deployment:
    envoyGateway:
        resources:
            requests:
              cpu: 20m
              memory: 50Mi

I enable the backend API to use the Backend resource for routing traffic to my services. Specifically I use HttpRouteFilters to block specific paths.

Deploying Our Gateway

By default, Envoy Gateway creates a separate cloud LoadBalancer and Deployment for each Gateway resource, with individual HTTPRoute objects routing traffic to the appropriate backend services. In my previous ingress-nginx setup, I used a single LoadBalancer for all ingress traffic - mainly to keep costs down, therefore I wanted to replicate that behavior with Envoy Gateway.

Fortunately, Envoy Gateway supports this pattern out of the box. To share one underlying LoadBalancer and Deployment across multiple Gateway resources, you deploy a single Envoy Gateway instance and have each Gateway reference it. The first step is to create an

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: default
  namespace: gateway
spec:
  logging:
    level:
      default: info
  mergeGateways: true
  provider:
    kubernetes:
      envoyDeployment:
        patch:
          type: StrategicMerge
          value:
            spec:
              template:
                spec:
                  containers:
                  - name: envoy
                    resources:
                      requests:
                        cpu: 20m
                        memory: 100Mi
                  - name: shutdown-manager
                    resources:
                      requests:
                        cpu: 5m
                        memory: 10Mi
      envoyHpa:
        maxReplicas: 6
        metrics:
        - resource:
            name: cpu
            target:
              averageUtilization: 80
              type: Utilization
          type: Resource
        minReplicas: 3
      envoyPDB:
        maxUnavailable: 1
    type: Kubernetes

Note the mergeGateways: true field - this instructs the Envoy Gateway controller to merge multiple Gateway resources into a single Envoy deployment (and therefore a single LoadBalancer). Everything else in the spec is standard Kubernetes configuration: PDBs, HPAs, and resource requests/limits.

Next we'll need a GatewayClass resource:

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: default
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
  parametersRef:
    group: gateway.envoyproxy.io
    kind: EnvoyProxy
    name: default
    namespace: gateway

Finally, we can create multiple Gateway resources that refer to the same EnvoyProxy:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: hodovi-cc
  namespace: apps
spec:
  gatewayClassName: default
  listeners:
  - allowedRoutes:
      namespaces:
        from: Same
    hostname: hodovi.cc
    name: hodovi-cc
    port: 443
    protocol: HTTPS
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: hodovi-cc-tls
      mode: Terminate
  - allowedRoutes:
      namespaces:
        from: Same
    hostname: www.hodovi.cc
    name: www-hodovi-cc
    port: 443
    protocol: HTTPS
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: hodovi-cc-tls
      mode: Terminate

Note the gatewayClassName: default, which refers to the GatewayClass we created earlier. The Gateway resource defines two listeners for hodovi.cc and www.hodovi.cc.

We can then have another Gateway resource for another domain, all sharing the same LoadBalancer and Envoy deployment:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: teleport
  namespace: auth
spec:
  gatewayClassName: default
  listeners:
  - allowedRoutes:
      namespaces:
        from: All
    hostname: '*.teleport.honeylogic.io'
    name: wildcard-teleport-honeylogic-io
    port: 443
    protocol: HTTPS
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: teleport-cluster-tls
      mode: Terminate
  - allowedRoutes:
      namespaces:
        from: All
    hostname: '*.findwork.dev'
    name: wildcard-findwork-dev
    port: 443
    protocol: HTTPS
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: teleport-cluster-tls
      mode: Terminate
  - allowedRoutes:
      namespaces:
        from: All
    hostname: '*.honeylogic.io'
    name: wildcard-honeylogic-io
    port: 443
    protocol: HTTPS
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: teleport-cluster-tls
      mode: Terminate

The above Gateway resource defines multiple listeners for wildcard domains, all sharing the same LoadBalancer and Envoy deployment.

Lastly, we'll need to create HttpRoute resources to route traffic to our services. Here's an example:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: hodovi-cc
  namespace: apps
spec:
  hostnames:
  - hodovi.cc
  - www.hodovi.cc
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: hodovi-cc
    namespace: apps
  rules:
  - backendRefs:
    - group: ""
      kind: Service
      name: hodovi-cc
      port: 80
      weight: 1
    filters: []
    matches:
    - path:
        type: PathPrefix
        value: /

Cert-manager Integration

cert-manager supports GatewayAPI resources out of the box. However, to solve HTTP-01 challenges, we need to create a dedicated Gateway that cert-manager can use for the challenge endpoint. Here's an example:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: letsencrypt
  namespace: gateway
spec:
  gatewayClassName: default
  listeners:
  - allowedRoutes:
      namespaces:
        from: All
    name: http
    port: 80
    protocol: HTTP

Now we can annotate our Gateway resources to use the letsencrypt gateway for cert-manager challenges:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-issuer

The cert-manager.io/cluster-issuer annotation tells cert-manager to use the letsencrypt-issuer ClusterIssuer for obtaining certificates for the domains defined in the Gateway resource. It has to exist in the cluster, the migration from Ingress to Gateway doesn't change cert-manager configuration.

Metrics

Envoy Gateway exposes metrics in Prometheus format out of the box. To scrape these metrics, we can create a ServiceMonitor resource if you're using the prometheus-operator. Here's an example:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: envoy-gateway
  namespace: gateway
spec:
  endpoints:
  - interval: 30s
    port: metrics
  namespaceSelector:
    matchNames:
    - gateway
  selector:
    matchLabels:
      app.kubernetes.io/instance: envoy-gateway
      app.kubernetes.io/name: gateway-helm
      control-plane: envoy-gateway

Now you should have Envoy Gateway metrics being scraped by Prometheus. Next, we'll want to scrape the Envoy proxy metrics as well. The default service doesn't expose the metrics port so we'll just create one:

apiVersion: v1
kind: Service
metadata:
  name: envoy-default-metrics
  namespace: gateway
spec:
  clusterIP: 10.55.1.35
  clusterIPs:
  - 10.55.1.35
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: metrics
    port: 19001
    protocol: TCP
    targetPort: metrics
  - name: admin
    port: 19000
    protocol: TCP
    targetPort: admin
  - name: xds
    port: 18000
    protocol: TCP
    targetPort: xds
  selector:
    app.kubernetes.io/component: proxy
    app.kubernetes.io/managed-by: envoy-gateway
    app.kubernetes.io/name: envoy
  sessionAffinity: None
  type: ClusterIP

The selector section matches the Envoy proxy pods created by the Envoy Gateway controller. Now we can create another ServiceMonitor to scrape the Envoy proxy metrics:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: envoy-default
  namespace: gateway
spec:
  endpoints:
  - interval: 15s
    path: /stats/prometheus
    port: metrics
  namespaceSelector:
    matchNames:
    - gateway
  selector:
    matchLabels:
      app.kubernetes.io/name: envoy-metrics

At this point, both Envoy Gateway and the Envoy proxy should be successfully scraped by Prometheus. To simplify observability, I've created a monitoring-mixin for Envoy and Envoy Gateway, which provides pre-configured Grafana dashboards and Prometheus alerts out of the box.

If you want a deeper walkthrough of the setup, I’ve also written a detailed guide: Monitoring Envoy and Envoy Gateway with Prometheus and Grafana.

Tracing

I used tracing in my ingress-nginx setupand Envoy Gateway also supports distributed tracing out of the box. You can configure tracing by adding the following configuration to the EnvoyProxy resource:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: default
  namespace: gateway
spec:
  logging:
    level:
      default: info
  mergeGateways: true
  provider:
    kubernetes:
      envoyDeployment:
        patch:
          type: StrategicMerge
          value:
            spec:
              template:
                spec:
                  containers:
                  - name: envoy
                    resources:
                      requests:
                        cpu: 20m
                        memory: 100Mi
                  - name: shutdown-manager
                    resources:
                      requests:
                        cpu: 5m
                        memory: 10Mi
      envoyHpa:
        maxReplicas: 6
        metrics:
        - resource:
            name: cpu
            target:
              averageUtilization: 80
              type: Utilization
          type: Resource
        minReplicas: 3
      envoyPDB:
        maxUnavailable: 1
    type: Kubernetes
  # Add the following tracing configuration
  telemetry:
    tracing:
      provider:
        backendRefs:
        - group: ""
          kind: Service
          name: alloy
          namespace: monitoring
          port: 4317
        port: 4317
        type: OpenTelemetry
      samplingRate: 1

The backendRefs section specifies the tracing backend to use. In this example, I'm using Grafana's Alloy and Tempo as the tracing backend, which is deployed as a Kubernetes Service in the monitoring namespace. The samplingRate is set to 1, which means that 1% of requests will be sampled.

Blocking Specific Paths

I sucessfully circumvented most ingress annotations I was using by better application logic and saner defaults by Envoy. However, one specific use-case I had was blocking access to the Prometheus metrics endpoint and other endpoints like /admin to the public.

Envoy Gateway allows you to use HttpRouteFilters to block specific paths. Here's an example of how to block access to the /admin path:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: HTTPRouteFilter
metadata:
  name: hodovi-cc-block-prometheus-metrics
  namespace: apps
spec:
  directResponse:
    body:
      inline: Forbidden
      type: Inline
    contentType: text/plain
    statusCode: 403

Then we'll reference this filter in the HttpRoute resource:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: hodovi-cc
  namespace: apps
spec:
  hostnames:
  - hodovi.cc
  - www.hodovi.cc
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: hodovi-cc
    namespace: apps
  rules:
  - backendRefs:
    - group: ""
      kind: Service
      name: hodovi-cc
      port: 80
      weight: 1
    filters: []
    matches:
    - path:
        type: PathPrefix
        value: /
  # Block Prometheus metrics endpoint
  - backendRefs:
    - group: ""
      kind: Service
      name: hodovi-cc
      port: 80
      weight: 1
    filters:
    - extensionRef:
        group: gateway.envoyproxy.io
        kind: HTTPRouteFilter
        name: hodovi-cc-block-prometheus-metrics
      type: ExtensionRef
    matches:
    - path:
        type: PathPrefix
        value: /prometheus/metrics

Conclusion

Overall, my experience replacing ingress-nginx with Envoy Gateway has been very positive. The migration was straightforward, and I was able to reproduce my previous setup using a single LoadBalancer shared across multiple Gateway resources. Integration with cert-manager for TLS was seamless, and having metrics and tracing available out of the box was a great improvement.

The only part that required extra effort was monitoring. With ingress-nginx, I relied on the ingress-nginx-mixin for ready-made dashboards and alerts. Envoy didn’t have an equivalent at the time, so I created my own mixin and worked through which metrics and alerts made the most sense. It took some work, but the end result has been worth it.

If you're considering an alternative to ingress-nginx, I highly recommend giving Envoy Gateway a try. And if you want a head start on observability, you can try the mixin here: adinhodovic/envoy-mixin.

Related Posts

Monitoring Envoy and Envoy Gateway with Prometheus and Grafana

Envoy is a popular open source edge and service proxy that's widely used in modern cloud-native architectures. Envoy gateway is a controller that manages Envoy proxies in a Kubernetes environment. Monitoring Envoy and Envoy gateway is crucial for ensuring the reliability and performance of your applications. In this blog post, we'll explore how to monitor Envoy and Envoy gateway using Prometheus and Grafana and we'll also introduce a new monitoring-mixin for Envoy.

With the retirement of ingress-nginx, many users are looking for alternatives for ingress controllers. Envoy gateway is a great option for those who want to leverage the power of Envoy in their Kubernetes clusters. I recently migrated from ingress-nginx and you can read more about it here.

Monitoring Kubernetes InitContainers with Prometheus

Kubernetes InitContainers are a neat way to run arbitrary code before your container starts. It ensures that certain pre-conditions are met before your app is up and running. For example it allows you to:

  • run database migrations with Django or Rails before your app starts
  • ensure a microservice or API you depend on to is running

Creating a Low Cost Managed Kubernetes Cluster for Personal Development using Terraform

Kubernetes is an open-source system that's popular amongst developers. It automatically deploys, scales, and manages containerized applications. Yet for those working outside of the traditional startup or corporate setting, Kubernetes can be CPU and memory-intensive, disincentivizing developers from using a strategically beneficial tool. We'll highlight some of the constraints posed by using Kubernetes and offer a series of low-cost, practically-beneficial solution: Google Kubernetes Engine deployed and managed with Terraform.