External S3
Use objectStorage.kind: external when object storage is provided by your cloud or platform team — AWS S3, Google Cloud Storage (S3-compatible HMAC keys), Azure Blob (S3 gateway), Backblaze B2, Wasabi, an external SeaweedFS cluster, MinIO, JFrog Artifactory S3 backend, etc.
The chart stops rendering SeaweedFS (seaweedfs.deploy: false) and wires every consumer to your endpoint via S3_INTERNAL_HOST, S3_HOST, and S3_ACCESS_KEY / S3_SECRET_KEY in the platform ConfigMap/Secret.
When to use which backend
objectStorage.kind | Chart renders | Best for |
|---|---|---|
seaweedfs (default) | SeaweedFS subchart + bucket-init Job | Self-contained installs, air-gap friendly, namespace-scoped |
external | Nothing — env vars + credentials only | Enterprise managed S3, existing object-store fleet, multi-env shared bucket account |
The legacy minio kind was removed — helm template fail-fasts if an old overlay still sets it.
Requirements
S3 API compatibility
| Requirement | Detail |
|---|---|
| API | S3-compatible REST API (SigV4 signing). All platform consumers use boto3 / aws-sdk-go paths. |
| Path-style vs virtual-hosted | Some backends (SeaweedFS, MinIO) prefer path-style addressing. If you see redirect loops or BucketRegionError, set AWS_S3_FORCE_PATH_STYLE=true in consumer overlays or front the bucket with a path-based gateway. |
| Region label | objectStorage.region / S3_REGION must be non-empty (many backends ignore the value but SDKs require it). Use the provider’s region string (e.g. us-east-1, auto). |
| TLS | In-cluster traffic should use TLS where policy requires it. Set internalHost to https://… when the endpoint expects HTTPS. |
| Multipart uploads | Workspace artifacts and Mimir blocks use multipart PUTs — ensure load balancers allow large bodies (no accidental client-body-buffer-size: "0" on nginx). |
Network
| Path | Purpose |
|---|---|
objectStorage.external.internalHost | Host:port (or URL) pods use for S3 API calls (e.g. s3.us-east-1.amazonaws.com, 10.0.0.5:9000, VPC endpoint DNS) |
objectStorage.external.host | Public hostname baked into presigned URLs for browser/runner uploads (e.g. s3.example.com, CloudFront distribution, or same as internal if runners reach it) |
At least one of host or internalHost must be set — the chart fail-fasts otherwise.
Credentials
Access key and secret are rendered into <release>-platform-secrets as S3_ACCESS_KEY and S3_SECRET_KEY. Supply them via:
objectStorage.accessKey/objectStorage.secretKeyin values (lab only), orsecrets.values.s3AccessKey/secrets.values.s3SecretKey, or- Pre-populate the platform Secret out-of-band (ExternalSecrets, Vault CSI)
The IAM (or equivalent) principal needs read/write/list on every bucket below.
Buckets (operator-provisioned)
Unlike bundled SeaweedFS, the chart does not create buckets when kind=external. Pre-create each bucket before install or first upload:
| Bucket (default name) | Purpose |
|---|---|
shared-workspace | Workspace file uploads, presigned URL target (SHARED_WORKSPACE_BUCKET) |
mimir-blocks | Mimir TSDB block storage |
mimir-ruler | Mimir ruler state |
mimir-alertmanager | Mimir Alertmanager state |
agentfarm-artifacts | AgentFarm ADK chat artifacts (when agentfarm.artifacts.serviceType=s3) |
postgres-backups | Spilo WAL-G (only if postgresql.kind=spilo and WAL-G enabled) |
vault-backups | Vault backup CronJob (when vault.backup.enabled) |
Names are configurable via objectStorage.buckets[] and related sub-keys — the table lists chart defaults. Keep names DNS-compatible (lowercase, hyphens).
Helm configuration example
Turn off SeaweedFS and point at external S3:
objectStorage: kind: external region: us-east-1 accessKey: "" # prefer secrets.values or BYO Secret secretKey: "" sharedWorkspaceBucket: shared-workspace buckets: - shared-workspace - mimir-blocks - mimir-ruler - mimir-alertmanager - agentfarm-artifacts - postgres-backups - vault-backups external: backend: s3 # logical label; app enum accepts gcs|minio — external S3 uses minio client path host: s3.us-east-1.amazonaws.com internalHost: s3.us-east-1.amazonaws.com region: us-east-1 bucket: "" # optional; defaults to sharedWorkspaceBucket
seaweedfs: deploy: false
secrets: method: k8s-secret values: s3AccessKey: "AKIA..." s3SecretKey: "..."Example: AWS S3 (private cluster egress)
objectStorage: kind: external region: us-east-1 external: backend: s3 # Runners/browsers hit the regional endpoint in presigned URLs host: s3.us-east-1.amazonaws.com # Pods use the same endpoint (or a VPC interface endpoint hostname) internalHost: s3.us-east-1.amazonaws.com region: us-east-1
seaweedfs: deploy: falseIAM policy sketch (adjust ARNs):
{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"], "Resource": [ "arn:aws:s3:::shared-workspace", "arn:aws:s3:::shared-workspace/*", "arn:aws:s3:::mimir-blocks", "arn:aws:s3:::mimir-blocks/*" ] }]}Repeat Resource entries for every bucket in objectStorage.buckets[].
Example: External SeaweedFS or MinIO (in another namespace)
objectStorage: kind: external region: us-east-1 external: backend: s3 host: s3.storage.example.com # public DNS for presigned URLs internalHost: seaweedfs-s3.storage.svc.cluster.local:8333 region: us-east-1
seaweedfs: deploy: false
ingress: enabled: true services: s3: # Chart S3 Ingress renders only when seaweedfs.deploy=true. # For external backends, expose `host` yourself (corporate ingress, LB, or DNS to the external gateway). proxyBodySize: "0" clientBodyBufferSize: "1m"Ensure the external gateway’s credentials match S3_ACCESS_KEY / S3_SECRET_KEY in platform secrets.
Public presigned URLs
PAPI generates presigned URLs using S3_HOST (from objectStorage.external.host). When seaweedfs.deploy: false, the chart does not render the s3.<domain> Ingress — you must:
- Set
external.hostto a hostname runners and browsers can reach, and - Route that hostname to your S3 gateway (corporate ingress, cloud front door, or provider public endpoint)
If runners only reach the internal endpoint, presigned URLs that embed the public host will fail unless network policy allows the public path.
Post-upgrade rollout
Changing objectStorage.kind updates ConfigMaps/Secrets but does not automatically restart Deployments. After helm upgrade, restart workloads that mount platform config:
kubectl -n runwhen-platform rollout restart deployment -l app.kubernetes.io/instance=rw-platform# Include StatefulSets (mimir, etc.) as neededVerify after install
- ConfigMap —
kubectl get cm rw-platform-platform-config -o yaml | rg S3_ - Secret —
S3_ACCESS_KEYpresent inrw-platform-platform-secrets - Smoke PUT — from a debug pod:
aws --endpoint-url https://$INTERNAL s3 ls s3://shared-workspace/(install aws-cli image) - UI upload — workspace file upload via presigned URL (validates
S3_HOST) - Mimir — blocks appearing in
mimir-blocksafter metrics flow
Troubleshooting
| Symptom | Likely cause |
|---|---|
objectStorage.kind=external requires … host or internalHost | Missing external.host / external.internalHost |
| Presigned URL 403/404 | Wrong host, IAM policy, or bucket missing |
| Upload timeout via ingress | nginx client-body-buffer-size: "0" or body size limit — use positive buffer + proxy-body-size: "0" |
| Mimir empty after restart | Unrelated local-path bug if on old chart — ensure Mimir PVC paths pinned (see chart release notes) |
STORAGE_BACKEND validation errors | Chart maps external S3 to minio client path; use STORAGE_BACKEND_KIND for observability |
Related documentation
| Topic | Link |
|---|---|
| Self-hosted prerequisites | Self-Hosted Deployment Requirements |
| External PostgreSQL (often paired) | External PostgreSQL |
| JFrog / air-gap registry | JFrog Registry Setup |
| GAR setup | GAR Registry Setup |