> For the complete documentation index, see [llms.txt](https://docs.sonarsource.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.sonarsource.com/sonarqube-server/server-installation/on-kubernetes-or-openshift/migrating-from-ingress-nginx-to-gateway-api.md).

# 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 [Customizing Helm chart](/sonarqube-server/server-installation/on-kubernetes-or-openshift/customizing-helm-chart.md#ingress-nginx-removed) for the deprecation notice.

This page describes how to migrate to the [Gateway API](https://gateway-api.sigs.k8s.io/), the modern, controller-agnostic successor to Ingress, using [Istio](https://istio.io/) as a concrete, validated example.

> **Note:** A migration script, [`gateway-api-migration-scripts/nginx-to-istio-migration.sh`](https://github.com/SonarSource/helm-chart-sonarqube/blob/master/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](#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](#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 [Customizing Helm chart](/sonarqube-server/server-installation/on-kubernetes-or-openshift/customizing-helm-chart.md#httproute-creating).

## 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 <a href="#choose-a-migration-approach" id="choose-a-migration-approach"></a>

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](https://istio.io/) 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.

{% tabs %}
{% tab title="Migration script" %}
`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.

```bash
./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`](https://github.com/mikefarah/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.
> {% endtab %}

{% tab title="Manual steps" %}

### 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:

```bash
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/latest/download/standard-install.yaml

kubectl wait --for=condition=Established \
  crd/gateways.gateway.networking.k8s.io \
  crd/httproutes.gateway.networking.k8s.io \
  crd/grpcroutes.gateway.networking.k8s.io \
  crd/referencegrants.gateway.networking.k8s.io \
  --timeout=120s
```

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

```bash
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo update
```

### 3. Install the base chart (Istio CRDs)

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

```bash
helm install istio-base istio/base -n istio-system --create-namespace --set defaultRevision=default
```

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

### 4. Install istiod (control plane)

```bash
helm install istiod istio/istiod -n istio-system --wait
```

Verify:

```bash
helm status istiod -n istio-system
kubectl get deployments -n istio-system --output wide
kubectl get gatewayclass istio -o jsonpath='{.spec.controllerName}'
# Expected: istio.io/gateway-controller
```

> **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:

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

> **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](https://kubernetes-sigs.github.io/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`:

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: sonarqube-gateway
  namespace: sonarqube
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "instance"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" # or "internal"
    service.beta.kubernetes.io/aws-load-balancer-subnets: "<subnet-id(s)>"
    service.beta.kubernetes.io/aws-load-balancer-name: "sonarqube-nlb"
    service.beta.kubernetes.io/aws-load-balancer-attributes: "load_balancing.cross_zone.enabled=true"
    # Only needed if TLS terminates at the NLB rather than at Istio/the app:
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert: "<acm-certificate-arn>"
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "tcp"
spec:
  gatewayClassName: istio
  listeners:
    - name: http
      port: 80
      protocol: HTTP
      hostname: sonarqube.your-org.com
      allowedRoutes:
        namespaces:
          from: All
    - name: https
      port: 443
      protocol: HTTP   # The NLB terminates TLS via the ssl-cert annotation above, so this stays HTTP
      hostname: sonarqube.your-org.com
      allowedRoutes:
        namespaces:
          from: All
```

`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:

```yaml
# Same values.yaml you're running today, plus this block. ingress/ingress-nginx stay as-is.
httproute:
  enabled: true
  gateway: sonarqube-gateway
  gatewayNamespace: sonarqube
  hostnames:
    - sonarqube.your-org.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: <release-name>-sonarqube   # or <release-name>-sonarqube-dce
          port: 9000
```

```bash
helm upgrade sonarqube sonarqube/sonarqube --version 2026.4.0 -f values-coexist.yaml -n sonarqube
```

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:

```bash
kubectl port-forward -n istio-system svc/istio-ingressgateway 8080:80
curl -v -H "Host: sonarqube.your-org.com" http://localhost:8080/
```

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:

```yaml
ingress:
  enabled: false

httproute:
  enabled: true
  gateway: sonarqube-gateway
  gatewayNamespace: sonarqube
  hostnames:
    - sonarqube.your-org.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: <release-name>-sonarqube   # or <release-name>-sonarqube-dce
          port: 9000
```

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`:

```bash
helm upgrade sonarqube sonarqube/sonarqube -f values-gateway-api.yaml -n sonarqube
```

### 9. Verify

```bash
kubectl get httproute -n sonarqube
curl -v http://sonarqube.your-org.com/
```

SonarQube Server should return a response. A `404` indicates the route is misconfigured.
{% endtab %}
{% endtabs %}

## Related pages

* [Customizing Helm chart](/sonarqube-server/server-installation/on-kubernetes-or-openshift/customizing-helm-chart.md#ingress-nginx-removed)
* [Customizing Helm chart](/sonarqube-server/server-installation/on-kubernetes-or-openshift/customizing-helm-chart.md)
* Installing Data Center Edition on Kubernetes: [Installing on Kubernetes or Openshift](/sonarqube-server/server-installation/data-center-edition/on-kubernetes-or-openshift.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.sonarsource.com/sonarqube-server/server-installation/on-kubernetes-or-openshift/migrating-from-ingress-nginx-to-gateway-api.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
