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.0or later. -
Helm 3.8 or later. Check it with:
helm version --shortExpected:
v3.8.0or 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 addressSteps
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 updatehelm 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.yamlThen 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-controllerThe 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 aicnamespace/aic createdPoint 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=aicWhat 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 updatehelm 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/apisixSTATUS: deployedBoth 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 podspod/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 2m53sWhat each value does:
apisix.deployment.roleand itsrole_traditional.config_providerstart APISIX in API-driven standalone mode (opens in Apache APISIX docs).etcd.enabled=falsedrops the etcd deployment.ingress-controller.enabled=trueinstalls APISIX Ingress Controller alongside APISIX.ingress-controller.config.provider.type=apisix-standalonehas the controller push configuration straight to the gateway instead of through etcd.ingress-controller.apisix.adminService.namespaceis the namespace of the APISIX Admin Service the controller pushes to.ingress-controller.gatewayProxy.createDefault=truecreates aGatewayProxyholding the connection details for APISIX.ingress-controller.webhook.enabled=trueturns on the admission webhook, so invalid Ingress Controller resources are rejected before reconciliation.ingress-controller.config.listenerPortMatchMode=offavoids route mismatches when Kubernetes Service ports such as80and443map to APISIX container ports9080and9443. The other matching modes are in Configuration File.
See the GatewayProxy resource
Find the name of the GatewayProxy:
kubectl get gatewayproxyYou should see the name of the GatewayProxy:
NAME AGE
apisix-config 12sShow the configuration of the GatewayProxy resource in YAML format:
kubectl get gatewayproxy apisix-config -o yamlYou 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: ControlPlaneWhat 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$'3One 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:
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-configThe 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.yamlgatewayclass.gateway.networking.k8s.io/apisix created
gateway.gateway.networking.k8s.io/apisix createdIf you will be using Ingress or APISIX CRDs, the following IngressClass is already applied during installation and you can go straight to the validation below.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: apisix
spec:
controller: apisix.apache.org/apisix-ingress-controller
parameters:
apiGroup: apisix.apache.org
kind: GatewayProxy
name: apisix-config
namespace: aic
scope: NamespaceEach 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}'TrueIn 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.0If 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}'TrueRoll 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 aicHelm 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
| Symptom | Cause | Fix |
|---|---|---|
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. |
Related
- 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.