deploy: SETUP-GUIDE — UI-agnostic Phase B + provision-do.sh as doctl path
- SHA
9df906531e0fbb694a112e02859887f848049c3f- Parents
-
6093ed9 - Tree
164c886
9df9065
9df906531e0fbb694a112e02859887f848049c3f6093ed9
164c886| Status | File | + | - |
|---|---|---|---|
| M |
deploy/cutover/SETUP-GUIDE.md
|
82 | 50 |
| A |
deploy/cutover/provision-do.sh
|
200 | 0 |
deploy/cutover/SETUP-GUIDE.mdmodified@@ -179,59 +179,91 @@ surface the current location. (Path varies; "Settings → Security | |||
| 179 | verification happens at droplet-create time when you tick the | 179 | verification happens at droplet-create time when you tick the |
| 180 | box. | 180 | box. |
| 181 | 181 | ||
| 182 | -### B2. Create Spaces buckets (do these BEFORE droplets — they need to exist for the docs CNAME to resolve) | 182 | +### B2. Create Spaces buckets (do these BEFORE droplets — the docs CNAME depends on the docs bucket existing) |
| 183 | - | 183 | + |
| 184 | -1. Left sidebar → **Spaces Object Storage** → **Create Spaces | 184 | +> **About this section.** DO's web UI for Spaces changes |
| 185 | - Bucket**. | 185 | +> regularly (region availability, form layout, post-create |
| 186 | -2. **Bucket #1 — primary backups + docs:** | 186 | +> settings paths). This section describes **what each bucket |
| 187 | - - Region: **NYC3** | 187 | +> needs to be**, not where to click. Find the create form via |
| 188 | - - Name: **shithub-prod** (this matches the inventory's | 188 | +> the dashboard's left sidebar (**Spaces Object Storage** at the |
| 189 | - `s3_bucket: shithub-prod`) | 189 | +> time of writing) or the top search bar — type "Spaces". For |
| 190 | - - File listing: **Restrict** (private; presigned URLs only) | 190 | +> a UI-free path, see Phase B0 (`provision-do.sh`) at the |
| 191 | - - CDN: **Enable** (for the docs subdomain) | 191 | +> bottom of this guide. |
| 192 | - - Project: **shithub-prod** | 192 | + |
| 193 | -3. **Bucket #2 — DR mirror:** | 193 | +You need three buckets. **Storage type for all three: Standard.** |
| 194 | - - Region: **SFO3** | 194 | +(Cold Storage has a 30-day minimum retention that surprise-bills |
| 195 | - - Name: **shithub-prod-dr** | 195 | +when our daily backups churn.) **First bucket triggers the $5/mo |
| 196 | - - Same other settings. | 196 | +Spaces subscription** which covers all three up to 250 GiB total |
| 197 | -4. **Bucket #3 — docs site:** | 197 | ++ 1000 GiB bandwidth. |
| 198 | - - Region: **NYC3** | 198 | + |
| 199 | - - Name: **shithub-docs** | 199 | +| # | Bucket name | Region | CDN | Notes | |
| 200 | - - File listing: **Public** (it's the docs site; everyone | 200 | +|---|-----------------------|---------------------------------------------|---------|--------------------------------------------------------| |
| 201 | - reads) | 201 | +| 1 | `shithub-backups` | **Region A** — pick whichever DO offers (e.g. SFO3) | off | Primary backups (WAL + daily pg_dump). | |
| 202 | - - CDN: **Enable** | 202 | +| 2 | `shithub-backups-dr` | **Region B** — DIFFERENT region from A | off | Cross-region DR mirror; pick anything other than A. | |
| 203 | - - Project: **shithub-prod** | 203 | +| 3 | `shithub-docs` | **Same as A** | **on** | Docs site frontend; CDN serves `docs.shithub.sh`. | |
| 204 | -5. **Generate Spaces access keys:** Account → API → **Spaces | 204 | + |
| 205 | - Keys** → **Generate New Key**. Name it `shithub-prod-app`. | 205 | +After all three exist: |
| 206 | - Copy the access key + secret — Postmark-style, the secret | 206 | + |
| 207 | - is shown once. | 207 | +1. **Assign the docs custom domain.** Go to the `shithub-docs` |
| 208 | - | 208 | + bucket → its CDN settings (path varies; the create form notes |
| 209 | -**Verify:** three buckets listed in Spaces, all in their | 209 | + "you can assign a custom domain in CDN settings after the |
| 210 | -respective regions. Endpoint URLs follow the pattern | 210 | + Space is created"). Set custom domain to `docs.shithub.sh`. |
| 211 | -`<bucket>.<region>.digitaloceanspaces.com`. | 211 | + DO will tell you the CNAME target it expects on your DNS; |
| 212 | + match the value in Phase A5 to that. | ||
| 213 | +2. **Generate Spaces access keys.** Find the Spaces Keys | ||
| 214 | + management page (left sidebar **API** section, or top search | ||
| 215 | + bar → "Spaces Keys"). Generate a new key named | ||
| 216 | + `shithub-prod-app`. **Copy the secret immediately** — only | ||
| 217 | + shown once. | ||
| 218 | + | ||
| 219 | +**Verify:** three buckets listed under Spaces. Endpoint URL | ||
| 220 | +follows `<bucket>.<region>.digitaloceanspaces.com`. The | ||
| 221 | +inventory `s3_endpoint` field gets `<region>.digitaloceanspaces.com` | ||
| 222 | +(no bucket name in front). | ||
| 223 | + | ||
| 224 | +**Project assignment:** if you haven't created a `shithub-prod` | ||
| 225 | +project yet, put the buckets in any existing project for now — | ||
| 226 | +they're trivially moved later via the dashboard. Project | ||
| 227 | +membership is workspace-grouping, not access control. | ||
| 212 | 228 | ||
| 213 | ### B3. Create the four droplets | 229 | ### B3. Create the four droplets |
| 214 | 230 | ||
| 215 | -Use the DO web UI for the first one to confirm the shape; then | 231 | +> **UI-stable description.** The DO droplet-create form changes |
| 216 | -duplicate. | 232 | +> field layouts every few quarters. This section describes the |
| 217 | - | 233 | +> **shape each droplet needs to take**; locate the create form |
| 218 | -1. Left sidebar → **Droplets** → **Create Droplet**. | 234 | +> via the dashboard's **Droplets** sidebar entry or top search |
| 219 | -2. **Region:** NYC3 → Datacenter NYC3. | 235 | +> bar. |
| 220 | -3. **Image:** Marketplace? **No** — Distributions tab → Ubuntu | 236 | + |
| 221 | - 24.04 (LTS) x64. | 237 | +Required for each droplet: |
| 222 | -4. **Size:** Basic → Regular SSD → **2 vCPU / 4 GB RAM / | 238 | + |
| 223 | - 80 GB SSD** ($24/mo). | 239 | +- **Image:** Ubuntu 24.04 LTS x64 (Distributions tab; not Marketplace). |
| 224 | -5. **VPC Network:** default VPC for NYC3 (DO selects this | 240 | +- **Region:** **same region as the primary Spaces bucket** (Region |
| 225 | - automatically). All four droplets must be in the same VPC so | 241 | + A from B2). All four droplets in the same region keeps |
| 226 | - they see each other on private IPs. | 242 | + intra-VPC traffic free. |
| 227 | -6. **Authentication:** SSH Key → check the key you added in B1. | 243 | +- **VPC Network:** the default VPC in that region. **All four |
| 228 | -7. **Hostname:** `shithub-app` (this is droplet #1). | 244 | + droplets MUST be in the same VPC** — that's how they reach |
| 229 | -8. **Tags:** `shithub`, `shithub-app`. | 245 | + each other over private IPs. |
| 230 | -9. **Project:** shithub-prod. | 246 | +- **Authentication:** SSH Key. On the first droplet, click |
| 231 | -10. **Backups:** off (we have our own backup pipeline). | 247 | + "+ New SSH Key" and paste your laptop's `~/.ssh/id_ed25519.pub` |
| 232 | -11. **Monitoring:** **on** (DO's free agent — useful baseline | 248 | + (this saves it to your account). On droplets #2–4, just tick |
| 233 | - metrics in their UI). | 249 | + the same key. |
| 234 | -12. Create. | 250 | +- **DO Backups:** **off** (our own backup pipeline runs). |
| 251 | +- **DO Monitoring:** **on** (free agent, useful baseline metrics). | ||
| 252 | +- **Tags:** `shithub` plus a per-role tag (e.g., `shithub-app`). | ||
| 253 | +- **Project:** the project you're putting everything in. | ||
| 254 | + | ||
| 255 | +Per-droplet variations: | ||
| 256 | + | ||
| 257 | +| # | Hostname | Size (DO slug) | Cost/mo | Per-role tag | | ||
| 258 | +|---|-----------------------|---------------------|---------|------------------------| | ||
| 259 | +| 1 | `shithub-app` | s-2vcpu-4gb | $24 | `shithub-app` | | ||
| 260 | +| 2 | `shithub-db` | s-2vcpu-4gb | $24 | `shithub-db` | | ||
| 261 | +| 3 | `shithub-backup` | s-1vcpu-2gb | $12 | `shithub-backup` | | ||
| 262 | +| 4 | `shithub-monitoring` | s-2vcpu-4gb | $24 | `shithub-monitoring` | | ||
| 263 | + | ||
| 264 | +Size selection: in the create form, look for **Basic** plan → | ||
| 265 | +**Regular SSD** → the size matrix. The slug names above are | ||
| 266 | +DO's API identifiers and appear under each tile in the form. | ||
| 235 | 267 | ||
| 236 | Repeat for droplets #2–#4 with these differences: | 268 | Repeat for droplets #2–#4 with these differences: |
| 237 | 269 | ||
deploy/cutover/provision-do.shadded@@ -0,0 +1,200 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# SPDX-License-Identifier: AGPL-3.0-or-later | ||
| 3 | +# | ||
| 4 | +# UI-free provision script for the four-droplet + three-Spaces + | ||
| 5 | +# one-volume topology described in deploy/cutover/SETUP-GUIDE.md | ||
| 6 | +# Phase B. Uses `doctl`, DO's CLI — its commands are stable across | ||
| 7 | +# UI redesigns and the source of truth for what Phase B should | ||
| 8 | +# produce. | ||
| 9 | +# | ||
| 10 | +# Why prefer this over the dashboard: | ||
| 11 | +# - Reproducible: same flags → same resources, every time. | ||
| 12 | +# - No UI drift: `doctl` is versioned and changelogged. | ||
| 13 | +# - Cheaper to recover from a botched run: destroy + re-run. | ||
| 14 | +# - The dashboard remains a fine choice; this is the alternative. | ||
| 15 | +# | ||
| 16 | +# Prereqs (do these once, then re-run is idempotent-friendly): | ||
| 17 | +# - `doctl` installed: brew install doctl / apt-get install doctl | ||
| 18 | +# - Authenticated: doctl auth init (paste a Personal Access Token | ||
| 19 | +# from DO → API → Tokens with read+write scope) | ||
| 20 | +# - Your laptop's ~/.ssh/id_ed25519.pub uploaded to your account | ||
| 21 | +# (doctl compute ssh-key import or via the dashboard). | ||
| 22 | +# | ||
| 23 | +# Usage: | ||
| 24 | +# PRIMARY_REGION=sfo3 DR_REGION=ams3 \ | ||
| 25 | +# PROJECT_NAME=shithub-prod \ | ||
| 26 | +# SSH_KEY_NAME=macbook-pro \ | ||
| 27 | +# ./deploy/cutover/provision-do.sh | ||
| 28 | +# | ||
| 29 | +# What it creates: | ||
| 30 | +# - 1 project (if not present) | ||
| 31 | +# - 4 droplets (app, db, backup, monitoring) — all in PRIMARY_REGION | ||
| 32 | +# - 1 100 GB volume attached to shithub-app | ||
| 33 | +# - 3 Spaces buckets (shithub-backups in PRIMARY_REGION, | ||
| 34 | +# shithub-backups-dr in DR_REGION, shithub-docs in PRIMARY_REGION) | ||
| 35 | +# | ||
| 36 | +# What it does NOT do: | ||
| 37 | +# - Generate Spaces access keys (do that via API → Spaces Keys in | ||
| 38 | +# the UI; doctl can't surface the secret either). | ||
| 39 | +# - Configure CDN custom domain on shithub-docs. | ||
| 40 | +# - Set DNS records on Namecheap. | ||
| 41 | +# | ||
| 42 | +# Safe to re-run: it skips resources that already exist by name. | ||
| 43 | + | ||
| 44 | +set -euo pipefail | ||
| 45 | + | ||
| 46 | +PRIMARY_REGION="${PRIMARY_REGION:-sfo3}" | ||
| 47 | +DR_REGION="${DR_REGION:-ams3}" | ||
| 48 | +PROJECT_NAME="${PROJECT_NAME:-shithub-prod}" | ||
| 49 | +SSH_KEY_NAME="${SSH_KEY_NAME:?set SSH_KEY_NAME to the name of the SSH key in your DO account}" | ||
| 50 | + | ||
| 51 | +if ! command -v doctl >/dev/null 2>&1; then | ||
| 52 | + echo "fatal: doctl not on PATH; install from https://docs.digitalocean.com/reference/doctl/" >&2 | ||
| 53 | + exit 2 | ||
| 54 | +fi | ||
| 55 | + | ||
| 56 | +if ! doctl account get >/dev/null 2>&1; then | ||
| 57 | + echo "fatal: doctl not authenticated; run 'doctl auth init'" >&2 | ||
| 58 | + exit 2 | ||
| 59 | +fi | ||
| 60 | + | ||
| 61 | +# Resolve the SSH key id by name. | ||
| 62 | +SSH_KEY_ID="$(doctl compute ssh-key list --no-header --format ID,Name | awk -v n="$SSH_KEY_NAME" '$2==n {print $1; exit}')" | ||
| 63 | +if [[ -z "$SSH_KEY_ID" ]]; then | ||
| 64 | + echo "fatal: no SSH key named $SSH_KEY_NAME in your DO account" >&2 | ||
| 65 | + echo "(list with: doctl compute ssh-key list)" >&2 | ||
| 66 | + exit 2 | ||
| 67 | +fi | ||
| 68 | +echo "using SSH key: $SSH_KEY_NAME (id $SSH_KEY_ID)" | ||
| 69 | + | ||
| 70 | +# --- 1. Project (idempotent: skip if name already exists) --- | ||
| 71 | +PROJECT_ID="$(doctl projects list --no-header --format ID,Name | awk -v n="$PROJECT_NAME" '$2==n {print $1; exit}')" | ||
| 72 | +if [[ -z "$PROJECT_ID" ]]; then | ||
| 73 | + echo "creating project $PROJECT_NAME..." | ||
| 74 | + PROJECT_ID="$(doctl projects create \ | ||
| 75 | + --name "$PROJECT_NAME" \ | ||
| 76 | + --purpose "Service or API" \ | ||
| 77 | + --environment Production \ | ||
| 78 | + --description "shithub.sh production environment" \ | ||
| 79 | + --no-header --format ID)" | ||
| 80 | +fi | ||
| 81 | +echo "project: $PROJECT_NAME (id $PROJECT_ID)" | ||
| 82 | + | ||
| 83 | +# --- 2. Droplets --- | ||
| 84 | +create_or_skip_droplet() { | ||
| 85 | + local name="$1" size="$2" tag="$3" | ||
| 86 | + local existing | ||
| 87 | + existing="$(doctl compute droplet list --no-header --format ID,Name | awk -v n="$name" '$2==n {print $1; exit}')" | ||
| 88 | + if [[ -n "$existing" ]]; then | ||
| 89 | + echo "droplet $name already exists (id $existing); skipping" | ||
| 90 | + echo "$existing" | ||
| 91 | + return | ||
| 92 | + fi | ||
| 93 | + echo "creating droplet $name (size $size)..." | ||
| 94 | + local id | ||
| 95 | + id="$(doctl compute droplet create "$name" \ | ||
| 96 | + --image ubuntu-24-04-x64 \ | ||
| 97 | + --region "$PRIMARY_REGION" \ | ||
| 98 | + --size "$size" \ | ||
| 99 | + --ssh-keys "$SSH_KEY_ID" \ | ||
| 100 | + --enable-monitoring \ | ||
| 101 | + --tag-names "shithub,$tag" \ | ||
| 102 | + --wait \ | ||
| 103 | + --no-header --format ID)" | ||
| 104 | + echo "$id" | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +APP_ID="$(create_or_skip_droplet shithub-app s-2vcpu-4gb shithub-app)" | ||
| 108 | +DB_ID="$(create_or_skip_droplet shithub-db s-2vcpu-4gb shithub-db)" | ||
| 109 | +BAK_ID="$(create_or_skip_droplet shithub-backup s-1vcpu-2gb shithub-backup)" | ||
| 110 | +MON_ID="$(create_or_skip_droplet shithub-monitoring s-2vcpu-4gb shithub-monitoring)" | ||
| 111 | + | ||
| 112 | +# --- 3. Block volume + attach to shithub-app --- | ||
| 113 | +VOL_NAME="shithub-data" | ||
| 114 | +VOL_ID="$(doctl compute volume list --no-header --format ID,Name | awk -v n="$VOL_NAME" '$2==n {print $1; exit}')" | ||
| 115 | +if [[ -z "$VOL_ID" ]]; then | ||
| 116 | + echo "creating 100 GB volume $VOL_NAME..." | ||
| 117 | + VOL_ID="$(doctl compute volume create "$VOL_NAME" \ | ||
| 118 | + --region "$PRIMARY_REGION" \ | ||
| 119 | + --size 100GiB \ | ||
| 120 | + --fs-type ext4 \ | ||
| 121 | + --no-header --format ID)" | ||
| 122 | + echo "attaching $VOL_NAME to shithub-app..." | ||
| 123 | + doctl compute volume-action attach "$VOL_ID" "$APP_ID" --wait | ||
| 124 | +else | ||
| 125 | + echo "volume $VOL_NAME already exists (id $VOL_ID); skipping create" | ||
| 126 | +fi | ||
| 127 | + | ||
| 128 | +# --- 4. Spaces buckets --- | ||
| 129 | +# doctl's spaces support exists but is limited; fall through to the s3-compatible API | ||
| 130 | +# via the standard awscli isn't worth the extra dep here. We use doctl's native | ||
| 131 | +# Spaces commands where they exist, and fall back to instructions for the rest. | ||
| 132 | +create_or_skip_space() { | ||
| 133 | + local name="$1" region="$2" | ||
| 134 | + if doctl spaces buckets list --no-header --format Name | grep -qx "$name" 2>/dev/null; then | ||
| 135 | + echo "Spaces bucket $name already exists; skipping" | ||
| 136 | + return | ||
| 137 | + fi | ||
| 138 | + echo "creating Spaces bucket $name in $region..." | ||
| 139 | + # Newer doctl versions support `doctl spaces buckets create`; older ones don't. | ||
| 140 | + if doctl spaces buckets create "$name" --region "$region" >/dev/null 2>&1; then | ||
| 141 | + echo " ok" | ||
| 142 | + else | ||
| 143 | + echo " doctl can't create the bucket (CLI version doesn't support it);" | ||
| 144 | + echo " create '$name' in region '$region' via the dashboard." | ||
| 145 | + fi | ||
| 146 | +} | ||
| 147 | + | ||
| 148 | +create_or_skip_space "shithub-backups" "$PRIMARY_REGION" | ||
| 149 | +create_or_skip_space "shithub-backups-dr" "$DR_REGION" | ||
| 150 | +create_or_skip_space "shithub-docs" "$PRIMARY_REGION" | ||
| 151 | + | ||
| 152 | +# --- 5. Move resources into the project --- | ||
| 153 | +echo "assigning resources to project $PROJECT_NAME..." | ||
| 154 | +doctl projects resources assign "$PROJECT_ID" \ | ||
| 155 | + --resource "do:droplet:$APP_ID" \ | ||
| 156 | + --resource "do:droplet:$DB_ID" \ | ||
| 157 | + --resource "do:droplet:$BAK_ID" \ | ||
| 158 | + --resource "do:droplet:$MON_ID" \ | ||
| 159 | + --resource "do:volume:$VOL_ID" >/dev/null | ||
| 160 | + | ||
| 161 | +# --- 6. Print the summary the operator needs for the inventory --- | ||
| 162 | +cat <<SUMMARY | ||
| 163 | + | ||
| 164 | +============================================================== | ||
| 165 | +provisioned. summary for inventory: | ||
| 166 | + | ||
| 167 | +Region (primary): $PRIMARY_REGION | ||
| 168 | +Region (DR): $DR_REGION | ||
| 169 | +Project: $PROJECT_NAME (id $PROJECT_ID) | ||
| 170 | + | ||
| 171 | +Droplets (public IPv4 → private IPv4): | ||
| 172 | +SUMMARY | ||
| 173 | + | ||
| 174 | +doctl compute droplet list --no-header --tag-name shithub \ | ||
| 175 | + --format Name,PublicIPv4,PrivateIPv4 \ | ||
| 176 | + | column -t | ||
| 177 | + | ||
| 178 | +cat <<SUMMARY | ||
| 179 | + | ||
| 180 | +Volume: $VOL_NAME (id $VOL_ID; attached to shithub-app) | ||
| 181 | + | ||
| 182 | +Spaces buckets: | ||
| 183 | + shithub-backups ($PRIMARY_REGION) — endpoint: $PRIMARY_REGION.digitaloceanspaces.com | ||
| 184 | + shithub-backups-dr ($DR_REGION) — endpoint: $DR_REGION.digitaloceanspaces.com | ||
| 185 | + shithub-docs ($PRIMARY_REGION, CDN target) | ||
| 186 | + | ||
| 187 | +NEXT STEPS (manual, no doctl path): | ||
| 188 | + 1. Generate Spaces access keys via dashboard: | ||
| 189 | + API → Spaces Keys → Generate New Key | ||
| 190 | + Name: shithub-prod-app | ||
| 191 | + Copy the secret immediately (shown once). | ||
| 192 | + 2. Enable CDN on the shithub-docs bucket and set custom domain | ||
| 193 | + to docs.shithub.sh (DO will print a CNAME target). | ||
| 194 | + 3. Set Namecheap DNS records: | ||
| 195 | + A @ <shithub-app public IPv4> | ||
| 196 | + A www <shithub-app public IPv4> | ||
| 197 | + CNAME docs <CDN target from step 2> | ||
| 198 | + 4. Continue with SETUP-GUIDE.md Phase B5 (SSH-bootstrap). | ||
| 199 | +============================================================== | ||
| 200 | +SUMMARY | ||