Skip to content

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.

ApproachobjectStorage.external.backendCredentialsBest for
Native GCS (recommended)gcsNone — ADC via Workload IdentityGKE clusters; keyless operation
S3-interops3HMAC access key + secretNon-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.

PurposeChart valueRequired when
Workspace file uploadsobjectStorage.external.bucketAlways
Metrics block storagemetricstore.buckets.blocksmetricstore.deploy: true
Metrics rule statemetricstore.buckets.rulermetricstore.deploy: true
Metrics alert statemetricstore.buckets.alertmanagermetricstore.deploy: true
AI Assistant chat artifactsagentfarm.artifacts.gcsBucketagentfarm.artifacts.serviceType: gcs
Vault backupsvault.backup.bucketvault.backup.enabled: true
PostgreSQL backupspostgresql.spilo.walg.bucketpostgresql.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:

RoleWhy it is needed
roles/storage.objectAdminRead, write, list, and delete objects. Grant on each bucket (preferred) or at project level.
roles/iam.serviceAccountTokenCreatorRequired for file uploads and downloads in the UI. Granted on the GSA to itself.

roles/iam.serviceAccountTokenCreator is 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 IAM signBlob API — and that call requires the service account to hold iam.serviceAccounts.signBlob on 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:

  1. An iam.gke.io/gcp-service-account annotation pointing at the GSA, and
  2. A roles/iam.workloadIdentityUser binding on the GSA for that KSA
Kubernetes service accountUsed byRequired when
<release>-platformAll platform workloads — workspace files, metrics storage, AI Assistant artifacts, database backupsAlways
<release>-vault-backupVault backup jobvault.backup.enabled: true

<release>-vault-backup needs its own binding. It is a separate, narrowly-scoped service account and is not covered by the <release>-platform binding. Vault backups fail with storage.objects.create denied if you bind only the platform account.


Setting up GCP resources

Substitute your own project, region, and bucket prefix.

Terminal window
PROJECT_ID="my-project"
REGION="us-central1"
NAMESPACE="runwhen-platform" # release namespace
RELEASE="rw-platform" # helm release name — used as bucket prefix
GSA="runwhen-platform-storage"
GSA_EMAIL="${GSA}@${PROJECT_ID}.iam.gserviceaccount.com"

1. Create the buckets

Terminal window
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-access
done

2. Create the service account

Terminal window
gcloud iam service-accounts create "${GSA}" \
--project="${PROJECT_ID}" \
--display-name="RunWhen Platform object storage"

3. Grant object access on each bucket

Terminal window
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"
done

4. Allow the service account to sign URLs

Terminal window
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

Terminal window
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"
done

If 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 Identity
serviceAccount:
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 store
metricstore:
buckets:
blocks: ${RELEASE}-mimir-blocks
ruler: ${RELEASE}-mimir-ruler
alertmanager: ${RELEASE}-mimir-alertmanager
# AI Assistant chat artifacts — native GCS
agentfarm:
artifacts:
serviceType: gcs
gcsBucket: ${RELEASE}-agentfarm-artifacts
vault:
backup:
bucket: ${RELEASE}-vault-backups

Notice 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-backups

Create 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

Terminal window
kubectl -n runwhen-platform get cm ${RELEASE}-platform-config -o yaml | grep STORAGE_BACKEND

Expect STORAGE_BACKEND: "gcs".

2. Confirm the Workload Identity annotation landed

Terminal window
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

Terminal window
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

Terminal window
gcloud storage ls "gs://${RELEASE}-mimir-blocks/**" | head

Troubleshooting

SymptomCause and fix
403 Permission 'iam.serviceAccounts.signBlob' denied on file upload or downloadGSA is missing roles/iam.serviceAccountTokenCreator on itself. See step 4 above.
403 … does not have storage.objects.get access and the bucket looks correctThe 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 onlyThe <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 accountMissing 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 storageTwo 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 backendConfiguration 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:

Terminal window
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}

TopicLink
S3-compatible object storageExternal S3
Storage sizing and StorageClassesStorage Requirements
External PostgreSQLExternal PostgreSQL
Self-hosted prerequisitesSelf-Hosted Deployment Requirements