Google Cloud Storage (GCS)
Use native Google Cloud Storage when running the RunWhen Platform on GKE and object storage is provided by GCS. Every storage consumer authenticates with Application Default Credentials (ADC) through GKE Workload Identity — no access keys, no HMAC credentials, and nothing to rotate.
This is the recommended path on GKE. If you need S3-compatible access instead (non-GKE clusters, or a mixed fleet standardized on the S3 API), see External S3.
Native GCS vs S3-interop
GCS can be consumed two ways. Pick one before you start.
| Approach | objectStorage.external.backend | Credentials | Best for |
|---|---|---|---|
| Native GCS (recommended) | gcs | None — ADC via Workload Identity | GKE clusters; keyless operation |
| S3-interop | s3 | HMAC access key + secret | Non-GKE clusters; standardizing on the S3 API |
With native GCS, storage consumers use the Google Cloud Storage API directly. With S3-interop, they use GCS’s S3-compatible XML API and require HMAC keys — follow the External S3 guide and point the endpoint at storage.googleapis.com.
The rest of this page covers native GCS.
Requirements
Buckets (operator-provisioned)
The chart does not create buckets for external backends. Pre-create each bucket before install.
| Purpose | Chart value | Required when |
|---|---|---|
| Workspace file uploads | objectStorage.external.bucket | Always |
| Metrics block storage | metricstore.buckets.blocks | metricstore.deploy: true |
| Metrics rule state | metricstore.buckets.ruler | metricstore.deploy: true |
| Metrics alert state | metricstore.buckets.alertmanager | metricstore.deploy: true |
| AI Assistant chat artifacts | agentfarm.artifacts.gcsBucket | agentfarm.artifacts.serviceType: gcs |
| Vault backups | vault.backup.bucket | vault.backup.enabled: true |
| PostgreSQL backups | postgresql.spilo.walg.bucket | postgresql.kind: spilo with WAL-G enabled |
GCS bucket names are globally unique. Unlike bundled SeaweedFS, where bucket names are namespace-local, GCS bucket names are unique across all of Google Cloud. The chart’s default names (
shared-workspace,mimir-blocks, …) will not be available. Prefix every bucket with the release name — for example${RELEASE}-shared-workspace,${RELEASE}-mimir-blocks— and set the matching chart value for each.
Place buckets in the same region as your GKE cluster to avoid cross-region egress charges. Enable uniform bucket-level access so permissions come from IAM rather than per-object ACLs.
Service account and IAM roles
Create one Google service account (GSA) for the platform to impersonate, then grant it these roles:
| Role | Why it is needed |
|---|---|
roles/storage.objectAdmin | Read, write, list, and delete objects. Grant on each bucket (preferred) or at project level. |
roles/iam.serviceAccountTokenCreator | Required for file uploads and downloads in the UI. Granted on the GSA to itself. |
roles/iam.serviceAccountTokenCreatoris not optional. The platform issues signed URLs so browsers and runners can upload and download workspace files directly. Under Workload Identity the pod holds no private key, so signing falls back to the IAMsignBlobAPI — and that call requires the service account to holdiam.serviceAccounts.signBlobon itself.Without this role, object reads and writes still succeed but every file upload or download fails with
403 Permission 'iam.serviceAccounts.signBlob' denied on resource. This is the single most common GCS misconfiguration — grant it even if object access already works.
Workload Identity bindings
Workload Identity must be enabled on the cluster and node pools. Each Kubernetes service account (KSA) that touches storage needs:
- An
iam.gke.io/gcp-service-accountannotation pointing at the GSA, and - A
roles/iam.workloadIdentityUserbinding on the GSA for that KSA
| Kubernetes service account | Used by | Required when |
|---|---|---|
<release>-platform | All platform workloads — workspace files, metrics storage, AI Assistant artifacts, database backups | Always |
<release>-vault-backup | Vault backup job | vault.backup.enabled: true |
<release>-vault-backupneeds its own binding. It is a separate, narrowly-scoped service account and is not covered by the<release>-platformbinding. Vault backups fail withstorage.objects.createdenied if you bind only the platform account.
Setting up GCP resources
Substitute your own project, region, and bucket prefix.
PROJECT_ID="my-project"REGION="us-central1"NAMESPACE="runwhen-platform" # release namespaceRELEASE="rw-platform" # helm release name — used as bucket prefixGSA="runwhen-platform-storage"GSA_EMAIL="${GSA}@${PROJECT_ID}.iam.gserviceaccount.com"1. Create the buckets
for B in shared-workspace mimir-blocks mimir-ruler mimir-alertmanager \ agentfarm-artifacts vault-backups; do gcloud storage buckets create "gs://${RELEASE}-${B}" \ --project="${PROJECT_ID}" \ --location="${REGION}" \ --uniform-bucket-level-accessdone2. Create the service account
gcloud iam service-accounts create "${GSA}" \ --project="${PROJECT_ID}" \ --display-name="RunWhen Platform object storage"3. Grant object access on each bucket
for B in shared-workspace mimir-blocks mimir-ruler mimir-alertmanager \ agentfarm-artifacts vault-backups; do gcloud storage buckets add-iam-policy-binding "gs://${RELEASE}-${B}" \ --member="serviceAccount:${GSA_EMAIL}" \ --role="roles/storage.objectAdmin"done4. Allow the service account to sign URLs
gcloud iam service-accounts add-iam-policy-binding "${GSA_EMAIL}" \ --project="${PROJECT_ID}" \ --member="serviceAccount:${GSA_EMAIL}" \ --role="roles/iam.serviceAccountTokenCreator"5. Bind Workload Identity for each Kubernetes service account
for KSA in "${RELEASE}-platform" "${RELEASE}-vault-backup"; do gcloud iam service-accounts add-iam-policy-binding "${GSA_EMAIL}" \ --project="${PROJECT_ID}" \ --member="serviceAccount:${PROJECT_ID}.svc.id.goog[${NAMESPACE}/${KSA}]" \ --role="roles/iam.workloadIdentityUser"doneIf your organization provisions cloud resources through Terraform, Crossplane, or Config Connector, create the equivalent resources with your existing tooling — the roles and bindings above are what matter.
Helm configuration
objectStorage: kind: external region: us-central1 external: backend: gcs host: storage.googleapis.com internalHost: storage.googleapis.com region: us-central1 bucket: ${RELEASE}-shared-workspace # Bucket provisioning and maintenance are SeaweedFS-only bucketInit: enabled: false maintenance: enabled: false
seaweedfs: deploy: false
# Annotate each service account for Workload IdentityserviceAccount: platform: create: true annotations: iam.gke.io/gcp-service-account: runwhen-platform-storage@my-project.iam.gserviceaccount.com vaultBackup: create: true annotations: iam.gke.io/gcp-service-account: runwhen-platform-storage@my-project.iam.gserviceaccount.com
# Metrics storage — one bucket per storemetricstore: buckets: blocks: ${RELEASE}-mimir-blocks ruler: ${RELEASE}-mimir-ruler alertmanager: ${RELEASE}-mimir-alertmanager
# AI Assistant chat artifacts — native GCSagentfarm: artifacts: serviceType: gcs gcsBucket: ${RELEASE}-agentfarm-artifacts
vault: backup: bucket: ${RELEASE}-vault-backupsNotice there are no credentials anywhere in this configuration. Setting external.backend: gcs switches every consumer to the native GCS client, which resolves credentials through Workload Identity at runtime.
PostgreSQL backups
When running the bundled PostgreSQL with postgresql.kind: spilo, WAL archiving and base backups also use native GCS through Workload Identity:
postgresql: kind: spilo spilo: walg: enabled: true bucket: ${RELEASE}-postgres-backupsCreate that bucket and grant roles/storage.objectAdmin on it the same way as the others. Backups are written under a spilo/<cluster>/ prefix, and no service account key file is required.
Verify after install
1. Confirm the resolved backend
kubectl -n runwhen-platform get cm ${RELEASE}-platform-config -o yaml | grep STORAGE_BACKENDExpect STORAGE_BACKEND: "gcs".
2. Confirm the Workload Identity annotation landed
kubectl -n runwhen-platform get sa ${RELEASE}-platform \ -o jsonpath='{.metadata.annotations.iam\.gke\.io/gcp-service-account}'3. Verify token exchange from inside a pod
POD=$(kubectl -n runwhen-platform get pod \ -l app.kubernetes.io/instance=${RELEASE} -o name | head -1)
kubectl -n runwhen-platform exec "$POD" -- \ curl -s -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"This must return your GSA email. If it returns a -compute@developer.gserviceaccount.com address, the annotation or the workloadIdentityUser binding is missing.
4. Upload a file in the UI — open a workspace, upload a file, and confirm it lands in the workspace bucket. This exercises signed URLs and proves roles/iam.serviceAccountTokenCreator is in place.
5. Confirm metrics blocks are being written
gcloud storage ls "gs://${RELEASE}-mimir-blocks/**" | headTroubleshooting
| Symptom | Cause and fix |
|---|---|
403 Permission 'iam.serviceAccounts.signBlob' denied on file upload or download | GSA is missing roles/iam.serviceAccountTokenCreator on itself. See step 4 above. |
403 … does not have storage.objects.get access and the bucket looks correct | The bucket does not exist under that exact name, or the GSA lacks roles/storage.objectAdmin. GCS returns 403 (not 404) for buckets you cannot list, so a typo or missing prefix looks like a permission error. |
403 storage.objects.create from the Vault backup job only | The <release>-vault-backup service account is missing its annotation or workloadIdentityUser binding. It is separate from <release>-platform. |
| Metadata server returns the default compute service account | Missing iam.gke.io/gcp-service-account annotation on the KSA, or Workload Identity is not enabled on the node pool. |
| Metrics service will not start, logs mention overlapping storage | Two or more metric stores point at the same bucket without distinct prefixes. Give each its own bucket. |
| Storage settings changed but pods still use the old backend | Configuration is mounted from a ConfigMap; running pods do not reload it. Restart the affected workloads (see below). |
Restart after changing storage settings
Changing storage values updates ConfigMaps but does not restart running workloads:
kubectl -n runwhen-platform rollout restart deployment \ -l app.kubernetes.io/instance=${RELEASE}kubectl -n runwhen-platform rollout restart statefulset \ -l app.kubernetes.io/instance=${RELEASE}Related documentation
| Topic | Link |
|---|---|
| S3-compatible object storage | External S3 |
| Storage sizing and StorageClasses | Storage Requirements |
| External PostgreSQL | External PostgreSQL |
| Self-hosted prerequisites | Self-Hosted Deployment Requirements |