用 GCP 免費額度自架 Ghost 部落格,讓 Terraform 自動化幫你部署

用 GCP 免費額度自架 Ghost 部落格,讓 Terraform 自動化幫你部署
Photo by Andra C Taylor Jr / Unsplash

如果想要一個自己掌控的部落格:不要被平台綁死,不想每次搬家都重新設定一輪,也希望未來真的要換雲端或換 VPS 時,至少基礎設施不是靠記憶復原。

所以這篇不是「在 GCP Console 點一台 VM 起來」的教學,而是把 Ghost 部落格部署流程整理成 Terraform。你改幾個變數,執行 terraform apply,GCP 上該有的 VM、static IP、防火牆規則與開機安裝腳本就會一起建立。

前提假設你已經申請好 GCP,也已經有一個 Project。還沒做的話,先完成 GCP 帳號、Billing account、Project 的啟用,再回來看這篇。

範例 repo 放在這裡:https://github.com/zhjiangexe/ghost-terraform-gcp-free-tier

裡面有完整的 Terraform 設定、startup script、.gitignoreREADME.md

免費額度不是魔法

GCP Free Tier 可以讓這個架構接近免費,但不是保證永遠零成本。

我查官方文件時,Compute Engine Free Tier 包含每月 1 台 non-preemptible e2-micro VM,可以放在:

  • Oregon:us-west1
  • Iowa:us-central1
  • South Carolina:us-east1

另外還包含 30 GB-months standard persistent disk,以及 1 GB 從 North America 出站到多數目的地的流量額度。

官方文件也寫有寫:Free Tier 有使用上限,超過就會照標準費率計費,而且 Google 可以調整 Free Tier 條件。部署前請再看一次官方頁面:

https://cloud.google.com/free/docs/free-cloud-features#compute

這篇的範例預設使用 us-west1/us-west1-a。理由很簡單:如果讀者人在台灣,美國西岸通常比美國中部或東岸更近,latency 有機會漂亮一點。但這不是 CDN,也不是保證最快;實際路由還是要看你的 ISP。

還有幾個可能花錢的地方:

  • 網域本身通常要申請要錢。
  • 流量超過 Free Tier 可能要錢。(初期不太容易不用擔心)
  • static IP 如果沒有綁在 VM 上,可能會開始計費。
  • 磁碟超過 30 GB 或改用 SSD 會計費。
  • 同時跑兩台 e2-micro,超出的那台會計費。

所以我的做法是:Terraform 中以 Free Tier 為主,但不能忘記雲端還是有帳單。

架構長這樣

這個版本的架構很樸素:

讀者瀏覽器
    |
DNS A record
    |
GCP static external IP
    |
Compute Engine e2-micro
    |
Nginx 80/443
    |
Ghost Docker container
    |
SQLite + Docker volume

VM 開機時會執行 GCP metadata 裡的 startup-script,自動完成:

  • 安裝 Docker
  • 建立 Ghost docker-compose.yml
  • 建立 Ghost .env
  • 安裝並設定 Nginx
  • 預留 Certbot SSL 流程
  • 建立每日備份腳本
  • age public key 加密備份
  • rclone 上傳備份

Ghost 使用 SQLite,對個人部落格來說夠簡單,也比較符合一台 Free Tier VM 的資源限制。

為什麼用 Terraform

手動點 Console 最大的問題是做完之後就忘記有哪些設定。

今天開了哪些 port?VM 用哪個 image?磁碟是 standard 還是 SSD?Static IP 名稱是什麼?不用太久,要搬家或重建時,差不多就忘光光了。

Terraform 的價值是把這些決定寫進檔案:

terraform/gcp/
├── main.tf
├── variables.tf
├── outputs.tf
├── startup-script.sh.tpl
└── terraform.tfvars.example

main.tf 描述要建立哪些 GCP 資源。
variables.tf 定義哪些值需要由使用者提供。
terraform.tfvars.example 是可以公開的填寫範本。
startup-script.sh.tpl 是 VM 開機後要跑的安裝腳本。

真正的秘密放在 terraform.tfvars,而這個檔案不進 git。

先把會洩漏個人資料的地方抽成變數

這一步很重要。

如果你要把專案放到 GitHub 或寫成文章給讀者參考,不要把自己的資料寫死在 main.tf 裡。至少這些都應該抽成變數:

  • project_id
  • domain
  • ghost_url
  • smtp_user
  • smtp_password
  • ssh_public_key
  • backup_age_recipient
  • VM 名稱與資源前綴
  • region / zone
  • SMTP host / port
  • 備份目的地

範例的 variables.tf 裡,domainghost_url 沒有 default,代表使用者一定要自己填。terraform.tfvars.example 只放 example.com 這種 placeholder。

smtp_password 有設定:

sensitive = true

這會避免 Terraform 在 planapply 輸出中直接印出密碼。不過要注意,它不會加密 Terraform state。只要 secret 有進入 Terraform,state 就可能保存敏感資料,所以 terraform.tfstate 一樣不能公開。

準備本機環境

先登入 GCP:

gcloud auth login
gcloud auth application-default login
gcloud config set project YOUR_PROJECT_ID

啟用 Compute Engine API:

gcloud services enable compute.googleapis.com --project=YOUR_PROJECT_ID

確認你有 SSH public key:

cat ~/.ssh/id_ed25519.pub

如果沒有,就建立一組:

ssh-keygen -t ed25519 -C "your@email.com"
cat ~/.ssh/id_ed25519.pub

備份加密會用到 age public key。你可以在本機產生:

age-keygen -o ~/ghost-backup-age-key.txt

檔案裡的 age1... 是 public key,可以放進 VM。AGE-SECRET-KEY-... 是 private key,要放密碼管理器,不能丟。

填 terraform.tfvars

進入範例 repo:

cd blog/ghost-gcp-terraform/terraform/gcp
cp terraform.tfvars.example terraform.tfvars

打開 terraform.tfvars,把 placeholder 換掉:

project_id = "your-gcp-project-id"

region = "us-west1"
zone   = "us-west1-a"

domain    = "example.com"
ghost_url = "https://example.com"

mail_from     = "hello@example.com"
smtp_host     = "smtp-relay.brevo.com"
smtp_port     = 587
smtp_user     = "your-brevo-account@email.com"
smtp_password = "your-brevo-smtp-key"

ssh_user       = "debian"
ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your@email.com"

backup_age_recipient = "age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
backup_remote        = "gdrive:ghost-backups/"
backup_schedule      = "0 3 * * *"

如果你目前已經有一台 Free Tier VM 在跑,先不要同時開第二台長時間運作。Free Tier 是每個 billing account 合併計算,不是每個 Project 都送一台。

部署

初始化 Terraform:

terraform init

先看它打算建立什麼:

terraform plan

確認沒有奇怪的資源後再 apply:

terraform apply

完成後 Terraform 會輸出 VM 的 static IP:

vm_external_ip = "xx.xx.xx.xx"

把這個 IP 拿去 DNS provider 設定 A record:

A    @      xx.xx.xx.xx
A    www    xx.xx.xx.xx

DNS 生效後,再到 VM 上跑 Certbot:

sudo certbot --nginx -d example.com -d www.example.com

Ghost 的 url 已經在 .env 裡設定成 https://example.com,所以 SSL 完成後,後台和前台網址會一致。

別忘了處理 SSH 防火牆

Terraform 這邊會建立一條 IAP-only SSH 規則,只允許 Google IAP TCP forwarding 的來源段連到 22 port。

但是 GCP 的 default network 常常會內建這兩條規則:

  • default-allow-ssh
  • default-allow-rdp

它們不是這份 Terraform 建的,所以 Terraform 不會自動刪。確認你可以用 IAP 或 Console SSH 連線之後,建議刪掉:

gcloud compute firewall-rules delete default-allow-ssh default-allow-rdp --quiet

公開 SSH 就是在等掃描器敲門;IAP 至少讓 SSH 入口先經過 Google IAM。

Ghost 起來後要檢查什麼

SSH 進 VM 後可以看 startup script log:

sudo tail -f /var/log/ghost-startup.log

看到這行代表安裝跑完:

=== Ghost setup complete: ...

檢查 container:

sudo docker ps

檢查 Nginx 轉發:

curl -I http://localhost/

如果 SMTP 設錯,Ghost 本站通常還是會跑,但忘記密碼、邀請使用者、通知信就會出問題。部署完最好進 Ghost Admin 送一封測試信。

備份:不要等壞掉才想

範例 startup script 會建立 /opt/ghost/backup.sh,每天用 cron 跑一次。

備份流程是:

  1. 暫停 Ghost container
  2. 壓縮 Ghost content volume
  3. 用 age public key 加密
  4. 啟動 Ghost container
  5. 用 rclone 上傳到 backup_remote

這裡刻意讓 VM 只持有 public key。能解密備份的 private key 不應該放在 VM 上。

不過 rclone 的遠端設定仍需要你自己完成,因為那會牽涉 Google Drive 或其他備份目的地的授權。部署完請務必手動跑一次備份,確認遠端真的有檔案。

更新和清除

如果只是改 Ghost 的 .env,例如換 SMTP password,進 VM 修改後要用 down + up,不要只用 restart

sudo docker compose -f /opt/ghost/docker-compose.yml down
sudo docker compose -f /opt/ghost/docker-compose.yml up -d

如果要清掉所有 Terraform 建的資源:

terraform destroy

測試完不用就 destroy。特別是 static IP,沒有綁定 VM 時可能會產生成本。

結語

這套做法不是最豪華的 Ghost 架構,但它很適合個人部落格:成本低、可重現、設定看得見​,把部署過程變成可以被閱讀、修改、重跑的程式碼。哪天要換網域、換 SMTP、重建 VM,或把這套搬去別的 Project,至少不需要從瀏覽器點擊歷史裡考古。