For the complete documentation index, see llms.txt. This page is also available as Markdown.

Migrating from Ingress NGINX to Gateway API

How to migrate from the removed bundled Ingress NGINX controller to the Gateway API, with a worked example using Istio. This page is for SonarQube Server 2026.5.

The Helm chart's bundled Ingress NGINX controller subchart (ingress-nginx.enabled/nginx.enabled) has been removed, following the retirement of the community-maintained ingress-nginx project in November 2025. See Removal of the bundled Ingress NGINX controller for the deprecation notice.

This page describes how to migrate to the Gateway API, the modern, controller-agnostic successor to Ingress, using Istio as a concrete, validated example.

Note: A migration script, gateway-api-migration-scripts/nginx-to-istio-migration.sh, is available in the chart's GitHub repository. It detects your existing ingress-nginx configuration (hostnames, TLS, LoadBalancer Service annotations) and generates both a Gateway manifest and a replacement values.yaml. Run it directly with the flags for your environment. Review its generated output before applying it, rather than running it unmodified against your cluster. See Choose a migration approach below for the script and the manual, step-by-step alternative.

Who needs to migrate

  • When you're using the bundled ingress-nginx.enabled/nginx.enabled controller: migrate by turning httproute.enabled on instead. Continue reading below.

    • We recommend moving to Gateway API, the standard Kubernetes has settled on as Ingress's successor, and that's what this page covers. If you'd rather not switch controllers yet, only the bundled subchart is gone. You can still install and manage NGINX yourself outside the chart and point ingress.enabled at it. ingress.enabled with any self-managed controller remains fully supported.

  • When you're installing SonarQube Server fresh, with no prior release: you don't need to migrate anything, but this page still applies to you if you choose Gateway API. Steps 1 through 5 of the Manual steps tab in Choose a migration approach walk through installing the Gateway API custom resource definitions (CRDs), Istio, and a Gateway. A fresh install needs the same prerequisites. Skip steps 6 and 7, which only apply if you're replacing an existing ingress-nginx setup. Then follow steps 8 and 9 to apply the httproute values and verify the route. You can ignore the ingress.enabled: false line in step 8, since a fresh install has no ingress to disable. For the values.yaml example, see Creating an HTTPRoute with Gateway API.

What changes conceptually

Before: Ingress NGINX
After: Gateway API

ingress-nginx.enabled/nginx.enabled (bundled controller subchart, now removed)

httproute.enabled

NGINX Ingress controller (cluster-wide add-on)

A GatewayClass and Gateway, provided by whichever controller you choose (Istio, Envoy Gateway, GKE Gateway, and so on)

nginx.ingress.kubernetes.io/* annotations (for example, configuration-snippet rewrites, proxy-body-size)

Native Gateway API fields (for example, RequestRedirect) or controller-native equivalents

The chart's httproute values block produces a plain gateway.networking.k8s.io/v1 HTTPRoute with no controller-specific fields, so you can use any Gateway API implementation you like. Istio is our chosen example below, but you don't have to use Istio, and can choose any Gateway API controller that fits your environment (Envoy Gateway, GKE Gateway, and so on) instead.

Choose a migration approach

Migrate using the script, which automates most of the steps below, or by running each step yourself. Both approaches produce the same result: a Gateway and an HTTPRoute replacing the old Ingress NGINX controller, with Istio as the Gateway API controller. If you use a different controller (Envoy Gateway, GKE Gateway, and so on), follow the manual steps and swap in your controller's equivalent commands.

nginx-to-istio-migration.sh automates most of the steps in the manual tab. It never runs helm upgrade against your release; it only writes files (and, in --mode apply, applies cluster-scoped resources), then prints the helm upgrade command(s) for you to run yourself once you've reviewed the output.

./nginx-to-istio-migration.sh --cloud aws --release-name sonarqube --namespace sonarqube \
  --aws-subnets subnet-abc,subnet-def

If the ingress-nginx controller is still enabled in your current values, the script also generates a values-coexist-<release-name>.yaml that keeps ingress/ingress-nginx exactly as-is and only adds httproute, so both controllers can route traffic in parallel while you verify Istio. In that case it prints two helm upgrade commands instead of one:

  1. A coexistence upgrade, pinned to --coexist-chart-version (the last chart version that still bundles the ingress-nginx subchart — newer chart versions drop that subchart entirely regardless of values), using the coexistence values.yaml.

  2. Once you've verified Istio is handling traffic correctly (for example, port-forward the Istio ingress gateway Service and curl it with the Host header set to your hostname), a cutover upgrade to the target chart version, which removes the ingress-nginx controller.

If ingress-nginx isn't enabled, only the cutover upgrade is printed.

Key flags:

Flag
Purpose

--cloud aws|gcp|onprem

Required. Selects which cloud-specific annotations (--aws-*, --gcp-*, --metallb-pool) apply

--mode generate|apply

generate (default) only writes the Gateway manifest and replacement values.yaml(s). apply additionally installs the Gateway API CRDs and Istio (unless --skip-istio) and applies the Gateway, so it exists before you run the printed helm upgrade command(s)

--chart-flavor sonarqube|sonarqube-dce

Derives the correct backend Service name for httproute.rules[].backendRefs

--coexist-chart-version

Chart version used only in the printed coexistence-step helm upgrade command, when the ingress-nginx controller is still enabled. Must be the last chart version that still bundles the ingress-nginx subchart, since newer chart versions drop it entirely regardless of values (default: 2026.4.0)

--values-file path

Reads values from a local file instead of helm get values <release>, so you can generate manifests without cluster access

--hostnames h1,h2

Overrides the hostnames auto-detected from your existing ingress.hosts

--skip-istio

With --mode apply, assumes the Gateway API CRDs and Istio are already installed and only applies the Gateway

--aws-subnets, --aws-scheme, --aws-cert-arn

AWS-only: NLB subnets, internet-facing/internal scheme, and ACM certificate ARN (enables the HTTPS listener)

--gcp-internal, --gcp-static-ip

GCP-only: internal load balancer, reserved static IP

--metallb-pool

On-prem only: MetalLB address pool name

--verbose

Shows detailed diagnostics for every step, not just the outcome. Failing steps always show their diagnostics regardless of this flag

Run ./nginx-to-istio-migration.sh --help for the full list, including --namespace, --gateway-name, --gateway-namespace, --output-dir, and --kube-context.

The script only adds to or overrides annotations you already have on your existing ingress-nginx controller Service (under ingress-nginx.controller.service.annotations/nginx.controller.service.annotations). It never invents load balancer configuration on your behalf. It requires yq v4+ on PATH. kubectl and helm are also required, except in --mode generate with --values-file, which needs neither since it never touches the cluster.

Important: Keep the generated Gateway manifest (for example, commit it to version control). It isn't managed by helm upgrade, so it's your only record for re-applying it later — to this or another cluster, for disaster recovery, and so on.

1. Install the Gateway API CRDs

Istio doesn't bundle or manage the Gateway API CRDs. Install them in the cluster before installing Istio, otherwise Istio's istio GatewayClass won't auto-register:

This is the same Gateway API release the migration script installs in --mode apply, so the two paths stay consistent if you later switch between them.

2. Add the Istio Helm repo

3. Install the base chart (Istio CRDs)

Install Istio's own cluster-wide CRDs before the control plane:

Verify with helm ls -n istio-system. It should show istio-base as deployed.

4. Install istiod (control plane)

Verify:

Important: If the istio GatewayClass is missing, you likely installed istiod before the Gateway API CRDs from step 1 were Established. Reinstall istiod once you've confirmed the CRDs are established.

There's no separate helm install ... istio/gateway step. That chart deploys a static, always-on ingress gateway, but with the Gateway API pattern used here, Istio auto-provisions a dedicated Deployment and Service for each Gateway resource you create (step 5), scoped to that Gateway's own namespace.

5. Create a Gateway

Apply a minimal, cloud-agnostic Gateway resource:

Important: Keep the applied Gateway manifest under version control. It isn't managed by helm upgrade, so it's your only record for re-applying it later — to this or another cluster, for disaster recovery, and so on.

Depending on your cloud provider and how you expose the Gateway's Service externally (for example, a LoadBalancer Service), you may need to add provider-specific annotations for TLS termination, health checks, or load balancer configuration. That's specific to your environment, not to SonarQube or Istio.

On EKS, the AWS Load Balancer Controller watches for service.beta.kubernetes.io/aws-load-balancer-* annotations on the Service it provisions for a Gateway. Istio copies annotations from the Gateway object onto that Service, so you set them directly on the Gateway:

aws-load-balancer-scheme controls whether the NLB is internet-facing or restricted to your VPC. Drop the ssl-* annotations and the https listener entirely if you're not terminating TLS at the load balancer. Subnets, ACM certificate ARNs, and naming are specific to your AWS account and VPC; the values above are illustrative. The migration script (see above) derives these from your existing ingress-nginx Service annotations automatically instead of requiring you to write them by hand.

6. (Optional) Coexist with ingress-nginx while you verify Istio

The current chart version has removed the ingress-nginx subchart entirely, so you can't run both controllers side by side on it. To verify Istio is handling traffic correctly before committing to the cutover, temporarily stay on the last chart version that still bundles ingress-nginx (2026.4.0) and add httproute alongside your existing ingress/ingress-nginx configuration, unchanged:

Skip this step and go straight to step 8 if you don't need to verify Istio before cutting over.

7. Verify Istio is handling traffic correctly

For example, port-forward the Istio ingress gateway Service and curl it with the Host header set to your hostname:

Or use whatever else fits your setup — the Gateway's LoadBalancer URL, a temporary DNS record, and so on. Both ingress-nginx and Istio are serving traffic in parallel at this point, so this only confirms Istio can handle it; it doesn't yet affect which controller your users actually hit.

8. Cut over: point the chart at the Gateway

In your values.yaml, disable the old bundled controller and enable httproute. If you're also using ingress.enabled, disable that too. You can't run both an Ingress and an HTTPRoute for the same traffic:

You no longer need to set ingress-nginx.enabled/nginx.enabled to false. The subchart dependency has been removed entirely, so those keys have no effect either way.

Important: You must include backendRefs once you set a custom rules block. If you leave httproute.rules unset, the chart auto-adds a default rule with the correct backendRefs for you. As soon as you specify your own rules (for example, to add a redirect), that default is bypassed. If you omit backendRefs in that case, you get a valid route with nowhere to send traffic. The Gateway accepts it, but every request returns a plain 404 from the proxy itself, with no error pointing at the missing backend.

Apply it on your target chart version — this is what actually removes ingress-nginx:

9. Verify

SonarQube Server should return a response. A 404 indicates the route is misconfigured.

Last updated

Was this helpful?