tenseleyflow/shithub / 9df9065

Browse files

deploy: SETUP-GUIDE — UI-agnostic Phase B + provision-do.sh as doctl path

Authored by espadonne
SHA
9df906531e0fbb694a112e02859887f848049c3f
Parents
6093ed9
Tree
164c886

2 changed files

StatusFile+-
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