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.aidomain (for IAP SSO) - [ ] DNS management access for
swisper.io(to create an A record) - [ ]
gcloudCLI installed locally (https://cloud.google.com/sdk/docs/install) - [ ] The Swisper_Documentation repo cloned with Zensical installed
Step 1: Authenticate and Set Project¶
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:
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¶
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
HTTP → HTTPS Redirect (optional but recommended)¶
# 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):
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¶
9.2 Configure the OAuth Consent Screen¶
- Go to https://console.cloud.google.com/apis/credentials/consent
- Select Internal (only users in your Workspace org)
- Fill in: App name = "Swisper Docs", User support email, Developer contact
- 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 loads —
curl -s -o /dev/null -w "%{http_code}" https://docs.swisper.ioreturns302(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 valid —
echo | 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:
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
roles/iap.httpsResourceAccessUser.
gsutil rsync fails in CI¶
Check service account permissions:
Thedocs-deployer service account needs objectAdmin on the bucket.
Site shows 404 for all pages¶
Check website configuration:
Should showMainPageSuffix: index.html. Also verify files were uploaded:
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.