| 1 | # gcp-static-site |
| 2 | |
| 3 | A static website served from a public Cloud Storage bucket. The GCP |
| 4 | counterpart of [`aws-static-site`](../aws-static-site). |
| 5 | |
| 6 | ``` |
| 7 | site/ ──GCP.Storage.Files──> gs://…/index.html, styles.css, docs/index.html, 404.html |
| 8 | │ allUsers: roles/storage.objectViewer |
| 9 | v |
| 10 | https://storage.googleapis.com/<bucket>/index.html |
| 11 | ``` |
| 12 | |
| 13 | - `GCP.Storage.Bucket("Site")` — uniform bucket-level access, website |
| 14 | config `mainPageSuffix: "index.html"` and `notFoundPage: "404.html"`, |
| 15 | and `forceDestroy: true` so destroy empties it. |
| 16 | - `GCP.IAM.Member("PublicRead")` — grants `allUsers` |
| 17 | `roles/storage.objectViewer` on the bucket's own IAM policy. Anyone can |
| 18 | read (and list) the objects; nothing else in the project is exposed. |
| 19 | - `GCP.Storage.Files("SiteFiles")` — one `GCP.Storage.Object` per file |
| 20 | under `site/`, keyed by its relative path, with `Content-Type` inferred |
| 21 | from the extension (`text/html; charset=utf-8`, `text/css; charset=utf-8`, |
| 22 | `image/png`, …). HTML is served `Cache-Control: no-cache`; everything |
| 23 | else `public, max-age=300`. |
| 24 | |
| 25 | There are no Functions, bindings, or event sources — nothing runs |
| 26 | server-side. |
| 27 | |
| 28 | ## How redeploys work |
| 29 | |
| 30 | Each `Object` compares the MD5 Cloud Storage reports with the MD5 of the |
| 31 | local file (plus its `Content-Type` and `Cache-Control`). A redeploy |
| 32 | uploads only files whose bytes or headers changed, and deletes the objects |
| 33 | of files you removed from `site/`. |
| 34 | |
| 35 | ## Links on path-style URLs |
| 36 | |
| 37 | On `https://storage.googleapis.com/<bucket>/…` the bucket name is part of |
| 38 | the path, so the pages link to each other with relative URLs |
| 39 | (`styles.css`, `docs/index.html`). Root-relative links (`/styles.css`) |
| 40 | only work once the site is served from its own domain. |
| 41 | |
| 42 | ## Custom domain and HTTPS |
| 43 | |
| 44 | The website config (`mainPageSuffix`, `notFoundPage`) only takes effect |
| 45 | when the bucket is served from a domain: a `/docs/` → `docs/index.html` |
| 46 | rewrite and the `404.html` body are not applied to path-style URLs, which |
| 47 | answer a missing object with Cloud Storage's own 404. Serving the site on |
| 48 | a custom domain over HTTPS requires a global external Application Load |
| 49 | Balancer with a backend bucket, a managed certificate, and a DNS record — |
| 50 | out of scope for this example. |
| 51 | |
| 52 | ## Deploy |
| 53 | |
| 54 | Credentials come from your alchemy profile: run `alchemy profile` once and pick GCP (*Service account JSON* for a key file, or *Stored* for an access token or key kept in `~/.alchemy/credentials`, plus a default region), then deploy with `--profile <name>`. No Docker needed. |
| 55 | |
| 56 | ```sh |
| 57 | pnpm deploy --profile <name> |
| 58 | ``` |
| 59 | |
| 60 | ```sh |
| 61 | curl -i "$url" # index.html |
| 62 | curl -i "https://storage.googleapis.com/$bucketName/styles.css" |
| 63 | ``` |
| 64 | |
| 65 | The project must allow public buckets: the |
| 66 | `constraints/storage.publicAccessPrevention` and |
| 67 | `constraints/iam.allowedPolicyMemberDomains` organization policies both |
| 68 | block `allUsers` grants. |
| 69 | |
| 70 | ## Test |
| 71 | |
| 72 | ```sh |
| 73 | ALCHEMY_PROFILE=<name> bun test |
| 74 | ``` |
| 75 | |
| 76 | Deploys the stack, fetches `index.html`, `styles.css`, `docs/index.html` |
| 77 | and `404.html` anonymously and checks each body, `Content-Type` and |
| 78 | `Cache-Control`, checks a missing path is a 404, reads the bucket's |
| 79 | website config and IAM policy back from the Cloud Storage API, then |
| 80 | destroys the stack and checks the bucket is gone. |
| 81 | |
| 82 | ## Destroy |
| 83 | |
| 84 | ```sh |
| 85 | pnpm destroy --profile <name> |
| 86 | ``` |