Files
beardrive/deploy/gcp-cloudrun.sh
T
e02bd5330d feat(cloud): launch pricing guardrails — egress caps, ignore defaults, storage tiering (#114)
QuotaProvider grows a read half: CheckRead(org, bytes) and RecordEgress(org, bytes). CheckRead is enforced on /s/* only — a public share link is the sole unauthenticated door to stored bytes, so it is the only egress a plan can cap. The sync proxy and viewer merely RecordEgress: refusing a device mid-cycle surfaces as ErrForbidden, which the syncer reads as "access is gone — pause and touch nothing", and sync must never break over a bill. UnlimitedQuota stays the OSS default. countingWriter bills what actually reached the client rather than a size claimed before the write.

bdrive init warns past 1 GiB or 20k files and says how to narrow scope; syncer.Measure sizes that through the real Filter and the one walkFolder predicate. starterIgnore gains video/archive/disk-image globs and Library/ — every version is kept forever, so a big binary committed once is paid for forever on every device.

deploy: a Nearline-at-30-days lifecycle rule and the arithmetic for why it stops there. Coldline and Archive only pay off below roughly one read per month, and a first sync pulls every historical blob rather than just the current tree, so blob read rate tracks device onboarding.

docs/launch-plan.md said Cloud was waitlist-only and framed Product Hunt as an OSS launch whose goal was not signups or revenue; both are stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:16:42 +09:00

148 lines
6.7 KiB
Bash
Executable File

#!/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 <<JSON
{
"remote": "gs://$BUCKET/hub",
"addr": ":8080",
"upload": true,
"database": {
"driver": "postgres",
"dsn": "postgres://$DB_USER:$DB_PASS@/$DB_NAME?host=/cloudsql/$CONN_NAME&sslmode=disable"
},
"auth": {
"allow_signup": true,
"allowed_domains": ["$ADMIN_DOMAIN"],
"admins": ["$ADMIN_EMAIL"],
"brand": "$BRAND"
}
}
JSON
)"
printf '%s' "$CONFIG" | gcloud secrets create bdrive-config --data-file=- 2>/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."