Bash · 8212 bytes Raw Blame History
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)" >&2
82
83 # --- 2. Droplets ---
84 # Status messages must go to stderr so they don't pollute the captured
85 # stdout (which the caller assigns to APP_ID etc.). Only the bare ID
86 # goes to stdout.
87 create_or_skip_droplet() {
88 local name="$1" size="$2" tag="$3"
89 local existing
90 existing="$(doctl compute droplet list --no-header --format ID,Name | awk -v n="$name" '$2==n {print $1; exit}')"
91 if [[ -n "$existing" ]]; then
92 echo "droplet $name already exists (id $existing); skipping" >&2
93 echo "$existing"
94 return
95 fi
96 echo "creating droplet $name (size $size)..." >&2
97 local id
98 id="$(doctl compute droplet create "$name" \
99 --image ubuntu-24-04-x64 \
100 --region "$PRIMARY_REGION" \
101 --size "$size" \
102 --ssh-keys "$SSH_KEY_ID" \
103 --enable-monitoring \
104 --tag-names "shithub,$tag" \
105 --wait \
106 --no-header --format ID)"
107 echo "$id"
108 }
109
110 APP_ID="$(create_or_skip_droplet shithub-app s-2vcpu-4gb shithub-app)"
111 DB_ID="$(create_or_skip_droplet shithub-db s-2vcpu-4gb shithub-db)"
112 BAK_ID="$(create_or_skip_droplet shithub-backup s-1vcpu-2gb shithub-backup)"
113 MON_ID="$(create_or_skip_droplet shithub-monitoring s-2vcpu-4gb shithub-monitoring)"
114
115 # --- 3. Block volume + attach to shithub-app ---
116 # Volume creation and attach are independent steps; on a re-run we may
117 # find the volume created but never attached (if a prior run died here).
118 # Handle both checks separately.
119 VOL_NAME="shithub-data"
120 VOL_ID="$(doctl compute volume list --no-header --format ID,Name | awk -v n="$VOL_NAME" '$2==n {print $1; exit}')"
121 if [[ -z "$VOL_ID" ]]; then
122 echo "creating 100 GB volume $VOL_NAME..." >&2
123 VOL_ID="$(doctl compute volume create "$VOL_NAME" \
124 --region "$PRIMARY_REGION" \
125 --size 100GiB \
126 --fs-type ext4 \
127 --no-header --format ID)"
128 else
129 echo "volume $VOL_NAME already exists (id $VOL_ID); skipping create" >&2
130 fi
131
132 # Check whether the volume is already attached. doctl prints DropletIDs
133 # as a JSON-style array like [123456789] or [].
134 ATTACHED_TO="$(doctl compute volume get "$VOL_ID" --no-header --format DropletIDs | tr -d '[]' | xargs)"
135 if [[ -z "$ATTACHED_TO" ]]; then
136 echo "attaching $VOL_NAME to shithub-app (id $APP_ID)..." >&2
137 doctl compute volume-action attach "$VOL_ID" "$APP_ID" --wait >&2
138 else
139 echo "volume $VOL_NAME already attached to droplet(s) $ATTACHED_TO; skipping" >&2
140 fi
141
142 # --- 4. Spaces buckets ---
143 # doctl's spaces support varies by version. We attempt the native commands;
144 # if they're not present, we print a fallback and continue.
145 create_or_skip_space() {
146 local name="$1" region="$2"
147 if doctl spaces buckets list --no-header --format Name 2>/dev/null | grep -qx "$name"; then
148 echo "Spaces bucket $name already exists; skipping" >&2
149 return
150 fi
151 echo "creating Spaces bucket $name in $region..." >&2
152 if doctl spaces buckets create "$name" --region "$region" >/dev/null 2>&1; then
153 echo " ok" >&2
154 else
155 echo " doctl can't create the bucket (CLI version may not support it);" >&2
156 echo " create '$name' in region '$region' via the dashboard." >&2
157 fi
158 }
159
160 create_or_skip_space "shithub-backups" "$PRIMARY_REGION"
161 create_or_skip_space "shithub-backups-dr" "$DR_REGION"
162 create_or_skip_space "shithub-docs" "$PRIMARY_REGION"
163
164 # --- 5. Move resources into the project ---
165 echo "assigning resources to project $PROJECT_NAME..."
166 doctl projects resources assign "$PROJECT_ID" \
167 --resource "do:droplet:$APP_ID" \
168 --resource "do:droplet:$DB_ID" \
169 --resource "do:droplet:$BAK_ID" \
170 --resource "do:droplet:$MON_ID" \
171 --resource "do:volume:$VOL_ID" >/dev/null
172
173 # --- 6. Print the summary the operator needs for the inventory ---
174 cat <<SUMMARY
175
176 ==============================================================
177 provisioned. summary for inventory:
178
179 Region (primary): $PRIMARY_REGION
180 Region (DR): $DR_REGION
181 Project: $PROJECT_NAME (id $PROJECT_ID)
182
183 Droplets (public IPv4 → private IPv4):
184 SUMMARY
185
186 doctl compute droplet list --no-header --tag-name shithub \
187 --format Name,PublicIPv4,PrivateIPv4 \
188 | column -t
189
190 cat <<SUMMARY
191
192 Volume: $VOL_NAME (id $VOL_ID; attached to shithub-app)
193
194 Spaces buckets:
195 shithub-backups ($PRIMARY_REGION) — endpoint: $PRIMARY_REGION.digitaloceanspaces.com
196 shithub-backups-dr ($DR_REGION) — endpoint: $DR_REGION.digitaloceanspaces.com
197 shithub-docs ($PRIMARY_REGION, CDN target)
198
199 NEXT STEPS (manual, no doctl path):
200 1. Generate Spaces access keys via dashboard:
201 API → Spaces Keys → Generate New Key
202 Name: shithub-prod-app
203 Copy the secret immediately (shown once).
204 2. Enable CDN on the shithub-docs bucket and set custom domain
205 to docs.shithub.sh (DO will print a CNAME target).
206 3. Set Namecheap DNS records:
207 A @ <shithub-app public IPv4>
208 A www <shithub-app public IPv4>
209 CNAME docs <CDN target from step 2>
210 4. Continue with SETUP-GUIDE.md Phase B5 (SSH-bootstrap).
211 ==============================================================
212 SUMMARY