Docs

Set Up Ingress Controller and Gateway

Learn how to quickly deploy and configure API7 Ingress Controller or APISIX Ingress Controller for managing Kubernetes ingress traffic.

Applies to:API7 Ingress Controller (drives API7 Gateway)APISIX Ingress Controller (drives Apache APISIX)

Goal

An ingress controller running in your cluster, the gateway it drives reachable, and a GatewayClass and Gateway the controller has accepted — so that Kubernetes resources start becoming gateway configuration.

Creating the first route to an upstream Service is Proxy Requests.

Before you begin

  • A Kubernetes 1.31 or later cluster, and kubectl configured against it. 1.31 is the floor both controllers declare in Compatibility, and it is higher than the floor API7 Enterprise itself asks for — a cluster already running API7 Gateway is not necessarily new enough. Check it with:

    kubectl version -o json | jq -r '.serverVersion.gitVersion'

    Expected: v1.31.0 or later.

  • Helm 3.8 or later. Check it with:

    helm version --short

    Expected: v3.8.0 or later.

  • For API7 Ingress Controller: a running API7 control plane (API7 Dashboard) the cluster can reach, and a gateway group of type Ingress Controller created in it. The Dashboard is what issues the control plane endpoint and admin key the controller needs.

  • For APISIX Ingress Controller: the commands below are written against a local kind cluster, which needs Docker. Any Kubernetes 1.31 or later cluster works; the chart, the values and the resources are the same ones a production installation uses.

Set the variable this guide uses. The Ingress Controller is configured with Kubernetes resources rather than with an Admin API, so the only address it needs is the data plane's — and how that address is reached is your Service:

export GATEWAY_URL="http://127.0.0.1:9080"   # kubectl port-forward, or your Service's address

Steps

Two paths, and you follow exactly one of them: API7 Ingress Controller configures API7 Gateway through the API7 control plane, and APISIX Ingress Controller configures Apache APISIX. They are two separately built products — same CRD group, same binary name, different images and different charts — so a command from one path does not work on the other. Which one am I using? answers it row by row.

API7 Ingress Controller and API7 Gateway

Two steps: the controller, which is a Helm release you run from a terminal, and the GatewayProxy that points it at your control plane, whose endpoint and admin key API7 Dashboard generates.

Step 1: Install API7 Ingress Controller

Add the chart repository, then install the release that matches the controller image.

helm repo add api7 https://charts.api7.ai
helm repo update
helm upgrade --install api7-ingress api7/api7-ingress-controller \
  --namespace aic \
  --create-namespace \
  --version 0.1.28 \
  --set "deployment.image.repository=api7/api7-ingress-controller" \
  --set "deployment.image.tag=2.2.1" \
  --set "config.provider.type=api7ee" \
  --set "config.controllerName=api7.ai/api7-ingress-controller"
STATUS: deployed

--version 0.1.28 pins the chart and deployment.image.tag=2.2.1 runs the controller image that matches it; keep the chart, the image and the CRDs from one controller release. config.provider.type is already api7ee in the chart's own defaults and is set here so the installed value is visible in the command. config.controllerName is the name a GatewayClass must declare for this controller to reconcile it — the chart's default is apisix.apache.org/apisix-ingress-controller, and API7 Dashboard's generated manifests declare api7.ai/api7-ingress-controller, so set the same value in both places. Every setting is in Configuration File.

What changed: the controller is running in the aic namespace and is watching for a GatewayClass that names it; nothing is configured on any gateway yet.

Step 2: Apply the manifests API7 Dashboard generates

In API7 Dashboard, open the gateway group you created and enter the namespace and release name you installed with. The Dashboard generates the GatewayProxy that carries the control plane endpoint and this gateway group's admin key, the GatewayClass and Gateway that reference it (on the Gateway API tab) or the IngressClass in their place (on the Ingress tab), and the manifests that deploy an API7 Gateway instance into the cluster. Apply them — kubectl prints one created line per generated resource:

kubectl apply -f api7-generated-manifests.yaml

Then check that the generated class names the controller you installed:

kubectl get gatewayclass \
  -o custom-columns='NAME:.metadata.name,CONTROLLER:.spec.controllerName'
NAME   CONTROLLER
…      api7.ai/api7-ingress-controller

The name in the first column is whatever the Dashboard generated. The second column has to be the config.controllerName from step 1. If the two differ, the controller ignores the class and no Gateway under it is reconciled — which looks like nothing happening rather than like an error. For an OpenShift cluster use Install API7 Ingress Controller on OpenShift, which adds the two SCC-related flags, and for a declarative rollout see Prepare for GitOps.

What changed: the controller is synchronizing Kubernetes resources into the gateway group, and an API7 Gateway instance is deployed in the cluster.

APISIX Ingress Controller and Apache APISIX

Three steps, installed in standalone mode: a namespace, one Helm release that carries both the gateway and the controller, and the GatewayClass and Gateway you declare yourself.

Deployment Mode

The standalone mode is recommended over the traditional etcd-based deployment, as it avoids the stability issues that can occur when running APISIX and etcd together inside Kubernetes.

Step 1: Create the namespace

Everything on this path goes into one namespace.

kubectl create namespace aic
namespace/aic created

Point your context at it, so that the commands below need no -n flag. This command is optional and only changes your own kubeconfig:

kubectl config set-context --current --namespace=aic

What changed: an empty namespace exists and your context points at it.

Step 2: Install Apache APISIX and APISIX Ingress Controller

The APISIX umbrella chart installs the gateway and the controller as one release. Confirm first that the current umbrella chart selects a compatible controller chart, because overriding only the controller image on an older umbrella chart does not update the CRDs, the RBAC or the webhook configuration:

helm repo add apisix https://apache.github.io/apisix-helm-chart
helm repo update
helm show chart apisix/apisix | grep -A 4 'name: apisix-ingress-controller'

Expected: the dependency's version: reads 1.3.0 or a later compatible version.

helm install apisix \
  --namespace aic \
  --create-namespace \
  --set apisix.deployment.role=traditional \
  --set apisix.deployment.role_traditional.config_provider=yaml \
  --set etcd.enabled=false \
  --set ingress-controller.enabled=true \
  --set ingress-controller.config.provider.type=apisix-standalone \
  --set ingress-controller.apisix.adminService.namespace=aic \
  --set ingress-controller.gatewayProxy.createDefault=true \
  --set ingress-controller.webhook.enabled=true \
  --set ingress-controller.config.listenerPortMatchMode=off \
  apisix/apisix
STATUS: deployed

Both the gateway and the controller have to reach Running before the controller can configure anything:

kubectl wait --for=condition=Ready pod --all --timeout=300s

kubectl get pods
pod/apisix-bffb459b8-rcqz7 condition met
pod/apisix-ingress-controller-ff66c9585-wlxfh condition met
NAME                                        READY   STATUS    RESTARTS   AGE
apisix-bffb459b8-rcqz7                      1/1     Running   0          2m53s
apisix-ingress-controller-ff66c9585-wlxfh   2/2     Running   0          2m53s

What each value does:

  • apisix.deployment.role and its role_traditional.config_provider start APISIX in API-driven standalone mode (opens in Apache APISIX docs).
  • etcd.enabled=false drops the etcd deployment.
  • ingress-controller.enabled=true installs APISIX Ingress Controller alongside APISIX.
  • ingress-controller.config.provider.type=apisix-standalone has the controller push configuration straight to the gateway instead of through etcd.
  • ingress-controller.apisix.adminService.namespace is the namespace of the APISIX Admin Service the controller pushes to.
  • ingress-controller.gatewayProxy.createDefault=true creates a GatewayProxy holding the connection details for APISIX.
  • ingress-controller.webhook.enabled=true turns on the admission webhook, so invalid Ingress Controller resources are rejected before reconciliation.
  • ingress-controller.config.listenerPortMatchMode=off avoids route mismatches when Kubernetes Service ports such as 80 and 443 map to APISIX container ports 9080 and 9443. The other matching modes are in Configuration File.
See the GatewayProxy resource

Find the name of the GatewayProxy:

kubectl get gatewayproxy

You should see the name of the GatewayProxy:

NAME            AGE
apisix-config   12s

Show the configuration of the GatewayProxy resource in YAML format:

kubectl get gatewayproxy apisix-config -o yaml

You should see the configuration of the GatewayProxy resource similar to the following:

apiVersion: apisix.apache.org/v1alpha1
kind: GatewayProxy
metadata:
  annotations:
    meta.helm.sh/release-name: apisix
    meta.helm.sh/release-namespace: aic
  creationTimestamp: "2025-11-25T10:07:40Z"
  generation: 1
  labels:
    app.kubernetes.io/managed-by: Helm
  name: apisix-config
  namespace: aic
  resourceVersion: "3073"
  uid: 6b0a30db-530c-4f14-b127-024d0507b18e
spec:
  provider:
    controlPlane:
      auth:
        adminKey:
          value: edd1c9f034335f136f87ad84b625c8f1
        type: AdminKey
      service:
        name: apisix-admin
        port: 9180
    type: ControlPlane

What changed: one Helm release is running an APISIX gateway and a controller that can configure it, and a GatewayProxy holds the connection between them.

Step 3: Declare the GatewayClass and Gateway

On this path you write the class and the gateway yourself. The APISIX chart installs the Gateway API 1.6.0 CRDs; confirm the API server serves the v1 L4 route APIs these guides use before applying anything:

kubectl get crd \
  tcproutes.gateway.networking.k8s.io \
  udproutes.gateway.networking.k8s.io \
  tlsroutes.gateway.networking.k8s.io \
  -o jsonpath='{.items[*].spec.versions[?(@.served==true)].name}' | tr ' ' '\n' | grep -c '^v1$'
3

One served v1 for each of the three CRDs. If your platform manages Gateway API CRDs separately, install the Gateway API 1.6.0 standard bundle through that owner before installing the controller, and configure the controller release to skip its bundled CRDs.

Then declare the GatewayClass and Gateway:

gatewayclass-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: apisix
spec:
  controllerName: apisix.apache.org/apisix-ingress-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  namespace: aic
  name: apisix
spec:
  gatewayClassName: apisix
  listeners:
  - name: http
    protocol: HTTP
    port: 80
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-config

The port declares the Gateway API listener; it does not open a corresponding data-plane port. Step 2 turned listener-port matching off so a route still matches when Service port 80 maps to APISIX container port 9080.

Apply it:

kubectl apply -f gatewayclass-gateway.yaml
gatewayclass.gateway.networking.k8s.io/apisix created
gateway.gateway.networking.k8s.io/apisix created

Each parameter and how to adjust it is in Define Controller and Gateway.

What changed: the controller has a class it owns and a gateway under it, so a route attached to that gateway will now be reconciled.

Validate

API7 Ingress Controller

The controller reports back onto the Gateway it reconciled, so the cluster — not the Dashboard — is where the answer is:

kubectl -n aic get gateway \
  -o jsonpath='{.items[*].status.conditions[?(@.type=="Programmed")].status}'
True

In API7 Dashboard, the gateway group you selected in step 2 now lists the gateway instance the generated manifests deployed.

APISIX Ingress Controller

Forward the gateway Service to your machine and ask the data plane what it is:

kubectl -n aic port-forward svc/apisix-gateway 9080:80 &
curl -sI "${GATEWAY_URL}" | grep -i '^server'
Server: APISIX/3.18.0

If you would like to use a load balancer to expose the service on the kind cluster as an alternative to port forwarding, see load balancer kind documentation.

The Gateway carries the same Programmed condition as on the API7 path:

kubectl -n aic get gateway \
  -o jsonpath='{.items[*].status.conditions[?(@.type=="Programmed")].status}'
True

Roll back / Clean up

Remove the release, then the resources you applied by hand, then the namespace. Uninstalling either release stops reconciliation and takes the gateway pods with it, so every route attached to that gateway stops being served at the first command.

# API7 Ingress Controller
helm -n aic uninstall api7-ingress
kubectl delete -f api7-generated-manifests.yaml

# APISIX Ingress Controller
helm -n aic uninstall apisix
kubectl delete -f gatewayclass-gateway.yaml

kubectl delete namespace aic

Helm does not remove CRDs installed from a chart's crds/ directory, so the apisix.apache.org and Gateway API CRDs survive the uninstall along with any custom resources still using them. Delete them only when no other release in the cluster depends on them.

Troubleshoot

SymptomCauseFix
Nothing is reconciled and no Gateway reports a Programmed condition, with no error anywhere.The GatewayClass's spec.controllerName is not the controller's own config.controllerName, so the controller does not own the class. The two disagree by default: the chart's default is apisix.apache.org/apisix-ingress-controller and API7 Dashboard's generated manifests declare api7.ai/api7-ingress-controller.Print both columns with the kubectl get gatewayclass -o custom-columns=… command in step 2 of the API7 path, then reinstall with the matching config.controllerName.
A route exists and is accepted, but requests through the gateway return 404.Listener-port matching is on, so the Gateway listener's port is injected as a server_port match — and a Service port of 80 or 443 does not match the APISIX container ports 9080 and 9443.Set config.listenerPortMatchMode to off, as step 2 does. The effective mode and the physical ports are in Configuration File.
helm search repo api7 offers a chart called apisix-ingress-controller as well, and installing it produces a 1.x controller with none of the CRDs these guides use.charts.api7.ai also hosts a 2024 copy of the Apache chart under that name, at chart version 0.14.0 and app version 1.8.0.Install api7/api7-ingress-controller for API7 Ingress Controller, and add https://apache.github.io/apisix-helm-chart for the Apache charts. Helm Charts maps every release to its chart.
  • Proxy Requests — create the first route to an upstream Service on this cluster.
  • Ingress Controller documentation — the "Which one am I using?" decision block, row by row, for the two products this section covers.
  • Configuration File — every controller setting, including the provider types and the listener-port matching modes.
  • Helm Charts — which chart version ships which controller release, for both products.
  • High Availability — replicas and leader election before this goes to production; Prepare for GitOps covers the declarative rollout of the same chart.