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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions braintrust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ Size the request for the pod's full local-storage usage:

When you enable `tmpVolume`, make sure the `ephemeralStorage.request` still covers that extra space.

## GKE API Autoscaling

The API can autoscale on GKE using a Horizontal Pod Autoscaler backed by GKE's native `AutoscalingMetric` resource. When enabled, the API scales on three signals - CPU, Node.js event-loop utilization, and mean event-loop delay.

This is underpinned by a **Preview (Pre-GA)** GKE feature. It requires:

- GKE **1.35.1-gke.1396000** or later
- The Performance HPA profile and the Autoscaling API enabled on the cluster
- `roles/autoscaling.metricsWriter` granted to all node service accounts
- The Autoscaling API included in your service perimeter when using VPC Service Controls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google documents this Preview feature for GKE 1.35.1-gke.1396000 or later on the Rapid release channel.

This list names the version and the Performance HPA profile. It does not name the Rapid channel.

Add Rapid to the list here and in values.yaml. Clusters on Regular or Stable will not get this feature.

@brianvans brianvans (brianvans) Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rapid channel was a launch constraint from when this feature initially became a preview. I was on the stable regular channel while testing this and all required functionality existed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure the cluster itself was on the stable channel? When I looked right now it showed Rapid in the GCP console.

CleanShot 2026-08-10 at 16 41 52@2x

@brianvans brianvans (brianvans) Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops yeah I mis-spoke re: stable. I tested on regular (the channel we default to in the gcp terraform module), and then switched to rapid to test against the newest version. At least in us-central1, these are the channels that meet the requirements

$ gcloud container get-server-config --flatten="channels" --format="yaml(channels.channel,channels.defaultVersion)" --location=us-central1
Fetching server config for us-central1
---
channels:
  channel: EXTENDED
  defaultVersion: 1.35.6-gke.1250000
---
channels:
  channel: RAPID
  defaultVersion: 1.36.2-gke.2064000
---
channels:
  channel: REGULAR
  defaultVersion: 1.35.6-gke.1250000
---
channels:
  channel: STABLE
  defaultVersion: 1.34.9-gke.1065000

I'm not in love with documenting a channel requirement, as a) these change over time (docs burden) and b) there's no guarantee that a cluster in a channel is at the most current version.

How about we see if helm can ensure the CRD is available and fail with a friendly error if not?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an explicit check for the required CRD in 7acb818

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow this channel stuff is so complicated! I like the solution you came up with.


See [Expose custom metrics for autoscaling](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/expose-custom-metrics-autoscaling) for more details on `AutoscalingMetric` in GKE.

Enable it in your values:

```yaml
api:
autoscaling:
enabled: true
minReplicas: 4
maxReplicas: 50
```

When enabled, `api.replicas` is ignored and the HPA controls the replica count.

## Testing

This Helm chart includes comprehensive automated unit tests.
Expand Down
8 changes: 8 additions & 0 deletions braintrust/examples/google-autopilot-cel/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ api:
service:
networking.gke.io/load-balancer-type: "Internal"
replicas: 4
# Alternatively, autoscale the API on CPU and event-loop metrics.
# This is a Preview (Pre-GA) GKE feature requiring 1.35.1-gke.1396000 or later
# and the prerequisites documented in the main values.yaml.
# See api.autoscaling in the main values.yaml file for more details.
# autoscaling:
# enabled: true
# minReplicas: 4
# maxReplicas: 50
service:
type: LoadBalancer
port: 8000
Expand Down
8 changes: 8 additions & 0 deletions braintrust/examples/google-autopilot/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ api:
service:
networking.gke.io/load-balancer-type: "Internal"
replicas: 4
# Alternatively, autoscale the API on CPU and event-loop metrics.
# This is a Preview (Pre-GA) GKE feature requiring 1.35.1-gke.1396000 or later
# and the prerequisites documented in the main values.yaml.
# See api.autoscaling in the main values.yaml file for more details.
# autoscaling:
# enabled: true
# minReplicas: 4
# maxReplicas: 50
# Uncomment the following section to use a different image or tag from the version in the Helm release
#image:
#repository: public.ecr.aws/braintrust/standalone-api
Expand Down
12 changes: 12 additions & 0 deletions braintrust/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ Internal cluster URL for the AI Gateway service.
http://{{ .Values.aiGateway.service.name | default .Values.aiGateway.name }}.{{ include "braintrust.namespace" . }}:{{ .Values.aiGateway.service.port }}
{{- end -}}

{{/*
Validate API autoscaling prerequisites (GKE + AutoscalingMetric CRD).
*/}}
{{- define "braintrust.apiAutoscaling.validate" -}}
{{- if ne .Values.cloud "google" }}
{{- fail "api.autoscaling is currently only supported when cloud is google (GKE)" }}
{{- end }}
{{- if not (.Capabilities.APIVersions.Has "autoscaling.gke.io/v1beta1") }}
{{- fail "api.autoscaling requires the AutoscalingMetric API (autoscaling.gke.io/v1beta1). Use GKE 1.35.1 or later, or verify with: kubectl api-resources | grep autoscalingmetric. For helm template without a cluster, pass --api-versions=autoscaling.gke.io/v1beta1." }}
{{- end }}
{{- end -}}

{{/*
Render Brainstore container resources with provider-specific ephemeral storage.

Expand Down
34 changes: 34 additions & 0 deletions braintrust/templates/api-autoscaling-metric.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{- if .Values.api.autoscaling.enabled }}
{{- include "braintrust.apiAutoscaling.validate" . }}
apiVersion: autoscaling.gke.io/v1beta1
kind: AutoscalingMetric
metadata:
name: {{ .Values.api.name }}
namespace: {{ include "braintrust.namespace" . }}
{{- with (merge (deepCopy .Values.api.labels) .Values.global.labels) }}
labels:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.api.annotations.autoscalingMetric }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
metrics:
- pod:
selector:
matchLabels:
app: {{ .Values.api.name }}
containers:
- endpoint:
port: {{ .Values.api.healthServer.port }}
path: {{ .Values.api.autoscaling.metricsPath }}
metrics:
- gauge:
# GKE gauge names must match ^[a-z]([-a-z0-9]*[a-z0-9])?
name: braintrust-api-event-loop-utilization-ratio
prometheusMetricName: braintrust_api_event_loop_utilization_ratio
- gauge:
name: braintrust-api-event-loop-delay-mean-seconds
prometheusMetricName: braintrust_api_event_loop_delay_mean_seconds
{{- end }}
9 changes: 9 additions & 0 deletions braintrust/templates/api-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.api.autoscaling.enabled }}
replicas: {{ .Values.api.replicas }}
{{- end }}
strategy:
type: RollingUpdate
rollingUpdate:
Expand Down Expand Up @@ -81,6 +83,9 @@ spec:
{{- end }}
ports:
- containerPort: {{ .Values.api.service.port }}
{{- if .Values.api.autoscaling.enabled }}
- containerPort: {{ .Values.api.healthServer.port }}
{{- end }}
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
{{- with .Values.api.livenessProbe }}
Expand Down Expand Up @@ -156,6 +161,10 @@ spec:
{{- if .Values.api.extraEnvVars }}
{{- toYaml .Values.api.extraEnvVars | nindent 12 }}
{{- end }}
{{- if .Values.api.autoscaling.enabled }}
- name: ENABLE_PROMETHEUS_METRICS
value: "true"
{{- end }}
{{- if or .Values.api.tmpVolume.enabled (and (eq .Values.cloud "azure") .Values.azure.enableAzureKeyVaultDriver) $customCA.enabled }}
volumeMounts:
{{- if .Values.api.tmpVolume.enabled }}
Expand Down
48 changes: 48 additions & 0 deletions braintrust/templates/api-hpa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{{- if .Values.api.autoscaling.enabled }}
{{- include "braintrust.apiAutoscaling.validate" . }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ .Values.api.name }}
namespace: {{ include "braintrust.namespace" . }}
{{- with (merge (deepCopy .Values.api.labels) .Values.global.labels) }}
labels:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.api.annotations.hpa }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ .Values.api.name }}
minReplicas: {{ .Values.api.autoscaling.minReplicas }}
maxReplicas: {{ .Values.api.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.api.autoscaling.cpu.targetAverageUtilization }}
- type: Pods
pods:
metric:
name: autoscaling.gke.io|{{ .Values.api.name }}|braintrust-api-event-loop-utilization-ratio
target:
type: AverageValue
averageValue: {{ .Values.api.autoscaling.eventLoopUtilization.targetAverageValue | quote }}
- type: Pods
pods:
metric:
name: autoscaling.gke.io|{{ .Values.api.name }}|braintrust-api-event-loop-delay-mean-seconds
target:
type: AverageValue
averageValue: {{ .Values.api.autoscaling.eventLoopDelayMean.targetAverageValue | quote }}
{{- with .Values.api.autoscaling.behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
17 changes: 17 additions & 0 deletions braintrust/tests/api-autoscaling-crd_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
suite: test API autoscaling CRD requirement
templates:
- api-hpa.yaml
# Do not advertise autoscaling.gke.io here — this suite asserts the fail path.
tests:
- it: should fail when AutoscalingMetric API is unavailable
template: api-hpa.yaml
values:
- __fixtures__/base-values.yaml
set:
cloud: google
api.autoscaling.enabled: true
release:
namespace: "braintrust"
asserts:
- failedTemplate:
errorPattern: "api\\.autoscaling requires the AutoscalingMetric API \\(autoscaling\\.gke\\.io/v1beta1\\)"
Loading
Loading