iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Claude AI

奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲系列 第 9

Day 09:Terraform 開疆:沒有 VPC 的雲端疆域(DynamoDB、S3、IAM 最小權限)

  • 分享至 

  • xImage
  •  

claude_09

系列:奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲
今日工具:Claude Code + Terraform
今日進度infra/terraform/modules/foundation 完成,terraform plan 顯示 12 個資源待建立,未 apply

前言

Day 8 的 Go 骨架已經能在本機吐出遊戲資料,但它終究要住進雲端。Day 5 的 ADR-0005 在 Terraform 與 CDK 之間選了 Terraform,理由很單純:這個專案的基礎設施不需要程式語言的抽象能力,需要的是「一眼看得懂的宣告」;而且 plan 的輸出是純文字,我可以整段貼回去請 Claude 解釋。

同一份 ADR 還定了另一件事:這個架構完全沒有 VPC。Lambda、API Gateway、DynamoDB、S3、CloudFront 全是託管服務,沒有子網要切、沒有 NAT 要開、沒有安全群組鏈要串。我本來以為今天會因此變得很短,結果相反——VPC 消失之後,「基礎設施的第一層到底該放什麼」這個問題得重新回答一次。

一、疆域規劃:先決定「刪掉會痛的是什麼」

舊的三階段分法是網路 → 運算 → 監控,那是有 VPC 的世界才成立的順序。沒有 VPC 之後我換了一條軸線:會留下資料的東西 → 隨時可重建的東西 → 會叫的東西

claude_09_diagram_01

這條軸線的好處是它一次回答了三個問題:誰該開 deletion_protection、誰可以隨手 destroy 重來、哪個 module 出事我要被吵醒。DynamoDB 表刪掉,玩家的存檔就沒了;Lambda 函式刪掉,deploy-api.yml 兩分鐘補回來。這兩種東西不該住在同一個 module。

region 的表格今天只剩一列:

元件 為什麼
Region ap-east-2(台北) 從台灣延遲最低;但它是 opt-in region,帳號要先啟用

opt-in 是我在紙上階段就查清楚的坑:新帳號預設不啟用 ap-east-2,未啟用時任何呼叫回 UnrecognizedClientExceptionAuthFailure,錯誤訊息完全不會提到「你要先啟用區域」。所以今天的第一行指令不是 terraform

aws ec2 describe-regions --all-regions \
  --query "Regions[?RegionName=='ap-east-2'].OptInStatus" --output text
# 期望輸出:opted-in

第二個坑是 STS:opt-in region 預設不接受 global endpoint 發出的 session token。兩道保險——把帳號的 STS global endpoint 設成「Valid in all AWS Regions」,以及客戶端一律走區域端點。

二、用 Claude Code 生成 modules/foundation

Prompt(給 Claude Code)
「在 modules/foundation 建地基模組:一張 on-demand DynamoDB 表(此時只有 PK/SK)、一個私有 S3 網站 bucket 加封鎖公開存取/ownership/版本控制/AES256 四個硬化資源、兩個 retention 14 天的 log group、一個 Lambda 執行角色配兩條自己寫的 inline policy、一個 AWS Budgets。不要用任何會打 AWS API 的 data source,帳號 id 走變數。」

產出的十二個資源,正好對上第一節那條「會留下資料」的軸線:

claude_09_diagram_02

兩個地方值得單獨講。

log group 的名字錯一個字都不行。 Lambda 的預設 log group 是 /aws/lambda/<函式名>,我先建就是為了鎖住 retention_in_days = 14;名字打錯的話 Terraform 建的那個沒人用,而 Lambda 執行時會自己再開一個永不過期的,帳單慢慢往上爬,而且畫面上一切正常。所以 local.name 定死 emberhold-prod,函式名與 log group 名都從它接出來,中間不准有人手打。

IAM 沒有掛 AWSLambdaBasicExecutionRole 那張 AWS 託管政策給的是 arn:aws:logs:*:*:*——帳號裡任何一個 log group 都能寫。我改成兩條自己寫的 inline policy:lambda_logs 只給 logs:CreateLogStreamlogs:PutLogEvents,資源寫 "${aws_cloudwatch_log_group.lambda_api.arn}:*"lambda_dynamodb 同理,只列這張表的 ARN。少掉的那個 logs:CreateLogGroup 反而是好事:函式沒辦法在名字打錯時偷偷幫自己建一個。

至於「AI 比我可靠」的橋段,今天演的是反過來的版本。Claude 第一版把 aws_s3_bucket_policy.web 也放進 foundation,policy 裡要引用 CloudFront distribution 的 ARN,於是 foundation → cdn → foundationterraform validate 直接吐 Cycle:。這個錯很合理——bucket 是它的、policy 當然也是它的——但模組邊界不是照「東西屬於誰」畫的,是照依賴方向畫的。圖右邊那個節點就是這段對話的墓碑。

三、envs/prod:state 放哪裡、provider 怎麼寫

環境層只做兩件事:宣告 backend 與 provider,然後呼叫模組。

# envs/prod/backend.tf
terraform {
  backend "s3" {
    bucket       = "emberhold-tfstate"
    key          = "prod/terraform.tfstate"
    region       = "ap-east-2"
    encrypt      = true
    use_lockfile = true
  }
}

四個細節。

第一,鎖表這件事要講清楚。 state 鎖用 S3 原生 lockfile,不另外開一張 DynamoDB 鎖表emberhold-prod 那張表是玩家的存檔,跟 state 鎖完全是兩件事。另外 provider 掛著 default_tagsProject=emberhold 等三個),Day 28 的月費表就靠這個標籤在 Cost Explorer 篩出來。

第二,state bucket 是全專案唯一手動建立的資源。 backend block 不支援變數內插,所以它不可能由 Terraform 自己建:

B=emberhold-tfstate   # 全球唯一,得自己換一個
aws s3api create-bucket --bucket $B --region ap-east-2 --create-bucket-configuration LocationConstraint=ap-east-2
aws s3api put-bucket-versioning --bucket $B --versioning-configuration Status=Enabled

再補 put-public-access-block(四個旗標全 true)與 put-bucket-encryption(AES256)。LocationConstraint=ap-east-2 不可省,只有 us-east-1 能省。

第三,provider 一定要 ~> 6.0 舊版 provider 內建一份區域白名單,~> 5.60 大機率不認得 ap-east-2,而且錯誤訊息長得像你把區域名打錯,會讓人往完全錯的方向查。

第四,skip_credentials_validationskip_requesting_account_id 這兩行是誠實的妥協。 它們讓 plan 能用假憑證離線跑完——這也是為什麼整份 envs/prod 不准出現 aws_caller_identity 這類會打 AWS API 的 data source,帳號 id 一律走 var.aws_account_id。但要說清楚:它們只讓 plan 與 validate 不必帶真憑證,apply 照樣要——而這個妥協還有一個我今天沒想到的代價,Day 16 會付。

四、plan、讀 plan、以及不准 apply

envs/prod 依序跑 terraform initfmt -recursive -checkvalidate,再跑 plan,最後一行是 Plan: 12 to add, 0 to change, 0 to destroy.

先講 validate,因為它的輸出不是乾淨的一行。它給 Success! The configuration is valid.,但前面掛著兩則 Warning: Argument is deprecatedhash_key is deprecated. Use key_schema instead.range_key 那則一模一樣。aws provider 6 把主鍵改推 key_schema 區塊,我這個舊寫法還能跑但已進倒數;今天先不動,因為換寫法要先確認 diff 不會變成 destroy/create——那是拿玩家存檔在賭。跟著做的人看到這兩則警告是預期輸出,不是你打錯字;它和「今天為什麼不改」一起記進踩坑筆記。

我把完整輸出貼回給 Claude Code,請它「用一句話解釋每個資源存在的理由,標出哪些會產生每月固定費用」。它回了一張十二列的表,最後一列寫了一句我看了很久的話:這十二個資源裡,沒有任何一個按小時固定計費。

舊架構的同一張表最下面掛著一個按小時跑錶的 NAT Gateway,玩家一個都沒有時照樣計費。現在整個地基層只剩 DynamoDB 的 on-demand、S3 的儲存量、log 的攝入量與免費的 Budgets,零流量的月費趨近於零。這不是省錢技巧,是換了一種計費形狀。

也因為如此,aws_budgets_budget.monthly 才有意義:它是「這款遊戲月費 < US$3」這個承諾的自動化守門員。承諾寫在 README 裡沒人會去對帳,寫成資源就會自己來吵你。

一個必須誠實交代的操作細節:backend.tf 裡有 backend "s3" 之後,terraform init -backend=false 只能讓 validate 跑起來,plan 還是會擋在 Error: Backend initialization required——-backend=false 是「跳過設定 backend」,不是「退回本機 state」。所以 make tf-validate 與 CI 的 infra job 都只做到 validate;我要在本機看那行 12 to add,就跑 make plan-offline——它背後的 scripts/tf-plan-offline.sh 把整棵 infra/terraform 複製到 mktemp -d、只把複製品的 backend 換成 backend "local" {}、再用假憑證 plan,要 commit 的那份一個字都不動。

今天沒有 apply,而且是技術上沒辦法:Day 6 的 .claude/settings.json 已經 deny 了 Bash(terraform apply:*),Claude 在 session 裡想跑也會被擋。真正的 apply 會在 Day 16 交給 GitHub Actions,經過 environment 的手動核准才執行。這不是不信任 AI,是不信任任何「在本機終端機按下去」的部署,包括我自己。

最後留一句伏筆:plan 綠燈跟 apply 會成功是兩件事。 plan 只算 diff,不會問 AWS「這個區域有沒有這個服務」「這組參數合不合法」,離線 plan 更是連 AWS 都不打。這行 12 to add 只證明資源圖畫得出來;這棵樹後來長到 51 個資源,其中兩個要等真的 apply 打下去,才知道在台北建不起來。

小結

沒有 VPC 之後,Terraform 的第一天從「畫網路圖」變成「畫責任邊界」。程式碼幾乎全是 Claude Code 生成的,但真正花時間的是生成前那個問題:這東西刪掉會不會痛? 今天最實用的一招仍是「把 plan 貼回去請它解釋成本」,而這次它回的是一句「沒有任何一個按小時計費」——那句話比任何架構圖都更能說明無伺服器換到了什麼。

今日產出

  • [x] infra/terraform/modules/foundation/{main,variables,outputs}.tf
  • [x] infra/terraform/envs/prod/{backend,providers,variables,main,outputs}.tf
  • [x] scripts/tf-plan-offline.shmake plan-offline:離線 plan 唯一入口
  • [x] terraform validate 通過(帶兩則 key_schema 警告)、plan 12 個資源、未 apply
  • [x] docs/adr/0005-iac.md 補「唯一手動建立的 state bucket」段落

明日預告

Day 10:劇情引擎後端化:用 Go 設計分支劇情狀態機與存檔系統。


上一篇
Day 08:建造要塞:Go 後端專案架構設計,用 Claude Code 生成 Clean Architecture 骨架
系列文
奇幻塔防開發實錄:用 Claude 打造一款有靈魂的塔防遊戲9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言