Skip to content

GCP Setup

Provision the GCP resources needed to run folio.

Resources

Resource Purpose
GCS Bucket Stores all documentation sites
Cloud Run Service Runs the folio server
Secret Manager Stores login password and HMAC key
Service Account Used by Cloud Run and GitHub Actions
Workload Identity Federation GitHub Actions OIDC authentication

Create the Bucket

export PROJECT_ID=your-project
export REGION=us-central1
export BUCKET_NAME=your-docs-bucket

gcloud storage buckets create gs://$BUCKET_NAME \
  --project=$PROJECT_ID \
  --location=$REGION \
  --uniform-bucket-level-access

Create Secrets

Secrets follow the {env}-{app}-{purpose} naming convention.

echo -n "your-password" | gcloud secrets create prod-folio-server-login-password \
  --data-file=- --project=$PROJECT_ID

echo -n "$(openssl rand -hex 32)" | gcloud secrets create prod-folio-server-hmac-key \
  --data-file=- --project=$PROJECT_ID

Create Service Account

export SA_NAME=folio-server

gcloud iam service-accounts create $SA_NAME \
  --display-name="Folio Server" \
  --project=$PROJECT_ID

# Grant GCS read access.
gcloud storage buckets add-iam-policy-binding gs://$BUCKET_NAME \
  --member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"

# Grant secret access.
for SECRET in prod-folio-server-login-password prod-folio-server-hmac-key; do
  gcloud secrets add-iam-policy-binding $SECRET \
    --member="serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor" \
    --project=$PROJECT_ID
done

IAM scoping

Every Cloud Run secret folio references is named {env}-folio-server-{purpose} (e.g. prod-folio-server-login-password). Generic names are avoided because two apps sharing one generic name in the same GCP project can silently read each other's values — a shared prod-database-url caused a cross-app outage in April 2026, which is why the convention is now enforced across the fleet.

As a second line of defense, folio-server has no project-level roles/secretmanager.secretAccessor. Access is granted per-secret on the prod-folio-server-* names only, so a typo or misconfiguration that points the service at another app's secret fails loudly at container start with an IAM error.

Roles granted by infra/setup.sh

Service Account Role Scope Why
folio-server roles/storage.objectViewer bucket gs://$BUCKET_NAME Serve static docs from GCS
folio-server roles/secretmanager.secretAccessor per-secret on each prod-folio-server-* Read only folio's secrets at startup. No project-level grant — see above.
folio-ci roles/storage.objectAdmin bucket gs://$BUCKET_NAME Publish docs from CI
folio-ci roles/run.developer project Deploy Cloud Run service
folio-ci roles/iam.serviceAccountUser on folio-server Deploy services that run as folio-server

Verify no broad secret grant has crept in

gcloud projects get-iam-policy $PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:serviceAccount:folio-server@$PROJECT_ID.iam.gserviceaccount.com AND bindings.role:roles/secretmanager.secretAccessor" \
  --format="value(bindings.role)"

Expected: empty output. If anything prints, remove the broad binding with gcloud projects remove-iam-policy-binding.

Deploy to Cloud Run

The service's env vars, secret bindings, service account, and resource limits are defined declaratively in deploy/cloud-run-service.yaml. Render the template with the target image tag and apply it with gcloud run services replace:

export IMAGE=ghcr.io/charliek/folio:latest

sed "s|__IMAGE__|$IMAGE|" deploy/cloud-run-service.yaml \
  | gcloud run services replace - --region=$REGION --project=$PROJECT_ID

On subsequent deploys, use the Deploy GitHub Actions workflow — it runs the same sed + replace flow against the tag you pick.

If the service account email, GCS bucket name, or secret names in your environment differ from the defaults baked into the template, edit deploy/cloud-run-service.yaml before applying.

Allow public ingress (required so the login page can be reached):

gcloud run services add-iam-policy-binding folio-server \
  --region=$REGION --project=$PROJECT_ID \
  --member=allUsers --role=roles/run.invoker

Set Up Domain Mapping

export DOMAIN=docs.example.com

gcloud beta run domain-mappings create \
  --service=$SERVICE_NAME \
  --domain=$DOMAIN \
  --region=$REGION \
  --project=$PROJECT_ID

Add a CNAME record for your domain pointing to the target shown in the output.

Workload Identity Federation

Set up WIF so GitHub Actions can authenticate to GCP without service account keys.

export POOL_NAME=github-pool
export PROVIDER_NAME=github-provider
export GITHUB_ORG=your-github-username

# Create pool.
gcloud iam workload-identity-pools create $POOL_NAME \
  --location=global \
  --project=$PROJECT_ID

# Create provider.
gcloud iam workload-identity-pools providers create-oidc $PROVIDER_NAME \
  --location=global \
  --workload-identity-pool=$POOL_NAME \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == '$GITHUB_ORG'" \
  --project=$PROJECT_ID

Create a service account for CI and grant it GCS write access:

export CI_SA_NAME=folio-ci

gcloud iam service-accounts create $CI_SA_NAME \
  --display-name="Folio CI" \
  --project=$PROJECT_ID

gcloud storage buckets add-iam-policy-binding gs://$BUCKET_NAME \
  --member="serviceAccount:$CI_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.objectAdmin"

# Grant Cloud Run deploy access (for the deploy workflow).
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$CI_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/run.developer"

gcloud iam service-accounts add-iam-policy-binding \
  $SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \
  --member="serviceAccount:$CI_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/iam.serviceAccountUser"

# Allow GitHub Actions to impersonate this SA.
gcloud iam service-accounts add-iam-policy-binding \
  $CI_SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')/locations/global/workloadIdentityPools/$POOL_NAME/attribute.repository_owner/$GITHUB_ORG"

Set these as GitHub Actions secrets and variables in your repos:

Secrets:

Secret Value
GCP_WIF_PROVIDER projects/{project-number}/locations/global/workloadIdentityPools/{pool}/providers/{provider}
GCP_SA_EMAIL folio-ci@{project}.iam.gserviceaccount.com

Variables:

Variable Value
GCS_BUCKET Your GCS bucket name

Subsequent Deployments

After the initial setup, deploy new server versions using the deploy.yml workflow:

gh workflow run deploy.yml -f image_tag=v0.1.0

This triggers a manual deployment that updates the Cloud Run service image via WIF authentication.

See the infra/setup.sh script for a complete provisioning example.