Deploy AISIX Gateways on Kubernetes
Use the api7/aisix Helm chart to deploy, expose, and scale AISIX gateways in your Kubernetes cluster. The gateways serve live AI traffic in your environment and connect to an existing AISIX Cloud control plane for configuration.
The chart requires the data-plane manager endpoint and a gateway certificate bundle issued for the target environment. Once connected, the gateways receive models, caller API keys, and policies from the control plane.
Prerequisites
- A Kubernetes cluster with
kubectlconfigured to access it and Helm 3 installed. - Access to an AISIX Cloud environment where you can issue a gateway certificate.
- Network access from the cluster to the AISIX Cloud data-plane manager endpoint. If you use On-Premises, complete On-Premises Installation first.
Install the Chart
In the dashboard, open the target environment's Data planes view and issue a gateway certificate. The Kubernetes (Helm) tab provides the data-plane manager endpoint and certificate bundle used below. The example also pins the initial deployment to one replica. The private key is shown only once.
Store the bundle in a Secret so the private key stays out of your values file:
kubectl create namespace aisix
kubectl -n aisix create secret generic aisix-gateway-certificate \
--from-file=cert.pem=./cert.pem \
--from-file=key.pem=./key.pem \
--from-file=ca.pem=./ca.pem
Install the chart against the data-plane manager endpoint from the same view. The Dev documentation overrides the chart's released gateway image with the dev image:
helm repo add api7 https://charts.api7.ai
helm repo update
helm install aisix api7/aisix --namespace aisix \
--set image.repository=ghcr.io/api7/aisix \
--set image.tag=dev \
--set controlPlane.baseURL=https://dp-manager.example.com:7944 \
--set controlPlane.certificate.existingSecret=aisix-gateway-certificate \
--set replicaCount=1
The gateway appears in the environment's Data planes view once its first heartbeat lands. The chart defaults to two replicas, but this initial command starts one because rate-limit counters use per-replica memory until you configure shared Redis. The dashboard Helm snippet omits replicaCount, so installing that snippet unchanged starts two replicas. Add --set replicaCount=1 until Redis is configured, as shown above. Share Rate-Limit Counters across Replicas before increasing the replica count or enabling an autoscaler.
Each replica registers as its own instance, and they all share the one certificate.
To see every value the chart accepts:
helm show values api7/aisix
The chart source is published in the api7/api7-helm-chart repository.
Expose the Gateway
The chart creates a ClusterIP Service for the proxy by default. To publish it through a cloud load balancer:
service:
type: LoadBalancer
port: 80
# Preserve the client source IP, which per-model IP allowlists match on.
externalTrafficPolicy: Local
By default the chart gives the gateway one plain-HTTP proxy listener: it binds containerPorts.proxy, port 3000, inside the container, and the Service publishes it as service.port. A Service can expose port 80 or 443 without making the process bind a privileged container port.
Serve HTTPS and Plain HTTP Together
Set listeners to serve several proxy ports at once, each with its own TLS:
listeners:
- name: https # Port name, shared by the container port and the Service port.
containerPort: 3443
servicePort: 443
# nodePort: 30443 # Optional; only for non-ClusterIP Service types.
tls:
secretName: aisix-proxy-tls
- name: http
containerPort: 3000
servicePort: 80
A non-empty listeners is the complete set of proxy listeners and replaces the single default one. Nothing binds containerPorts.proxy, and service.port and service.nodePort are not read, because each entry carries its own. There is still one proxy Service, and it publishes one port per entry, targeting that entry's container port by name.
Every listener serves the same routes, /livez and /readyz included, so the startup, readiness, and liveness probes target the first entry — over HTTPS when that entry terminates TLS, and the kubelet does not verify the certificate. The chart passes the set to the gateway as AISIX_PROXY__LISTENERS. It still sets AISIX_PROXY__ADDR, which the gateway then ignores and logs an informational line about; no action is needed for that line. This needs a gateway image that supports proxy.listeners.
For the gateway-side rules behind these values, including why a plaintext listener needs a trusted network interface, see Serve HTTPS and HTTP at the Same Time.
The gateway reads TLS material from files, so each TLS listener needs its own kubernetes.io/tls Secret, with the keys tls.crt and tls.key. The chart mounts it read-only at /etc/aisix/tls/<name>. Create it from a certificate and key you already hold:
kubectl -n aisix create secret tls aisix-proxy-tls \
--cert=./tls.crt --key=./tls.key
Or have cert-manager issue and renew it into the same Secret:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: aisix-proxy-tls
namespace: aisix
spec:
secretName: aisix-proxy-tls
dnsNames:
- gateway.example.com
issuerRef:
name: letsencrypt
kind: ClusterIssuer
A rotated certificate reaches the gateway as a changed file in that mount. Roll the pods to pick it up:
kubectl rollout restart deploy/<release>-aisix -n <namespace>
Bind a Privileged Container Port
Bind a port below 1024 inside the container only when the gateway must listen on that port directly. The published image runs as non-root UID 10001, and the gateway binary carries the effective CAP_NET_BIND_SERVICE file capability:
containerPorts:
proxy: 80
When listeners is set, put the privileged port on the entry that should bind it, as its containerPort; containerPorts.proxy is not read then.
If you customize the rendered Pod to drop all capabilities, add NET_BIND_SERVICE back to the AISIX container:
spec:
containers:
- name: aisix
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
The Kubernetes Restricted Pod Security Standard allows this capability. Because the binary's file capability has the effective bit set, the container can fail with exec: Operation not permitted when the runtime prevents the capability from being granted.
Use hostNetwork or hostPort only when the gateway must bind directly on a node and the cluster policy allows it. These options introduce node-port conflicts and reduce network isolation; the Baseline and Restricted Pod Security Standards also disallow them.
See Network and Security for the full exposure and credential model.
Run on OpenShift
The chart installs on OpenShift under the default restricted-v2 security context constraint. No custom SCC and no service-account changes are needed.
The chart pins no UID. Its default pod security context is exactly runAsNonRoot: true plus seccompProfile.type: RuntimeDefault, so restricted-v2 supplies the runAsUser and fsGroup from the namespace's allocated range. The gateway image declares a numeric user and runs under any assigned UID with GID 0; the only path written at runtime is an emptyDir volume. The container security context already satisfies restricted-v2 — all capabilities dropped except NET_BIND_SERVICE, a read-only root filesystem, and no privilege escalation.
To pin a fixed UID instead, set the values back:
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
For the control plane on OpenShift, see Install on OpenShift.
Share Rate-Limit Counters across Replicas
Rate-limit counters live in each gateway's own memory by default, so N replicas enforce N times every configured request, token, and concurrency limit. Before running more than one replica — including any replica an autoscaler adds — point every replica at one Redis:
rateLimit:
backend: redis
redis:
url: redis://redis.default.svc:6379
Use rateLimit.redis.existingSecret instead when the connection URL carries a password.
After every replica points to the same Redis deployment, increase replicaCount or enable one of the autoscaling options below. If the deployment does not use request, token, or concurrency limits, you can deliberately accept the per-replica memory backend instead.
Scale on CPU or Memory
autoscaling creates a HorizontalPodAutoscaler for the gateway Deployment:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
The targets are percentages of the pod's resource requests, so the chart sets a CPU request by default. It deliberately sets no CPU limit: throttling adds tail latency to a proxy and suppresses the signal the autoscaler reads. Scaling on CPU requires metrics-server in the cluster.
On Linux, proxy.workers defaults to the CPU parallelism available to the gateway process. A Kubernetes CPU request does not constrain that value, but a CPU limit does. Because the chart sets no CPU limit, set AISIX_PROXY__WORKERS explicitly when each replica needs a stable worker count, and keep it within the CPU capacity planned for the replica. See Thread-per-Core Workers for worker configuration and sizing considerations.
When autoscaling is enabled, the Deployment omits spec.replicas so that a later helm upgrade cannot reset the replica count the autoscaler chose. The replicaCount value is ignored from then on.
Pass any behavior policy through unchanged, for example to make scale-down gentler than the Kubernetes default:
autoscaling:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
Use autoscaling.extraMetrics for Pods, Object, or External metrics such as a series exposed through a Prometheus adapter.
Scale on Request Load with KEDA
CPU is a proxy for load. To scale on the gateway's own traffic instead, use KEDA and a Prometheus query over the gateway metrics.
Before enabling the example, prepare these cluster components:
- Install KEDA, including the
ScaledObjectCRD and controller. - Provide a Prometheus server that can query the gateway metrics.
- If the chart should create the scrape configuration shown below, install Prometheus Operator or another controller that consumes
ServiceMonitorobjects. Installing only the CRD lets Helm create the object, but nothing turns it into scrape configuration. Setmetrics.serviceMonitor.labelswhen the Prometheus instance selects ServiceMonitors by label. If Prometheus discovers the metrics Service another way, leavemetrics.serviceMonitor.enabledset tofalseand omit that block.
Confirm the CRDs required by the values you enable:
kubectl get crd scaledobjects.keda.sh
# Required only when metrics.serviceMonitor.enabled is true.
kubectl get crd servicemonitors.monitoring.coreos.com
Configure the chart after those prerequisites are available:
metrics:
serviceMonitor:
enabled: true
keda:
enabled: true
minReplicas: 2
maxReplicas: 20
pollingInterval: 15
cooldownPeriod: 300
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
query: sum(rate(aisix_llm_requests_total[2m]))
threshold: "100"
aisix_llm_requests_total counts model-inference requests, such as /v1/chat/completions. It does not include MCP or A2A calls. Use aisix_proxy_requests_total if that is the load you want to scale on.
autoscaling and keda are mutually exclusive. Enabling both fails the Helm render rather than letting two controllers write spec.replicas.
What Happens during a Scaling Event
A replica the autoscaler adds does not receive traffic before it can serve it. Until the gateway has applied configuration from the control plane it does not bind the proxy listener at all, so every probe the chart points at that listener is refused rather than answered.
The chart's startupProbe is what covers that wait. Kubernetes holds the readiness and liveness probes back until a startup probe succeeds, so during the wait the readiness probe does not run and the pod simply never becomes ready — Kubernetes keeps it out of the Service endpoints, and liveness does not restart a pod that is still reaching the control plane. The startup probe's budget is periodSeconds x failureThreshold, and it has to cover reaching the control plane and applying what it holds. A pod whose control-plane connection stays unreachable for longer than that budget has its container killed and restarted, and repeated restarts show as CrashLoopBackOff — the intended outcome for an instance that has never had anything to serve. See Startup and the First Configuration for the readiness contract and how to size the budget.
When a replica scales down, Kubernetes starts removing it from Service endpoints and terminating the pod concurrently. Two chart values protect in-flight requests while that happens.
preStopSleepSeconds pauses before the container receives SIGTERM, so endpoint removal reaches every node before the gateway begins shutting down. That covers a balancer watching the Kubernetes API; one that polls a health check instead sees a still-ready pod throughout the pause and is covered by the gateway's own drain window, shutdown.min_drain_secs.
terminationGracePeriodSeconds caps the pause, the drain window, and the in-flight drain that follows. When snapshot persistence is enabled, it also covers up to five more seconds for an outstanding cache write. The gateway drains in-flight requests without a deadline of its own. The Kubernetes default of 30 seconds would cut a streaming response, so the chart ships a longer one.
Both values ship with defaults sized for endpoint withdrawal and long-running requests; helm show values api7/aisix reports what they currently are. Raise terminationGracePeriodSeconds for workloads that stream beyond the remaining budget after the pause and drain window. When snapshot persistence is enabled, also leave five seconds for the cache-write drain.
To watch scaling decisions and the metrics behind them:
kubectl -n aisix get hpa aisix --watch
kubectl -n aisix describe hpa aisix
Survive Node Disruption
A PodDisruptionBudget keeps voluntary disruptions — node drains, cluster upgrades — from taking every gateway down at once. Spread constraints keep replicas out of a single failure domain:
podDisruptionBudget:
enabled: true
minAvailable: 50%
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: aisix
The chart uses an emptyDir volume for /var/lib/aisix. It survives a container restart within the same Pod but is lost when the Pod is replaced. The chart supplies the certificate bundle from a Secret at every start. A replacement Pod therefore reconstructs its identity from that bundle and downloads its configuration from the control plane before receiving traffic. Keep multiple replicas across failure domains so an existing replica can continue serving during a replacement.
To recover cached configuration after a Pod replacement while the control plane is unavailable, customize the workload in two ways. Give each replica its own persistent state directory, and set AISIX_MANAGED__SNAPSHOT_CACHE_ENABLED to true. Persistent storage alone does not enable the cache. See Restart from Cached Configuration for the full recovery and security requirements.
See High Availability for the wider deployment pattern.
Set Any Other Gateway Configuration
The control plane owns dynamic resources, and the chart owns the pod. For a startup setting that the chart does not expose as a value, set the environment variable directly — every configuration field is reachable as AISIX_<SECTION>__<FIELD>:
extraEnvVars:
- name: AISIX_OBSERVABILITY__LOG_LEVEL
value: "debug"
- name: AISIX_UPSTREAM__POOL_MAX_IDLE_PER_HOST
value: "32"
See Environment Variables for the naming rules.