From 7fe8cd89b5ee0984b2534ea1a4f9960fd7e62152 Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:09:54 +0800 Subject: [PATCH 01/16] feat: add aisix helm chart scaffold (Chart.yaml, values.yaml, helpers, serviceaccount) --- charts/aisix/.helmignore | 13 ++ charts/aisix/Chart.yaml | 20 +++ charts/aisix/templates/_helpers.tpl | 92 ++++++++++ charts/aisix/templates/serviceaccount.yaml | 13 ++ charts/aisix/values.yaml | 187 +++++++++++++++++++++ 5 files changed, 325 insertions(+) create mode 100644 charts/aisix/.helmignore create mode 100644 charts/aisix/Chart.yaml create mode 100644 charts/aisix/templates/_helpers.tpl create mode 100644 charts/aisix/templates/serviceaccount.yaml create mode 100644 charts/aisix/values.yaml diff --git a/charts/aisix/.helmignore b/charts/aisix/.helmignore new file mode 100644 index 0000000..56e6584 --- /dev/null +++ b/charts/aisix/.helmignore @@ -0,0 +1,13 @@ +# Patterns to ignore when building packages. +.DS_Store +.git/ +.gitignore +.gitmodules +*.swp +*.bak +*.tmp +*.orig +*~ +.vscode/ +*.tmproj +.idea/ diff --git a/charts/aisix/Chart.yaml b/charts/aisix/Chart.yaml new file mode 100644 index 0000000..9734626 --- /dev/null +++ b/charts/aisix/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: aisix +description: A Helm chart for AISIX AI Gateway + +type: application + +version: 0.1.0 + +appVersion: "0.1" + +maintainers: + - name: API7 + email: support@api7.ai + url: https://api7.ai + +dependencies: + - name: etcd + version: 8.7.7 + repository: https://charts.bitnami.com/bitnami + condition: etcd.enabled diff --git a/charts/aisix/templates/_helpers.tpl b/charts/aisix/templates/_helpers.tpl new file mode 100644 index 0000000..c3b24fb --- /dev/null +++ b/charts/aisix/templates/_helpers.tpl @@ -0,0 +1,92 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "aisix.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "aisix.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "aisix.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "aisix.labels" -}} +helm.sh/chart: {{ include "aisix.chart" . }} +{{ include "aisix.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aisix.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aisix.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "aisix.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aisix.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Scheme to use while connecting etcd +*/}} +{{- define "aisix.etcd.scheme" -}} +{{- if .Values.etcd.auth.tls.enabled }} +{{- "https" }} +{{- else }} +{{- "http" }} +{{- end }} +{{- end }} + +{{/* +Etcd host URL(s) to inject into config.yaml. +When etcd subchart is enabled, construct the in-cluster FQDN automatically. +When disabled, use the user-supplied deployment.etcd.host list. +*/}} +{{- define "aisix.etcd.hosts" -}} +{{- if .Values.etcd.enabled }} +{{- $scheme := include "aisix.etcd.scheme" . }} +{{- if .Values.etcd.fullnameOverride }} +- "{{ $scheme }}://{{ .Values.etcd.fullnameOverride }}:{{ .Values.etcd.service.port }}" +{{- else }} +- "{{ $scheme }}://{{ .Release.Name }}-etcd.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.etcd.service.port }}" +{{- end }} +{{- else }} +{{- toYaml .Values.deployment.etcd.host }} +{{- end }} +{{- end }} diff --git a/charts/aisix/templates/serviceaccount.yaml b/charts/aisix/templates/serviceaccount.yaml new file mode 100644 index 0000000..80bb2d7 --- /dev/null +++ b/charts/aisix/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "aisix.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml new file mode 100644 index 0000000..b04d4cc --- /dev/null +++ b/charts/aisix/values.yaml @@ -0,0 +1,187 @@ +global: + # e.g. + # imagePullSecrets: + # - my-registry-secrets + # -- Global Docker registry secret names as an array + imagePullSecrets: [] + +image: + # -- AISIX image repository + repository: ghcr.io/api7/aisix + # -- AISIX image pull policy + pullPolicy: IfNotPresent + # -- AISIX image tag; overrides the chart appVersion + tag: "0.1" + +# -- Number of AISIX replicas +replicaCount: 1 + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # -- Specifies whether a service account should be created + create: false + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use. + name: "" + +# -- Annotations to add to the pod +podAnnotations: {} +# -- Labels to add to the pod +podLabels: {} +# -- Set the securityContext for AISIX pods +podSecurityContext: {} +# -- Set the securityContext for AISIX container +securityContext: {} + +# -- Set pod resource requests & limits +resources: {} + # limits: + # cpu: 500m + # memory: 512Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# -- Node labels for pod assignment +nodeSelector: {} +# -- List of node taints to tolerate +tolerations: [] +# -- Set affinity for deploy +affinity: {} + +updateStrategy: {} + # type: RollingUpdate + +autoscaling: + enabled: false + # -- HPA version, the value is "v2" or "v2beta1", default "v2" + version: v2 + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +# -- Additional volumes +extraVolumes: [] +# -- Additional volume mounts +extraVolumeMounts: [] +# -- Additional init containers +extraInitContainers: [] + +# -- Additional environment variables +extraEnvVars: [] +extraEnvVarsCM: "" +extraEnvVarsSecret: "" + +# -- timezone for the container, e.g. "UTC" or "Asia/Shanghai" +timezone: "" + +# AISIX deployment configuration — rendered into /etc/aisix/config.yaml +deployment: + etcd: + # -- List of etcd hosts. Ignored when etcd.enabled is true (auto-constructed). + host: + - "http://etcd.host:2379" + # -- Key prefix used by aisix in etcd + prefix: /aisix + # -- etcd request timeout in seconds + timeout: 30 + admin: + # -- Admin API key(s). Each entry must have a `key` field. + # IMPORTANT: change this before deploying to production. + adminKey: + - key: "changeme" + # -- Name of an existing Secret that contains an `admin-key` field. + # If set, adminKey above is ignored and the key is read from the Secret. + existingSecret: "" + # -- Key inside the existing Secret that holds the admin key value + existingSecretKey: "admin-key" + +server: + proxy: + # -- Proxy API listen address (host:port inside the container) + listen: "0.0.0.0:3000" + tls: + # -- Enable TLS for the proxy port + enabled: false + # -- Certificate file path (must be present in the container) + certFile: "cert.pem" + # -- Key file path (must be present in the container) + keyFile: "key.pem" + admin: + # -- Admin API listen address. Use 0.0.0.0:3001 to allow in-cluster access. + listen: "0.0.0.0:3001" + +# Proxy Service (port 3000) — user traffic +proxyService: + # -- Service type: ClusterIP, NodePort, or LoadBalancer + type: NodePort + # -- Service port + port: 3000 + # -- Container port + containerPort: 3000 + # -- Optional static nodePort (only relevant when type is NodePort) + nodePort: "" + # -- externalTrafficPolicy (for NodePort / LoadBalancer) + externalTrafficPolicy: Cluster + externalIPs: [] + annotations: {} + +# Admin Service (port 3001) — management UI and Admin API +adminService: + # -- Service type: ClusterIP recommended for security + type: ClusterIP + # -- Service port + port: 3001 + # -- Container port + containerPort: 3001 + annotations: {} + +# -- Kubernetes liveness probe override +livenessProbe: {} +# -- Kubernetes readiness probe override +readinessProbe: {} + +# -- Ingress for the proxy service +ingress: + enabled: false + annotations: {} + hosts: + - host: aisix.local + paths: [] + tls: [] + +# -- Ingress for the admin service +adminIngress: + enabled: false + annotations: {} + hosts: + - host: aisix-admin.local + paths: + - "/ui" + - "/aisix/admin" + tls: [] + +# -- etcd subchart (bitnami/etcd) +etcd: + # -- Install etcd as a subchart. Set false to use an external etcd. + enabled: true + image: + repository: bitnami/etcd + auth: + rbac: + create: false + rootPassword: "" + tls: + enabled: false + existingSecret: "" + certFilename: "" + certKeyFilename: "" + verify: false + sni: "" + service: + port: 2379 + replicaCount: 1 From cfa3a04b7838b44e86308460188503d7b5500f2f Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:11:03 +0800 Subject: [PATCH 02/16] feat(aisix): add ConfigMap template for config.yaml --- charts/aisix/templates/configmap.yaml | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 charts/aisix/templates/configmap.yaml diff --git a/charts/aisix/templates/configmap.yaml b/charts/aisix/templates/configmap.yaml new file mode 100644 index 0000000..22fcd29 --- /dev/null +++ b/charts/aisix/templates/configmap.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "aisix.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} +data: + config.yaml: |- + deployment: + etcd: + host: + {{- include "aisix.etcd.hosts" . | nindent 10 }} + prefix: {{ .Values.deployment.etcd.prefix | quote }} + timeout: {{ .Values.deployment.etcd.timeout }} + admin: + admin_key: + {{- if .Values.deployment.admin.existingSecret }} + - key: "{{"{{"}}AISIX_ADMIN_KEY{{"}}"}}" + {{- else }} + {{- toYaml .Values.deployment.admin.adminKey | nindent 10 }} + {{- end }} + + server: + proxy: + listen: {{ .Values.server.proxy.listen | quote }} + tls: + enabled: {{ .Values.server.proxy.tls.enabled }} + cert_file: {{ .Values.server.proxy.tls.certFile | quote }} + key_file: {{ .Values.server.proxy.tls.keyFile | quote }} + admin: + listen: {{ .Values.server.admin.listen | quote }} From d0709677f70428305c81a785cf089be8eb15d8be Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:11:46 +0800 Subject: [PATCH 03/16] feat(aisix): add Deployment template --- charts/aisix/templates/deployment.yaml | 119 +++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 charts/aisix/templates/deployment.yaml diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml new file mode 100644 index 0000000..e1a1d8d --- /dev/null +++ b/charts/aisix/templates/deployment.yaml @@ -0,0 +1,119 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aisix.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "aisix.selectorLabels" . | nindent 6 }} + {{- if .Values.updateStrategy }} + strategy: {{ toYaml .Values.updateStrategy | nindent 4 }} + {{- end }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "aisix.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.global.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "aisix.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.extraInitContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: aisix + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: proxy + containerPort: {{ .Values.proxyService.containerPort }} + protocol: TCP + - name: admin + containerPort: {{ .Values.adminService.containerPort }} + protocol: TCP + {{- if .Values.livenessProbe }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + {{- end }} + {{- if .Values.readinessProbe }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + {{- if .Values.timezone }} + - name: TZ + value: {{ .Values.timezone | quote }} + {{- end }} + - name: RUST_LOG + value: "info" + {{- if .Values.deployment.admin.existingSecret }} + - name: AISIX_ADMIN_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.deployment.admin.existingSecret }} + key: {{ .Values.deployment.admin.existingSecretKey }} + {{- end }} + {{- with .Values.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.extraEnvVarsCM }} + envFrom: + - configMapRef: + name: {{ .Values.extraEnvVarsCM }} + {{- end }} + volumeMounts: + - name: aisix-config + mountPath: /etc/aisix/config.yaml + subPath: config.yaml + readOnly: true + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: aisix-config + configMap: + name: {{ include "aisix.fullname" . }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} From e440565132301dfe0f689932b5d704d835918d1a Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:12:04 +0800 Subject: [PATCH 04/16] feat(aisix): add Service, Ingress, and HPA templates --- charts/aisix/templates/hpa.yaml | 37 +++++++++++++++++++++++ charts/aisix/templates/ingress-admin.yaml | 33 ++++++++++++++++++++ charts/aisix/templates/ingress.yaml | 33 ++++++++++++++++++++ charts/aisix/templates/service-admin.yaml | 21 +++++++++++++ charts/aisix/templates/service-proxy.yaml | 33 ++++++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 charts/aisix/templates/hpa.yaml create mode 100644 charts/aisix/templates/ingress-admin.yaml create mode 100644 charts/aisix/templates/ingress.yaml create mode 100644 charts/aisix/templates/service-admin.yaml create mode 100644 charts/aisix/templates/service-proxy.yaml diff --git a/charts/aisix/templates/hpa.yaml b/charts/aisix/templates/hpa.yaml new file mode 100644 index 0000000..9e1a94a --- /dev/null +++ b/charts/aisix/templates/hpa.yaml @@ -0,0 +1,37 @@ +{{- if .Values.autoscaling.enabled }} +{{- if eq .Values.autoscaling.version "v2" }} +apiVersion: autoscaling/v2 +{{- else }} +apiVersion: autoscaling/v2beta1 +{{- end }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aisix.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aisix.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/aisix/templates/ingress-admin.yaml b/charts/aisix/templates/ingress-admin.yaml new file mode 100644 index 0000000..8c3ff9f --- /dev/null +++ b/charts/aisix/templates/ingress-admin.yaml @@ -0,0 +1,33 @@ +{{- if .Values.adminIngress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "aisix.fullname" . }}-admin + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- with .Values.adminIngress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.adminIngress.tls }} + tls: + {{- toYaml .Values.adminIngress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.adminIngress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ include "aisix.fullname" $ }}-admin + port: + number: {{ $.Values.adminService.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/aisix/templates/ingress.yaml b/charts/aisix/templates/ingress.yaml new file mode 100644 index 0000000..f967353 --- /dev/null +++ b/charts/aisix/templates/ingress.yaml @@ -0,0 +1,33 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "aisix.fullname" . }}-proxy + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + pathType: Prefix + backend: + service: + name: {{ include "aisix.fullname" $ }}-proxy + port: + number: {{ $.Values.proxyService.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/aisix/templates/service-admin.yaml b/charts/aisix/templates/service-admin.yaml new file mode 100644 index 0000000..12049ca --- /dev/null +++ b/charts/aisix/templates/service-admin.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix.fullname" . }}-admin + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + app.kubernetes.io/service: aisix-admin + {{- with .Values.adminService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.adminService.type }} + ports: + - name: admin + port: {{ .Values.adminService.port }} + targetPort: admin + protocol: TCP + selector: + {{- include "aisix.selectorLabels" . | nindent 4 }} diff --git a/charts/aisix/templates/service-proxy.yaml b/charts/aisix/templates/service-proxy.yaml new file mode 100644 index 0000000..e72e26e --- /dev/null +++ b/charts/aisix/templates/service-proxy.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix.fullname" . }}-proxy + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + app.kubernetes.io/service: aisix-proxy + {{- with .Values.proxyService.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.proxyService.type }} + {{- if or (eq .Values.proxyService.type "NodePort") (eq .Values.proxyService.type "LoadBalancer") }} + externalTrafficPolicy: {{ .Values.proxyService.externalTrafficPolicy }} + {{- end }} + {{- if gt (len .Values.proxyService.externalIPs) 0 }} + externalIPs: + {{- range .Values.proxyService.externalIPs }} + - {{ . }} + {{- end }} + {{- end }} + ports: + - name: proxy + port: {{ .Values.proxyService.port }} + targetPort: proxy + protocol: TCP + {{- if and (eq .Values.proxyService.type "NodePort") (not (empty .Values.proxyService.nodePort)) }} + nodePort: {{ .Values.proxyService.nodePort }} + {{- end }} + selector: + {{- include "aisix.selectorLabels" . | nindent 4 }} From 0ea1e1bf6d4026f8b9cfe28d0c096a23486f7fac Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:14:20 +0800 Subject: [PATCH 05/16] feat(aisix): add NOTES.txt, chart README, and update top-level README --- README.md | 1 + charts/aisix/README.md | 65 ++++++++++++++++++++++++++++++++ charts/aisix/templates/NOTES.txt | 33 ++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 charts/aisix/README.md create mode 100644 charts/aisix/templates/NOTES.txt diff --git a/README.md b/README.md index 3fa8834..8bd2880 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ 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](./charts/aisix/README.md) diff --git a/charts/aisix/README.md b/charts/aisix/README.md new file mode 100644 index 0000000..8e31b96 --- /dev/null +++ b/charts/aisix/README.md @@ -0,0 +1,65 @@ +# AISIX Helm Chart + +A Helm chart for [AISIX](https://github.com/api7/aisix) — an open source, high-performance AI Gateway and LLM proxy built in Rust. + +## Prerequisites + +- Kubernetes 1.21+ +- Helm 3.7+ + +## Installing the Chart + +```bash +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install my-aisix api7/aisix \ + --set deployment.admin.adminKey[0].key= +``` + +## Uninstalling the Chart + +```bash +helm uninstall my-aisix +``` + +## Configuration + +The following table lists the key configurable parameters. See `values.yaml` for the full list. + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.repository` | AISIX image repository | `ghcr.io/api7/aisix` | +| `image.tag` | AISIX image tag | `0.1` | +| `replicaCount` | Number of replicas | `1` | +| `deployment.admin.adminKey` | Admin API key list | `[{key: "changeme"}]` | +| `deployment.admin.existingSecret` | Existing Secret for admin key | `""` | +| `deployment.etcd.host` | External etcd hosts (when `etcd.enabled=false`) | `["http://etcd.host:2379"]` | +| `deployment.etcd.prefix` | etcd key prefix | `/aisix` | +| `server.proxy.listen` | Proxy API listen address | `0.0.0.0:3000` | +| `server.admin.listen` | Admin API listen address | `0.0.0.0:3001` | +| `proxyService.type` | Proxy Service type | `NodePort` | +| `adminService.type` | Admin Service type | `ClusterIP` | +| `etcd.enabled` | Install bundled etcd | `true` | +| `ingress.enabled` | Enable Ingress for proxy | `false` | +| `adminIngress.enabled` | Enable Ingress for admin | `false` | +| `autoscaling.enabled` | Enable HPA | `false` | + +## Using an Existing Secret for the Admin Key + +```bash +kubectl create secret generic aisix-admin-secret \ + --from-literal=admin-key= + +helm install my-aisix api7/aisix \ + --set deployment.admin.existingSecret=aisix-admin-secret +``` + +## Using an External etcd + +```bash +helm install my-aisix api7/aisix \ + --set etcd.enabled=false \ + --set deployment.etcd.host[0]="http://my-etcd:2379" \ + --set deployment.admin.adminKey[0].key= +``` diff --git a/charts/aisix/templates/NOTES.txt b/charts/aisix/templates/NOTES.txt new file mode 100644 index 0000000..1270ee3 --- /dev/null +++ b/charts/aisix/templates/NOTES.txt @@ -0,0 +1,33 @@ +AISIX has been installed. Check its status by running: + kubectl --namespace {{ .Release.Namespace }} get pods -l "app.kubernetes.io/name={{ include "aisix.name" . }}" + +1. Get the Proxy API URL: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else if eq .Values.proxyService.type "NodePort" }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "aisix.fullname" . }}-proxy) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo "Proxy API: http://$NODE_IP:$NODE_PORT" +{{- else if eq .Values.proxyService.type "LoadBalancer" }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "aisix.fullname" . }}-proxy --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo "Proxy API: http://$SERVICE_IP:{{ .Values.proxyService.port }}" +{{- else }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aisix.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:{{ .Values.proxyService.containerPort }} + echo "Proxy API: http://127.0.0.1:3000" +{{- end }} + +2. Get the Admin UI URL: +{{- if .Values.adminIngress.enabled }} +{{- range .Values.adminIngress.hosts }} + http{{ if $.Values.adminIngress.tls }}s{{ end }}://{{ .host }}/ui +{{- end }} +{{- else }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aisix.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3001:{{ .Values.adminService.containerPort }} + echo "Admin UI: http://127.0.0.1:3001/ui" + echo "Admin API: http://127.0.0.1:3001/aisix/admin" +{{- end }} From 73965e7ba3254ded9526e653eca88a87d44a64fa Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:15:30 +0800 Subject: [PATCH 06/16] feat(aisix): add Chart.lock and vendored etcd subchart --- charts/aisix/Chart.lock | 6 ++++++ charts/aisix/charts/etcd-8.7.7.tgz | Bin 0 -> 43592 bytes 2 files changed, 6 insertions(+) create mode 100644 charts/aisix/Chart.lock create mode 100644 charts/aisix/charts/etcd-8.7.7.tgz diff --git a/charts/aisix/Chart.lock b/charts/aisix/Chart.lock new file mode 100644 index 0000000..e78307d --- /dev/null +++ b/charts/aisix/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: etcd + repository: https://charts.bitnami.com/bitnami + version: 8.7.7 +digest: sha256:c03ff5323e855335d94ce6cfce35745b84d03538dbb197e1a27520a7d69ea3ef +generated: "2026-04-15T17:14:56.064889296+08:00" diff --git a/charts/aisix/charts/etcd-8.7.7.tgz b/charts/aisix/charts/etcd-8.7.7.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5f68f5d92eb199ae1898d936003faa35edf6dca8 GIT binary patch literal 43592 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 literal 0 HcmV?d00001 From 932d1cd5d9cdd827ee6f554eaaf2eaab4b333362 Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:23:59 +0800 Subject: [PATCH 07/16] fix(aisix): correct image tag and appVersion to 0.1.0 --- charts/aisix/Chart.yaml | 2 +- charts/aisix/README.md | 2 +- charts/aisix/values.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/aisix/Chart.yaml b/charts/aisix/Chart.yaml index 9734626..b454f74 100644 --- a/charts/aisix/Chart.yaml +++ b/charts/aisix/Chart.yaml @@ -6,7 +6,7 @@ type: application version: 0.1.0 -appVersion: "0.1" +appVersion: "0.1.0" maintainers: - name: API7 diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 8e31b96..7dc88f1 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -30,7 +30,7 @@ The following table lists the key configurable parameters. See `values.yaml` for | Parameter | Description | Default | |-----------|-------------|---------| | `image.repository` | AISIX image repository | `ghcr.io/api7/aisix` | -| `image.tag` | AISIX image tag | `0.1` | +| `image.tag` | AISIX image tag | `0.1.0` | | `replicaCount` | Number of replicas | `1` | | `deployment.admin.adminKey` | Admin API key list | `[{key: "changeme"}]` | | `deployment.admin.existingSecret` | Existing Secret for admin key | `""` | diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index b04d4cc..cea21a7 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -11,7 +11,7 @@ image: # -- AISIX image pull policy pullPolicy: IfNotPresent # -- AISIX image tag; overrides the chart appVersion - tag: "0.1" + tag: "0.1.0" # -- Number of AISIX replicas replicaCount: 1 From a8feeaee18790e62a0746dd9dfba9c3b8e4b1220 Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:28:15 +0800 Subject: [PATCH 08/16] fix(aisix): use api7/etcd image for etcd subchart --- charts/aisix/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index cea21a7..d535f9f 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -170,7 +170,7 @@ etcd: # -- Install etcd as a subchart. Set false to use an external etcd. enabled: true image: - repository: bitnami/etcd + repository: api7/etcd auth: rbac: create: false From eca980e26093d85b28778f3d43842f3afb292b6c Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:34:37 +0800 Subject: [PATCH 09/16] fix(aisix): align etcd defaults with gateway chart (disabled by default, replicaCount 3, full comments) --- charts/aisix/values.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index d535f9f..3be490a 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -168,20 +168,28 @@ adminIngress: # -- etcd subchart (bitnami/etcd) etcd: # -- Install etcd as a subchart. Set false to use an external etcd. - enabled: true + enabled: false image: repository: api7/etcd auth: rbac: + # -- No authentication by default. Switch to enable RBAC authentication create: false + # -- root password for etcd. Requires etcd.auth.rbac.create to be true. rootPassword: "" tls: + # -- enable etcd client certificate enabled: false + # -- name of the secret contains etcd client cert existingSecret: "" + # -- etcd client cert filename using in etcd.auth.tls.existingSecret certFilename: "" + # -- etcd client cert key filename using in etcd.auth.tls.existingSecret certKeyFilename: "" + # -- whether to verify the etcd endpoint certificate when setup a TLS connection to etcd verify: false + # -- specify the TLS Server Name Indication extension, the ETCD endpoint hostname will be used when this setting is unset. sni: "" service: port: 2379 - replicaCount: 1 + replicaCount: 3 From 6f0de841d36730505b89e50168c9f08c7feb7948 Mon Sep 17 00:00:00 2001 From: rongxin Date: Wed, 15 Apr 2026 17:42:45 +0800 Subject: [PATCH 10/16] refactor(aisix): restructure proxy/admin into gateway and admin blocks with enabled switch --- charts/aisix/templates/NOTES.txt | 30 ++++---- charts/aisix/templates/configmap.yaml | 8 +- charts/aisix/templates/deployment.yaml | 6 +- charts/aisix/templates/ingress-admin.yaml | 12 +-- charts/aisix/templates/ingress.yaml | 16 ++-- charts/aisix/templates/service-admin.yaml | 8 +- charts/aisix/templates/service-proxy.yaml | 22 +++--- charts/aisix/values.yaml | 90 +++++++++++------------ 8 files changed, 97 insertions(+), 95 deletions(-) diff --git a/charts/aisix/templates/NOTES.txt b/charts/aisix/templates/NOTES.txt index 1270ee3..ed2fab8 100644 --- a/charts/aisix/templates/NOTES.txt +++ b/charts/aisix/templates/NOTES.txt @@ -2,32 +2,36 @@ AISIX has been installed. Check its status by running: kubectl --namespace {{ .Release.Namespace }} get pods -l "app.kubernetes.io/name={{ include "aisix.name" . }}" 1. Get the Proxy API URL: -{{- if .Values.ingress.enabled }} -{{- range .Values.ingress.hosts }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- if .Values.gateway.ingress.enabled }} +{{- range .Values.gateway.ingress.hosts }} + http{{ if $.Values.gateway.ingress.tls }}s{{ end }}://{{ .host }} {{- end }} -{{- else if eq .Values.proxyService.type "NodePort" }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "aisix.fullname" . }}-proxy) +{{- else if eq .Values.gateway.type "NodePort" }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "aisix.fullname" . }}-gateway) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo "Proxy API: http://$NODE_IP:$NODE_PORT" -{{- else if eq .Values.proxyService.type "LoadBalancer" }} +{{- else if eq .Values.gateway.type "LoadBalancer" }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "aisix.fullname" . }}-proxy --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo "Proxy API: http://$SERVICE_IP:{{ .Values.proxyService.port }}" + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "aisix.fullname" . }}-gateway --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo "Proxy API: http://$SERVICE_IP:{{ .Values.gateway.servicePort }}" {{- else }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aisix.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:{{ .Values.proxyService.containerPort }} + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:{{ .Values.gateway.containerPort }} echo "Proxy API: http://127.0.0.1:3000" {{- end }} 2. Get the Admin UI URL: -{{- if .Values.adminIngress.enabled }} -{{- range .Values.adminIngress.hosts }} - http{{ if $.Values.adminIngress.tls }}s{{ end }}://{{ .host }}/ui +{{- if .Values.admin.enabled }} +{{- if .Values.admin.ingress.enabled }} +{{- range .Values.admin.ingress.hosts }} + http{{ if $.Values.admin.ingress.tls }}s{{ end }}://{{ .host }}/ui {{- end }} {{- else }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "aisix.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3001:{{ .Values.adminService.containerPort }} + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3001:{{ .Values.admin.containerPort }} echo "Admin UI: http://127.0.0.1:3001/ui" echo "Admin API: http://127.0.0.1:3001/aisix/admin" {{- end }} +{{- else }} + Admin service is disabled. Enable it with --set admin.enabled=true +{{- end }} diff --git a/charts/aisix/templates/configmap.yaml b/charts/aisix/templates/configmap.yaml index 22fcd29..52d0540 100644 --- a/charts/aisix/templates/configmap.yaml +++ b/charts/aisix/templates/configmap.yaml @@ -23,10 +23,8 @@ data: server: proxy: - listen: {{ .Values.server.proxy.listen | quote }} + listen: "{{ .Values.gateway.ip }}:{{ .Values.gateway.containerPort }}" tls: - enabled: {{ .Values.server.proxy.tls.enabled }} - cert_file: {{ .Values.server.proxy.tls.certFile | quote }} - key_file: {{ .Values.server.proxy.tls.keyFile | quote }} + enabled: false admin: - listen: {{ .Values.server.admin.listen | quote }} + listen: "{{ .Values.admin.ip }}:{{ .Values.admin.containerPort }}" diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml index e1a1d8d..38ffbe3 100644 --- a/charts/aisix/templates/deployment.yaml +++ b/charts/aisix/templates/deployment.yaml @@ -51,11 +51,13 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: proxy - containerPort: {{ .Values.proxyService.containerPort }} + containerPort: {{ .Values.gateway.containerPort }} protocol: TCP + {{- if .Values.admin.enabled }} - name: admin - containerPort: {{ .Values.adminService.containerPort }} + containerPort: {{ .Values.admin.containerPort }} protocol: TCP + {{- end }} {{- if .Values.livenessProbe }} livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} diff --git a/charts/aisix/templates/ingress-admin.yaml b/charts/aisix/templates/ingress-admin.yaml index 8c3ff9f..af07ec1 100644 --- a/charts/aisix/templates/ingress-admin.yaml +++ b/charts/aisix/templates/ingress-admin.yaml @@ -1,4 +1,4 @@ -{{- if .Values.adminIngress.enabled -}} +{{- if and .Values.admin.enabled .Values.admin.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -6,17 +6,17 @@ metadata: namespace: {{ .Release.Namespace }} labels: {{- include "aisix.labels" . | nindent 4 }} - {{- with .Values.adminIngress.annotations }} + {{- with .Values.admin.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - {{- if .Values.adminIngress.tls }} + {{- if .Values.admin.ingress.tls }} tls: - {{- toYaml .Values.adminIngress.tls | nindent 4 }} + {{- toYaml .Values.admin.ingress.tls | nindent 4 }} {{- end }} rules: - {{- range .Values.adminIngress.hosts }} + {{- range .Values.admin.ingress.hosts }} - host: {{ .host | quote }} http: paths: @@ -27,7 +27,7 @@ spec: service: name: {{ include "aisix.fullname" $ }}-admin port: - number: {{ $.Values.adminService.port }} + number: {{ $.Values.admin.servicePort }} {{- end }} {{- end }} {{- end }} diff --git a/charts/aisix/templates/ingress.yaml b/charts/aisix/templates/ingress.yaml index f967353..6394f3a 100644 --- a/charts/aisix/templates/ingress.yaml +++ b/charts/aisix/templates/ingress.yaml @@ -1,22 +1,22 @@ -{{- if .Values.ingress.enabled -}} +{{- if .Values.gateway.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "aisix.fullname" . }}-proxy + name: {{ include "aisix.fullname" . }}-gateway namespace: {{ .Release.Namespace }} labels: {{- include "aisix.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} + {{- with .Values.gateway.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - {{- if .Values.ingress.tls }} + {{- if .Values.gateway.ingress.tls }} tls: - {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- toYaml .Values.gateway.ingress.tls | nindent 4 }} {{- end }} rules: - {{- range .Values.ingress.hosts }} + {{- range .Values.gateway.ingress.hosts }} - host: {{ .host | quote }} http: paths: @@ -25,9 +25,9 @@ spec: pathType: Prefix backend: service: - name: {{ include "aisix.fullname" $ }}-proxy + name: {{ include "aisix.fullname" $ }}-gateway port: - number: {{ $.Values.proxyService.port }} + number: {{ $.Values.gateway.servicePort }} {{- end }} {{- end }} {{- end }} diff --git a/charts/aisix/templates/service-admin.yaml b/charts/aisix/templates/service-admin.yaml index 12049ca..9dcabcd 100644 --- a/charts/aisix/templates/service-admin.yaml +++ b/charts/aisix/templates/service-admin.yaml @@ -1,3 +1,4 @@ +{{- if .Values.admin.enabled }} apiVersion: v1 kind: Service metadata: @@ -6,16 +7,17 @@ metadata: labels: {{- include "aisix.labels" . | nindent 4 }} app.kubernetes.io/service: aisix-admin - {{- with .Values.adminService.annotations }} + {{- with .Values.admin.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - type: {{ .Values.adminService.type }} + type: {{ .Values.admin.type }} ports: - name: admin - port: {{ .Values.adminService.port }} + port: {{ .Values.admin.servicePort }} targetPort: admin protocol: TCP selector: {{- include "aisix.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/aisix/templates/service-proxy.yaml b/charts/aisix/templates/service-proxy.yaml index e72e26e..b2842d8 100644 --- a/charts/aisix/templates/service-proxy.yaml +++ b/charts/aisix/templates/service-proxy.yaml @@ -1,33 +1,33 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "aisix.fullname" . }}-proxy + name: {{ include "aisix.fullname" . }}-gateway namespace: {{ .Release.Namespace }} labels: {{- include "aisix.labels" . | nindent 4 }} - app.kubernetes.io/service: aisix-proxy - {{- with .Values.proxyService.annotations }} + app.kubernetes.io/service: aisix-gateway + {{- with .Values.gateway.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - type: {{ .Values.proxyService.type }} - {{- if or (eq .Values.proxyService.type "NodePort") (eq .Values.proxyService.type "LoadBalancer") }} - externalTrafficPolicy: {{ .Values.proxyService.externalTrafficPolicy }} + type: {{ .Values.gateway.type }} + {{- if or (eq .Values.gateway.type "NodePort") (eq .Values.gateway.type "LoadBalancer") }} + externalTrafficPolicy: {{ .Values.gateway.externalTrafficPolicy }} {{- end }} - {{- if gt (len .Values.proxyService.externalIPs) 0 }} + {{- if gt (len .Values.gateway.externalIPs) 0 }} externalIPs: - {{- range .Values.proxyService.externalIPs }} + {{- range .Values.gateway.externalIPs }} - {{ . }} {{- end }} {{- end }} ports: - name: proxy - port: {{ .Values.proxyService.port }} + port: {{ .Values.gateway.servicePort }} targetPort: proxy protocol: TCP - {{- if and (eq .Values.proxyService.type "NodePort") (not (empty .Values.proxyService.nodePort)) }} - nodePort: {{ .Values.proxyService.nodePort }} + {{- if and (eq .Values.gateway.type "NodePort") (not (empty .Values.gateway.nodePort)) }} + nodePort: {{ .Values.gateway.nodePort }} {{- end }} selector: {{- include "aisix.selectorLabels" . | nindent 4 }} diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 3be490a..d84eca7 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -100,71 +100,67 @@ deployment: # -- Key inside the existing Secret that holds the admin key value existingSecretKey: "admin-key" -server: - proxy: - # -- Proxy API listen address (host:port inside the container) - listen: "0.0.0.0:3000" - tls: - # -- Enable TLS for the proxy port - enabled: false - # -- Certificate file path (must be present in the container) - certFile: "cert.pem" - # -- Key file path (must be present in the container) - keyFile: "key.pem" - admin: - # -- Admin API listen address. Use 0.0.0.0:3001 to allow in-cluster access. - listen: "0.0.0.0:3001" - -# Proxy Service (port 3000) — user traffic -proxyService: - # -- Service type: ClusterIP, NodePort, or LoadBalancer +# -- AISIX proxy service settings (port 3000) — user traffic +gateway: + # -- proxy service type type: NodePort + # -- Setting how the Service route external traffic + externalTrafficPolicy: Cluster + # -- IPs for which nodes in the cluster will also accept traffic for the service + externalIPs: [] + # -- which ip to listen on for the proxy service + ip: 0.0.0.0 # -- Service port - port: 3000 + servicePort: 3000 # -- Container port containerPort: 3000 # -- Optional static nodePort (only relevant when type is NodePort) nodePort: "" - # -- externalTrafficPolicy (for NodePort / LoadBalancer) - externalTrafficPolicy: Cluster - externalIPs: [] annotations: {} - -# Admin Service (port 3001) — management UI and Admin API -adminService: - # -- Service type: ClusterIP recommended for security + # -- Using ingress access AISIX proxy service + ingress: + enabled: false + # -- Ingress annotations + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: aisix.local + paths: [] + tls: [] + +# -- AISIX admin service settings (port 3001) — Admin API and UI +admin: + # -- Enable admin service + enabled: true + # -- admin service type type: ClusterIP + # -- which ip to listen on for the admin service + ip: 0.0.0.0 # -- Service port - port: 3001 + servicePort: 3001 # -- Container port containerPort: 3001 annotations: {} + # -- Using ingress access AISIX admin service + ingress: + enabled: false + # -- Ingress annotations + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: aisix-admin.local + paths: + - "/ui" + - "/aisix/admin" + tls: [] # -- Kubernetes liveness probe override livenessProbe: {} # -- Kubernetes readiness probe override readinessProbe: {} -# -- Ingress for the proxy service -ingress: - enabled: false - annotations: {} - hosts: - - host: aisix.local - paths: [] - tls: [] - -# -- Ingress for the admin service -adminIngress: - enabled: false - annotations: {} - hosts: - - host: aisix-admin.local - paths: - - "/ui" - - "/aisix/admin" - tls: [] - # -- etcd subchart (bitnami/etcd) etcd: # -- Install etcd as a subchart. Set false to use an external etcd. From c4cbbc93333f88d5a1df2f9362636cca57063e73 Mon Sep 17 00:00:00 2001 From: rongxin Date: Thu, 16 Apr 2026 08:49:00 +0800 Subject: [PATCH 11/16] fix(aisix): address review comments - hpa.yaml: use correct v2beta1 metrics schema (targetAverageUtilization) to match v2 schema split, matching gateway chart pattern - deployment.yaml: fix imagePullSecrets to emit name: wrapper; wire up extraEnvVarsSecret in envFrom; always inject AISIX_ADMIN_KEY from Secret - configmap.yaml: always use {{AISIX_ADMIN_KEY}} placeholder instead of embedding key in ConfigMap - secret.yaml: new template to create internal Secret from adminKey when existingSecret is not set - values.yaml: default ingress paths to ["/"] (was []); improve etcd RBAC comment; clarify adminKey comment - README.md: fix hyphenation, table row format, stale field names, etcd.enabled default, prefer Secret-based install example --- charts/aisix/README.md | 31 +++++++++++++++----------- charts/aisix/templates/configmap.yaml | 4 ---- charts/aisix/templates/deployment.yaml | 19 ++++++++++++---- charts/aisix/templates/hpa.yaml | 21 ++++++++++++----- charts/aisix/templates/secret.yaml | 12 ++++++++++ charts/aisix/values.yaml | 12 +++++----- 6 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 charts/aisix/templates/secret.yaml diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 7dc88f1..f6f566a 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,6 +1,6 @@ # AISIX Helm Chart -A Helm chart for [AISIX](https://github.com/api7/aisix) — an open source, high-performance AI Gateway and LLM proxy built in Rust. +A Helm chart for [AISIX](https://github.com/api7/aisix) — an open-source, high-performance AI Gateway and LLM proxy built in Rust. ## Prerequisites @@ -13,8 +13,12 @@ A Helm chart for [AISIX](https://github.com/api7/aisix) — an open source, high helm repo add api7 https://charts.api7.ai helm repo update +# Recommended: use an existing Secret for the admin key +kubectl create secret generic aisix-admin-secret \ + --from-literal=admin-key= + helm install my-aisix api7/aisix \ - --set deployment.admin.adminKey[0].key= + --set deployment.admin.existingSecret=aisix-admin-secret ``` ## Uninstalling the Chart @@ -32,17 +36,18 @@ The following table lists the key configurable parameters. See `values.yaml` for | `image.repository` | AISIX image repository | `ghcr.io/api7/aisix` | | `image.tag` | AISIX image tag | `0.1.0` | | `replicaCount` | Number of replicas | `1` | -| `deployment.admin.adminKey` | Admin API key list | `[{key: "changeme"}]` | -| `deployment.admin.existingSecret` | Existing Secret for admin key | `""` | +| `deployment.admin.adminKey` | Admin API key (used to create an internal Secret) | `[{key: "changeme"}]` | +| `deployment.admin.existingSecret` | Existing Secret for admin key (overrides adminKey) | `""` | | `deployment.etcd.host` | External etcd hosts (when `etcd.enabled=false`) | `["http://etcd.host:2379"]` | | `deployment.etcd.prefix` | etcd key prefix | `/aisix` | -| `server.proxy.listen` | Proxy API listen address | `0.0.0.0:3000` | -| `server.admin.listen` | Admin API listen address | `0.0.0.0:3001` | -| `proxyService.type` | Proxy Service type | `NodePort` | -| `adminService.type` | Admin Service type | `ClusterIP` | -| `etcd.enabled` | Install bundled etcd | `true` | -| `ingress.enabled` | Enable Ingress for proxy | `false` | -| `adminIngress.enabled` | Enable Ingress for admin | `false` | +| `gateway.type` | Proxy Service type | `NodePort` | +| `gateway.servicePort` | Proxy Service port | `3000` | +| `gateway.ingress.enabled` | Enable Ingress for proxy | `false` | +| `admin.enabled` | Enable admin Service and port | `true` | +| `admin.type` | Admin Service type | `ClusterIP` | +| `admin.servicePort` | Admin Service port | `3001` | +| `admin.ingress.enabled` | Enable Ingress for admin | `false` | +| `etcd.enabled` | Install bundled etcd | `false` | | `autoscaling.enabled` | Enable HPA | `false` | ## Using an Existing Secret for the Admin Key @@ -60,6 +65,6 @@ helm install my-aisix api7/aisix \ ```bash helm install my-aisix api7/aisix \ --set etcd.enabled=false \ - --set deployment.etcd.host[0]="http://my-etcd:2379" \ - --set deployment.admin.adminKey[0].key= + --set "deployment.etcd.host[0]=http://my-etcd:2379" \ + --set deployment.admin.existingSecret=aisix-admin-secret ``` diff --git a/charts/aisix/templates/configmap.yaml b/charts/aisix/templates/configmap.yaml index 52d0540..8c82f02 100644 --- a/charts/aisix/templates/configmap.yaml +++ b/charts/aisix/templates/configmap.yaml @@ -15,11 +15,7 @@ data: timeout: {{ .Values.deployment.etcd.timeout }} admin: admin_key: - {{- if .Values.deployment.admin.existingSecret }} - key: "{{"{{"}}AISIX_ADMIN_KEY{{"}}"}}" - {{- else }} - {{- toYaml .Values.deployment.admin.adminKey | nindent 10 }} - {{- end }} server: proxy: diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml index 38ffbe3..2bb7f07 100644 --- a/charts/aisix/templates/deployment.yaml +++ b/charts/aisix/templates/deployment.yaml @@ -30,7 +30,9 @@ spec: spec: {{- with .Values.global.imagePullSecrets }} imagePullSecrets: - {{- toYaml . | nindent 8 }} + {{- range $.Values.global.imagePullSecrets }} + - name: {{ . }} + {{- end }} {{- end }} serviceAccountName: {{ include "aisix.serviceAccountName" . }} {{- with .Values.podSecurityContext }} @@ -77,20 +79,29 @@ spec: {{- end }} - name: RUST_LOG value: "info" - {{- if .Values.deployment.admin.existingSecret }} - name: AISIX_ADMIN_KEY valueFrom: secretKeyRef: + {{- if .Values.deployment.admin.existingSecret }} name: {{ .Values.deployment.admin.existingSecret }} key: {{ .Values.deployment.admin.existingSecretKey }} - {{- end }} + {{- else }} + name: {{ include "aisix.fullname" . }}-admin-key + key: admin-key + {{- end }} {{- with .Values.extraEnvVars }} {{- toYaml . | nindent 12 }} {{- end }} - {{- if .Values.extraEnvVarsCM }} + {{- if or .Values.extraEnvVarsCM .Values.extraEnvVarsSecret }} envFrom: + {{- if .Values.extraEnvVarsCM }} - configMapRef: name: {{ .Values.extraEnvVarsCM }} + {{- end }} + {{- if .Values.extraEnvVarsSecret }} + - secretRef: + name: {{ .Values.extraEnvVarsSecret }} + {{- end }} {{- end }} volumeMounts: - name: aisix-config diff --git a/charts/aisix/templates/hpa.yaml b/charts/aisix/templates/hpa.yaml index 9e1a94a..683cce5 100644 --- a/charts/aisix/templates/hpa.yaml +++ b/charts/aisix/templates/hpa.yaml @@ -1,9 +1,5 @@ {{- if .Values.autoscaling.enabled }} -{{- if eq .Values.autoscaling.version "v2" }} -apiVersion: autoscaling/v2 -{{- else }} -apiVersion: autoscaling/v2beta1 -{{- end }} +apiVersion: autoscaling/{{ .Values.autoscaling.version }} kind: HorizontalPodAutoscaler metadata: name: {{ include "aisix.fullname" . }} @@ -18,6 +14,7 @@ spec: minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: + {{- if eq .Values.autoscaling.version "v2" }} {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: @@ -34,4 +31,18 @@ spec: type: Utilization averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} + {{- else }} + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- end }} {{- end }} diff --git a/charts/aisix/templates/secret.yaml b/charts/aisix/templates/secret.yaml new file mode 100644 index 0000000..906e70d --- /dev/null +++ b/charts/aisix/templates/secret.yaml @@ -0,0 +1,12 @@ +{{- if and .Values.admin.enabled (not .Values.deployment.admin.existingSecret) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "aisix.fullname" . }}-admin-key + namespace: {{ .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} +type: Opaque +stringData: + admin-key: {{ (index .Values.deployment.admin.adminKey 0).key | quote }} +{{- end }} diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index d84eca7..822e03c 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -90,11 +90,11 @@ deployment: # -- etcd request timeout in seconds timeout: 30 admin: - # -- Admin API key(s). Each entry must have a `key` field. - # IMPORTANT: change this before deploying to production. + # -- Admin API key. Used to create an internal Secret when existingSecret is not set. + # WARNING: change this before deploying to production. adminKey: - key: "changeme" - # -- Name of an existing Secret that contains an `admin-key` field. + # -- Name of an existing Secret that contains an admin key field. # If set, adminKey above is ignored and the key is read from the Secret. existingSecret: "" # -- Key inside the existing Secret that holds the admin key value @@ -126,7 +126,8 @@ gateway: # kubernetes.io/tls-acme: "true" hosts: - host: aisix.local - paths: [] + paths: + - "/" tls: [] # -- AISIX admin service settings (port 3001) — Admin API and UI @@ -169,7 +170,8 @@ etcd: repository: api7/etcd auth: rbac: - # -- No authentication by default. Switch to enable RBAC authentication + # -- No authentication by default. Enable RBAC (set create: true and configure rootPassword) + # for production or multi-tenant clusters to prevent unauthenticated etcd access. create: false # -- root password for etcd. Requires etcd.auth.rbac.create to be true. rootPassword: "" From 77c6cbb9ce571bbf12a76a107f30784a988215f4 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 21 Apr 2026 09:03:26 +0800 Subject: [PATCH 12/16] docs(aisix): regenerate README with helm-docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- charts/aisix/README.md | 159 +++++++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 69 deletions(-) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index f6f566a..4f61583 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,70 +1,91 @@ -# AISIX Helm Chart +# aisix + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) + +A Helm chart for AISIX AI Gateway + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| API7 | | | + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://charts.bitnami.com/bitnami | etcd | 8.7.7 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| admin | object | `{"annotations":{},"containerPort":3001,"enabled":true,"ingress":{"annotations":{},"enabled":false,"hosts":[{"host":"aisix-admin.local","paths":["/ui","/aisix/admin"]}],"tls":[]},"ip":"0.0.0.0","servicePort":3001,"type":"ClusterIP"}` | AISIX admin service settings (port 3001) — Admin API and UI | +| admin.containerPort | int | `3001` | Container port | +| admin.enabled | bool | `true` | Enable admin service | +| admin.ingress | object | `{"annotations":{},"enabled":false,"hosts":[{"host":"aisix-admin.local","paths":["/ui","/aisix/admin"]}],"tls":[]}` | Using ingress access AISIX admin service | +| admin.ingress.annotations | object | `{}` | Ingress annotations | +| admin.ip | string | `"0.0.0.0"` | which ip to listen on for the admin service | +| admin.servicePort | int | `3001` | Service port | +| admin.type | string | `"ClusterIP"` | admin service type | +| affinity | object | `{}` | Set affinity for deploy | +| autoscaling.enabled | bool | `false` | | +| autoscaling.maxReplicas | int | `10` | | +| autoscaling.minReplicas | int | `1` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| autoscaling.targetMemoryUtilizationPercentage | int | `80` | | +| autoscaling.version | string | `"v2"` | HPA version, the value is "v2" or "v2beta1", default "v2" | +| deployment.admin.adminKey | list | `[{"key":"changeme"}]` | Admin API key. Used to create an internal Secret when existingSecret is not set. WARNING: change this before deploying to production. | +| deployment.admin.existingSecret | string | `""` | Name of an existing Secret that contains an admin key field. If set, adminKey above is ignored and the key is read from the Secret. | +| deployment.admin.existingSecretKey | string | `"admin-key"` | Key inside the existing Secret that holds the admin key value | +| deployment.etcd.host | list | `["http://etcd.host:2379"]` | List of etcd hosts. Ignored when etcd.enabled is true (auto-constructed). | +| deployment.etcd.prefix | string | `"/aisix"` | Key prefix used by aisix in etcd | +| deployment.etcd.timeout | int | `30` | etcd request timeout in seconds | +| etcd | object | `{"auth":{"rbac":{"create":false,"rootPassword":""},"tls":{"certFilename":"","certKeyFilename":"","enabled":false,"existingSecret":"","sni":"","verify":false}},"enabled":false,"image":{"repository":"api7/etcd"},"replicaCount":3,"service":{"port":2379}}` | etcd subchart (bitnami/etcd) | +| etcd.auth.rbac.create | bool | `false` | No authentication by default. Enable RBAC (set create: true and configure rootPassword) for production or multi-tenant clusters to prevent unauthenticated etcd access. | +| etcd.auth.rbac.rootPassword | string | `""` | root password for etcd. Requires etcd.auth.rbac.create to be true. | +| etcd.auth.tls.certFilename | string | `""` | etcd client cert filename using in etcd.auth.tls.existingSecret | +| etcd.auth.tls.certKeyFilename | string | `""` | etcd client cert key filename using in etcd.auth.tls.existingSecret | +| etcd.auth.tls.enabled | bool | `false` | enable etcd client certificate | +| etcd.auth.tls.existingSecret | string | `""` | name of the secret contains etcd client cert | +| etcd.auth.tls.sni | string | `""` | specify the TLS Server Name Indication extension, the ETCD endpoint hostname will be used when this setting is unset. | +| etcd.auth.tls.verify | bool | `false` | whether to verify the etcd endpoint certificate when setup a TLS connection to etcd | +| etcd.enabled | bool | `false` | Install etcd as a subchart. Set false to use an external etcd. | +| extraEnvVars | list | `[]` | Additional environment variables | +| extraEnvVarsCM | string | `""` | | +| extraEnvVarsSecret | string | `""` | | +| extraInitContainers | list | `[]` | Additional init containers | +| extraVolumeMounts | list | `[]` | Additional volume mounts | +| extraVolumes | list | `[]` | Additional volumes | +| fullnameOverride | string | `""` | | +| gateway | object | `{"annotations":{},"containerPort":3000,"externalIPs":[],"externalTrafficPolicy":"Cluster","ingress":{"annotations":{},"enabled":false,"hosts":[{"host":"aisix.local","paths":["/"]}],"tls":[]},"ip":"0.0.0.0","nodePort":"","servicePort":3000,"type":"NodePort"}` | AISIX proxy service settings (port 3000) — user traffic | +| gateway.containerPort | int | `3000` | Container port | +| gateway.externalIPs | list | `[]` | IPs for which nodes in the cluster will also accept traffic for the service | +| gateway.externalTrafficPolicy | string | `"Cluster"` | Setting how the Service route external traffic | +| gateway.ingress | object | `{"annotations":{},"enabled":false,"hosts":[{"host":"aisix.local","paths":["/"]}],"tls":[]}` | Using ingress access AISIX proxy service | +| gateway.ingress.annotations | object | `{}` | Ingress annotations | +| gateway.ip | string | `"0.0.0.0"` | which ip to listen on for the proxy service | +| gateway.nodePort | string | `""` | Optional static nodePort (only relevant when type is NodePort) | +| gateway.servicePort | int | `3000` | Service port | +| gateway.type | string | `"NodePort"` | proxy service type | +| global.imagePullSecrets | list | `[]` | Global Docker registry secret names as an array | +| image.pullPolicy | string | `"IfNotPresent"` | AISIX image pull policy | +| image.repository | string | `"ghcr.io/api7/aisix"` | AISIX image repository | +| image.tag | string | `"0.1.0"` | AISIX image tag; overrides the chart appVersion | +| livenessProbe | object | `{}` | Kubernetes liveness probe override | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | Node labels for pod assignment | +| podAnnotations | object | `{}` | Annotations to add to the pod | +| podLabels | object | `{}` | Labels to add to the pod | +| podSecurityContext | object | `{}` | Set the securityContext for AISIX pods | +| readinessProbe | object | `{}` | Kubernetes readiness probe override | +| replicaCount | int | `1` | Number of AISIX replicas | +| resources | object | `{}` | Set pod resource requests & limits | +| securityContext | object | `{}` | Set the securityContext for AISIX container | +| serviceAccount.annotations | object | `{}` | Annotations to add to the service account | +| serviceAccount.create | bool | `false` | Specifies whether a service account should be created | +| serviceAccount.name | string | `""` | The name of the service account to use. | +| timezone | string | `""` | timezone for the container, e.g. "UTC" or "Asia/Shanghai" | +| tolerations | list | `[]` | List of node taints to tolerate | +| updateStrategy | object | `{}` | | -A Helm chart for [AISIX](https://github.com/api7/aisix) — an open-source, high-performance AI Gateway and LLM proxy built in Rust. - -## Prerequisites - -- Kubernetes 1.21+ -- Helm 3.7+ - -## Installing the Chart - -```bash -helm repo add api7 https://charts.api7.ai -helm repo update - -# Recommended: use an existing Secret for the admin key -kubectl create secret generic aisix-admin-secret \ - --from-literal=admin-key= - -helm install my-aisix api7/aisix \ - --set deployment.admin.existingSecret=aisix-admin-secret -``` - -## Uninstalling the Chart - -```bash -helm uninstall my-aisix -``` - -## Configuration - -The following table lists the key configurable parameters. See `values.yaml` for the full list. - -| Parameter | Description | Default | -|-----------|-------------|---------| -| `image.repository` | AISIX image repository | `ghcr.io/api7/aisix` | -| `image.tag` | AISIX image tag | `0.1.0` | -| `replicaCount` | Number of replicas | `1` | -| `deployment.admin.adminKey` | Admin API key (used to create an internal Secret) | `[{key: "changeme"}]` | -| `deployment.admin.existingSecret` | Existing Secret for admin key (overrides adminKey) | `""` | -| `deployment.etcd.host` | External etcd hosts (when `etcd.enabled=false`) | `["http://etcd.host:2379"]` | -| `deployment.etcd.prefix` | etcd key prefix | `/aisix` | -| `gateway.type` | Proxy Service type | `NodePort` | -| `gateway.servicePort` | Proxy Service port | `3000` | -| `gateway.ingress.enabled` | Enable Ingress for proxy | `false` | -| `admin.enabled` | Enable admin Service and port | `true` | -| `admin.type` | Admin Service type | `ClusterIP` | -| `admin.servicePort` | Admin Service port | `3001` | -| `admin.ingress.enabled` | Enable Ingress for admin | `false` | -| `etcd.enabled` | Install bundled etcd | `false` | -| `autoscaling.enabled` | Enable HPA | `false` | - -## Using an Existing Secret for the Admin Key - -```bash -kubectl create secret generic aisix-admin-secret \ - --from-literal=admin-key= - -helm install my-aisix api7/aisix \ - --set deployment.admin.existingSecret=aisix-admin-secret -``` - -## Using an External etcd - -```bash -helm install my-aisix api7/aisix \ - --set etcd.enabled=false \ - --set "deployment.etcd.host[0]=http://my-etcd:2379" \ - --set deployment.admin.existingSecret=aisix-admin-secret -``` From d4c1beb97f7428007f9993443c44f19fee11c8b6 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 21 Apr 2026 13:18:29 +0800 Subject: [PATCH 13/16] docs(aisix): add control plane connection guide to README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- charts/aisix/README.md | 91 ++++++++++++++++++++++++++++++----- charts/aisix/README.md.gotmpl | 87 +++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 charts/aisix/README.md.gotmpl diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 4f61583..de2ca12 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,20 +1,88 @@ -# aisix +## AISIX AI Gateway -![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) +AISIX is an open-source, high-performance AI Gateway and LLM proxy. -A Helm chart for AISIX AI Gateway +This chart bootstraps AISIX on a Kubernetes cluster using [Helm](https://helm.sh). -## Maintainers +## Prerequisites -| Name | Email | Url | -| ---- | ------ | --- | -| API7 | | | +* Kubernetes 1.21+ +* Helm 3.7+ -## Requirements +## Install -| Repository | Name | Version | -|------------|------|---------| -| https://charts.bitnami.com/bitnami | etcd | 8.7.7 | +```sh +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install [RELEASE_NAME] api7/aisix +``` + +## Uninstall + +```sh +helm delete [RELEASE_NAME] +``` + +## Connecting to API7 EE Control Plane + +When using AISIX with an API7 EE control plane, you need to provide mTLS credentials +via environment variables: `API7_CONTROL_PLANE_ENDPOINTS`, `API7_GATEWAY_GROUP_ID`, +`API7_CONTROL_PLANE_CA`, `API7_CONTROL_PLANE_CERT`, and `API7_CONTROL_PLANE_KEY`. + +> **Note:** The CA, cert, and key values are multi-line PEM strings. Use `--from-file` +> when creating the Secret to preserve line breaks correctly. + +**Step 1: Save your certificates to files** + +```sh +# Save CA certificate +cat > ca.pem << 'EOF' +-----BEGIN CERTIFICATE----- + +-----END CERTIFICATE----- +EOF + +# Save client certificate +cat > cert.pem << 'EOF' +-----BEGIN CERTIFICATE----- + +-----END CERTIFICATE----- +EOF + +# Save client private key +cat > key.pem << 'EOF' +-----BEGIN PRIVATE KEY----- + +-----END PRIVATE KEY----- +EOF +``` + +**Step 2: Create a Kubernetes Secret from the certificate files** + +```sh +kubectl create secret generic aisix-cp-creds \ + --from-file=API7_CONTROL_PLANE_CA=ca.pem \ + --from-file=API7_CONTROL_PLANE_CERT=cert.pem \ + --from-file=API7_CONTROL_PLANE_KEY=key.pem +``` + +**Step 3: Install the chart** + +```sh +helm install my-aisix api7/aisix \ + --set extraEnvVarsSecret=aisix-cp-creds \ + --set extraEnvVars[0].name=API7_CONTROL_PLANE_ENDPOINTS \ + --set 'extraEnvVars[0].value=["https://:"]' \ + --set extraEnvVars[1].name=API7_GATEWAY_GROUP_ID \ + --set extraEnvVars[1].value= \ + --set etcd.enabled=false +``` + +Replace `:` and `` with the values from +your API7 EE console under **Gateway Group → Connection Info**. + +## Parameters ## Values @@ -88,4 +156,3 @@ A Helm chart for AISIX AI Gateway | timezone | string | `""` | timezone for the container, e.g. "UTC" or "Asia/Shanghai" | | tolerations | list | `[]` | List of node taints to tolerate | | updateStrategy | object | `{}` | | - diff --git a/charts/aisix/README.md.gotmpl b/charts/aisix/README.md.gotmpl new file mode 100644 index 0000000..6895be5 --- /dev/null +++ b/charts/aisix/README.md.gotmpl @@ -0,0 +1,87 @@ +## AISIX AI Gateway + +AISIX is an open-source, high-performance AI Gateway and LLM proxy. + +This chart bootstraps AISIX on a Kubernetes cluster using [Helm](https://helm.sh). + +## Prerequisites + +* Kubernetes 1.21+ +* Helm 3.7+ + +## Install + +```sh +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install [RELEASE_NAME] api7/aisix +``` + +## Uninstall + +```sh +helm delete [RELEASE_NAME] +``` + +## Connecting to API7 EE Control Plane + +When using AISIX with an API7 EE control plane, you need to provide mTLS credentials +via environment variables: `API7_CONTROL_PLANE_ENDPOINTS`, `API7_GATEWAY_GROUP_ID`, +`API7_CONTROL_PLANE_CA`, `API7_CONTROL_PLANE_CERT`, and `API7_CONTROL_PLANE_KEY`. + +> **Note:** The CA, cert, and key values are multi-line PEM strings. Use `--from-file` +> when creating the Secret to preserve line breaks correctly. + +**Step 1: Save your certificates to files** + +```sh +# Save CA certificate +cat > ca.pem << 'EOF' +-----BEGIN CERTIFICATE----- + +-----END CERTIFICATE----- +EOF + +# Save client certificate +cat > cert.pem << 'EOF' +-----BEGIN CERTIFICATE----- + +-----END CERTIFICATE----- +EOF + +# Save client private key +cat > key.pem << 'EOF' +-----BEGIN PRIVATE KEY----- + +-----END PRIVATE KEY----- +EOF +``` + +**Step 2: Create a Kubernetes Secret from the certificate files** + +```sh +kubectl create secret generic aisix-cp-creds \ + --from-file=API7_CONTROL_PLANE_CA=ca.pem \ + --from-file=API7_CONTROL_PLANE_CERT=cert.pem \ + --from-file=API7_CONTROL_PLANE_KEY=key.pem +``` + +**Step 3: Install the chart** + +```sh +helm install my-aisix api7/aisix \ + --set extraEnvVarsSecret=aisix-cp-creds \ + --set extraEnvVars[0].name=API7_CONTROL_PLANE_ENDPOINTS \ + --set 'extraEnvVars[0].value=["https://:"]' \ + --set extraEnvVars[1].name=API7_GATEWAY_GROUP_ID \ + --set extraEnvVars[1].value= \ + --set etcd.enabled=false +``` + +Replace `:` and `` with the values from +your API7 EE console under **Gateway Group → Connection Info**. + +## Parameters + +{{ template "chart.valuesSection" . }} From 4a6f013f0a03d0b9d8a65f1341f92d9fe2831b05 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 21 Apr 2026 15:18:56 +0800 Subject: [PATCH 14/16] docs(aisix): remove README.md.gotmpl and regenerate README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- charts/aisix/README.md | 91 +++++------------------------------ charts/aisix/README.md.gotmpl | 87 --------------------------------- 2 files changed, 12 insertions(+), 166 deletions(-) delete mode 100644 charts/aisix/README.md.gotmpl diff --git a/charts/aisix/README.md b/charts/aisix/README.md index de2ca12..4f61583 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -1,88 +1,20 @@ -## AISIX AI Gateway +# aisix -AISIX is an open-source, high-performance AI Gateway and LLM proxy. +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.1.0](https://img.shields.io/badge/AppVersion-0.1.0-informational?style=flat-square) -This chart bootstraps AISIX on a Kubernetes cluster using [Helm](https://helm.sh). +A Helm chart for AISIX AI Gateway -## Prerequisites +## Maintainers -* Kubernetes 1.21+ -* Helm 3.7+ +| Name | Email | Url | +| ---- | ------ | --- | +| API7 | | | -## Install +## Requirements -```sh -helm repo add api7 https://charts.api7.ai -helm repo update - -helm install [RELEASE_NAME] api7/aisix -``` - -## Uninstall - -```sh -helm delete [RELEASE_NAME] -``` - -## Connecting to API7 EE Control Plane - -When using AISIX with an API7 EE control plane, you need to provide mTLS credentials -via environment variables: `API7_CONTROL_PLANE_ENDPOINTS`, `API7_GATEWAY_GROUP_ID`, -`API7_CONTROL_PLANE_CA`, `API7_CONTROL_PLANE_CERT`, and `API7_CONTROL_PLANE_KEY`. - -> **Note:** The CA, cert, and key values are multi-line PEM strings. Use `--from-file` -> when creating the Secret to preserve line breaks correctly. - -**Step 1: Save your certificates to files** - -```sh -# Save CA certificate -cat > ca.pem << 'EOF' ------BEGIN CERTIFICATE----- - ------END CERTIFICATE----- -EOF - -# Save client certificate -cat > cert.pem << 'EOF' ------BEGIN CERTIFICATE----- - ------END CERTIFICATE----- -EOF - -# Save client private key -cat > key.pem << 'EOF' ------BEGIN PRIVATE KEY----- - ------END PRIVATE KEY----- -EOF -``` - -**Step 2: Create a Kubernetes Secret from the certificate files** - -```sh -kubectl create secret generic aisix-cp-creds \ - --from-file=API7_CONTROL_PLANE_CA=ca.pem \ - --from-file=API7_CONTROL_PLANE_CERT=cert.pem \ - --from-file=API7_CONTROL_PLANE_KEY=key.pem -``` - -**Step 3: Install the chart** - -```sh -helm install my-aisix api7/aisix \ - --set extraEnvVarsSecret=aisix-cp-creds \ - --set extraEnvVars[0].name=API7_CONTROL_PLANE_ENDPOINTS \ - --set 'extraEnvVars[0].value=["https://:"]' \ - --set extraEnvVars[1].name=API7_GATEWAY_GROUP_ID \ - --set extraEnvVars[1].value= \ - --set etcd.enabled=false -``` - -Replace `:` and `` with the values from -your API7 EE console under **Gateway Group → Connection Info**. - -## Parameters +| Repository | Name | Version | +|------------|------|---------| +| https://charts.bitnami.com/bitnami | etcd | 8.7.7 | ## Values @@ -156,3 +88,4 @@ your API7 EE console under **Gateway Group → Connection Info**. | timezone | string | `""` | timezone for the container, e.g. "UTC" or "Asia/Shanghai" | | tolerations | list | `[]` | List of node taints to tolerate | | updateStrategy | object | `{}` | | + diff --git a/charts/aisix/README.md.gotmpl b/charts/aisix/README.md.gotmpl deleted file mode 100644 index 6895be5..0000000 --- a/charts/aisix/README.md.gotmpl +++ /dev/null @@ -1,87 +0,0 @@ -## AISIX AI Gateway - -AISIX is an open-source, high-performance AI Gateway and LLM proxy. - -This chart bootstraps AISIX on a Kubernetes cluster using [Helm](https://helm.sh). - -## Prerequisites - -* Kubernetes 1.21+ -* Helm 3.7+ - -## Install - -```sh -helm repo add api7 https://charts.api7.ai -helm repo update - -helm install [RELEASE_NAME] api7/aisix -``` - -## Uninstall - -```sh -helm delete [RELEASE_NAME] -``` - -## Connecting to API7 EE Control Plane - -When using AISIX with an API7 EE control plane, you need to provide mTLS credentials -via environment variables: `API7_CONTROL_PLANE_ENDPOINTS`, `API7_GATEWAY_GROUP_ID`, -`API7_CONTROL_PLANE_CA`, `API7_CONTROL_PLANE_CERT`, and `API7_CONTROL_PLANE_KEY`. - -> **Note:** The CA, cert, and key values are multi-line PEM strings. Use `--from-file` -> when creating the Secret to preserve line breaks correctly. - -**Step 1: Save your certificates to files** - -```sh -# Save CA certificate -cat > ca.pem << 'EOF' ------BEGIN CERTIFICATE----- - ------END CERTIFICATE----- -EOF - -# Save client certificate -cat > cert.pem << 'EOF' ------BEGIN CERTIFICATE----- - ------END CERTIFICATE----- -EOF - -# Save client private key -cat > key.pem << 'EOF' ------BEGIN PRIVATE KEY----- - ------END PRIVATE KEY----- -EOF -``` - -**Step 2: Create a Kubernetes Secret from the certificate files** - -```sh -kubectl create secret generic aisix-cp-creds \ - --from-file=API7_CONTROL_PLANE_CA=ca.pem \ - --from-file=API7_CONTROL_PLANE_CERT=cert.pem \ - --from-file=API7_CONTROL_PLANE_KEY=key.pem -``` - -**Step 3: Install the chart** - -```sh -helm install my-aisix api7/aisix \ - --set extraEnvVarsSecret=aisix-cp-creds \ - --set extraEnvVars[0].name=API7_CONTROL_PLANE_ENDPOINTS \ - --set 'extraEnvVars[0].value=["https://:"]' \ - --set extraEnvVars[1].name=API7_GATEWAY_GROUP_ID \ - --set extraEnvVars[1].value= \ - --set etcd.enabled=false -``` - -Replace `:` and `` with the values from -your API7 EE console under **Gateway Group → Connection Info**. - -## Parameters - -{{ template "chart.valuesSection" . }} From 087c061085c69dc3b5fa37e9a87b7157bcc5dfd1 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 21 Apr 2026 15:32:35 +0800 Subject: [PATCH 15/16] fix(aisix): address review comments - Remove v2beta1 HPA support (removed in K8s 1.26), always use autoscaling/v2 - Guard Ingress rules against empty paths to avoid invalid Ingress objects - Decouple Secret creation from admin.enabled so AISIX_ADMIN_KEY env var always resolves - Add template validation when adminKey list is empty - Add checksum/secret annotation to trigger Pod rollout on admin key rotation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- charts/aisix/README.md | 1 - charts/aisix/templates/deployment.yaml | 1 + charts/aisix/templates/hpa.yaml | 17 +---------------- charts/aisix/templates/ingress-admin.yaml | 2 ++ charts/aisix/templates/ingress.yaml | 2 ++ charts/aisix/templates/secret.yaml | 5 ++++- charts/aisix/values.yaml | 2 -- 7 files changed, 10 insertions(+), 20 deletions(-) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 4f61583..933c81f 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -34,7 +34,6 @@ A Helm chart for AISIX AI Gateway | autoscaling.minReplicas | int | `1` | | | autoscaling.targetCPUUtilizationPercentage | int | `80` | | | autoscaling.targetMemoryUtilizationPercentage | int | `80` | | -| autoscaling.version | string | `"v2"` | HPA version, the value is "v2" or "v2beta1", default "v2" | | deployment.admin.adminKey | list | `[{"key":"changeme"}]` | Admin API key. Used to create an internal Secret when existingSecret is not set. WARNING: change this before deploying to production. | | deployment.admin.existingSecret | string | `""` | Name of an existing Secret that contains an admin key field. If set, adminKey above is ignored and the key is read from the Secret. | | deployment.admin.existingSecretKey | string | `"admin-key"` | Key inside the existing Secret that holds the admin key value | diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml index 2bb7f07..622008d 100644 --- a/charts/aisix/templates/deployment.yaml +++ b/charts/aisix/templates/deployment.yaml @@ -19,6 +19,7 @@ spec: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/aisix/templates/hpa.yaml b/charts/aisix/templates/hpa.yaml index 683cce5..e0cd2aa 100644 --- a/charts/aisix/templates/hpa.yaml +++ b/charts/aisix/templates/hpa.yaml @@ -1,5 +1,5 @@ {{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/{{ .Values.autoscaling.version }} +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ include "aisix.fullname" . }} @@ -14,7 +14,6 @@ spec: minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: - {{- if eq .Values.autoscaling.version "v2" }} {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: @@ -31,18 +30,4 @@ spec: type: Utilization averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} - {{- else }} - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} - {{- end }} {{- end }} diff --git a/charts/aisix/templates/ingress-admin.yaml b/charts/aisix/templates/ingress-admin.yaml index af07ec1..c04beef 100644 --- a/charts/aisix/templates/ingress-admin.yaml +++ b/charts/aisix/templates/ingress-admin.yaml @@ -17,6 +17,7 @@ spec: {{- end }} rules: {{- range .Values.admin.ingress.hosts }} + {{- if .paths }} - host: {{ .host | quote }} http: paths: @@ -30,4 +31,5 @@ spec: number: {{ $.Values.admin.servicePort }} {{- end }} {{- end }} + {{- end }} {{- end }} diff --git a/charts/aisix/templates/ingress.yaml b/charts/aisix/templates/ingress.yaml index 6394f3a..dec7980 100644 --- a/charts/aisix/templates/ingress.yaml +++ b/charts/aisix/templates/ingress.yaml @@ -17,6 +17,7 @@ spec: {{- end }} rules: {{- range .Values.gateway.ingress.hosts }} + {{- if .paths }} - host: {{ .host | quote }} http: paths: @@ -30,4 +31,5 @@ spec: number: {{ $.Values.gateway.servicePort }} {{- end }} {{- end }} + {{- end }} {{- end }} diff --git a/charts/aisix/templates/secret.yaml b/charts/aisix/templates/secret.yaml index 906e70d..8d01348 100644 --- a/charts/aisix/templates/secret.yaml +++ b/charts/aisix/templates/secret.yaml @@ -1,4 +1,7 @@ -{{- if and .Values.admin.enabled (not .Values.deployment.admin.existingSecret) }} +{{- if not .Values.deployment.admin.existingSecret }} +{{- if not .Values.deployment.admin.adminKey }} +{{- fail "deployment.admin.adminKey must not be empty when deployment.admin.existingSecret is not set" }} +{{- end }} apiVersion: v1 kind: Secret metadata: diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index 822e03c..b5adce4 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -57,8 +57,6 @@ updateStrategy: {} autoscaling: enabled: false - # -- HPA version, the value is "v2" or "v2beta1", default "v2" - version: v2 minReplicas: 1 maxReplicas: 10 targetCPUUtilizationPercentage: 80 From 9a0cfe18727c9f455915594ada6cbecfe3d845b4 Mon Sep 17 00:00:00 2001 From: rongxin Date: Tue, 21 Apr 2026 16:07:11 +0800 Subject: [PATCH 16/16] fix(aisix): address remaining review comments - Change deployment.admin.adminKey from list to single string; fail if empty and existingSecret is not set - Gate server.admin.listen config on admin.enabled so disabling the admin service actually suppresses the listener - Add ingressClassName support to ingress.yaml and ingress-admin.yaml, mirroring the semverCompare pattern used in charts/gateway Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- charts/aisix/README.md | 12 +++++++----- charts/aisix/templates/configmap.yaml | 2 ++ charts/aisix/templates/ingress-admin.yaml | 8 ++++++++ charts/aisix/templates/ingress.yaml | 8 ++++++++ charts/aisix/templates/secret.yaml | 4 ++-- charts/aisix/values.yaml | 9 ++++++--- 6 files changed, 33 insertions(+), 10 deletions(-) diff --git a/charts/aisix/README.md b/charts/aisix/README.md index 933c81f..f690758 100644 --- a/charts/aisix/README.md +++ b/charts/aisix/README.md @@ -20,11 +20,12 @@ A Helm chart for AISIX AI Gateway | Key | Type | Default | Description | |-----|------|---------|-------------| -| admin | object | `{"annotations":{},"containerPort":3001,"enabled":true,"ingress":{"annotations":{},"enabled":false,"hosts":[{"host":"aisix-admin.local","paths":["/ui","/aisix/admin"]}],"tls":[]},"ip":"0.0.0.0","servicePort":3001,"type":"ClusterIP"}` | AISIX admin service settings (port 3001) — Admin API and UI | +| admin | object | `{"annotations":{},"containerPort":3001,"enabled":true,"ingress":{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"aisix-admin.local","paths":["/ui","/aisix/admin"]}],"tls":[]},"ip":"0.0.0.0","servicePort":3001,"type":"ClusterIP"}` | AISIX admin service settings (port 3001) — Admin API and UI | | admin.containerPort | int | `3001` | Container port | | admin.enabled | bool | `true` | Enable admin service | -| admin.ingress | object | `{"annotations":{},"enabled":false,"hosts":[{"host":"aisix-admin.local","paths":["/ui","/aisix/admin"]}],"tls":[]}` | Using ingress access AISIX admin service | +| admin.ingress | object | `{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"aisix-admin.local","paths":["/ui","/aisix/admin"]}],"tls":[]}` | Using ingress access AISIX admin service | | admin.ingress.annotations | object | `{}` | Ingress annotations | +| admin.ingress.className | string | `""` | IngressClass that will be be used to implement the Ingress | | admin.ip | string | `"0.0.0.0"` | which ip to listen on for the admin service | | admin.servicePort | int | `3001` | Service port | | admin.type | string | `"ClusterIP"` | admin service type | @@ -34,7 +35,7 @@ A Helm chart for AISIX AI Gateway | autoscaling.minReplicas | int | `1` | | | autoscaling.targetCPUUtilizationPercentage | int | `80` | | | autoscaling.targetMemoryUtilizationPercentage | int | `80` | | -| deployment.admin.adminKey | list | `[{"key":"changeme"}]` | Admin API key. Used to create an internal Secret when existingSecret is not set. WARNING: change this before deploying to production. | +| deployment.admin.adminKey | string | `""` | Admin API key. Used to create an internal Secret when existingSecret is not set. Required when existingSecret is not set. | | deployment.admin.existingSecret | string | `""` | Name of an existing Secret that contains an admin key field. If set, adminKey above is ignored and the key is read from the Secret. | | deployment.admin.existingSecretKey | string | `"admin-key"` | Key inside the existing Secret that holds the admin key value | | deployment.etcd.host | list | `["http://etcd.host:2379"]` | List of etcd hosts. Ignored when etcd.enabled is true (auto-constructed). | @@ -57,12 +58,13 @@ A Helm chart for AISIX AI Gateway | extraVolumeMounts | list | `[]` | Additional volume mounts | | extraVolumes | list | `[]` | Additional volumes | | fullnameOverride | string | `""` | | -| gateway | object | `{"annotations":{},"containerPort":3000,"externalIPs":[],"externalTrafficPolicy":"Cluster","ingress":{"annotations":{},"enabled":false,"hosts":[{"host":"aisix.local","paths":["/"]}],"tls":[]},"ip":"0.0.0.0","nodePort":"","servicePort":3000,"type":"NodePort"}` | AISIX proxy service settings (port 3000) — user traffic | +| gateway | object | `{"annotations":{},"containerPort":3000,"externalIPs":[],"externalTrafficPolicy":"Cluster","ingress":{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"aisix.local","paths":["/"]}],"tls":[]},"ip":"0.0.0.0","nodePort":"","servicePort":3000,"type":"NodePort"}` | AISIX proxy service settings (port 3000) — user traffic | | gateway.containerPort | int | `3000` | Container port | | gateway.externalIPs | list | `[]` | IPs for which nodes in the cluster will also accept traffic for the service | | gateway.externalTrafficPolicy | string | `"Cluster"` | Setting how the Service route external traffic | -| gateway.ingress | object | `{"annotations":{},"enabled":false,"hosts":[{"host":"aisix.local","paths":["/"]}],"tls":[]}` | Using ingress access AISIX proxy service | +| gateway.ingress | object | `{"annotations":{},"className":"","enabled":false,"hosts":[{"host":"aisix.local","paths":["/"]}],"tls":[]}` | Using ingress access AISIX proxy service | | gateway.ingress.annotations | object | `{}` | Ingress annotations | +| gateway.ingress.className | string | `""` | IngressClass that will be be used to implement the Ingress | | gateway.ip | string | `"0.0.0.0"` | which ip to listen on for the proxy service | | gateway.nodePort | string | `""` | Optional static nodePort (only relevant when type is NodePort) | | gateway.servicePort | int | `3000` | Service port | diff --git a/charts/aisix/templates/configmap.yaml b/charts/aisix/templates/configmap.yaml index 8c82f02..bbf38ad 100644 --- a/charts/aisix/templates/configmap.yaml +++ b/charts/aisix/templates/configmap.yaml @@ -22,5 +22,7 @@ data: listen: "{{ .Values.gateway.ip }}:{{ .Values.gateway.containerPort }}" tls: enabled: false + {{- if .Values.admin.enabled }} admin: listen: "{{ .Values.admin.ip }}:{{ .Values.admin.containerPort }}" + {{- end }} diff --git a/charts/aisix/templates/ingress-admin.yaml b/charts/aisix/templates/ingress-admin.yaml index c04beef..a993377 100644 --- a/charts/aisix/templates/ingress-admin.yaml +++ b/charts/aisix/templates/ingress-admin.yaml @@ -1,4 +1,9 @@ {{- if and .Values.admin.enabled .Values.admin.ingress.enabled -}} +{{- if and .Values.admin.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.admin.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.admin.ingress.annotations "kubernetes.io/ingress.class" .Values.admin.ingress.className }} + {{- end }} +{{- end }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -11,6 +16,9 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: + {{- if and .Values.admin.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.admin.ingress.className }} + {{- end }} {{- if .Values.admin.ingress.tls }} tls: {{- toYaml .Values.admin.ingress.tls | nindent 4 }} diff --git a/charts/aisix/templates/ingress.yaml b/charts/aisix/templates/ingress.yaml index dec7980..377daba 100644 --- a/charts/aisix/templates/ingress.yaml +++ b/charts/aisix/templates/ingress.yaml @@ -1,4 +1,9 @@ {{- if .Values.gateway.ingress.enabled -}} +{{- if and .Values.gateway.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.gateway.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.gateway.ingress.annotations "kubernetes.io/ingress.class" .Values.gateway.ingress.className }} + {{- end }} +{{- end }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -11,6 +16,9 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: + {{- if and .Values.gateway.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.gateway.ingress.className }} + {{- end }} {{- if .Values.gateway.ingress.tls }} tls: {{- toYaml .Values.gateway.ingress.tls | nindent 4 }} diff --git a/charts/aisix/templates/secret.yaml b/charts/aisix/templates/secret.yaml index 8d01348..abfa7a9 100644 --- a/charts/aisix/templates/secret.yaml +++ b/charts/aisix/templates/secret.yaml @@ -1,6 +1,6 @@ {{- if not .Values.deployment.admin.existingSecret }} {{- if not .Values.deployment.admin.adminKey }} -{{- fail "deployment.admin.adminKey must not be empty when deployment.admin.existingSecret is not set" }} +{{- fail "deployment.admin.adminKey is required when deployment.admin.existingSecret is not set" }} {{- end }} apiVersion: v1 kind: Secret @@ -11,5 +11,5 @@ metadata: {{- include "aisix.labels" . | nindent 4 }} type: Opaque stringData: - admin-key: {{ (index .Values.deployment.admin.adminKey 0).key | quote }} + admin-key: {{ .Values.deployment.admin.adminKey | quote }} {{- end }} diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml index b5adce4..14a4855 100644 --- a/charts/aisix/values.yaml +++ b/charts/aisix/values.yaml @@ -89,9 +89,8 @@ deployment: timeout: 30 admin: # -- Admin API key. Used to create an internal Secret when existingSecret is not set. - # WARNING: change this before deploying to production. - adminKey: - - key: "changeme" + # Required when existingSecret is not set. + adminKey: "" # -- Name of an existing Secret that contains an admin key field. # If set, adminKey above is ignored and the key is read from the Secret. existingSecret: "" @@ -118,6 +117,8 @@ gateway: # -- Using ingress access AISIX proxy service ingress: enabled: false + # -- IngressClass that will be be used to implement the Ingress + className: "" # -- Ingress annotations annotations: {} # kubernetes.io/ingress.class: nginx @@ -144,6 +145,8 @@ admin: # -- Using ingress access AISIX admin service ingress: enabled: false + # -- IngressClass that will be be used to implement the Ingress + className: "" # -- Ingress annotations annotations: {} # kubernetes.io/ingress.class: nginx