#!/usr/bin/env bash # Deploy the BearDrive hub to Google Cloud Run (single instance) with Cloud SQL # Postgres for metadata and a GCS bucket for blobs/journals. # # The hub is single-process for now, so max-instances=1 (in-memory caches # assume one writer). Run from the repo root: bash example/deploy/gcp-cloudrun.sh set -euo pipefail # gcloud needs Python >= 3.10 (3.9 crashes `gcloud builds`); point it at a # newer interpreter if your system default is old: # export CLOUDSDK_PYTHON=$(command -v python3.11 || command -v python3.12) # ---- fill these in ----------------------------------------------------------- PROJECT_ID="${PROJECT_ID:?set PROJECT_ID}" # dedicated GCP project id BILLING_ACCOUNT="${BILLING_ACCOUNT:-}" # e.g. 0X0X0X-0X0X0X-0X0X0X (only needed if creating the project) REGION="${REGION:-us-central1}" ADMIN_EMAIL="${ADMIN_EMAIL:?set ADMIN_EMAIL}" # first hub admin ADMIN_DOMAIN="${ADMIN_DOMAIN:?set ADMIN_DOMAIN}" # signup limited to this email domain for bootstrap, e.g. runbear.io BRAND="${BRAND:-BearDrive}" # ------------------------------------------------------------------------------ BUCKET="${BUCKET:-${PROJECT_ID}-bdrive}" SQL_INSTANCE="${SQL_INSTANCE:-bdrive-pg}" SQL_TIER="${SQL_TIER:-db-f1-micro}" DB_NAME="bdrive"; DB_USER="bdrive" SERVICE="${SERVICE:-bdrive}" RUN_SA="bdrive-run@${PROJECT_ID}.iam.gserviceaccount.com" CONN_NAME="${PROJECT_ID}:${REGION}:${SQL_INSTANCE}" echo "== project ==" gcloud projects describe "$PROJECT_ID" >/dev/null 2>&1 || { echo "creating project $PROJECT_ID"; gcloud projects create "$PROJECT_ID" [ -n "$BILLING_ACCOUNT" ] && gcloud billing projects link "$PROJECT_ID" --billing-account "$BILLING_ACCOUNT" } gcloud config set project "$PROJECT_ID" echo "== enable APIs ==" gcloud services enable run.googleapis.com sqladmin.googleapis.com storage.googleapis.com \ secretmanager.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com echo "== GCS bucket for blobs/journals ==" gcloud storage buckets describe "gs://$BUCKET" >/dev/null 2>&1 || \ gcloud storage buckets create "gs://$BUCKET" --location "$REGION" --uniform-bucket-level-access # Age old objects down to Nearline. Every version of every file is retained # forever, so storage grows monotonically against flat per-seat revenue; at # 30 days Nearline halves the per-GB cost ($0.020 → $0.010 in us-central1). # # Nearline and NOT Coldline/Archive, deliberately — see deploy/README.md # "Storage tiering". Colder tiers only pay off when an object is read less # than roughly once a month (Coldline) or once a year (Archive), and today a # BearDrive device that syncs a project for the first time downloads EVERY # historical blob, not just the current file tree. So blob reads scale with # how often anyone adds a device, which is nowhere near cold enough for # Archive to be anything but a bill increase. Revisit when a first sync # fetches only current-state blobs. # # Applied bucket-wide rather than to blobs/ only: journal objects sit under # the same per-project prefixes and a GCS lifecycle prefix cannot express # "*/blobs/". That is safe here because a peer journal is re-fetched only # when the listing shows it GREW (internal/syncer pull), and one that grew # was just rewritten and is Standard again. if [ "${LIFECYCLE:-1}" = "1" ]; then echo "== GCS lifecycle: Nearline at 30 days (LIFECYCLE=0 to skip) ==" TMP_LIFECYCLE="$(mktemp)" cat >"$TMP_LIFECYCLE" <<'JSON' { "lifecycle": { "rule": [ { "action": { "type": "SetStorageClass", "storageClass": "NEARLINE" }, "condition": { "age": 30, "matchesStorageClass": ["STANDARD"] } } ] } } JSON gcloud storage buckets update "gs://$BUCKET" --lifecycle-file="$TMP_LIFECYCLE" rm -f "$TMP_LIFECYCLE" fi echo "== Cloud SQL Postgres (this takes several minutes) ==" gcloud sql instances describe "$SQL_INSTANCE" >/dev/null 2>&1 || \ gcloud sql instances create "$SQL_INSTANCE" --database-version POSTGRES_16 \ --edition ENTERPRISE --tier "$SQL_TIER" --region "$REGION" \ --storage-size 10 --storage-auto-increase gcloud sql databases describe "$DB_NAME" --instance "$SQL_INSTANCE" >/dev/null 2>&1 || \ gcloud sql databases create "$DB_NAME" --instance "$SQL_INSTANCE" DB_PASS="$(gcloud secrets versions access latest --secret bdrive-db-pass 2>/dev/null || true)" if [ -z "$DB_PASS" ]; then DB_PASS="$(openssl rand -base64 24 | tr -d '/+=')" printf '%s' "$DB_PASS" | gcloud secrets create bdrive-db-pass --data-file=- 2>/dev/null || \ printf '%s' "$DB_PASS" | gcloud secrets versions add bdrive-db-pass --data-file=- fi gcloud sql users create "$DB_USER" --instance "$SQL_INSTANCE" --password "$DB_PASS" 2>/dev/null || \ gcloud sql users set-password "$DB_USER" --instance "$SQL_INSTANCE" --password "$DB_PASS" echo "== runtime service account + IAM ==" gcloud iam service-accounts describe "$RUN_SA" >/dev/null 2>&1 || \ gcloud iam service-accounts create bdrive-run --display-name "BearDrive Cloud Run" gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \ --member "serviceAccount:$RUN_SA" --role roles/storage.objectAdmin gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member "serviceAccount:$RUN_SA" --role roles/cloudsql.client >/dev/null echo "== hub config secret (contains the DB DSN) ==" # Bootstrap posture: domain-gated self-signup so the first admin can create # their account and become owner; tighten to invite-only afterwards. CONFIG="$(cat </dev/null || \ printf '%s' "$CONFIG" | gcloud secrets versions add bdrive-config --data-file=- gcloud secrets add-iam-policy-binding bdrive-config \ --member "serviceAccount:$RUN_SA" --role roles/secretmanager.secretAccessor >/dev/null echo "== build + deploy to Cloud Run (single instance) ==" gcloud run deploy "$SERVICE" \ --source . \ --region "$REGION" \ --service-account "$RUN_SA" \ --add-cloudsql-instances "$CONN_NAME" \ --update-secrets "/config/config.json=bdrive-config:latest" \ --args "web,-c,/config/config.json" \ --min-instances 1 --max-instances 1 \ --cpu 1 --memory 512Mi \ --allow-unauthenticated echo echo "Deployed. URL:" gcloud run services describe "$SERVICE" --region "$REGION" --format 'value(status.url)' echo "Next: open the URL, Sign up as $ADMIN_EMAIL (domain-gated), then tighten" echo "auth to invite-only by editing the bdrive-config secret + redeploying."