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
Ingressresources converted toHTTPRoute,GRPCRoute, andBackendTrafficPolicy - Konstruct UI chart supports Gateway API natively — new
ingress.gatewayblock generates anHTTPRouteattached to the shared Gateway - Dex routed through the shared Gateway via a dedicated
HTTPRoute - cert-manager uses
gatewayHTTPRoutefor 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:
| File | Purpose | Sync wave |
|---|---|---|
application.yaml | Argo CD Application deploying the gateway-helm chart v1.7.0 | 10 |
wait.yaml | Job that blocks until the controller is ready | 20 |
gateway-class.yaml | GatewayClass: eg pointing at the Envoy Gateway controller | 30 |
envoy-proxy.yaml | EnvoyProxy config (replicas, resource limits, topology spread) | 30 |
gateway.yaml | Gateway: eg with one HTTPS listener per hostname plus one HTTP listener | 35 |
http-redirect.yaml | HTTPRoute 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 annotation | Gateway API equivalent |
|---|---|
nginx.ingress.kubernetes.io/force-ssl-redirect | HTTPRoute with RequestRedirect filter on the HTTP listener |
nginx.ingress.kubernetes.io/backend-protocol: GRPC | kind: GRPCRoute |
nginx.ingress.kubernetes.io/proxy-read-timeout | BackendTrafficPolicy.spec.timeout.http.requestTimeout |
nginx.ingress.kubernetes.io/proxy-send-timeout | BackendTrafficPolicy.spec.timeout.http.idleTimeout |
nginx.ingress.kubernetes.io/proxy-buffering: off | Envoy streams by default — no setting needed |
nginx.ingress.kubernetes.io/proxy-cache-bypass | Envoy 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 thetlsblock are dropped. TLS terminates at the Gateway listener (konstruct-https), not at the chart-managed resource- The new
gatewayfield tells the chart whichGateway+sectionNamethe generatedHTTPRouteshould attach to pathType: ImplementationSpecificwith a regex (/api/v1/(.*)) becomespathType: PathPrefixwith the plain prefix (/api) — Gateway API does not support nginx regex paths- The chart now emits an
HTTPRoutewith 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
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-nginxArgo CD Application and its Helm release are removed Ingressresources shipped with the GitOps registry are deleted in favor ofHTTPRoute/GRPCRoute- Nginx-specific annotations (
nginx.ingress.kubernetes.io/*) have no effect - TLS is terminated at the
Gatewaylistener;spec.tlsonHTTPRoutedoes 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:
| Wave | Resource |
|---|---|
| 10 | envoy-gateway Application (Helm chart) |
| 10 | envoy-gateway-components Application (GatewayClass, EnvoyProxy, Gateway, redirect) |
| 20 | Wait job (blocks until the controller is ready) |
| 30 | GatewayClass, EnvoyProxy, cert-manager, external-dns |
| 35 | Gateway, HTTP→HTTPS redirect HTTPRoute |
| 40 | ClusterIssuer 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.