Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
--charts charts/gateway \
--charts charts/ingress-controller \
--charts charts/aisix-cp \
--charts charts/aisix \
--charts charts/ngxdig'

- name: Verify Chart.lock files
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This repo (`api7/api7-helm-chart`) holds the Helm charts for API7 EE components
| `api7-ingress-controller` | `charts/ingress-controller` | no — independent product version |
| `developer-portal-fe` | `charts/developer-portal-fe` | no — independent product version |
| `aisix-cp` (AISIX private-deployment control plane) | `charts/aisix-cp` | no — independent product version (source of truth: `api7/AISIX-Cloud` `helm/aisix-cp`) |
| `aisix` (AISIX AI gateway data plane) | `charts/aisix` | no — independent product version; **this repo is the source of truth** (unlike `aisix-cp`, no dev-repo counterpart to sync from) |
| `ngxdig` (eBPF flame-graph / diagnosis agent, DaemonSet) | `charts/ngxdig` | no — independent product version (source of truth: `api7/ngx-flame`) |

## Multi-line maintenance model
Expand Down Expand Up @@ -39,6 +40,8 @@ Cost: within a single line the chart can no longer use a major bump for breaking

For **independently-versioned sub-charts** (`ingress-controller`, `developer-portal-fe`): their version axis is not the EE minor, so they are **shared across EE lines and not forked per line** by default. Only fork them onto their **own product minor** when an older EE line must ship a different version of them and keep patching it.

The **AISIX charts (`aisix-cp`, `aisix`) release as a pair**, on the AISIX OP version axis rather than the EE one: at each OP release both get `version` **and** `appVersion` set to that release. `appVersion` pins the image tag on both sides, so the two charts must never carry different `appVersion`s — a control plane and a data plane from different releases is not a combination we ship.

## Release decision

When releasing the chart for EE `X.Y.Z`:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Detailed instructions are enrolled in the README of each chart.
* [api7-control-plane](./charts/api7/README.md)
* [api7-gateway](./charts/gateway/README.md)
* [api7-ingress-controller](./charts/ingress-controller/README.md)
* [aisix-cp](./charts/aisix-cp/README.md) — AISIX AI gateway control plane
* [aisix](./charts/aisix/README.md) — AISIX AI gateway data plane
16 changes: 16 additions & 0 deletions charts/aisix/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.DS_Store
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
*.tmproj
*.orig
*.rej
*.swp
.idea/
.vscode/
README.md.gotmpl
ci/
21 changes: 21 additions & 0 deletions charts/aisix/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: v2
name: aisix
description: Helm chart for the AISIX AI gateway data plane
type: application
version: 0.6.0
appVersion: "0.6.0"

keywords:
- ai-gateway
- llm
- gateway
- proxy

home: https://api7.ai
sources:
- https://github.com/api7/api7-helm-chart

maintainers:
- name: API7
email: support@api7.ai
url: https://api7.ai
289 changes: 289 additions & 0 deletions charts/aisix/README.md

Large diffs are not rendered by default.

185 changes: 185 additions & 0 deletions charts/aisix/README.md.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
{{ template "chart.header" . }}

{{ template "chart.badgesSection" . }}

{{ template "chart.description" . }}

AISIX is an AI gateway: it fronts LLM providers with routing, rate limiting, budgets,
caching, guardrails, and observability behind an OpenAI-compatible API. This chart
installs the **data plane** — the component that serves live AI traffic.

The data plane is configured by the AISIX control plane, not by this chart. It
connects out to the control plane's data-plane manager over mutual TLS, using a
gateway certificate bundle issued from the console, and receives its models, API
keys, and policies from there. Install the control plane first — with the
[`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the
[on-premises installation guide](https://docs.api7.ai/ai-gateway/on-premises/deployment).

{{ template "chart.homepageLine" . }}

{{ template "chart.maintainersSection" . }}

{{ template "chart.sourcesSection" . }}

## Prerequisites

* Kubernetes v1.23+
* Helm v3+
* An AISIX control plane, reachable from the cluster
* A gateway certificate bundle for the environment this gateway should serve

## Install

In the console, open the target environment's **Data planes** view and issue a
gateway certificate. Keep the three PEM values — the private key is shown only once.

Put them in a Secret so the private key never lands in a values file:

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

Then install the chart, pointing it at the data-plane manager endpoint from the
same view:

```sh
helm repo add api7 https://charts.api7.ai
helm repo update

helm install aisix api7/aisix --namespace aisix \
--set controlPlane.baseURL=https://dp-manager.example.com:7944 \
--set controlPlane.certificate.existingSecret=aisix-gateway-certificate
```

The gateway appears in the environment's **Data planes** view once its first
heartbeat lands. Each replica registers as its own instance; they share one
certificate.

## Uninstall

```sh
helm delete aisix --namespace aisix
```

## Configuration examples

The full list of values is in the [Parameters](#parameters) table below. Put these
in a `values.yaml` and pass it with `-f values.yaml`.

### Autoscale on CPU

```yaml
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
```

Requires `metrics-server` in the cluster. A replica the autoscaler adds is kept out
of the Service until its readiness probe passes, which happens only after it has
loaded its configuration from the control plane — so a scaling event never routes
traffic to a gateway that cannot serve it.

Scale-down is guarded from the other side: a terminating pod leaves the Service
first, pauses while that removal propagates, then drains in-flight requests.
Because a streaming response can run for minutes, `terminationGracePeriodSeconds`
defaults to 120 rather than the Kubernetes default of 30. Raise it if your
workloads stream for longer.

### Autoscale on request load with KEDA

CPU is a proxy for load; the gateway's own metrics are the real signal. With
[KEDA](https://keda.sh) installed, scale on a Prometheus query instead:

```yaml
metrics:
serviceMonitor:
enabled: true

keda:
enabled: true
minReplicas: 2
maxReplicas: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
query: sum(rate(aisix_llm_requests_total[2m]))
threshold: "100"
```

`autoscaling` and `keda` are mutually exclusive — enabling both fails the render
rather than letting two controllers fight over the replica count.

### Share rate-limit counters across replicas

Rate-limit counters are per-replica by default, so N replicas enforce N× every
configured limit. Before running more than one replica — including any replica an
autoscaler adds — point the gateway at a shared Redis:

```yaml
rateLimit:
backend: redis
redis:
url: redis://redis.default.svc:6379
```

Use `rateLimit.redis.existingSecret` instead when the URL carries a password.

### Publish the gateway through a load balancer

```yaml
service:
type: LoadBalancer
port: 80
# Preserve the client source IP, which the gateway uses for IP allowlists.
externalTrafficPolicy: Local
```

### Bind a privileged port

The image carries the `CAP_NET_BIND_SERVICE` file capability, so the gateway binds
low ports without running as root:

```yaml
containerPorts:
proxy: 80
```

### Survive node drains

```yaml
podDisruptionBudget:
enabled: true
minAvailable: 50%

topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: aisix
```

### Set any other gateway configuration

Every field of the gateway's configuration file is reachable as an environment
variable named `AISIX_<SECTION>__<FIELD>`:

```yaml
extraEnvVars:
- name: AISIX_OBSERVABILITY__LOG_LEVEL
value: debug
- name: AISIX_CACHE__BACKEND
value: redis
```

## Parameters

{{ template "chart.valuesSection" . }}
8 changes: 8 additions & 0 deletions charts/aisix/ci/default-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Values used by chart-testing. The chart deliberately fails to render
# without a control-plane endpoint and a certificate bundle, so CI supplies
# placeholders for both. The referenced Secret is never created — this file
# drives `ct lint` only, not `ct install`.
controlPlane:
baseURL: "https://dp-manager.example.com:7944"
certificate:
existingSecret: "aisix-gateway-certificate"
57 changes: 57 additions & 0 deletions charts/aisix/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
AISIX gateway {{ .Chart.AppVersion }} has been deployed as {{ include "aisix.fullname" . }}.

It connects out to the control plane at {{ .Values.controlPlane.baseURL }} and
appears in that environment's Data planes view once its first heartbeat lands.

Watch it come up:

kubectl rollout status deploy/{{ include "aisix.fullname" . }} -n {{ .Release.Namespace }}

Send a request through it:
{{- if eq .Values.service.type "ClusterIP" }}

kubectl port-forward svc/{{ include "aisix.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }}
curl http://localhost:8080/v1/models -H "Authorization: Bearer <api-key>"
{{- else if eq .Values.service.type "NodePort" }}

export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}')
export NODE_PORT=$(kubectl get svc {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}')
curl http://$NODE_IP:$NODE_PORT/v1/models -H "Authorization: Bearer <api-key>"
{{- else }}

export LB_IP=$(kubectl get svc {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$LB_IP:{{ .Values.service.port }}/v1/models -H "Authorization: Bearer <api-key>"
{{- end }}

{{ if .Values.autoscaling.enabled -}}
Autoscaling is on (HorizontalPodAutoscaler, {{ .Values.autoscaling.minReplicas }}-{{ .Values.autoscaling.maxReplicas }} replicas). Watch scaling
decisions and the metrics behind them:

kubectl get hpa {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} --watch
kubectl describe hpa {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }}

CPU-target autoscaling needs metrics-server running in the cluster.
{{ else if .Values.keda.enabled -}}
Autoscaling is on (KEDA ScaledObject, {{ .Values.keda.minReplicas }}-{{ .Values.keda.maxReplicas }} replicas). Watch scaling
decisions and the triggers behind them:

kubectl get scaledobject {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }}
kubectl describe hpa keda-hpa-{{ include "aisix.fullname" . }} -n {{ .Release.Namespace }}
{{ else -}}
Running {{ .Values.replicaCount }} fixed replica(s). To scale on load, set
`autoscaling.enabled=true` (CPU / memory) or `keda.enabled=true` with triggers
of your own.
{{ end }}
{{- if eq .Values.rateLimit.backend "memory" }}
{{- if or .Values.autoscaling.enabled .Values.keda.enabled }}

WARNING: rateLimit.backend is "memory", so each replica counts requests on its
own and every replica the autoscaler adds multiplies the effective rate limit.
Point rateLimit.redis.url at a shared Redis and set rateLimit.backend=redis.
{{- else if gt (int .Values.replicaCount) 1 }}

WARNING: rateLimit.backend is "memory", so each replica counts requests on its
own and {{ .Values.replicaCount }} replicas enforce {{ .Values.replicaCount }}x every configured rate limit. Point
rateLimit.redis.url at a shared Redis and set rateLimit.backend=redis.
{{- end }}
{{- end }}
Loading
Loading