website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
infra/main.tf
| 778e0a4 | 1 | terraform { |
| 778e0a4 | 2 | required_providers { |
| 778e0a4 | 3 | cloudflare = { |
| 778e0a4 | 4 | source = "cloudflare/cloudflare" |
| 778e0a4 | 5 | version = "~> 5.0" |
| 778e0a4 | 6 | } |
| 778e0a4 | 7 | } |
| 778e0a4 | 8 | } |
| 778e0a4 | 9 | |
| 778e0a4 | 10 | provider "cloudflare" {} |
| 778e0a4 | 11 | |
| 778e0a4 | 12 | resource "cloudflare_r2_bucket" "website" { |
| 778e0a4 | 13 | account_id = var.cloudflare_account_id |
| 778e0a4 | 14 | name = "website" |
| 778e0a4 | 15 | } |
| 778e0a4 | 16 | |
| 778e0a4 | 17 | resource "cloudflare_r2_custom_domain" "website" { |
| 778e0a4 | 18 | account_id = var.cloudflare_account_id |
| 778e0a4 | 19 | bucket_name = cloudflare_r2_bucket.website.name |
| 778e0a4 | 20 | domain = "pyrossh.dev" |
| 778e0a4 | 21 | zone_id = var.cloudflare_zone_id |
| 778e0a4 | 22 | enabled = true |
| 778e0a4 | 23 | } |
| 778e0a4 | 24 | |
| 9bd972a | 25 | |
| 9bd972a | 26 | resource "cloudflare_r2_bucket" "git" { |
| 9bd972a | 27 | account_id = var.cloudflare_account_id |
| 9bd972a | 28 | name = "git" |
| 9bd972a | 29 | } |
| 9bd972a | 30 | |
| 9bd972a | 31 | resource "cloudflare_r2_custom_domain" "git" { |
| 9bd972a | 32 | account_id = var.cloudflare_account_id |
| 9bd972a | 33 | bucket_name = cloudflare_r2_bucket.git.name |
| 9bd972a | 34 | domain = "git.pyrossh.dev" |
| 9bd972a | 35 | zone_id = var.cloudflare_zone_id |
| 9bd972a | 36 | enabled = true |
| 9bd972a | 37 | } |
| 9bd972a | 38 | |
| de88ec5 | 39 | resource "cloudflare_r2_bucket" "apps" { |
| de88ec5 | 40 | account_id = var.cloudflare_account_id |
| de88ec5 | 41 | name = "apps" |
| de88ec5 | 42 | } |
| de88ec5 | 43 | |
| 5a9aa2d | 44 | # Runs the Eleventy build on every `terraform apply`, before the sync below, |
| 5a9aa2d | 45 | # so `terraform apply` alone is enough — no separate `npm run build` step |
| 5a9aa2d | 46 | # needed first. Same "always re-run" trigger pattern as sync_website. |
| 5a9aa2d | 47 | resource "terraform_data" "build_website" { |
| 5a9aa2d | 48 | triggers_replace = [timestamp()] |
| 5a9aa2d | 49 | |
| 5a9aa2d | 50 | provisioner "local-exec" { |
| 5a9aa2d | 51 | working_dir = "${path.module}/.." |
| 5a9aa2d | 52 | command = "npm run build" |
| 5a9aa2d | 53 | } |
| 5a9aa2d | 54 | } |
| 5a9aa2d | 55 | |
| 778e0a4 | 56 | # Syncs ../dist into the bucket on every `terraform apply` — `terraform_data` |
| 778e0a4 | 57 | # has no real infra state of its own, so `triggers_replace` on a timestamp is |
| 778e0a4 | 58 | # the standard way to force its provisioner to re-run every time rather than |
| 778e0a4 | 59 | # only on the first apply. `rclone sync` (not `copy`) both uploads changed |
| 778e0a4 | 60 | # files and deletes remote objects no longer present locally, so the bucket |
| 778e0a4 | 61 | # always matches the latest build exactly. |
| 778e0a4 | 62 | # |
| 778e0a4 | 63 | # Requires `rclone` on the machine running `terraform apply`, with a remote |
| 778e0a4 | 64 | # named "website" configured for R2 (`rclone config`) — an S3-compatible |
| 778e0a4 | 65 | # access key/secret scoped to this bucket, not the Cloudflare API token used |
| 778e0a4 | 66 | # above. |
| 5a9aa2d | 67 | # |
| fa93bad | 68 | resource "terraform_data" "sync_website" { |
| fa93bad | 69 | triggers_replace = [timestamp()] |
| fa93bad | 70 | |
| fa93bad | 71 | depends_on = [cloudflare_r2_bucket.website, terraform_data.build_website] |
| fa93bad | 72 | |
| fa93bad | 73 | provisioner "local-exec" { |
| 60e980e | 74 | command = "rclone sync ${path.module}/../dist website:${cloudflare_r2_bucket.website.name} --checksum --fast-list --multi-thread-streams=32 -P " |
| fa93bad | 75 | } |
| fa93bad | 76 | } |
| fa93bad | 77 | |
| 5a9aa2d | 78 | # The custom domain sits behind Cloudflare's edge cache, and by default only |
| 5a9aa2d | 79 | # static-asset extensions (css/js/images/fonts) are cached there — not HTML. |
| 5a9aa2d | 80 | # Since the sync reuses the same URLs on every deploy (no content-hashed |
| 5a9aa2d | 81 | # filenames), an updated asset at an unchanged path would keep serving the |
| 5a9aa2d | 82 | # stale cached copy at the edge until it expires on its own. Purging right |
| 5a9aa2d | 83 | # after the sync avoids that gap. Requires the CLOUDFLARE_API_TOKEN already |
| 5a9aa2d | 84 | # used above to also carry the "Zone > Cache Purge > Purge" permission for |
| 5a9aa2d | 85 | # this zone — it isn't included in the R2-only scope from the setup steps |
| 5a9aa2d | 86 | # earlier, so add it to the token if the purge call starts failing with 403. |
| fa93bad | 87 | # |
| fa93bad | 88 | # Kept as its own resource (not tacked onto sync_website's command) so a |
| fa93bad | 89 | # flaky purge call fails independently of the sync — you can tell from the |
| fa93bad | 90 | # apply output which of the two actually broke, rather than one opaque |
| fa93bad | 91 | # multi-line script's exit code. |
| fa93bad | 92 | resource "terraform_data" "purge_cache" { |
| 778e0a4 | 93 | triggers_replace = [timestamp()] |
| 778e0a4 | 94 | |
| fa93bad | 95 | depends_on = [terraform_data.sync_website] |
| 778e0a4 | 96 | |
| 778e0a4 | 97 | provisioner "local-exec" { |
| 5a9aa2d | 98 | command = <<-EOT |
| fa93bad | 99 | curl -sf --retry 3 -X POST "https://api.cloudflare.com/client/v4/zones/${var.cloudflare_zone_id}/purge_cache" \ |
| 5a9aa2d | 100 | -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ |
| 5a9aa2d | 101 | -H "Content-Type: application/json" \ |
| 5a9aa2d | 102 | --data '{"purge_everything": true}' |
| 5a9aa2d | 103 | EOT |
| 778e0a4 | 104 | } |
| 778e0a4 | 105 | } |