Last modified October 26, 2026

Installing Gateway API with Envoy Gateway

Prerequisites

Before setting up Gateway API, ensure you have:

  • A Giant Swarm workload cluster
  • kubectl configured to access your workload cluster
  • Access to the Giant Swarm platform API for app installation
  • Basic understanding of Kubernetes networking concepts
  • On AWS based clusters, aws-load-balancer-controller is required for configuring AWS Network Load Balancers.

Installation

Gateway API support is provided through three apps that work together. You can install them individually or use the Gateway API Bundle for simplified deployment. Our recommendation is to use the Gateway API Bundle, which installs all required components (Gateway API CRDs, Envoy Gateway and a preconfigured default Gateway):

Note: The Gateway API Bundle currently ships as an App custom resource, which is being phased out in favor of Flux HelmRelease. The bundle itself doesn’t yet have a HelmRelease equivalent because it deploys nested App resources internally. For application deployments outside the bundle, use HelmRelease.

apiVersion: v1
kind: ConfigMap
metadata:
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
data:
  values: |
    clusterID: <CLUSTER_NAME>
    organization: <ORGANIZATION>
---
apiVersion: application.giantswarm.io/v1alpha1
kind: App
metadata:
  labels:
    app-operator.giantswarm.io/version: 0.0.0
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
spec:
  catalog: giantswarm
  kubeConfig:
    inCluster: true
  name: gateway-api-bundle
  namespace: org-<ORGANIZATION>
  userConfig:
    configMap:
      name: <CLUSTER_NAME>-gateway-api-bundle
      namespace: org-<ORGANIZATION>
  version: 2.0.0

Run the kubectl apply -f <bundle-config.yaml> command on your management cluster to install the bundle, then wait until the child apps are deployed (CRDs, Envoy Gateway, and Gateway default config).

Configuration

Certificate management with cert-manager

In order to obtain TLS certificates for your domains served by Gateway API, cert-manager needs to be configured to handle Gateway resources.

In your clusters user ConfigMap, add the following section. Do this only after Gateway API CRDs are installed.

global:
  apps:
    certManager:
      values:
        config:
          apiVersion: controller.config.cert-manager.io/v1alpha1
          enableGatewayAPI: true
          kind: ControllerConfiguration

Enabling ListenerSet Support

A ListenerSet lets you add listeners to an existing Gateway from your own namespace, for example to serve your own domain with its own certificate, without changing the shared giantswarm-default Gateway. cert-manager can issue certificates for ListenerSets the same way it does for Gateways, but this support is an alpha feature that’s turned off by default.

ListenerSet support needs cert-manager v1.20 or newer, which ships with cluster release v35.0.0 and newer. These releases include cert-manager-app v4, which expects the upstream chart values under the cert-manager key.

To turn it on, extend the cert-manager configuration in your cluster’s user ConfigMap:

global:
  apps:
    certManager:
      values:
        cert-manager:
          config:
            apiVersion: controller.config.cert-manager.io/v1alpha1
            kind: ControllerConfiguration
            enableGatewayAPI: true
            enableGatewayAPIListenerSet: true
            featureGates:
              ListenerSets: true

All three settings are required:

  • enableGatewayAPI turns on the Gateway API integration that ListenerSet support builds on.
  • enableGatewayAPIListenerSet turns on the cert-manager controller that watches ListenerSets.
  • featureGates.ListenerSets enables the alpha feature gate. Without it, cert-manager ignores enableGatewayAPIListenerSet.

cert-manager checks for the ListenerSet CRD when it starts and fails to start if the CRD is missing. The Gateway API Bundle installs the CRD, so apply this configuration only after the bundle is deployed. If you install the CRDs later, restart the cert-manager controller.

ListenerSets created in your own namespaces can only attach to a Gateway that allows them. Open the default Gateway to ListenerSets in the bundle configuration:

apiVersion: v1
kind: ConfigMap
metadata:
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
data:
  values: |
    clusterID: <CLUSTER_NAME>
    organization: <ORGANIZATION>
    apps:
      gatewayApiConfig:
        userConfig:
          configMap:
            values: |
              gateways:
                default:
                  allowedListeners:
                    enabled: true
                    namespaces:
                      from: All

Use from: Selector with a selector to limit which namespaces may attach ListenerSets.

With both changes in place, annotate a ListenerSet with an issuer and reference a Secret in each HTTPS listener. cert-manager creates a Certificate for every listener’s hostname and stores it in that Secret:

apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
  name: my-listeners
  namespace: my-namespace
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-giantswarm
spec:
  parentRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: giantswarm-default
    namespace: envoy-gateway-system
  listeners:
    - name: https
      hostname: myapp.example.com
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: myapp-example-com-tls
      allowedRoutes:
        namespaces:
          from: Same

If the ListenerSet has no issuer annotation, it inherits the one from its parent Gateway. Check that the ListenerSet is attached with kubectl get listenerset -n my-namespace my-listeners, and that the certificate was issued with kubectl get certificate -n my-namespace.

DNS records for ListenerSet hostnames

cert-manager doesn’t create DNS records. external-dns can create them, but neither ListenerSets nor HTTPRoutes are among the sources it watches by default. It only creates records for Services and for the DNSEndpoint resources that the Gateway API Bundle generates.

ListenerSet support needs external-dns-app v3.5.0 or newer, which ships with cluster release v35.0.0 and newer.

To turn it on, add the following section to your cluster’s user ConfigMap:

global:
  apps:
    externalDns:
      values:
        sources:
          - service
          - crd
          - gateway-httproute
        enableGatewayListenerSets: true

Both settings are required:

  • sources adds gateway-httproute, so external-dns creates a record for every HTTPRoute hostname. Keep service and crd in the list, because the list replaces the default instead of extending it.
  • enableGatewayListenerSets lets external-dns follow an HTTPRoute that’s attached to a ListenerSet up to its parent Gateway, and use the Gateway’s load balancer address for the record.

external-dns creates the record from the HTTPRoute, not from the ListenerSet. A hostname only gets a record once an HTTPRoute attaches to the ListenerSet listener. The managed external-dns only creates records for subdomains of your cluster’s base domain. For other domains, run your own external-dns instance.

Using the default gateway

When you install the Gateway API Bundle, it automatically creates a default GatewayClass called giantswarm-default managed by the Envoy Gateway controller. You can verify it exists on your workload cluster:

kubectl get gatewayclass

Expected output:

NAME                 CONTROLLER                                      ACCEPTED
giantswarm-default   gateway.envoyproxy.io/gatewayclass-controller   True

Additionally, it also creates the giantswarm-default Gateway ready to use in the cluster:

NAME                 CLASS                ADDRESS                            PROGRAMMED
giantswarm-default   giantswarm-default   axx8.eu-west-2.elb.amazonaws.com   True

Note: Since DNS and TLS certificates in the Gateway API world are different from Ingress, you must configure which domains the Gateway accepts.

The default Gateway is configured to accept traffic for your cluster’s base domain (*.CLUSTER_ID.k8s.gigantic.io) and is ready to attach any HTTPRoute resources matching that pattern. However, no TLS certificates or DNS records are automatically created based on the HTTPRoute resources.

Option 1: Adding each subdomain

You can define a list of subdomains inside the HTTPS listener. This will automatically create a CNAME (Canonical Name) DNS record pointing to the Gateway load balancer and add it to the dnsNames list in the certificate.

apiVersion: v1
kind: ConfigMap
metadata:
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
data:
  values: |
    clusterID: <CLUSTER_NAME>
    organization: <ORGANIZATION>
    apps:
      gatewayApiConfig:
        userConfig:
          configMap:
            values: |
              gateways:
                default:
                  listeners:
                    https:
                      subdomains:
                      - myapplication

Note: The base domain is appended to each subdomain to compose the FQDN (Fully Qualified Domain Name), myapplication.CLUSTER_ID.k8s.gigantic.io in our example.

Option 2: Overriding the base domain

It’s possible to completely override the base domain and make the Gateway only accept a domain that is not the cluster domain.

apiVersion: v1
kind: ConfigMap
metadata:
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
data:
  values: |
    clusterID: <CLUSTER_NAME>
    organization: <ORGANIZATION>
    apps:
      gatewayApiConfig:
        userConfig:
          configMap:
            values: |
              gateways:
                default:
                  overrideBaseDomain: "example.com"
                  listeners:
                    https:
                      subdomains:
                      - myapplication

This results in a Gateway with an HTTPS listener that accepts traffic for myapplication.example.com with a TLS certificate attached.

Option 3: Adding a new listener

In this example, you observe how to add a new listener for the base domain example.com. In this case, TLS certificates are created by cert-manager, but external-dns integration is disabled.

apiVersion: v1
kind: ConfigMap
metadata:
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
data:
  values: |
    clusterID: <CLUSTER_NAME>
    organization: <ORGANIZATION>
    apps:
      gatewayApiConfig:
        userConfig:
          configMap:
            values: |
              gateways:
                default:
                  listeners:
                    example-https:
                      name: example-https
                      protocol: HTTPS
                      hostname: '*.example.com'
                      port: 443
                      allowedRoutes:
                        namespaces:
                          from: All
                      tls:
                        mode: Terminate
                        certificateRefs: []
                      subdomains:
                      - myapplication
                      - docs
                      - otherapp
                      certificate:
                        enabled: true
                      dnsEndpoints:
                        enabled: false

You can add multiple listeners to the same Gateway to accept traffic from different domains. Each listener can have its own configuration for TLS certificates, DNS endpoints, and subdomain lists, allowing you to manage multiple domains within a single Gateway resource. The giantswarm-default gateway comes with 2 listeners enabled by default: one on port 80 (HTTP) and one on port 443 (HTTPS). Keep in mind that there is a limit of 64 listeners per Gateway, and each listener must have a unique combination of port, protocol, and hostname.

Load balancers on AWS

On CAPA (AWS) clusters, each Gateway gets an AWS Network Load Balancer (NLB) in front of its Envoy proxy pods. You tune the NLB with the service.beta.kubernetes.io/aws-load-balancer-* annotations of the AWS Load Balancer Controller, which you set in apps.gatewayApiConfig.userConfig.configMap.values.gateways.<name>.service.annotations of the gateway-api-bundle. The gateway-api-config chart adds a few defaults so that the proxy pods and the NLB stay in step.

Zone spread of the proxy pods

Starting with gateway-api-config v1.13.0, the chart prefers an even spread of the proxy pods of each Gateway across availability zones. It adds this topologySpreadConstraint to the Envoy proxy deployment:

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: ScheduleAnyway
    nodeTaintsPolicy: Honor
    labelSelector:
      matchLabels:
        gateway.envoyproxy.io/owning-gateway-name: <GATEWAY_NAME>
        gateway.envoyproxy.io/owning-gateway-namespace: <GATEWAY_NAMESPACE>
    matchLabelKeys:
      - pod-template-hash

The existing preferred anti-affinity, one proxy pod per node, stays in place. The default only applies on CAPA clusters with an NLB. With nodeTaintsPolicy: Honor, only zones with nodes that the proxy pods can run on count. Zones that only have control plane nodes don’t count. Clusters with worker nodes in a single zone need no change.

As whenUnsatisfiable is ScheduleAnyway, the spread is a preference. If a zone has no free node, the scheduler puts the pod in a different zone, so a scale-out never stops. If you need a strict spread, set your own list with DoNotSchedule.

Note: The first gateway-api-bundle upgrade that includes gateway-api-config v1.13.0 restarts all proxy pods.

You can change the default in three ways:

  • Set apps.gatewayApiConfig.userConfig.configMap.values.gatewayClasses.<name>.envoyProxy.envoyDeployment.pod.topologySpreadConstraints to define the spread for all Gateways of a class. The chart then adds no default.
  • Set a non-empty list in apps.gatewayApiConfig.userConfig.configMap.values.gateways.<name>.envoyProxy.envoyDeployment.pod.topologySpreadConstraints to replace the default for one Gateway.
  • Set apps.gatewayApiConfig.userConfig.configMap.values.gateways.<name>.provider.aws.zoneSpread to false to remove the default for one Gateway.

Readiness gates in IP target mode

Starting with gateway-api-config v1.13.0, if a Gateway or GatewayClass on a CAPA cluster uses NLB IP target mode (service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip), the chart labels the envoy-gateway-system namespace with elbv2.k8s.aws/pod-readiness-gate-inject: enabled. New proxy pods then only become Ready once the NLB reports their target as healthy. If an update’s new pods aren’t ready, the NLB stops sending traffic to them, and the old pods keep serving traffic.

In instance target mode, the default, the chart doesn’t add the label.

IP target mode only works on clusters that run Cilium in ENI IPAM mode. In IP target mode, the NLB sends traffic straight to the pod IPs. By default, Cilium gives pods IPs from 100.64.0.0/12, which aren’t part of the VPC, so the NLB can’t register them as targets. The readiness gate then never turns true, and the new proxy pods never become Ready. You can only set the IPAM mode when you create the cluster. On clusters with the default IPAM mode, keep instance target mode.

Load balancer subnets

If you set the subnets of the NLB with service.beta.kubernetes.io/aws-load-balancer-subnets, the list must contain one subnet for each availability zone that can run proxy pods. The NLB marks targets in zones without a subnet as unused, and traffic to the Gateway times out.

Starting with gateway-api-config v1.13.0, the chart refuses to render if the list has an empty entry or if the value is empty. A trailing comma such as subnet-a, adds an empty entry. To unblock the upgrade, remove the empty entry or the annotation.

If the NLB must stay in fewer zones, pin the proxy pods to the same zones with a node affinity on topology.kubernetes.io/zone. The node affinity merges with the default anti-affinity. This example keeps the default Gateway in two zones:

apiVersion: v1
kind: ConfigMap
metadata:
  name: <CLUSTER_NAME>-gateway-api-bundle
  namespace: org-<ORGANIZATION>
data:
  values: |
    clusterID: <CLUSTER_NAME>
    organization: <ORGANIZATION>
    apps:
      gatewayApiConfig:
        userConfig:
          configMap:
            values: |
              gateways:
                default:
                  service:
                    annotations:
                      # One subnet per zone: eu-central-1a and eu-central-1b
                      service.beta.kubernetes.io/aws-load-balancer-subnets: subnet-0aaa1111,subnet-0bbb2222
                  envoyProxy:
                    envoyDeployment:
                      pod:
                        affinity:
                          nodeAffinity:
                            requiredDuringSchedulingIgnoredDuringExecution:
                              nodeSelectorTerms:
                                - matchExpressions:
                                    - key: topology.kubernetes.io/zone
                                      operator: In
                                      values:
                                        - eu-central-1a
                                        - eu-central-1b

Keep the zones in the node affinity and the subnets in the annotation in sync. The zone spread from v1.13.0 then only spreads the proxy pods across these two zones.

Troubleshooting

Envoy Gateway provides the egctl command-line tool for diagnostics. To get an overview of the status of all Gateway API resources across all namespaces, run:

egctl x status all -A

This displays the status conditions (such as Accepted, Programmed, or ResolvedRefs) for GatewayClasses, Gateways, HTTPRoutes, and other Gateway API resources, making it easier to spot misconfigurations or issues.

Find more information about the command in the official documentation.

Proxy pods never become ready in IP target mode

If a Gateway uses NLB IP target mode and its new proxy pods never become ready, although their containers are ready, kubectl get pods -n envoy-gateway-system -o wide shows 0/1 in the READINESS GATES column. List the conditions of such a pod:

kubectl get pod -n envoy-gateway-system <POD_NAME> -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'

If the target-health.elbv2.k8s.aws/... condition stays False or is missing, check which IPAM mode Cilium uses:

kubectl get configmap -n kube-system cilium-config -o jsonpath='{.data.ipam}'

If the command doesn’t print eni, the pod IPs aren’t part of the VPC and the NLB can’t register them. Switch the Gateway back to instance target mode, or use a cluster with Cilium ENI IPAM mode.