Skip to main content
Version: 0.5 (Next)

Konstruct 0.3.12

Released: April 2026

Highlights

  • Envoy Gateway replaces nginx-ingress — the default ingress stack is now Envoy Gateway v1.7.0 driven by the Kubernetes Gateway API
  • All Ingress resources converted to HTTPRoute, GRPCRoute, and BackendTrafficPolicy
  • Konstruct UI chart supports Gateway API natively — new ingress.gateway block generates an HTTPRoute attached to the shared Gateway
  • Dex routed through the shared Gateway via a dedicated HTTPRoute
  • cert-manager uses gatewayHTTPRoute for ACME HTTP-01 challenges
  • external-dns now watches Gateway API resources (gateway-httproute, gateway-tlsroute, gateway-tcproute, gateway-udproute)

What Changed

Konstruct GitOps repositories are migrating from nginx-ingress to Envoy Gateway with the Kubernetes Gateway API. All traffic routing is now expressed through gateway.networking.k8s.io/v1 resources rather than the legacy networking.k8s.io/v1 Ingress API.

A single Gateway named eg in the default namespace acts as the central entry point. Each exposed hostname gets its own HTTPS listener on port 443, plus one shared HTTP listener on port 80 used for the HTTP→HTTPS redirect route. Routes live in their own namespaces and attach to the Gateway via parentRefs with sectionName.

Migration Steps

The steps below reflect the exact changes applied to a live Konstruct GitOps repository when moving from nginx-ingress to Envoy Gateway. Apply them in order — the sequence matters because routes and ClusterIssuer resources depend on the Gateway existing first.

1. Add the Envoy Gateway component directory

Create a new components/envoy-gateway/ directory in your cluster registry folder with the following files:

FilePurposeSync wave
application.yamlArgo CD Application deploying the gateway-helm chart v1.7.010
wait.yamlJob that blocks until the controller is ready20
gateway-class.yamlGatewayClass: eg pointing at the Envoy Gateway controller30
envoy-proxy.yamlEnvoyProxy config (replicas, resource limits, topology spread)30
gateway.yamlGateway: eg with one HTTPS listener per hostname plus one HTTP listener35
http-redirect.yamlHTTPRoute with a RequestRedirect filter (HTTP → HTTPS, status 301)35

Annotate the Gateway with cert-manager.io/cluster-issuer: "letsencrypt-prod" so cert-manager provisions a certificate Secret for each listener automatically.

The wait.yaml from the old components/ingress-nginx/ directory can be moved here with the label selector adjusted to app.kubernetes.io/name=gateway-helm.

2. Register Envoy Gateway with Argo CD

Rename the cluster-level Argo CD Application:

registry/clusters/<cluster>/ingress-nginx.yaml  →  envoy-gateway.yaml

Point it at the new components/envoy-gateway directory.

3. Remove the nginx-ingress component

Delete the entire components/ingress-nginx/ directory (application.yaml, wait.yaml). Once the Argo CD Application is removed, the nginx-ingress controller will be pruned from the cluster.

4. Convert Ingress resources to HTTPRoute

For every Ingress resource in the registry, create an equivalent HTTPRoute (or GRPCRoute for gRPC backends) alongside it, then delete the original.

# BEFORE: Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-http-ingress
namespace: argocd
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
name: http
tls:
- hosts: [argocd.example.com]
secretName: argocd-ingress-http
# AFTER: HTTPRoute (TLS now lives on the Gateway listener, not the route)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: argocd-server-http
namespace: argocd
spec:
parentRefs:
- name: eg
namespace: default
sectionName: https-argocd # matches a listener on the Gateway
hostnames:
- argocd.example.com
rules:
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: argocd-server
port: 80

For gRPC backends (previously nginx.ingress.kubernetes.io/backend-protocol: GRPC), use kind: GRPCRoute instead — no annotation required.

5. Add an HTTPS listener per hostname to the Gateway

Every HTTPRoute / GRPCRoute attaches to a named listener on the central Gateway. For each hostname you expose, add a listener:

- name: https-argocd
hostname: argocd.example.com
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: argocd-ingress-http
allowedRoutes:
namespaces:
from: All

allowedRoutes.namespaces.from: All permits routes in other namespaces (argocd, crossplane-system, environment namespaces, etc.) to attach to the shared Gateway.

6. Disable Ingress in component Helm values

Component Helm charts (Argo Workflows, Atlantis, ChartMuseum, Grafana, Vault, Kubefirst, Dex, etc.) often generate their own Ingress resources when ingress.enabled: true. Turn them off so they don't fight with the new HTTPRoute manifests:

# BEFORE
ingress:
enabled: true
ingressClassName: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- argo.example.com
tls:
- secretName: argo-tls
hosts: [argo.example.com]
# AFTER
ingress:
enabled: false

The HTTPRoute (added in step 4) now owns the routing; the chart's Ingress block is dead weight.

7. Replace nginx annotations with Gateway API equivalents

Nginx-specific annotations on Ingress resources do not transfer to HTTPRoute. Use the typed Gateway API equivalents:

Nginx annotationGateway API equivalent
nginx.ingress.kubernetes.io/force-ssl-redirectHTTPRoute with RequestRedirect filter on the HTTP listener
nginx.ingress.kubernetes.io/backend-protocol: GRPCkind: GRPCRoute
nginx.ingress.kubernetes.io/proxy-read-timeoutBackendTrafficPolicy.spec.timeout.http.requestTimeout
nginx.ingress.kubernetes.io/proxy-send-timeoutBackendTrafficPolicy.spec.timeout.http.idleTimeout
nginx.ingress.kubernetes.io/proxy-buffering: offEnvoy streams by default — no setting needed
nginx.ingress.kubernetes.io/proxy-cache-bypassEnvoy has no cache layer by default — no setting needed

For services that previously relied on long timeouts (for example, a log-streaming service exposed through Crossplane), replace the annotations with a BackendTrafficPolicy targeting the HTTPRoute:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: log-streamer-timeout
namespace: crossplane-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: log-streamer
timeout:
http:
requestTimeout: "3600s"
idleTimeout: "3600s"
tcp:
connectTimeout: "30s"

8. Migrate Konstruct UI and Dex

The Konstruct UI Helm chart (konstruct-ui) gained a gateway block under ingress that makes the chart generate an HTTPRoute instead of an Ingress. Bump the chart to 0.3.12 and replace the existing konstruct-ui.ingress block in your konstruct.yaml with the new one — the old className, nginx annotations, hosts.*.paths entries, and tls block are all removed in favor of a gateway reference and standard PathPrefix paths:

# Replace this entire block…
konstruct-ui:
ingress:
className: nginx
enabled: true
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/service-upstream: "true"
nginx.ingress.kubernetes.io/use-regex: "true"
hosts:
- host: konstruct.example.com
paths:
- path: /api/v1/(.*)
pathType: ImplementationSpecific
service: backend
- path: /
pathType: Prefix
service: frontend
tls:
- secretName: konstruct-ui-tls
hosts:
- konstruct.example.com
# …with this one. The ingress key stays; its contents are rewritten.
konstruct-ui:
ingress:
enabled: true
annotations: {}
gateway:
name: "eg"
namespace: "default"
sectionName: "konstruct-https"
hosts:
- host: konstruct.example.com
paths:
- path: /api
pathType: PathPrefix
service: backend
- path: /
pathType: PathPrefix
service: frontend

What changes in the new block:

  • className, nginx annotations, and the tls block are dropped. TLS terminates at the Gateway listener (konstruct-https), not at the chart-managed resource
  • The new gateway field tells the chart which Gateway + sectionName the generated HTTPRoute should attach to
  • pathType: ImplementationSpecific with a regex (/api/v1/(.*)) becomes pathType: PathPrefix with the plain prefix (/api) — Gateway API does not support nginx regex paths
  • The chart now emits an HTTPRoute with separate rules per backend service instead of one Ingress with multiple paths

Dex no longer renders an Ingress from its Helm values. Disable it in the Dex Argo CD Application values and add a separate HTTPRoute manifest alongside dex.yaml:

# dex.yaml — disable the built-in Ingress
ingress:
enabled: false
# dex-httproute.yaml — route Dex via the shared Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: dex
namespace: dex
spec:
parentRefs:
- name: eg
namespace: default
sectionName: dex-https
hostnames:
- dex.example.com
rules:
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: dex
port: 5556

Add matching konstruct-https and dex-https listeners on the central Gateway (with certificateRefs pointing at konstruct-ui-tls and dex-tls respectively).

9. Update ClusterIssuer ACME solver

cert-manager's HTTP-01 solver must target the Gateway, not an Ingress class:

# BEFORE
solvers:
- http01:
ingress:
class: nginx

# AFTER
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: eg
namespace: default
kind: Gateway

Also enable Gateway API support in the cert-manager Helm values:

extraArgs:
- --enable-gateway-api

10. Point external-dns at Gateway API resources

external-dns previously watched Ingress resources. Switch it to Gateway API sources and grant it read access:

# BEFORE
sources:
- ingress

# AFTER
sources:
- service
- gateway-httproute
- gateway-tlsroute
- gateway-tcproute
- gateway-udproute

Add a ClusterRole / ClusterRoleBinding so external-dns can read Gateway API objects:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns-gateway
rules:
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways", "httproutes", "grpcroutes", "tlsroutes"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-gateway
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns-gateway
subjects:
- kind: ServiceAccount
name: external-dns
namespace: external-dns

Bump the external-dns chart to a version that supports Gateway API sources (1.20.0 or later).

11. Update per-environment app routes

For every environment (development, staging, production, etc.), add an HTTPRoute manifest for each application and remove any ingress blocks from the application values.yaml. Each route attaches to a dedicated per-environment listener on the Gateway — for example https-<app>-dev, https-<app>-staging, https-<app>-prod.

Breaking Changes

danger

Upgrading removes the nginx-ingress controller. Any Ingress resources left in the GitOps repository will stop resolving. Convert all custom Ingress resources to HTTPRoute before the nginx-ingress Argo CD Application is deleted.

  • The ingress-nginx Argo CD Application and its Helm release are removed
  • Ingress resources shipped with the GitOps registry are deleted in favor of HTTPRoute / GRPCRoute
  • Nginx-specific annotations (nginx.ingress.kubernetes.io/*) have no effect
  • TLS is terminated at the Gateway listener; spec.tls on HTTPRoute does not exist
  • ACME HTTP-01 challenges are served through the Gateway, not through an Ingress class

Sync Wave Ordering

Envoy Gateway and its dependents reconcile in this order — mirror this in your Argo CD annotations if you add custom resources to the flow:

WaveResource
10envoy-gateway Application (Helm chart)
10envoy-gateway-components Application (GatewayClass, EnvoyProxy, Gateway, redirect)
20Wait job (blocks until the controller is ready)
30GatewayClass, EnvoyProxy, cert-manager, external-dns
35Gateway, HTTP→HTTPS redirect HTTPRoute
40ClusterIssuer resources (depend on the Gateway for ACME)
40+Per-component HTTPRoute / GRPCRoute / BackendTrafficPolicy

Upgrade

Update targetRevision to 0.3.12 in your Konstruct Argo CD Application manifest:

spec:
source:
chart: konstruct
repoURL: oci://europe-west2-docker.pkg.dev/civo-com/charts
targetRevision: 0.3.12

Commit and push — Argo CD syncs the new version automatically. Watch the sync waves to confirm Envoy Gateway becomes healthy before the Gateway and routes attempt to reconcile.

Verification

After the upgrade, confirm the new stack is healthy:

# Envoy Gateway controller is running
kubectl get deploy -n envoy-gateway-system

# Gateway has an address and Programmed=True
kubectl get gateway eg -n default -o wide

# Routes are Accepted by the Gateway
kubectl get httproute -A
kubectl get grpcroute -A

# cert-manager has issued certificates
kubectl get certificate -A

# external-dns is syncing records from Gateway API sources
kubectl logs -n external-dns deploy/external-dns | grep -i httproute

If a route reports Accepted=False, verify that its sectionName matches a listener on the Gateway and that the listener's hostname matches one of the route's hostnames.