Skip to content

Deploying docs.swisper.io on Google Cloud

Overview

This runbook describes how to deploy the Swisper documentation site on Google Cloud Platform using Cloud Storage + Cloud Load Balancer + Identity-Aware Proxy (IAP) for team-only access.

Property Value
Architecture Static site in GCS bucket → Cloud Load Balancer → IAP authentication
Region europe-west6 (Zurich) or europe-west3 (Frankfurt)
Domain docs.swisper.io
Authentication Google Workspace SSO via IAP (team members log in with their @swisper.ai accounts)
TLS Google-managed SSL certificate (automatic renewal)
CI/CD GitHub Actions → gsutil rsync to GCS bucket
Monthly cost ~$1-5 (storage + LB + minimal traffic)
Estimated setup time 30-45 minutes

Why This Approach

Alternative Why Not
Cloud Run + nginx container Over-engineered for static files
Firebase Hosting No built-in team-only auth
GCE VM + nginx Server to maintain, more expensive
GCS + LB + IAP Zero server maintenance, Google-managed TLS, SSO with Workspace accounts

Prerequisites

Before starting, ensure you have:

  • [ ] A Google Cloud project (create one at https://console.cloud.google.com)
  • [ ] Billing enabled on the project
  • [ ] Google Workspace set up for swisper.ai domain (for IAP SSO)
  • [ ] DNS management access for swisper.io (to create an A record)
  • [ ] gcloud CLI installed locally (https://cloud.google.com/sdk/docs/install)
  • [ ] The Swisper_Documentation repo cloned with Zensical installed

Step 1: Authenticate and Set Project

gcloud auth login
gcloud config set project YOUR_PROJECT_ID

Replace YOUR_PROJECT_ID with your actual GCP project ID.


Step 2: Create a Cloud Storage Bucket

# Create bucket in EU region
gsutil mb -l europe-west6 gs://docs-swisper-io

# Enable website hosting on the bucket
gsutil web set -m index.html -e 404.html gs://docs-swisper-io

Important: The bucket name must be globally unique. docs-swisper-io is suggested but use another name if it's taken.


Step 3: Build and Upload the Site

# Build the site
cd /path/to/Swisper_Documentation
zensical build --clean

# Upload to GCS
gsutil -m rsync -r -d site/ gs://docs-swisper-io/

The -d flag deletes files in the bucket that no longer exist locally (handles renames and deletions).

Verify the upload:

gsutil ls gs://docs-swisper-io/ | head -10


Step 4: Reserve a Static IP Address

gcloud compute addresses create docs-swisper-ip \
  --global \
  --ip-version=IPV4

# Note the IP address
gcloud compute addresses describe docs-swisper-ip --global --format='get(address)'

Write down the IP address — you'll need it for DNS.


Step 5: Create a Google-Managed SSL Certificate

gcloud compute ssl-certificates create docs-swisper-cert \
  --domains=docs.swisper.io \
  --global

The certificate will be provisioned automatically once DNS points to the load balancer IP. It may take 15-60 minutes to become active.


Step 6: Create the Backend Bucket

gcloud compute backend-buckets create docs-swisper-backend \
  --gcs-bucket-name=docs-swisper-io \
  --enable-cdn

Step 7: Create the URL Map and HTTP(S) Proxy

# URL map (routes all traffic to the backend bucket)
gcloud compute url-maps create docs-swisper-lb \
  --default-backend-bucket=docs-swisper-backend

# HTTPS proxy (terminates TLS)
gcloud compute target-https-proxies create docs-swisper-https-proxy \
  --url-map=docs-swisper-lb \
  --ssl-certificates=docs-swisper-cert

# Forwarding rule (binds the static IP to the proxy)
gcloud compute forwarding-rules create docs-swisper-https-rule \
  --global \
  --target-https-proxy=docs-swisper-https-proxy \
  --address=docs-swisper-ip \
  --ports=443
# Create a redirect URL map
gcloud compute url-maps import docs-swisper-http-redirect --source=- <<'EOF'
name: docs-swisper-http-redirect
defaultUrlRedirect:
  redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
  httpsRedirect: true
EOF

# HTTP proxy
gcloud compute target-http-proxies create docs-swisper-http-proxy \
  --url-map=docs-swisper-http-redirect

# HTTP forwarding rule
gcloud compute forwarding-rules create docs-swisper-http-rule \
  --global \
  --target-http-proxy=docs-swisper-http-proxy \
  --address=docs-swisper-ip \
  --ports=80

Step 8: Configure DNS

Create an A record for docs.swisper.io pointing to the static IP from Step 4:

Record Type Value TTL
docs.swisper.io A (IP from Step 4) 300

If your DNS is managed in Google Cloud DNS:

gcloud dns record-sets create docs.swisper.io. \
  --zone=YOUR_DNS_ZONE \
  --type=A \
  --ttl=300 \
  --rrdatas=IP_FROM_STEP_4

Wait for DNS propagation (usually 5-15 minutes):

dig +short docs.swisper.io


Step 9: Enable Identity-Aware Proxy (IAP)

This is what restricts access to team members only. Users must be logged in with their @swisper.ai Google Workspace account.

9.1 Enable the IAP API

gcloud services enable iap.googleapis.com
  1. Go to https://console.cloud.google.com/apis/credentials/consent
  2. Select Internal (only users in your Workspace org)
  3. Fill in: App name = "Swisper Docs", User support email, Developer contact
  4. Save

9.3 Enable IAP on the Backend Bucket

gcloud iap web enable \
  --resource-type=backend-buckets \
  --backend-bucket-name=docs-swisper-backend

9.4 Grant Access to Team Members

Grant access to your entire organization:

gcloud iap web add-iam-policy-binding \
  --resource-type=backend-buckets \
  --backend-bucket-name=docs-swisper-backend \
  --member="domain:swisper.ai" \
  --role="roles/iap.httpsResourceAccessUser"

Or grant access to specific users:

gcloud iap web add-iam-policy-binding \
  --resource-type=backend-buckets \
  --backend-bucket-name=docs-swisper-backend \
  --member="user:heiko.sundermann@swisper.ai" \
  --role="roles/iap.httpsResourceAccessUser"


Step 10: Set Up CI/CD (GitHub Actions)

Update .github/workflows/docs.yml to deploy to GCS instead of rsync to VPS:

name: docs

on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
      - 'zensical.toml'

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # Required for Workload Identity Federation

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Install Zensical
        run: pip install zensical

      - name: Build site
        run: zensical build --clean

      - name: Validate links
        uses: lycheeverse/lychee-action@v2
        with:
          args: --no-progress site/
          fail: true

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - name: Deploy to Cloud Storage
        run: gsutil -m rsync -r -d site/ gs://docs-swisper-io/

GitHub Actions Authentication (Workload Identity Federation)

This is the recommended approach — no service account keys to manage.

# Create a service account for CI/CD
gcloud iam service-accounts create docs-deployer \
  --display-name="Docs Deployer (GitHub Actions)"

# Grant storage permissions
gsutil iam ch \
  serviceAccount:docs-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com:objectAdmin \
  gs://docs-swisper-io

# Set up Workload Identity Federation (one-time)
gcloud iam workload-identity-pools create github-pool \
  --location=global \
  --display-name="GitHub Actions Pool"

gcloud iam workload-identity-pools providers create-oidc github-provider \
  --location=global \
  --workload-identity-pool=github-pool \
  --display-name="GitHub Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

# Allow the GitHub repo to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding \
  docs-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/Fintama/Swisper_Documentation"

Add these GitHub secrets: - GCP_WORKLOAD_IDENTITY_PROVIDER: projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider - GCP_SERVICE_ACCOUNT: docs-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com


Verification Checklist

After setup, verify:

  • [ ] V1: Site loadscurl -s -o /dev/null -w "%{http_code}" https://docs.swisper.io returns 302 (redirect to Google login)
  • [ ] V2: Auth works — Open https://docs.swisper.io in a browser logged into your @swisper.ai account → site loads
  • [ ] V3: Auth blocks — Open in an incognito window → Google login prompt, non-swisper.ai accounts rejected
  • [ ] V4: TLS validecho | openssl s_client -connect docs.swisper.io:443 2>/dev/null | grep "subject=" shows Google-managed cert
  • [ ] V5: Content correct — Landing page shows "Swisper Documentation" with all 11 modules
  • [ ] V6: CI/CD works — Push a trivial doc change to main → GitHub Actions deploys → change visible within 5 minutes

Troubleshooting

SSL certificate stuck in PROVISIONING

The certificate needs DNS to be pointing at the load balancer IP first. Check:

gcloud compute ssl-certificates describe docs-swisper-cert --global
dig +short docs.swisper.io
If DNS is correct, wait up to 60 minutes.

IAP returns 403 for authenticated users

Check IAM binding:

gcloud iap web get-iam-policy \
  --resource-type=backend-buckets \
  --backend-bucket-name=docs-swisper-backend
Ensure the user's email or domain is listed with roles/iap.httpsResourceAccessUser.

gsutil rsync fails in CI

Check service account permissions:

gsutil iam get gs://docs-swisper-io
The docs-deployer service account needs objectAdmin on the bucket.

Site shows 404 for all pages

Check website configuration:

gsutil web get gs://docs-swisper-io
Should show MainPageSuffix: index.html. Also verify files were uploaded:
gsutil ls gs://docs-swisper-io/index.html


Cost Estimate

Component Monthly Cost
Cloud Storage (< 1 GB) ~$0.02
Cloud Load Balancer ~$18 (minimum charge)
SSL Certificate Free (Google-managed)
IAP Free
Egress (< 10 GB/month) ~$1
Total ~$19/month

Note: The load balancer has a minimum monthly charge of ~$18 regardless of traffic. For a lower-cost alternative, consider Cloud Run (serves from a container, scales to zero, ~$0-3/month for a docs site).


Rollback

To remove the deployment:

gcloud compute forwarding-rules delete docs-swisper-https-rule --global
gcloud compute forwarding-rules delete docs-swisper-http-rule --global
gcloud compute target-https-proxies delete docs-swisper-https-proxy
gcloud compute target-http-proxies delete docs-swisper-http-proxy
gcloud compute url-maps delete docs-swisper-lb
gcloud compute url-maps delete docs-swisper-http-redirect
gcloud compute backend-buckets delete docs-swisper-backend
gcloud compute ssl-certificates delete docs-swisper-cert --global
gcloud compute addresses delete docs-swisper-ip --global
gsutil rm -r gs://docs-swisper-io

Remove DNS A record for docs.swisper.io.


Alternative: Cloud Run (Lower Cost)

If the $18/month load balancer minimum is too much for a docs site, Cloud Run is a cheaper alternative:

# Create a simple Dockerfile
cat > Dockerfile <<'EOF'
FROM nginx:alpine
COPY site/ /usr/share/nginx/html/
EOF

# Build and deploy
zensical build --clean
gcloud run deploy docs-swisper \
  --source=. \
  --region=europe-west6 \
  --allow-unauthenticated=false

Cloud Run with IAP provides the same auth experience at ~$0-3/month. The trade-off: you need a Dockerfile and the deployment is slightly more complex. But for a small team docs site, it's often the better value.