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
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ Always use the namespace helper:
namespace: {{ include "braintrust.namespace" . }}
```

## GKE mode recommendation

GKE Autopilot is the preferred solution for new Braintrust deployments.
Standard is supported when customer requirements prevent Autopilot use.
The separate Standard example does not change this preference.
Existing deployments must keep their initial cluster mode.
A mode change replaces the cluster, causes downtime, and requires Helm release redeployment.

Recommend Autopilot first for new deployments.
Present Standard as a supported alternative for customer constraints.
Do not imply that existing Standard customers must switch modes.

## Critical Safety Constraints

These constraints exist because of real incidents and confirmed engineering guidance. Do not "simplify" or "clean up" code that implements them.
Expand All @@ -82,6 +94,27 @@ These constraints apply to customers migrating from Data Plane 1.x to 2.0. New d

The three brainstore configmaps (`brainstore-reader-configmap.yaml`, `brainstore-writer-configmap.yaml`, `brainstore-fastreader-configmap.yaml`) must have identical environment variable logic for `BRAINSTORE_RESPONSE_CACHE_URI`, `BRAINSTORE_CODE_BUNDLE_URI`, `BRAINSTORE_ASYNC_SCORING_OBJECTS`, and `BRAINSTORE_LOG_AUTOMATIONS_OBJECTS`. If you modify one, you must update all three.

### Disruption budgets and GKE Standard

Brainstore readers, fast readers, and writers each expose an optional `podDisruptionBudget`.
Budgets default to disabled on every cloud. An enabled Brainstore budget defaults to `maxUnavailable: 1`.
The API retains `minAvailable` unless an explicit `maxUnavailable` takes precedence.
The GKE Standard example enables separate budgets for the API and all Brainstore roles.
A single writer can stop briefly during eviction. With multiple writers, the budget permits one unavailable replica.
Each role has an independent budget, so different roles can lose a replica simultaneously.
Readiness probes determine healthy replicas. Deployment rollout settings and `minReadySeconds` do not control node eviction.
Stable `braintrust/node-pool` labels connect Helm selectors to Terraform pools.
The Terraform pool map key sets this label. Automatic replacement preserves the label.
GKE deletion protection must exist on the source pool before hardware replacement. Its PDB protection expires after one hour.

- Keep PDB templates cloud-independent and disabled by default.
- Enable the budgets in the GKE Standard example.
- Preserve the single-writer interruption exception.
- Use stable workload labels instead of generated GKE pool names in selectors.
- Document the Helm deployment and source pool protection steps before hardware replacement.
- Do not describe PDBs as an unconditional zero-downtime guarantee.
- Test role isolation, API compatibility, and cross-cloud behavior after PDB changes.

### Version Numbers

Chart version numbers are semantically meaningful for the upgrade path:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ helm upgrade --install \
Before installing the Braintrust Helm chart, ensure you have run the appropriate braintrust terraform module [Google](https://github.com/braintrustdata/terraform-google-braintrust-data-plane) or [Azure](https://github.com/braintrustdata/terraform-azure-braintrust-data-plane) to deploy the base infrastructure.

See the [Braintrust Helm Chart](./braintrust/README.md) for more details.

## GKE deployment modes

GKE Autopilot is the preferred solution for new Braintrust deployments. GKE Standard is supported when customer requirements prevent Autopilot use.

## GKE Standard node pool changes

The [Standard example](braintrust/examples/google-standard/values.yaml) uses stable workload selectors and enables optional API and Brainstore disruption budgets.
The chart defaults remain unchanged for other deployments.
The [disruption budget guidance](braintrust/README.md#optional-disruption-budgets) explains the single-writer exception and the preparation steps for existing GKE pools.
91 changes: 80 additions & 11 deletions braintrust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Kubernetes limits each Secret to 1 MiB. A normal combined CA bundle is typically

## GKE with Local SSDs

GKE Autopilot is the preferred solution for new Braintrust deployments. GKE Standard is supported when customer requirements prevent Autopilot use.

Braintrust requires local SSDs for maximum disk performance. Configuration varies depending on whether you're using GKE Autopilot or Standard mode.

### GKE Autopilot
Expand Down Expand Up @@ -150,7 +152,13 @@ brainstore:

### GKE Standard Mode

For Standard mode clusters, create node pools with local SSDs, then deploy:
The Terraform module creates separate services and Brainstore pools. Brainstore uses a bundled Local SSD machine type without a disk count input.

The stable `braintrust/node-pool` label connects Helm selectors to replacement pools with generated GKE names.
The Terraform map key sets the label. The `services` and `brainstore` keys match the selectors below.

The example enables PDBs for node eviction. Existing pools need deletion protection before a hardware replacement.
See [Optional disruption budgets](#optional-disruption-budgets) for the preparation sequence and limits.

**Configure the Helm chart:**
```yaml
Expand All @@ -159,10 +167,24 @@ For Standard mode clusters, create node pools with local SSDs, then deploy:
google:
mode: "standard"

api:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
nodeSelector:
braintrust/node-pool: "services"

aiGateway:
nodeSelector:
braintrust/node-pool: "services"

brainstore:
reader:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
nodeSelector:
cloud.google.com/gke-nodepool: "brainstore" # Target your node pool
braintrust/node-pool: "brainstore"
resources:
requests:
cpu: "44"
Expand All @@ -179,9 +201,18 @@ For Standard mode clusters, create node pools with local SSDs, then deploy:
- brainstore-reader
- brainstore-writer
topologyKey: kubernetes.io/hostname
fastreader:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
nodeSelector:
braintrust/node-pool: "brainstore"
writer:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
nodeSelector:
cloud.google.com/gke-nodepool: "brainstore"
braintrust/node-pool: "brainstore"
resources:
requests:
cpu: "44"
Expand All @@ -200,10 +231,13 @@ For Standard mode clusters, create node pools with local SSDs, then deploy:
topologyKey: kubernetes.io/hostname
```

**What happens:**
- Pods are scheduled on your pre-configured node pools
- Local SSDs are automatically available via emptyDir volumes
- Pod anti-affinity ensures readers and writers don't share nodes (each pod gets dedicated node access)
The configuration has these effects:

- Pods use the stable labels to select eligible nodes.
- Brainstore uses Local SSD storage through `emptyDir` volumes.
- Anti-affinity separates readers and writers across nodes. It does not exclude other workloads from those nodes.
- Separate PDBs limit voluntary evictions for each role.
- A single writer can stop briefly during a drain.

## AWS EKS Local Storage

Expand Down Expand Up @@ -363,10 +397,45 @@ creating replacements. This causes a complete role outage and, with the default
single writer, pauses background processing until the replacement becomes
Ready.

These settings pace Deployment-managed rollouts only. The chart does not
currently create PodDisruptionBudgets for Brainstore, so these controls do not
limit voluntary disruptions such as node drains or protect against involuntary
pod or node failures.
These settings pace Deployment rollouts only. Optional PodDisruptionBudgets protect voluntary evictions such as node drains.
Neither mechanism protects against node failure.

## Optional disruption budgets

Brainstore readers, fast readers, and writers each support an optional PDB.
PDBs default to disabled on every cloud. Each enabled role defaults to `maxUnavailable: 1`.
The budget permits a single writer to stop briefly. Replicated roles retain all but one healthy replica during permitted evictions.

```yaml
brainstore:
reader:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
fastreader:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
writer:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
api:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
```

The API retains its existing `minAvailable` behavior unless `maxUnavailable` is set. An explicit maximum takes precedence over the inherited minimum.
Each role has an independent budget. Evictions can proceed simultaneously across roles.
Readiness probes determine healthy replicas for PDBs. Deployment `minReadySeconds` does not delay PDB eviction permission.
The GKE Standard example enables these budgets. Existing EKS and AKS defaults remain unchanged.
GKE requires deletion protection on the source pool before a replacement, and its PDB protection expires after one hour.

1. Deploy the Helm budgets before a node pool replacement.
2. Enable GKE deletion protection on existing pools in a separate Terraform apply before hardware changes.
3. Verify sufficient replicas and replacement capacity.


## Testing

Expand Down
43 changes: 33 additions & 10 deletions braintrust/examples/google-standard/values.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# GKE Autopilot is preferred for new deployments.
# Use this supported Standard example when customer requirements prevent Autopilot use.
# Sample values for GKE Standard deployment
#
# GKE Standard requires manual node pool configuration:
# - Create a dedicated node pool with local NVMe SSDs for Brainstore workloads
# - Recommended machine types: c4-standard-32 or higher with local SSDs
# - Configure local SSDs: Use 4x375GB local SSDs (1500GB total) or more
# - Total local SSD capacity should exceed the volume.size configured below
# Terraform creates the services and brainstore node pools.
# Brainstore requires a bundled Local SSD machine type, such as c4a-standard-48-lssd.
# GKE selects the fixed SSD count from the machine type.
# Enable PDB protection on existing pools before a hardware replacement.
# Deploy these PDBs before that replacement.

# Global configs
global:
Expand All @@ -28,6 +30,9 @@ api:
annotations:
service:
networking.gke.io/load-balancer-type: "Internal"
podDisruptionBudget:
enabled: true
maxUnavailable: 1
replicas: 4
# Uncomment the following section to use a different image or tag from the version in the Helm release
#image:
Expand All @@ -43,7 +48,7 @@ api:
# Native GCS authentication via Workload Identity. Set to true to use GCS natively instead of S3-compatible access.
enableGcsAuth: false
nodeSelector:
cloud.google.com/gke-nodepool: "api"
braintrust/node-pool: "services"
resources:
requests:
cpu: "4"
Expand All @@ -56,6 +61,10 @@ api:
- name: AWS_REGION
value: "us-central1"

aiGateway:
nodeSelector:
braintrust/node-pool: "services"

# Brainstore configuration (split into reader and writer)
brainstore:
serviceAccount:
Expand All @@ -70,6 +79,9 @@ brainstore:

# Brainstore Reader configuration
reader:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
name: "brainstore-reader"
replicas: 2
service:
Expand All @@ -78,7 +90,7 @@ brainstore:
port: 4000
portName: http
nodeSelector:
cloud.google.com/gke-nodepool: "brainstore" # Target your node pool
braintrust/node-pool: "brainstore"
resources:
requests:
cpu: "16"
Expand All @@ -104,9 +116,21 @@ brainstore:
volume:
size: "200Gi"
extraEnvVars:


# Brainstore Fast Reader configuration
fastreader:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
nodeSelector:
braintrust/node-pool: "brainstore"

# Brainstore Writer configuration
# One writer can stop briefly during a node drain.
writer:
podDisruptionBudget:
enabled: true
maxUnavailable: 1
name: "brainstore-writer"
replicas: 1
service:
Expand All @@ -115,7 +139,7 @@ brainstore:
port: 4000
portName: http
nodeSelector:
cloud.google.com/gke-nodepool: "brainstore"
braintrust/node-pool: "brainstore"
resources:
requests:
cpu: "32"
Expand All @@ -141,4 +165,3 @@ brainstore:
volume:
size: "200Gi"
extraEnvVars:

4 changes: 4 additions & 0 deletions braintrust/templates/api-pdb.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if hasKey $api.podDisruptionBudget "maxUnavailable" }}
maxUnavailable: {{ $api.podDisruptionBudget.maxUnavailable | toYaml | trim }}
{{- else }}
minAvailable: {{ $api.podDisruptionBudget.minAvailable }}
{{- end }}
selector:
matchLabels:
app: {{ $api.name }}
Expand Down
25 changes: 25 additions & 0 deletions braintrust/templates/brainstore-pdb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{{- $root := . -}}
{{- $rendered := 0 -}}
{{- range $role := list "reader" "fastreader" "writer" -}}
{{- $config := index $root.Values.brainstore $role -}}
{{- if $config.podDisruptionBudget.enabled }}
{{- if gt $rendered 0 }}
---
{{- end }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $config.name }}
namespace: {{ include "braintrust.namespace" $root }}
{{- with (merge (deepCopy $config.labels) $root.Values.global.labels) }}
labels:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
maxUnavailable: {{ $config.podDisruptionBudget.maxUnavailable | toYaml | trim }}
selector:
matchLabels:
app: {{ $config.name }}
{{- $rendered = add1 $rendered -}}
{{- end }}
{{- end }}
Loading
Loading