Repository navigation
Add Loop Runtime on GKE with isolated Sandbox Service workers #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: jeff/gke-standard
Are you sure you want to change the base?
Changes from all commits
0edb5b1
2171be4
a524285
729bce4
6345ce6
6d76bdb
494f50d
04312d1
e002b6d
be9d997
5545930
1e15953
028a2a2
3b5396f
b017ecc
a4903f4
bbfd6d8
7d83d40
a447ce7
d6bfc19
7196a6f
5c194cc
6337c84
0939ef0
5472a43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Loop Runtime on GKE | ||
|
|
||
| Loop Runtime runs in the primary GKE cluster. Its sandboxes run on Sandbox | ||
| Service workers in a separate isolated cluster. Both clusters come from the | ||
| GCP Terraform module with: | ||
|
|
||
| ```hcl | ||
| gke_isolated_workers = { | ||
| network_cidr = "10.64.0.0/16" # must not overlap existing ranges | ||
| authorized_cidrs = ["192.0.2.10/32"] # replace with operator and CI egress ranges | ||
| discovery = { dns_name = "sandbox.<deployment>.internal" } | ||
| } | ||
| ``` | ||
|
|
||
| `<...>` values below come from `terraform output gke_isolated_workers` and | ||
| `terraform output loop_runtime_service_account`. | ||
|
|
||
| Use compatible API, Brainstore, and AI Gateway images for the runtime build. | ||
| The API must report `ff_version` 82 or newer, and the Rust gateway must support | ||
| `x-bt-provider-model-map`. | ||
| Deploy the gateway with `aiGateway.enabled=true` first and wait for it to become | ||
| ready before applying this example, which turns on `aiGateway.useGateway`. | ||
| The TypeScript API proxy alone cannot restore the provider model IDs sent by Codex. | ||
|
|
||
| 1. Create the mTLS Secrets in both clusters: | ||
|
|
||
| ```sh | ||
| braintrust/examples/gke-loop-runtime/create-sandbox-tls.sh "<discovery.hostname>" <worker> <primary> | ||
| ``` | ||
|
|
||
| 2. Install the workers. The configured Sandbox Service image prepares local SSDs before starting the worker: | ||
|
|
||
| ```sh | ||
| helm --kube-context <worker> upgrade --install sandbox-service \ | ||
| https://braintrust-cf-us-east-1.s3.amazonaws.com/sandbox-service/1f4b6826c61064575b3db956b67625133257bd8a6e49e4ea86cdb55012dd53a7/sandbox-service-0.1.0.tgz \ | ||
| --namespace braintrust-sandbox --values braintrust/examples/gke-loop-runtime/sandbox-service.yaml --wait | ||
| ``` | ||
|
|
||
| 3. Install this chart with your GKE values plus `values.yaml` from this directory. | ||
| For a Standard primary cluster, set `loopRuntime.nodeSelector` to | ||
| `{"braintrust/workload": "services"}` to use the services pool: | ||
|
|
||
| ```sh | ||
| helm --kube-context <primary> upgrade --install braintrust ./braintrust --namespace braintrust \ | ||
| --values <your GKE values> --values braintrust/examples/gke-loop-runtime/values.yaml --wait | ||
| ``` | ||
|
|
||
| Workers run at a fixed count. One `c4-standard-8-lssd` worker holds about seven | ||
| 1 vCPU / 2 GiB sandboxes; beyond that, new sandboxes fail with | ||
| `CapacityExhausted`. Raise `replicas` in `sandbox-service.yaml` to add workers. | ||
|
|
||
| Each runtime pod claims at most two conversations and has a 256Mi in-memory cache budget. | ||
| Sandbox administration is disabled by default. Set `loopRuntime.enableSandboxAdmin: true` | ||
| to allow authorized sysadmins to inspect and terminate sandboxes. | ||
|
|
||
| To shut down, set the worker release to `replicas: 0`, wait for the workers to | ||
| drain, then disable `loopRuntime.enabled` and uninstall the worker release. Its | ||
| pre-delete hook refuses to uninstall until workers drain. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| #!/bin/bash | ||
| # Creates the mTLS Secrets for Loop Runtime and Sandbox Service, matching the | ||
| # AWS module: separate server and client CAs, P-256 keys, ten-year validity. | ||
| # | ||
| # Usage: create-sandbox-tls.sh <worker hostname> <worker kube context> <runtime kube context> | ||
| # worker hostname: Terraform output gke_isolated_workers.discovery.hostname | ||
| set -euo pipefail | ||
|
|
||
| if [ "$#" -ne 3 ]; then | ||
| echo "usage: $0 <worker hostname> <worker kube context> <runtime kube context>" >&2 | ||
| exit 2 | ||
| fi | ||
| HOSTNAME_SAN="$1" | ||
| WORKER_CONTEXT="$2" | ||
| RUNTIME_CONTEXT="$3" | ||
| WORKER_NAMESPACE="${WORKER_NAMESPACE:-braintrust-sandbox}" | ||
| RUNTIME_NAMESPACE="${RUNTIME_NAMESPACE:-braintrust}" | ||
| DAYS=3650 | ||
|
|
||
| for context in "$WORKER_CONTEXT" "$RUNTIME_CONTEXT"; do | ||
| kubectl --context "$context" get namespaces >/dev/null | ||
| done | ||
| # sandbox-tls is written last, so a failed run can be rerun. | ||
| if kubectl --context "$WORKER_CONTEXT" -n "$WORKER_NAMESPACE" get secret sandbox-tls >/dev/null 2>&1; then | ||
| echo "sandbox-tls already exists in $WORKER_CONTEXT/$WORKER_NAMESPACE; refusing to replace it." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| dir="$(mktemp -d)" | ||
| trap 'rm -rf "$dir"' EXIT | ||
| cd "$dir" | ||
|
|
||
| for purpose in server client; do | ||
| openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$purpose-ca.key" 2>/dev/null | ||
| openssl req -x509 -new -sha256 -key "$purpose-ca.key" -days "$DAYS" \ | ||
| -subj "/CN=sandbox-$purpose-ca" \ | ||
| -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \ | ||
| -addext "keyUsage=critical,keyCertSign,cRLSign" \ | ||
| -out "$purpose-ca.pem" | ||
| openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out "$purpose.key" 2>/dev/null | ||
| { | ||
| echo "basicConstraints=critical,CA:FALSE" | ||
| echo "keyUsage=critical,digitalSignature" | ||
| echo "authorityKeyIdentifier=keyid" | ||
| if [ "$purpose" = server ]; then | ||
| echo "extendedKeyUsage=serverAuth" | ||
| echo "subjectAltName=DNS:$HOSTNAME_SAN" | ||
| else | ||
| echo "extendedKeyUsage=clientAuth" | ||
| fi | ||
| } >"$purpose.ext" | ||
| openssl req -new -key "$purpose.key" -subj "/CN=sandbox-$purpose" | | ||
| openssl x509 -req -sha256 -days "$DAYS" -CA "$purpose-ca.pem" -CAkey "$purpose-ca.key" \ | ||
| -set_serial "0x$(openssl rand -hex 16)" -extfile "$purpose.ext" -out "$purpose.pem" 2>/dev/null | ||
| done | ||
|
|
||
| apply_secret() { | ||
| kubectl --context "$1" create namespace "$2" --dry-run=client -o yaml | kubectl --context "$1" apply --server-side -f - >/dev/null | ||
| kubectl --context "$1" -n "$2" create secret generic "$3" "${@:4}" --dry-run=client -o yaml | | ||
| kubectl --context "$1" apply --server-side -f - | ||
| kubectl --context "$1" -n "$2" annotate secret "$3" kubectl.kubernetes.io/last-applied-configuration- | ||
| } | ||
|
|
||
| # The worker controller and Loop Runtime present the client certificate and trust the server CA. | ||
| for target in "$RUNTIME_CONTEXT $RUNTIME_NAMESPACE" "$WORKER_CONTEXT $WORKER_NAMESPACE"; do | ||
| # shellcheck disable=SC2086 | ||
| apply_secret $target sandbox-client-tls \ | ||
| --from-file=client.pem=client.pem --from-file=client.key=client.key --from-file=server-ca.pem=server-ca.pem | ||
| done | ||
| # Workers present the server certificate and trust the client CA. | ||
| apply_secret "$WORKER_CONTEXT" "$WORKER_NAMESPACE" sandbox-tls \ | ||
| --from-file=cert.pem=server.pem --from-file=key.pem=server.key --from-file=ca_cert.pem=client-ca.pem |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Values for the Sandbox Service worker chart, installed in the isolated worker cluster. | ||
| # Every <...> value comes from the GCP Terraform output gke_isolated_workers. | ||
| hostname: "<discovery.hostname>" | ||
| image: public.ecr.aws/braintrust/sandbox-service:38b05599cdc5e4c3a213bbf0718211b397183a7a | ||
| # Fixed worker capacity. The worker node pool autoscales, so raising this is a values change. | ||
| replicas: 1 | ||
| gitResources: true | ||
| nodePrep: | ||
| enabled: true | ||
| prewarmTemplates: | ||
| - image: public.ecr.aws/braintrust/codex-runtime@sha256:490ab364fd6a787f73957d3d46bc5e173fb418c47d53bcf7a2d3afba8b419049 | ||
| resources: | ||
| vcpu_count: 1 | ||
| memory_mib: 2048 | ||
| mounts: [] | ||
| durable_file_systems: [] | ||
| policy: | ||
| networking: | ||
| type: disabled | ||
| credentials: [] | ||
| default_workdir: /home/exo/workspace | ||
| externalDns: | ||
| enabled: true | ||
| provider: google | ||
| # One entry per argument in discovery.external_dns_extra_args. | ||
| extraArgs: | ||
| - "<argument>" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Layer on top of your existing GKE values (for example, ../google-autopilot/values.yaml). | ||
| # The sandbox and service-account values come from the GCP Terraform module outputs. | ||
| aiGateway: | ||
| enabled: true | ||
| useGateway: true | ||
|
|
||
| loopRuntime: | ||
| enabled: true | ||
| serviceAccount: | ||
| googleServiceAccount: "<loop_runtime_service_account>" | ||
| sandbox: | ||
| dnsUrl: "<gke_isolated_workers.discovery.endpoint>" | ||
| egressGateway: | ||
| address: "<gke_isolated_workers.loop_egress.address>" | ||
| # One entry per range in gke_isolated_workers.loop_egress.source_ranges. | ||
| sourceRanges: | ||
| - "<range>" | ||
| url: "<gke_isolated_workers.loop_egress.url>" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,6 +146,61 @@ 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 -}} | ||
|
|
||
| {{/* | ||
| Whether Loop Runtime is enabled. Values reused from a chart release that | ||
| predates Loop Runtime have no loopRuntime map, which means disabled. | ||
| */}} | ||
| {{- define "braintrust.loopRuntime.enabled" -}} | ||
| {{- if (.Values.loopRuntime | default dict).enabled -}}true{{- end -}} | ||
| {{- end -}} | ||
|
|
||
| {{/* Validate the Loop Runtime configuration. */}} | ||
| {{- define "braintrust.loopRuntime.validate" -}} | ||
| {{- if include "braintrust.loopRuntime.enabled" . -}} | ||
| {{- range $key := list "sandbox" "image" "service" "serviceAccount" "resources" "annotations" -}} | ||
| {{- if not (kindIs "map" (get $.Values.loopRuntime $key)) -}} | ||
| {{- fail "loopRuntime defaults are missing. Values reused from a chart release that predates Loop Runtime omit them; upgrade with --reset-then-reuse-values." -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
| {{- if ne .Values.cloud "google" -}} | ||
|
Comment on lines
+159
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| {{- fail "loopRuntime.enabled requires cloud: google. On AWS, Loop Runtime runs on ECS through the Terraform module." -}} | ||
| {{- end -}} | ||
|
Comment on lines
+165
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an existing installation retains an older API image such as AGENTS.md reference: AGENTS.md:L7-L11 Useful? React with 👍 / 👎. |
||
| {{- $sandbox := .Values.loopRuntime.sandbox -}} | ||
| {{- $egress := $sandbox.egressGateway | default dict -}} | ||
| {{- $_ := required "objectStorage.google.brainstoreBucket is required" .Values.objectStorage.google.brainstoreBucket -}} | ||
| {{- $_ := required "objectStorage.google.apiBucket is required" .Values.objectStorage.google.apiBucket -}} | ||
| {{- $_ := required "loopRuntime.image.repository is required" .Values.loopRuntime.image.repository -}} | ||
| {{- $_ := required "loopRuntime.image.tag is required" (.Values.loopRuntime.image.tag | default "" | toString) -}} | ||
| {{- $_ := required "loopRuntime.serviceAccount.googleServiceAccount is required" .Values.loopRuntime.serviceAccount.googleServiceAccount -}} | ||
| {{- $_ := required "loopRuntime.sandbox.dnsUrl is required" $sandbox.dnsUrl -}} | ||
| {{- $_ := required "loopRuntime.sandbox.image is required" $sandbox.image -}} | ||
| {{- $_ := required "loopRuntime.sandbox.egressGateway.address is required" $egress.address -}} | ||
| {{- $_ := required "loopRuntime.sandbox.egressGateway.url is required" $egress.url -}} | ||
|
Comment on lines
+174
to
+178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Terraform output is missing or a templated value resolves to whitespace, values such as AGENTS.md reference: AGENTS.md:L7-L7 Useful? React with 👍 / 👎. |
||
| {{- if not $egress.sourceRanges -}} | ||
| {{- fail "loopRuntime.sandbox.egressGateway.sourceRanges is required" -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
|
|
||
| {{/* Internal cluster URL for the Loop Runtime service. */}} | ||
| {{- define "braintrust.loopRuntimeInternalUrl" -}} | ||
| http://{{ .Values.loopRuntime.service.name | default .Values.loopRuntime.name }}.{{ include "braintrust.namespace" . }}:{{ .Values.loopRuntime.service.port }} | ||
| {{- end -}} | ||
|
|
||
| {{/* Product-owned Loop Runtime routes for an Istio VirtualService. */}} | ||
| {{- define "braintrust.loopRuntimeVirtualServiceRoutes" -}} | ||
| - match: | ||
| - uri: | ||
| exact: /loop/runtime | ||
| - uri: | ||
| prefix: /loop/runtime/ | ||
| route: | ||
| - destination: | ||
| host: {{ .Values.loopRuntime.service.name | default .Values.loopRuntime.name }} | ||
| port: | ||
| number: {{ .Values.loopRuntime.service.port }} | ||
| {{- end -}} | ||
|
|
||
| {{/* | ||
| Validate API autoscaling prerequisites (GKE + AutoscalingMetric CRD). | ||
| */}} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| {{- include "braintrust.loopRuntime.validate" . -}} | ||
| {{- if include "braintrust.loopRuntime.enabled" . -}} | ||
| {{- $bucket := printf "gs://%s" .Values.objectStorage.google.brainstoreBucket -}} | ||
| {{- $sandbox := .Values.loopRuntime.sandbox -}} | ||
| apiVersion: v1 | ||
| kind: ConfigMap | ||
| metadata: | ||
| name: {{ .Values.loopRuntime.name }} | ||
| namespace: {{ include "braintrust.namespace" . }} | ||
| {{- with (mergeOverwrite (deepCopy .Values.global.labels) (deepCopy .Values.loopRuntime.labels)) }} | ||
| labels: | ||
| {{- toYaml . | nindent 4 }} | ||
| {{- end }} | ||
| {{- with .Values.loopRuntime.annotations.configmap }} | ||
| annotations: | ||
| {{- toYaml . | nindent 4 }} | ||
| {{- end }} | ||
| # Matches the ECS Loop Runtime environment in the AWS Terraform module. | ||
| data: | ||
| BRAINTRUST_LEGACY_IDS: "true" | ||
| LOOP_RUNTIME_HOST: "0.0.0.0" | ||
| LOOP_RUNTIME_PORT: {{ .Values.loopRuntime.service.port | quote }} | ||
| LOOP_RUNTIME_DRAIN_TIMEOUT_SECONDS: "90" | ||
| LOOP_RUNTIME_REDIS_NAMESPACE: "loop-runtime:{{ .Release.Name }}" | ||
| LOOP_RUNTIME_STATE_DIR: /mnt/tmp/loop-runtime | ||
| {{- /* The runtime refuses an empty ORG_NAME; the API treats empty as every org. */}} | ||
| ORG_NAME: {{ .Values.global.orgName | default "" | toString | trim | default "*" | quote }} | ||
| BRAINTRUST_API_URL: {{ include "braintrust.apiInternalUrl" . | quote }} | ||
| LOOP_RUNTIME_AI_PROXY_URL: {{ include "braintrust.apiAiProxyInternalUrl" . | quote }} | ||
| LOOP_RUNTIME_BRAINSTORE_READER_URL: "http://{{ .Values.brainstore.fastreader.service.name | default .Values.brainstore.fastreader.name }}.{{ include "braintrust.namespace" . }}:{{ .Values.brainstore.fastreader.service.port }}" | ||
| BRAINSTORE_VERBOSE: "1" | ||
| BRAINSTORE_INDEX_URI: "{{ $bucket }}/brainstore/index" | ||
| BRAINSTORE_REALTIME_WAL_URI: "{{ $bucket }}/brainstore/wal" | ||
| BRAINSTORE_CODE_BUNDLE_URI: "gs://{{ .Values.objectStorage.google.apiBucket }}" | ||
| BRAINSTORE_CACHE_DIR: /mnt/tmp/brainstore | ||
| BRAINSTORE_OBJECT_STORE_CACHE_FILE_SIZE: 5Gi | ||
| BRAINSTORE_OBJECT_STORE_CACHE_MEMORY_LIMIT: 256Mi | ||
| LOOP_RUNTIME_MAX_CLAIMED_CONVERSATIONS: "2" | ||
| BRAINSTORE_CONTROL_PLANE_TELEMETRY: {{ .Values.global.controlPlaneTelemetry | quote }} | ||
| NO_COLOR: "1" | ||
| EXO_SANDBOX_SERVICE_DNS_URL: {{ $sandbox.dnsUrl | quote }} | ||
| EXO_SANDBOX_SERVICE_IMAGE: {{ $sandbox.image | quote }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an enabled deployment sets AGENTS.md reference: AGENTS.md:L15-L17 Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the required sandbox image check in 5472a43. Empty and null/missing image values fail when Loop is enabled; disabled Loop ignores missing sandbox configuration. The chart checks non-empty egress ranges and leaves CIDR validation to Kubernetes. |
||
| EXO_SANDBOX_VCPU_COUNT: {{ $sandbox.vcpuCount | quote }} | ||
| EXO_SANDBOX_MEMORY_MIB: {{ $sandbox.memoryMib | quote }} | ||
|
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L15-L17 Useful? React with 👍 / 👎. |
||
| SANDBOX_SERVICE_TLS_CERT: /etc/sandbox-tls/client.pem | ||
| SANDBOX_SERVICE_TLS_KEY: /etc/sandbox-tls/client.key | ||
| SANDBOX_SERVICE_TLS_CA: /etc/sandbox-tls/server-ca.pem | ||
| LOOP_RUNTIME_SANDBOX_FORWARD_PROXY_LISTEN: "0.0.0.0:4002" | ||
| LOOP_RUNTIME_SANDBOX_EGRESS_GATEWAY_URL: {{ $sandbox.egressGateway.url | quote }} | ||
| LOOP_RUNTIME_SANDBOX_SERVICE_EGRESS_GATEWAY_URL: {{ $sandbox.egressGateway.url | quote }} | ||
| LOOP_RUNTIME_SANDBOX_EGRESS_MODE: restricted | ||
| LOOP_RUNTIME_ENABLE_SANDBOX_ADMIN: {{ .Values.loopRuntime.enableSandboxAdmin | default false | quote }} | ||
| {{- with (.Values.global.allowedOrgIds | default "" | toString | trim) }} | ||
| ALLOWED_ORG_IDS: {{ . | quote }} | ||
| {{- end }} | ||
| {{- if eq .Values.brainstore.locksBackend "objectStorage" }} | ||
| BRAINSTORE_LOCKS_URI: "{{ $bucket }}/brainstore/locks" | ||
| {{- end }} | ||
| {{- if .Values.brainstoreWalFooterVersion }} | ||
| BRAINSTORE_WAL_FOOTER_VERSION: {{ .Values.brainstoreWalFooterVersion | quote }} | ||
| {{- end }} | ||
| {{- if .Values.skipPgForBrainstoreObjects }} | ||
| SKIP_PG_FOR_BRAINSTORE_OBJECTS: {{ .Values.skipPgForBrainstoreObjects | quote }} | ||
| {{- end }} | ||
| {{- if or .Values.brainstoreWalFooterVersion .Values.skipPgForBrainstoreObjects }} | ||
| BRAINSTORE_WAL_USE_EFFICIENT_FORMAT: "true" | ||
| {{- end }} | ||
| {{- end }} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a pre-Loop-Runtime release is upgraded with
--reuse-valuesand partial overrides supply each outer map but omit nested defaults such asimage.pullPolicy,service.type,service.portName, orserviceAccount.name, this guard passes because it checks only the map types. The templates then emit empty Kubernetes fields or an unnamed ServiceAccount, so admission fails instead of returning the intended--reset-then-reuse-valuesguidance; validate the required nested structure or overlay template defaults before rendering.AGENTS.md reference: AGENTS.md:L14-L17
Useful? React with 👍 / 👎.