diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d001cd3..b28ef92 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,6 +38,7 @@ jobs: --charts charts/gateway \ --charts charts/ingress-controller \ --charts charts/aisix-cp \ + --charts charts/aisix \ --charts charts/ngxdig' - name: Verify Chart.lock files diff --git a/AGENTS.md b/AGENTS.md index 7224295..00d2e5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ This repo (`api7/api7-helm-chart`) holds the Helm charts for API7 EE components | `api7-ingress-controller` | `charts/ingress-controller` | no — independent product version | | `developer-portal-fe` | `charts/developer-portal-fe` | no — independent product version | | `aisix-cp` (AISIX private-deployment control plane) | `charts/aisix-cp` | no — independent product version (source of truth: `api7/AISIX-Cloud` `helm/aisix-cp`) | +| `aisix` (AISIX AI gateway data plane) | `charts/aisix` | no — independent product version; **this repo is the source of truth** (unlike `aisix-cp`, no dev-repo counterpart to sync from) | | `ngxdig` (eBPF flame-graph / diagnosis agent, DaemonSet) | `charts/ngxdig` | no — independent product version (source of truth: `api7/ngx-flame`) | ## Multi-line maintenance model @@ -39,6 +40,8 @@ Cost: within a single line the chart can no longer use a major bump for breaking For **independently-versioned sub-charts** (`ingress-controller`, `developer-portal-fe`): their version axis is not the EE minor, so they are **shared across EE lines and not forked per line** by default. Only fork them onto their **own product minor** when an older EE line must ship a different version of them and keep patching it. +The **AISIX charts (`aisix-cp`, `aisix`) release as a pair**, on the AISIX OP version axis rather than the EE one: at each OP release both get `version` **and** `appVersion` set to that release. `appVersion` pins the image tag on both sides, so the two charts must never carry different `appVersion`s — a control plane and a data plane from different releases is not a combination we ship. + ## Release decision When releasing the chart for EE `X.Y.Z`: diff --git a/README.md b/README.md index 3fa8834..442dd31 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,5 @@ Detailed instructions are enrolled in the README of each chart. * [api7-control-plane](./charts/api7/README.md) * [api7-gateway](./charts/gateway/README.md) * [api7-ingress-controller](./charts/ingress-controller/README.md) +* [aisix-cp](./charts/aisix-cp/README.md) — AISIX AI gateway control plane +* [aisix](./charts/aisix/README.md) — AISIX AI gateway data plane diff --git a/charts/aisix/.helmignore b/charts/aisix/.helmignore new file mode 100644 index 0000000..6937952 --- /dev/null +++ b/charts/aisix/.helmignore @@ -0,0 +1,16 @@ +.DS_Store +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +*.tmproj +*.orig +*.rej +*.swp +.idea/ +.vscode/ +README.md.gotmpl +ci/ diff --git a/charts/aisix/Chart.yaml b/charts/aisix/Chart.yaml new file mode 100644 index 0000000..fd20969 --- /dev/null +++ b/charts/aisix/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: aisix +description: Helm chart for the AISIX AI gateway data plane +type: application +version: 0.6.0 +appVersion: "0.6.0" + +keywords: + - ai-gateway + - llm + - gateway + - proxy + +home: https://api7.ai +sources: + - https://github.com/api7/api7-helm-chart + +maintainers: + - name: API7 + email: support@api7.ai + url: https://api7.ai diff --git a/charts/aisix/README.md b/charts/aisix/README.md new file mode 100644 index 0000000..9850a35 --- /dev/null +++ b/charts/aisix/README.md @@ -0,0 +1,289 @@ +# aisix + +![Version: 0.6.0](https://img.shields.io/badge/Version-0.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.6.0](https://img.shields.io/badge/AppVersion-0.6.0-informational?style=flat-square) + +Helm chart for the AISIX AI gateway data plane + +AISIX is an AI gateway: it fronts LLM providers with routing, rate limiting, budgets, +caching, guardrails, and observability behind an OpenAI-compatible API. This chart +installs the **data plane** — the component that serves live AI traffic. + +The data plane is configured by the AISIX control plane, not by this chart. It +connects out to the control plane's data-plane manager over mutual TLS, using a +gateway certificate bundle issued from the console, and receives its models, API +keys, and policies from there. Install the control plane first — with the +[`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the +[on-premises installation guide](https://docs.api7.ai/ai-gateway/on-premises/deployment). + +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| API7 | | | + +## Source Code + +* + +## Prerequisites + +* Kubernetes v1.23+ +* Helm v3+ +* An AISIX control plane, reachable from the cluster +* A gateway certificate bundle for the environment this gateway should serve + +## Install + +In the console, open the target environment's **Data planes** view and issue a +gateway certificate. Keep the three PEM values — the private key is shown only once. + +Put them in a Secret so the private key never lands in a values file: + +```sh +kubectl create namespace aisix +kubectl -n aisix create secret generic aisix-gateway-certificate \ + --from-file=cert.pem=./cert.pem \ + --from-file=key.pem=./key.pem \ + --from-file=ca.pem=./ca.pem +``` + +Then install the chart, pointing it at the data-plane manager endpoint from the +same view: + +```sh +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install aisix api7/aisix --namespace aisix \ + --set controlPlane.baseURL=https://dp-manager.example.com:7944 \ + --set controlPlane.certificate.existingSecret=aisix-gateway-certificate +``` + +The gateway appears in the environment's **Data planes** view once its first +heartbeat lands. Each replica registers as its own instance; they share one +certificate. + +## Uninstall + +```sh +helm delete aisix --namespace aisix +``` + +## Configuration examples + +The full list of values is in the [Parameters](#parameters) table below. Put these +in a `values.yaml` and pass it with `-f values.yaml`. + +### Autoscale on CPU + +```yaml +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 20 + targetCPUUtilizationPercentage: 70 +``` + +Requires `metrics-server` in the cluster. A replica the autoscaler adds is kept out +of the Service until its readiness probe passes, which happens only after it has +loaded its configuration from the control plane — so a scaling event never routes +traffic to a gateway that cannot serve it. + +Scale-down is guarded from the other side: a terminating pod leaves the Service +first, pauses while that removal propagates, then drains in-flight requests. +Because a streaming response can run for minutes, `terminationGracePeriodSeconds` +defaults to 120 rather than the Kubernetes default of 30. Raise it if your +workloads stream for longer. + +### Autoscale on request load with KEDA + +CPU is a proxy for load; the gateway's own metrics are the real signal. With +[KEDA](https://keda.sh) installed, scale on a Prometheus query instead: + +```yaml +metrics: + serviceMonitor: + enabled: true + +keda: + enabled: true + minReplicas: 2 + maxReplicas: 20 + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus.monitoring.svc:9090 + query: sum(rate(aisix_llm_requests_total[2m])) + threshold: "100" +``` + +`autoscaling` and `keda` are mutually exclusive — enabling both fails the render +rather than letting two controllers fight over the replica count. + +### Share rate-limit counters across replicas + +Rate-limit counters are per-replica by default, so N replicas enforce N× every +configured limit. Before running more than one replica — including any replica an +autoscaler adds — point the gateway at a shared Redis: + +```yaml +rateLimit: + backend: redis + redis: + url: redis://redis.default.svc:6379 +``` + +Use `rateLimit.redis.existingSecret` instead when the URL carries a password. + +### Publish the gateway through a load balancer + +```yaml +service: + type: LoadBalancer + port: 80 + # Preserve the client source IP, which the gateway uses for IP allowlists. + externalTrafficPolicy: Local +``` + +### Bind a privileged port + +The image carries the `CAP_NET_BIND_SERVICE` file capability, so the gateway binds +low ports without running as root: + +```yaml +containerPorts: + proxy: 80 +``` + +### Survive node drains + +```yaml +podDisruptionBudget: + enabled: true + minAvailable: 50% + +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: aisix +``` + +### Set any other gateway configuration + +Every field of the gateway's configuration file is reachable as an environment +variable named `AISIX_
__`: + +```yaml +extraEnvVars: + - name: AISIX_OBSERVABILITY__LOG_LEVEL + value: debug + - name: AISIX_CACHE__BACKEND + value: redis +``` + +## Parameters + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Affinity rules for the gateway pods | +| autoscaling.behavior | object | `{}` | `spec.behavior` for the HPA. Empty uses the Kubernetes defaults (immediate scale-up, 5-minute scale-down stabilization). A gateway that carries long streaming responses usually wants a gentler scale-down, e.g. `scaleDown: {policies: [{type: Pods, value: 1, periodSeconds: 60}]}` | +| autoscaling.enabled | bool | `false` | Create a HorizontalPodAutoscaler for the gateway Deployment | +| autoscaling.extraMetrics | list | `[]` | Extra `spec.metrics` entries appended verbatim — Pods / Object / External metrics such as a Prometheus adapter series | +| autoscaling.maxReplicas | int | `10` | Upper replica bound | +| autoscaling.minReplicas | int | `2` | Lower replica bound | +| autoscaling.targetCPUUtilizationPercentage | int | `70` | Target average CPU utilization, in percent of the CPU request. Set to null to drop the CPU metric | +| autoscaling.targetMemoryUtilizationPercentage | string | `nil` | Target average memory utilization, in percent of the memory request. Null by default: gateway memory tracks in-flight streams more than load | +| containerPorts.metrics | int | `9090` | Port the Prometheus metrics listener binds inside the container | +| containerPorts.proxy | int | `3000` | Port the proxy listener binds inside the container. The image carries the `CAP_NET_BIND_SERVICE` file capability, so a privileged port works without running as root — see `securityContext` below | +| controlPlane.baseURL | string | `""` | Data-plane manager mTLS endpoint the gateway connects out to, e.g. `https://dpm.example.com:7944`. Required. | +| controlPlane.certificate.ca | string | `""` | CA bundle PEM. Used only when `existingSecret` is empty | +| controlPlane.certificate.caKey | string | `"ca.pem"` | Secret key holding the CA bundle PEM | +| controlPlane.certificate.cert | string | `""` | Client certificate PEM. Used only when `existingSecret` is empty | +| controlPlane.certificate.certKey | string | `"cert.pem"` | Secret key holding the client certificate PEM | +| controlPlane.certificate.existingSecret | string | `""` | Read the bundle from an existing Secret instead of the PEM values below. Recommended: it keeps the private key out of your values file | +| controlPlane.certificate.key | string | `""` | Private key PEM. Used only when `existingSecret` is empty | +| controlPlane.certificate.keyKey | string | `"key.pem"` | Secret key holding the private key PEM | +| controlPlane.etcdEndpoint | string | `""` | Control-plane etcd endpoint as bare `host:port`. Leave empty unless the control plane publishes an etcd endpoint distinct from `baseURL` | +| controlPlane.heartbeatIntervalSeconds | int | `15` | Heartbeat interval in seconds. The control plane marks a gateway connected on its first heartbeat. Clamped to [5, 300] by the gateway | +| extraEnvVars | list | `[]` | Extra environment variables for the gateway container. Every gateway configuration field is reachable as `AISIX_
__` | +| extraVolumeMounts | list | `[]` | Extra volume mounts for the gateway container | +| extraVolumes | list | `[]` | Extra volumes for the gateway pod | +| fullnameOverride | string | `""` | Override the fully qualified resource name prefix | +| global.imagePullSecrets | list | `[]` | Image pull secrets applied to every pod created by this chart | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| image.repository | string | `"docker.io/api7/aisix"` | Gateway image repository | +| image.tag | string | `""` | Image tag. Empty resolves to the chart `appVersion` | +| keda.annotations | object | `{}` | Extra annotations for the ScaledObject | +| keda.behavior | object | `{}` | `behavior` for the HPA KEDA creates. Empty uses the Kubernetes defaults | +| keda.cooldownPeriod | int | `300` | Seconds to wait after the last trigger fires before scaling down | +| keda.enabled | bool | `false` | Create a KEDA ScaledObject for the gateway Deployment | +| keda.fallback | object | `{}` | Replica count to fall back to when a trigger source is unreachable, e.g. `{failureThreshold: 3, replicas: 4}` | +| keda.maxReplicas | int | `10` | Upper replica bound | +| keda.minReplicas | int | `2` | Lower replica bound | +| keda.pollingInterval | int | `15` | How often KEDA evaluates the triggers, in seconds | +| keda.restoreToOriginalReplicaCount | bool | `false` | Restore the original replica count when the ScaledObject is deleted | +| keda.triggers | list | `[]` | KEDA triggers. Required when `keda.enabled` is true. For example: `[{type: prometheus, metadata: {serverAddress: "http://prometheus:9090", query: "sum(rate(aisix_llm_requests_total[2m]))", threshold: "100"}}]` | +| livenessProbe.enabled | bool | `true` | | +| livenessProbe.failureThreshold | int | `3` | | +| livenessProbe.initialDelaySeconds | int | `10` | | +| livenessProbe.periodSeconds | int | `10` | | +| metrics.enabled | bool | `true` | Publish the gateway's Prometheus metrics on a separate ClusterIP Service, so scraping never rides the (possibly public) proxy Service | +| metrics.service.annotations | object | `{}` | Extra annotations for the metrics Service | +| metrics.service.port | int | `9090` | Metrics Service port | +| metrics.serviceMonitor.enabled | bool | `false` | Create a Prometheus Operator ServiceMonitor for the metrics Service | +| metrics.serviceMonitor.interval | string | `"30s"` | Scrape interval | +| metrics.serviceMonitor.labels | object | `{}` | Extra labels, e.g. the `release` label your Prometheus selects on | +| metrics.serviceMonitor.metricRelabelings | list | `[]` | Metric relabeling rules | +| metrics.serviceMonitor.namespace | string | `""` | Namespace to create the ServiceMonitor in. Empty uses the release namespace | +| metrics.serviceMonitor.relabelings | list | `[]` | Scrape-time relabeling rules | +| metrics.serviceMonitor.scrapeTimeout | string | `""` | Scrape timeout. Empty leaves the Prometheus default | +| nameOverride | string | `""` | Override the chart name used in resource names | +| nodeSelector | object | `{}` | Node selector for the gateway pods | +| podAnnotations | object | `{}` | Annotations for the gateway pods | +| podDisruptionBudget.enabled | bool | `false` | Create a PodDisruptionBudget so voluntary disruptions (node drains, cluster upgrades) cannot take the whole gateway down at once | +| podDisruptionBudget.maxUnavailable | int | `1` | Maximum unavailable pods | +| podDisruptionBudget.minAvailable | string | `""` | Minimum available pods. Takes precedence over `maxUnavailable` | +| podLabels | object | `{}` | Labels for the gateway pods | +| podSecurityContext.fsGroup | int | `10001` | | +| podSecurityContext.runAsNonRoot | bool | `true` | | +| podSecurityContext.runAsUser | int | `10001` | | +| podSecurityContext.seccompProfile.type | string | `"RuntimeDefault"` | | +| preStopSleepSeconds | int | `5` | Seconds to sleep in a `preStop` hook before the gateway receives SIGTERM. Endpoint removal and SIGTERM are concurrent, so without this pause a terminating pod can still be handed new connections by a kube-proxy that has not caught up. Set to 0 to drop the hook. | +| priorityClassName | string | `""` | Pod priority class | +| rateLimit.backend | string | `"memory"` | Rate-limit counter backend: `memory` (per-replica) or `redis` (shared) | +| rateLimit.redis.existingSecret | string | `""` | Read the connection URL from an existing Secret instead, so a URL carrying a password stays out of your values file | +| rateLimit.redis.existingSecretKey | string | `"redis-url"` | Secret key holding the connection URL | +| rateLimit.redis.url | string | `""` | Redis connection URL, e.g. `redis://redis.default.svc:6379`. Required when `rateLimit.backend` is redis and `existingSecret` is empty | +| readinessProbe.enabled | bool | `true` | | +| readinessProbe.failureThreshold | int | `3` | | +| readinessProbe.periodSeconds | int | `3` | | +| replicaCount | int | `2` | Number of gateway replicas. Ignored once `autoscaling.enabled` or `keda.enabled` is true — the autoscaler owns the replica count from then on and the Deployment omits `spec.replicas` so `helm upgrade` cannot reset it. | +| resources.limits.memory | string | `"1Gi"` | | +| resources.requests.cpu | string | `"500m"` | | +| resources.requests.memory | string | `"256Mi"` | | +| securityContext.allowPrivilegeEscalation | bool | `false` | | +| securityContext.capabilities.add[0] | string | `"NET_BIND_SERVICE"` | | +| securityContext.capabilities.drop[0] | string | `"ALL"` | | +| securityContext.readOnlyRootFilesystem | bool | `true` | | +| service.annotations | object | `{}` | Extra annotations for the proxy Service, e.g. cloud load-balancer settings | +| service.externalTrafficPolicy | string | `""` | `externalTrafficPolicy` for the proxy Service. `Local` preserves the client source IP on NodePort / LoadBalancer types | +| service.nodePort | string | `""` | Proxy Service nodePort, when `service.type` is NodePort or LoadBalancer | +| service.port | int | `80` | Proxy Service port | +| service.type | string | `"ClusterIP"` | Proxy Service type | +| serviceAccount.annotations | object | `{}` | ServiceAccount annotations | +| serviceAccount.create | bool | `true` | Create a ServiceAccount for the gateway | +| serviceAccount.name | string | `""` | ServiceAccount name. Defaults to the release fullname | +| startupProbe.enabled | bool | `true` | Gate liveness and readiness until the proxy listener is bound. The budget here (period x threshold) must stay longer than the gateway's own boot retries — it connects to the control plane before it binds, retrying for about 25s — or a recoverable boot race becomes a crash loop. | +| startupProbe.failureThreshold | int | `30` | | +| startupProbe.periodSeconds | int | `2` | | +| terminationGracePeriodSeconds | int | `120` | Seconds the gateway may take to drain in-flight requests after SIGTERM. It drains without a deadline of its own, so this value is the real cap — and a streaming LLM response can run for minutes. The Kubernetes default of 30s would cut those connections during a scale-down or a rolling update. | +| tolerations | list | `[]` | Tolerations for the gateway pods | +| topologySpreadConstraints | list | `[]` | Topology spread constraints, e.g. to spread replicas across zones | +| updateStrategy | object | `{}` | Deployment update strategy | diff --git a/charts/aisix/README.md.gotmpl b/charts/aisix/README.md.gotmpl new file mode 100644 index 0000000..e9991dc --- /dev/null +++ b/charts/aisix/README.md.gotmpl @@ -0,0 +1,185 @@ +{{ template "chart.header" . }} + +{{ template "chart.badgesSection" . }} + +{{ template "chart.description" . }} + +AISIX is an AI gateway: it fronts LLM providers with routing, rate limiting, budgets, +caching, guardrails, and observability behind an OpenAI-compatible API. This chart +installs the **data plane** — the component that serves live AI traffic. + +The data plane is configured by the AISIX control plane, not by this chart. It +connects out to the control plane's data-plane manager over mutual TLS, using a +gateway certificate bundle issued from the console, and receives its models, API +keys, and policies from there. Install the control plane first — with the +[`aisix-cp`](../aisix-cp/README.md) chart, or any of the other options in the +[on-premises installation guide](https://docs.api7.ai/ai-gateway/on-premises/deployment). + +{{ template "chart.homepageLine" . }} + +{{ template "chart.maintainersSection" . }} + +{{ template "chart.sourcesSection" . }} + +## Prerequisites + +* Kubernetes v1.23+ +* Helm v3+ +* An AISIX control plane, reachable from the cluster +* A gateway certificate bundle for the environment this gateway should serve + +## Install + +In the console, open the target environment's **Data planes** view and issue a +gateway certificate. Keep the three PEM values — the private key is shown only once. + +Put them in a Secret so the private key never lands in a values file: + +```sh +kubectl create namespace aisix +kubectl -n aisix create secret generic aisix-gateway-certificate \ + --from-file=cert.pem=./cert.pem \ + --from-file=key.pem=./key.pem \ + --from-file=ca.pem=./ca.pem +``` + +Then install the chart, pointing it at the data-plane manager endpoint from the +same view: + +```sh +helm repo add api7 https://charts.api7.ai +helm repo update + +helm install aisix api7/aisix --namespace aisix \ + --set controlPlane.baseURL=https://dp-manager.example.com:7944 \ + --set controlPlane.certificate.existingSecret=aisix-gateway-certificate +``` + +The gateway appears in the environment's **Data planes** view once its first +heartbeat lands. Each replica registers as its own instance; they share one +certificate. + +## Uninstall + +```sh +helm delete aisix --namespace aisix +``` + +## Configuration examples + +The full list of values is in the [Parameters](#parameters) table below. Put these +in a `values.yaml` and pass it with `-f values.yaml`. + +### Autoscale on CPU + +```yaml +autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 20 + targetCPUUtilizationPercentage: 70 +``` + +Requires `metrics-server` in the cluster. A replica the autoscaler adds is kept out +of the Service until its readiness probe passes, which happens only after it has +loaded its configuration from the control plane — so a scaling event never routes +traffic to a gateway that cannot serve it. + +Scale-down is guarded from the other side: a terminating pod leaves the Service +first, pauses while that removal propagates, then drains in-flight requests. +Because a streaming response can run for minutes, `terminationGracePeriodSeconds` +defaults to 120 rather than the Kubernetes default of 30. Raise it if your +workloads stream for longer. + +### Autoscale on request load with KEDA + +CPU is a proxy for load; the gateway's own metrics are the real signal. With +[KEDA](https://keda.sh) installed, scale on a Prometheus query instead: + +```yaml +metrics: + serviceMonitor: + enabled: true + +keda: + enabled: true + minReplicas: 2 + maxReplicas: 20 + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus.monitoring.svc:9090 + query: sum(rate(aisix_llm_requests_total[2m])) + threshold: "100" +``` + +`autoscaling` and `keda` are mutually exclusive — enabling both fails the render +rather than letting two controllers fight over the replica count. + +### Share rate-limit counters across replicas + +Rate-limit counters are per-replica by default, so N replicas enforce N× every +configured limit. Before running more than one replica — including any replica an +autoscaler adds — point the gateway at a shared Redis: + +```yaml +rateLimit: + backend: redis + redis: + url: redis://redis.default.svc:6379 +``` + +Use `rateLimit.redis.existingSecret` instead when the URL carries a password. + +### Publish the gateway through a load balancer + +```yaml +service: + type: LoadBalancer + port: 80 + # Preserve the client source IP, which the gateway uses for IP allowlists. + externalTrafficPolicy: Local +``` + +### Bind a privileged port + +The image carries the `CAP_NET_BIND_SERVICE` file capability, so the gateway binds +low ports without running as root: + +```yaml +containerPorts: + proxy: 80 +``` + +### Survive node drains + +```yaml +podDisruptionBudget: + enabled: true + minAvailable: 50% + +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: aisix +``` + +### Set any other gateway configuration + +Every field of the gateway's configuration file is reachable as an environment +variable named `AISIX_
__`: + +```yaml +extraEnvVars: + - name: AISIX_OBSERVABILITY__LOG_LEVEL + value: debug + - name: AISIX_CACHE__BACKEND + value: redis +``` + +## Parameters + +{{ template "chart.valuesSection" . }} diff --git a/charts/aisix/ci/default-values.yaml b/charts/aisix/ci/default-values.yaml new file mode 100644 index 0000000..88eecbd --- /dev/null +++ b/charts/aisix/ci/default-values.yaml @@ -0,0 +1,8 @@ +# Values used by chart-testing. The chart deliberately fails to render +# without a control-plane endpoint and a certificate bundle, so CI supplies +# placeholders for both. The referenced Secret is never created — this file +# drives `ct lint` only, not `ct install`. +controlPlane: + baseURL: "https://dp-manager.example.com:7944" + certificate: + existingSecret: "aisix-gateway-certificate" diff --git a/charts/aisix/templates/NOTES.txt b/charts/aisix/templates/NOTES.txt new file mode 100644 index 0000000..cf0ea82 --- /dev/null +++ b/charts/aisix/templates/NOTES.txt @@ -0,0 +1,57 @@ +AISIX gateway {{ .Chart.AppVersion }} has been deployed as {{ include "aisix.fullname" . }}. + +It connects out to the control plane at {{ .Values.controlPlane.baseURL }} and +appears in that environment's Data planes view once its first heartbeat lands. + +Watch it come up: + + kubectl rollout status deploy/{{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} + +Send a request through it: +{{- if eq .Values.service.type "ClusterIP" }} + + kubectl port-forward svc/{{ include "aisix.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }} + curl http://localhost:8080/v1/models -H "Authorization: Bearer " +{{- else if eq .Values.service.type "NodePort" }} + + export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[0].address}') + export NODE_PORT=$(kubectl get svc {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.ports[0].nodePort}') + curl http://$NODE_IP:$NODE_PORT/v1/models -H "Authorization: Bearer " +{{- else }} + + export LB_IP=$(kubectl get svc {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + curl http://$LB_IP:{{ .Values.service.port }}/v1/models -H "Authorization: Bearer " +{{- end }} + +{{ if .Values.autoscaling.enabled -}} +Autoscaling is on (HorizontalPodAutoscaler, {{ .Values.autoscaling.minReplicas }}-{{ .Values.autoscaling.maxReplicas }} replicas). Watch scaling +decisions and the metrics behind them: + + kubectl get hpa {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} --watch + kubectl describe hpa {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} + +CPU-target autoscaling needs metrics-server running in the cluster. +{{ else if .Values.keda.enabled -}} +Autoscaling is on (KEDA ScaledObject, {{ .Values.keda.minReplicas }}-{{ .Values.keda.maxReplicas }} replicas). Watch scaling +decisions and the triggers behind them: + + kubectl get scaledobject {{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} + kubectl describe hpa keda-hpa-{{ include "aisix.fullname" . }} -n {{ .Release.Namespace }} +{{ else -}} +Running {{ .Values.replicaCount }} fixed replica(s). To scale on load, set +`autoscaling.enabled=true` (CPU / memory) or `keda.enabled=true` with triggers +of your own. +{{ end }} +{{- if eq .Values.rateLimit.backend "memory" }} +{{- if or .Values.autoscaling.enabled .Values.keda.enabled }} + +WARNING: rateLimit.backend is "memory", so each replica counts requests on its +own and every replica the autoscaler adds multiplies the effective rate limit. +Point rateLimit.redis.url at a shared Redis and set rateLimit.backend=redis. +{{- else if gt (int .Values.replicaCount) 1 }} + +WARNING: rateLimit.backend is "memory", so each replica counts requests on its +own and {{ .Values.replicaCount }} replicas enforce {{ .Values.replicaCount }}x every configured rate limit. Point +rateLimit.redis.url at a shared Redis and set rateLimit.backend=redis. +{{- end }} +{{- end }} diff --git a/charts/aisix/templates/_helpers.tpl b/charts/aisix/templates/_helpers.tpl new file mode 100644 index 0000000..27593b9 --- /dev/null +++ b/charts/aisix/templates/_helpers.tpl @@ -0,0 +1,127 @@ +{{/* +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. +*/}} +{{- 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" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: aisix +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "aisix.selectorLabels" -}} +app.kubernetes.io/name: {{ include "aisix.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: gateway +{{- end }} + +{{/* +ServiceAccount name. +*/}} +{{- define "aisix.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "aisix.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Image pull secrets. +*/}} +{{- define "aisix.imagePullSecrets" -}} +{{- with .Values.global.imagePullSecrets }} +imagePullSecrets: + {{- toYaml . | nindent 2 }} +{{- end }} +{{- end }} + +{{/* +Name of the Secret holding the gateway certificate bundle. +*/}} +{{- define "aisix.certSecretName" -}} +{{- if .Values.controlPlane.certificate.existingSecret }} +{{- .Values.controlPlane.certificate.existingSecret }} +{{- else }} +{{- printf "%s-mtls" (include "aisix.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Name of the Secret holding the rate-limit Redis URL. +*/}} +{{- define "aisix.redisSecretName" -}} +{{- if .Values.rateLimit.redis.existingSecret }} +{{- .Values.rateLimit.redis.existingSecret }} +{{- else }} +{{- printf "%s-ratelimit" (include "aisix.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Secret key holding the rate-limit Redis URL. +*/}} +{{- define "aisix.redisSecretKey" -}} +{{- if .Values.rateLimit.redis.existingSecret }} +{{- .Values.rateLimit.redis.existingSecretKey | default "redis-url" }} +{{- else }} +{{- "redis-url" }} +{{- end }} +{{- end }} + +{{/* +Reject value combinations that render successfully but cannot run. +*/}} +{{- define "aisix.validateValues" -}} +{{- if not .Values.controlPlane.baseURL }} +{{- fail "controlPlane.baseURL is required: set it to the data-plane manager endpoint shown in the control plane's Data planes view" }} +{{- end }} +{{- if not .Values.controlPlane.certificate.existingSecret }} +{{- if not (and .Values.controlPlane.certificate.cert .Values.controlPlane.certificate.key .Values.controlPlane.certificate.ca) }} +{{- fail "a gateway certificate bundle is required: set controlPlane.certificate.existingSecret, or all three of controlPlane.certificate.{cert,key,ca}" }} +{{- end }} +{{- end }} +{{- if and .Values.autoscaling.enabled .Values.keda.enabled }} +{{- fail "autoscaling.enabled and keda.enabled are mutually exclusive: two controllers writing spec.replicas fight over the replica count" }} +{{- end }} +{{- if and .Values.keda.enabled (not .Values.keda.triggers) }} +{{- fail "keda.enabled requires at least one entry in keda.triggers" }} +{{- end }} +{{- if eq .Values.rateLimit.backend "redis" }} +{{- if not (or .Values.rateLimit.redis.url .Values.rateLimit.redis.existingSecret) }} +{{- fail "rateLimit.backend=redis requires rateLimit.redis.url or rateLimit.redis.existingSecret" }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/aisix/templates/deployment.yaml b/charts/aisix/templates/deployment.yaml new file mode 100644 index 0000000..176dd1a --- /dev/null +++ b/charts/aisix/templates/deployment.yaml @@ -0,0 +1,194 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "aisix.fullname" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +spec: + {{- if not (or .Values.autoscaling.enabled .Values.keda.enabled) }} + replicas: {{ .Values.replicaCount }} + {{- end }} + {{- with .Values.updateStrategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "aisix.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- if or (not .Values.controlPlane.certificate.existingSecret) .Values.podAnnotations }} + annotations: + {{- if not .Values.controlPlane.certificate.existingSecret }} + # Roll the pods when the chart-managed bundle changes, so a rotated + # certificate takes effect without a manual restart. An existingSecret + # has its own lifecycle — restart the Deployment after rotating it. + checksum/mtls: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + labels: + {{- include "aisix.labels" . | nindent 8 }} + {{- include "aisix.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + # The gateway reads every AISIX_-prefixed environment variable as a + # configuration override and rejects unknown keys. Service links inject + # one variable per Service in the namespace, so any Service whose name + # starts with "aisix" — this chart's own, or a control plane installed + # alongside it — stops the gateway from booting ("unknown field + # `cp_ui_service_port_http`"). Not a value: there is no working setting + # other than false. + enableServiceLinks: false + {{- include "aisix.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ include "aisix.serviceAccountName" . }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + containers: + - name: aisix + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: proxy + containerPort: {{ .Values.containerPorts.proxy }} + protocol: TCP + - name: metrics + containerPort: {{ .Values.containerPorts.metrics }} + protocol: TCP + env: + - name: AISIX_CONFIG_PATH + value: /etc/aisix/config.managed.yaml + - name: AISIX_PROXY__ADDR + value: "0.0.0.0:{{ .Values.containerPorts.proxy }}" + - name: AISIX_OBSERVABILITY__METRICS__PROMETHEUS__ADDR + value: "0.0.0.0:{{ .Values.containerPorts.metrics }}" + - name: AISIX_MANAGED__CP_BASE_URL + value: {{ .Values.controlPlane.baseURL | quote }} + {{- with .Values.controlPlane.etcdEndpoint }} + - name: AISIX_MANAGED__CP_ETCD_ENDPOINT + value: {{ . | quote }} + {{- end }} + - name: AISIX_MANAGED__HEARTBEAT_INTERVAL_SECS + value: {{ .Values.controlPlane.heartbeatIntervalSeconds | quote }} + - name: AISIX_MANAGED__CP_CERT_PEM + valueFrom: + secretKeyRef: + name: {{ include "aisix.certSecretName" . }} + key: {{ .Values.controlPlane.certificate.certKey }} + - name: AISIX_MANAGED__CP_KEY_PEM + valueFrom: + secretKeyRef: + name: {{ include "aisix.certSecretName" . }} + key: {{ .Values.controlPlane.certificate.keyKey }} + - name: AISIX_MANAGED__CP_CA_PEM + valueFrom: + secretKeyRef: + name: {{ include "aisix.certSecretName" . }} + key: {{ .Values.controlPlane.certificate.caKey }} + {{- if eq .Values.rateLimit.backend "redis" }} + - name: AISIX_RATELIMIT__BACKEND + value: "redis" + - name: AISIX_RATELIMIT__REDIS__URL + valueFrom: + secretKeyRef: + name: {{ include "aisix.redisSecretName" . }} + key: {{ include "aisix.redisSecretKey" . }} + {{- end }} + {{- with .Values.extraEnvVars }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.startupProbe.enabled }} + # /livez answers as soon as the proxy listener is bound. Gating on + # it keeps liveness and readiness quiet during boot. + startupProbe: + httpGet: + path: /livez + port: proxy + periodSeconds: {{ .Values.startupProbe.periodSeconds }} + failureThreshold: {{ .Values.startupProbe.failureThreshold }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + # /status/ready on the metrics listener is the configuration-source + # readiness gate: 503 until the control plane's configuration has + # been applied, 200 from then on. A replica the autoscaler adds + # therefore joins the Service only once it can actually serve. + # + # Not the proxy listener's /readyz: up to and including the + # appVersion this chart pins, that one also withdraws an instance + # whose configuration watch has gone five minutes without an event, + # which an idle environment does routinely — every replica leaves + # the Service while perfectly healthy. Fixed in api7/aisix#841; + # switch this probe to /readyz once appVersion carries that fix. + readinessProbe: + httpGet: + path: /status/ready + port: metrics + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + {{- end }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: /livez + port: proxy + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + {{- end }} + {{- if gt (int .Values.preStopSleepSeconds) 0 }} + lifecycle: + preStop: + exec: + command: ["/bin/sleep", "{{ .Values.preStopSleepSeconds }}"] + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + # The gateway writes its mTLS material, gateway id, and + # configuration snapshot here. Per-pod and ephemeral by design: + # replicas share one certificate but register as separate + # instances, and a restarted pod re-registers. + - name: state + mountPath: /var/lib/aisix + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - name: state + emptyDir: {} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/aisix/templates/hpa.yaml b/charts/aisix/templates/hpa.yaml new file mode 100644 index 0000000..ec1c664 --- /dev/null +++ b/charts/aisix/templates/hpa.yaml @@ -0,0 +1,40 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "aisix.fullname" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "aisix.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + {{- with .Values.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} + metrics: + {{- with .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ . }} + {{- end }} + {{- with .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ . }} + {{- end }} + {{- with .Values.autoscaling.extraMetrics }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/aisix/templates/pdb.yaml b/charts/aisix/templates/pdb.yaml new file mode 100644 index 0000000..1659f23 --- /dev/null +++ b/charts/aisix/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "aisix.fullname" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +spec: + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "aisix.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/aisix/templates/scaledobject.yaml b/charts/aisix/templates/scaledobject.yaml new file mode 100644 index 0000000..6935186 --- /dev/null +++ b/charts/aisix/templates/scaledobject.yaml @@ -0,0 +1,33 @@ +{{- if .Values.keda.enabled }} +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ include "aisix.fullname" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} + {{- with .Values.keda.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + name: {{ include "aisix.fullname" . }} + pollingInterval: {{ .Values.keda.pollingInterval }} + cooldownPeriod: {{ .Values.keda.cooldownPeriod }} + minReplicaCount: {{ .Values.keda.minReplicas }} + maxReplicaCount: {{ .Values.keda.maxReplicas }} + {{- with .Values.keda.fallback }} + fallback: + {{- toYaml . | nindent 4 }} + {{- end }} + triggers: + {{- toYaml .Values.keda.triggers | nindent 4 }} + advanced: + restoreToOriginalReplicaCount: {{ .Values.keda.restoreToOriginalReplicaCount }} + {{- with .Values.keda.behavior }} + horizontalPodAutoscalerConfig: + behavior: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/aisix/templates/secret.yaml b/charts/aisix/templates/secret.yaml new file mode 100644 index 0000000..d0a0080 --- /dev/null +++ b/charts/aisix/templates/secret.yaml @@ -0,0 +1,31 @@ +{{- include "aisix.validateValues" . }} +{{- if not .Values.controlPlane.certificate.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "aisix.certSecretName" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +type: Opaque +stringData: + {{ .Values.controlPlane.certificate.certKey }}: |- +{{ .Values.controlPlane.certificate.cert | trim | indent 4 }} + {{ .Values.controlPlane.certificate.keyKey }}: |- +{{ .Values.controlPlane.certificate.key | trim | indent 4 }} + {{ .Values.controlPlane.certificate.caKey }}: |- +{{ .Values.controlPlane.certificate.ca | trim | indent 4 }} +{{- end }} +{{- if and (eq .Values.rateLimit.backend "redis") (not .Values.rateLimit.redis.existingSecret) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "aisix.redisSecretName" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} +type: Opaque +stringData: + redis-url: {{ .Values.rateLimit.redis.url | quote }} +{{- end }} diff --git a/charts/aisix/templates/service-metrics.yaml b/charts/aisix/templates/service-metrics.yaml new file mode 100644 index 0000000..9018fc5 --- /dev/null +++ b/charts/aisix/templates/service-metrics.yaml @@ -0,0 +1,22 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix.fullname" . }}-metrics + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} + {{- with .Values.metrics.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + ports: + - name: metrics + port: {{ .Values.metrics.service.port }} + targetPort: metrics + protocol: TCP + selector: + {{- include "aisix.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/aisix/templates/service.yaml b/charts/aisix/templates/service.yaml new file mode 100644 index 0000000..947a3b0 --- /dev/null +++ b/charts/aisix/templates/service.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "aisix.fullname" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- with .Values.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ . }} + {{- end }} + ports: + - name: proxy + port: {{ .Values.service.port }} + targetPort: proxy + protocol: TCP + {{- if and .Values.service.nodePort (ne (toString .Values.service.type) "ClusterIP") }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + selector: + {{- include "aisix.selectorLabels" . | nindent 4 }} diff --git a/charts/aisix/templates/serviceaccount.yaml b/charts/aisix/templates/serviceaccount.yaml new file mode 100644 index 0000000..306e227 --- /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" . }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/aisix/templates/servicemonitor.yaml b/charts/aisix/templates/servicemonitor.yaml new file mode 100644 index 0000000..b37e674 --- /dev/null +++ b/charts/aisix/templates/servicemonitor.yaml @@ -0,0 +1,35 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "aisix.fullname" . }} + namespace: {{ .Values.metrics.serviceMonitor.namespace | default .Release.Namespace }} + labels: + {{- include "aisix.labels" . | nindent 4 }} + {{- include "aisix.selectorLabels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "aisix.selectorLabels" . | nindent 6 }} + endpoints: + - port: metrics + path: /metrics + interval: {{ .Values.metrics.serviceMonitor.interval }} + {{- with .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.metricRelabelings }} + metricRelabelings: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/aisix/values.yaml b/charts/aisix/values.yaml new file mode 100644 index 0000000..2597cf3 --- /dev/null +++ b/charts/aisix/values.yaml @@ -0,0 +1,286 @@ +global: + # -- Image pull secrets applied to every pod created by this chart + imagePullSecrets: [] + +# -- Number of gateway replicas. Ignored once `autoscaling.enabled` or +# `keda.enabled` is true — the autoscaler owns the replica count from then on +# and the Deployment omits `spec.replicas` so `helm upgrade` cannot reset it. +replicaCount: 2 + +# -- Override the chart name used in resource names +nameOverride: "" +# -- Override the fully qualified resource name prefix +fullnameOverride: "" + +image: + # -- Gateway image repository + repository: docker.io/api7/aisix + # -- Image pull policy + pullPolicy: IfNotPresent + # -- Image tag. Empty resolves to the chart `appVersion` + tag: "" + +## The gateway runs as an AISIX Cloud data plane: it authenticates to the +## control plane's data-plane manager with the mTLS bundle issued from +## **Data planes** in the console, then reads its configuration from the +## control plane. Everything in this section comes from that view. +controlPlane: + # -- Data-plane manager mTLS endpoint the gateway connects out to, e.g. + # `https://dpm.example.com:7944`. Required. + baseURL: "" + # -- Control-plane etcd endpoint as bare `host:port`. Leave empty unless the + # control plane publishes an etcd endpoint distinct from `baseURL` + etcdEndpoint: "" + # -- Heartbeat interval in seconds. The control plane marks a gateway + # connected on its first heartbeat. Clamped to [5, 300] by the gateway + heartbeatIntervalSeconds: 15 + certificate: + # -- Read the bundle from an existing Secret instead of the PEM values + # below. Recommended: it keeps the private key out of your values file + existingSecret: "" + # -- Secret key holding the client certificate PEM + certKey: "cert.pem" + # -- Secret key holding the private key PEM + keyKey: "key.pem" + # -- Secret key holding the CA bundle PEM + caKey: "ca.pem" + # -- Client certificate PEM. Used only when `existingSecret` is empty + cert: "" + # -- Private key PEM. Used only when `existingSecret` is empty + key: "" + # -- CA bundle PEM. Used only when `existingSecret` is empty + ca: "" + +containerPorts: + # -- Port the proxy listener binds inside the container. The image carries + # the `CAP_NET_BIND_SERVICE` file capability, so a privileged port works + # without running as root — see `securityContext` below + proxy: 3000 + # -- Port the Prometheus metrics listener binds inside the container + metrics: 9090 + +service: + # -- Proxy Service type + type: ClusterIP + # -- Proxy Service port + port: 80 + # -- Proxy Service nodePort, when `service.type` is NodePort or LoadBalancer + nodePort: "" + # -- Extra annotations for the proxy Service, e.g. cloud load-balancer settings + annotations: {} + # -- `externalTrafficPolicy` for the proxy Service. `Local` preserves the + # client source IP on NodePort / LoadBalancer types + externalTrafficPolicy: "" + +metrics: + # -- Publish the gateway's Prometheus metrics on a separate ClusterIP + # Service, so scraping never rides the (possibly public) proxy Service + enabled: true + service: + # -- Metrics Service port + port: 9090 + # -- Extra annotations for the metrics Service + annotations: {} + serviceMonitor: + # -- Create a Prometheus Operator ServiceMonitor for the metrics Service + enabled: false + # -- Namespace to create the ServiceMonitor in. Empty uses the release namespace + namespace: "" + # -- Scrape interval + interval: 30s + # -- Scrape timeout. Empty leaves the Prometheus default + scrapeTimeout: "" + # -- Extra labels, e.g. the `release` label your Prometheus selects on + labels: {} + # -- Scrape-time relabeling rules + relabelings: [] + # -- Metric relabeling rules + metricRelabelings: [] + +## Rate-limit counters are per-replica by default, so N replicas enforce N× +## every configured limit. Point the gateway at a shared Redis before running +## more than one replica — including any replica an autoscaler adds. +rateLimit: + # -- Rate-limit counter backend: `memory` (per-replica) or `redis` (shared) + backend: memory + redis: + # -- Redis connection URL, e.g. `redis://redis.default.svc:6379`. Required + # when `rateLimit.backend` is redis and `existingSecret` is empty + url: "" + # -- Read the connection URL from an existing Secret instead, so a URL + # carrying a password stays out of your values file + existingSecret: "" + # -- Secret key holding the connection URL + existingSecretKey: "redis-url" + +## HorizontalPodAutoscaler. Scaling is safe by construction: a new replica is +## kept out of the Service until its readiness probe passes, which happens +## only after it has loaded its configuration from the control plane. +## +## Requires a metrics source for the chosen metrics (metrics-server for the +## CPU / memory targets below). Mutually exclusive with `keda.enabled`. +autoscaling: + # -- Create a HorizontalPodAutoscaler for the gateway Deployment + enabled: false + # -- Lower replica bound + minReplicas: 2 + # -- Upper replica bound + maxReplicas: 10 + # -- Target average CPU utilization, in percent of the CPU request. Set to + # null to drop the CPU metric + targetCPUUtilizationPercentage: 70 + # -- Target average memory utilization, in percent of the memory request. + # Null by default: gateway memory tracks in-flight streams more than load + targetMemoryUtilizationPercentage: null + # -- Extra `spec.metrics` entries appended verbatim — Pods / Object / + # External metrics such as a Prometheus adapter series + extraMetrics: [] + # -- `spec.behavior` for the HPA. Empty uses the Kubernetes defaults + # (immediate scale-up, 5-minute scale-down stabilization). A gateway that + # carries long streaming responses usually wants a gentler scale-down, e.g. + # `scaleDown: {policies: [{type: Pods, value: 1, periodSeconds: 60}]}` + behavior: {} + +## KEDA ScaledObject, for scaling on request-driven signals rather than +## CPU / memory — e.g. a Prometheus query over the gateway's own +## `aisix_llm_requests_total`. Requires KEDA in the cluster. +## Mutually exclusive with `autoscaling.enabled`. +keda: + # -- Create a KEDA ScaledObject for the gateway Deployment + enabled: false + # -- Lower replica bound + minReplicas: 2 + # -- Upper replica bound + maxReplicas: 10 + # -- How often KEDA evaluates the triggers, in seconds + pollingInterval: 15 + # -- Seconds to wait after the last trigger fires before scaling down + cooldownPeriod: 300 + # -- Restore the original replica count when the ScaledObject is deleted + restoreToOriginalReplicaCount: false + # -- Replica count to fall back to when a trigger source is unreachable, + # e.g. `{failureThreshold: 3, replicas: 4}` + fallback: {} + # -- Extra annotations for the ScaledObject + annotations: {} + # -- KEDA triggers. Required when `keda.enabled` is true. For example: + # `[{type: prometheus, metadata: {serverAddress: "http://prometheus:9090", + # query: "sum(rate(aisix_llm_requests_total[2m]))", threshold: "100"}}]` + triggers: [] + # -- `behavior` for the HPA KEDA creates. Empty uses the Kubernetes defaults + behavior: {} + +podDisruptionBudget: + # -- Create a PodDisruptionBudget so voluntary disruptions (node drains, + # cluster upgrades) cannot take the whole gateway down at once + enabled: false + # -- Minimum available pods. Takes precedence over `maxUnavailable` + minAvailable: "" + # -- Maximum unavailable pods + maxUnavailable: 1 + +serviceAccount: + # -- Create a ServiceAccount for the gateway + create: true + # -- ServiceAccount name. Defaults to the release fullname + name: "" + # -- ServiceAccount annotations + annotations: {} + +## No CPU limit by default: CFS throttling adds tail latency on a proxy and +## suppresses the very CPU signal the HorizontalPodAutoscaler scales on. The +## CPU *request* is mandatory for CPU-target autoscaling and is set below. +resources: + requests: + cpu: 500m + memory: 256Mi + limits: + memory: 1Gi + +## The image already runs as uid 10001. `runAsUser` is repeated here because +## the image declares its user by name, and the kubelet cannot verify a named +## user against `runAsNonRoot`. +podSecurityContext: + runAsNonRoot: true + runAsUser: 10001 + # fsGroup owns the state directory emptyDir so the gateway can write its + # mTLS material, gateway id, and configuration snapshot under a read-only + # root filesystem. + fsGroup: 10001 + seccompProfile: + type: RuntimeDefault + +## NET_BIND_SERVICE is added back after dropping ALL because the gateway +## binary carries `cap_net_bind_service=+ep` as a file capability (so it can +## bind privileged ports without root). With that capability's effective bit +## set, exec fails outright if the capability is missing from the container's +## bounding set — dropping ALL without adding it back yields a pod that never +## starts. +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + +## Liveness and startup probe the proxy listener's `/livez`; readiness probes +## `/status/ready` on the metrics listener, which reports 503 until the +## gateway has applied configuration from the control plane. A scaled-up +## replica therefore joins the Service only once it can actually serve. +startupProbe: + # -- Gate liveness and readiness until the proxy listener is bound. The + # budget here (period x threshold) must stay longer than the gateway's own + # boot retries — it connects to the control plane before it binds, retrying + # for about 25s — or a recoverable boot race becomes a crash loop. + enabled: true + periodSeconds: 2 + failureThreshold: 30 +readinessProbe: + enabled: true + periodSeconds: 3 + failureThreshold: 3 +livenessProbe: + enabled: true + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + +# -- Seconds the gateway may take to drain in-flight requests after SIGTERM. +# It drains without a deadline of its own, so this value is the real cap — and +# a streaming LLM response can run for minutes. The Kubernetes default of 30s +# would cut those connections during a scale-down or a rolling update. +terminationGracePeriodSeconds: 120 + +# -- Seconds to sleep in a `preStop` hook before the gateway receives SIGTERM. +# Endpoint removal and SIGTERM are concurrent, so without this pause a +# terminating pod can still be handed new connections by a kube-proxy that has +# not caught up. Set to 0 to drop the hook. +preStopSleepSeconds: 5 + +# -- Extra environment variables for the gateway container. Every gateway +# configuration field is reachable as `AISIX_
__` +extraEnvVars: [] + +# -- Extra volumes for the gateway pod +extraVolumes: [] +# -- Extra volume mounts for the gateway container +extraVolumeMounts: [] + +# -- Annotations for the gateway pods +podAnnotations: {} +# -- Labels for the gateway pods +podLabels: {} +# -- Node selector for the gateway pods +nodeSelector: {} +# -- Tolerations for the gateway pods +tolerations: [] +# -- Affinity rules for the gateway pods +affinity: {} +# -- Topology spread constraints, e.g. to spread replicas across zones +topologySpreadConstraints: [] +# -- Deployment update strategy +updateStrategy: {} +# -- Pod priority class +priorityClassName: ""