這篇整理在 GCP 上為費用失控設防的機制與判斷依據,範圍從專案已經連結到一個帳單帳戶開始。開發環境成本控制的〈Per-team 成本上限〉說明了為什麼要給團隊與環境設一個費用的上限;這一篇處理那個上限在 GCP 上由哪些機制執行、每一個機制在費用超過門檻時實際做了什麼,以及從費用發生到它動作之間隔了多久。三個機制分別是預算警示(花費達門檻時寄通知)、強制執行支出上限(暫停指定服務的新用量),以及由預算通知觸發的處置(由程式把專案從帳單帳戶解除連結,或只停止花錢的資源);另外寫停損開關放在另一個專案的理由、驗收為什麼分兩段,以及停用之後的恢復順序。建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令與實跑結果,在 GCP 預算警示與停損開關的實測紀錄。

文中的觀察來自 2026 年 10 月在一個 GCP 測試專案上的實跑;標「未實跑」的做法沒有在實跑中執行;引用 Cloud Billing 文件或 gcloud 說明的行為,出處寫在句子裡。主控台的選項與服務清單會變動,以操作當下看到的為準。

費用失控的來源與反應時間

費用失控指的是費用增加的速度快過人發現它、處理它的速度。來源分成兩類,兩類的處置方式不同:

  • 服務自己出錯:程式對失敗的請求無限重試、一個事件觸發的函式又產生同一種事件而形成遞迴、自動擴縮允許的最大執行個體數設得太高。費用花在自己的服務上,修好程式之後費用就停止增加。
  • 資源被別人濫用:外洩的 API 金鑰(API key)被拿去呼叫付費 API、被入侵的 VM 被拿去挖礦。費用花在攻擊者的用途上,處理的人要先切斷攻擊者能用的資源,再處理入侵。

兩類的共同難處在時間:費用從發生到任何機制動作,中間隔著一段延遲。一個防護機制從費用發生到動作,要經過三段:雲端把用量算成費用、寫進帳單資料;帳單資料觸發通知;通知觸發處置(人或程式)。把用量算成費用那一段的延遲由雲端決定,GCP 的帳單主控台寫著費用資料要數小時後才會顯示、有時超過 24 小時。實測時一個專案已經跑了幾個小時的 VM 與 GKE 叢集,那段期間送到的預算通知裡 costAmount(本期累計花費)仍是 0.0;那個預算用預設的抵免設定,所以這次分不出 0.0 來自延遲還是抵免。

預算警示與由預算通知觸發的處置讀的是帳單資料,反應時間至少是這段延遲。強制執行支出上限依的是 GCP 估算的費用,Cloud Billing 文件說這份估算比帳單報表上的費用快得多,但仍不是即時,延遲期間超出上限的部分照常計費。這三個機制的費用最後停在哪裡也不同:

  • 強制執行支出上限與解除帳單連結:費用停止增加,總額是觸發的金額再加上延遲期間已經發生的費用。
  • 只停止花錢的資源:被停掉的資源不再計費,磁碟與儲存照常計費。
  • 預算警示:不停止任何用量,費用停在哪裡取決於收到通知的人多快處理。

要在費用發生的當下限制它,得靠不經過帳單資料的限制,它們也決定了延遲期間最多能多花多少:

  • 能用多少:API 的配額、速率限制(rate limit)、服務允許的最大執行個體數(例如 Cloud Run 的 --max-instances、MIG 的最大台數),以及 Compute Engine 的區域 CPU 與 GPU 配額。配額調到實際用量附近,被入侵的帳號能開的 VM 台數就有上限。
  • 能建什麼:組織政策(Organization Policy,GCP 繁體中文介面寫作「機構政策」)可以限制專案能啟用的 API,搭配 Compute Engine 的自訂限制條件可以限制能用的機型,前提是專案屬於一個組織(未實跑:測試帳號底下沒有組織)。
  • 金鑰能做什麼:API 金鑰的使用範圍限定可呼叫的 API 與來源;實測 gcloud services api-keys create 不給 --api-target 時被拒,新建的金鑰一定帶 API 範圍。金鑰被盜用時,先撤銷或限制那一把金鑰,影響範圍比停用整個專案的帳單小。

預算警示:門檻依據、範圍、抵免與通知對象

預算(budget)是建在帳單帳戶上的一組門檻:本期花費達到預算的某個比例時,GCP 寄通知給帳單帳戶的管理員。門檻有兩種依據,實際花費(已經寫進帳單資料的費用)與預測花費(GCP 依目前的速度推估到期末的費用);預測花費的門檻通常比實際花費的門檻早觸發,代價是一次短暫的尖峰也可能觸發預測花費的門檻。預算只寄通知,扣款照常進行。建立指令(gcloud billing budgets create 加 --filter-projects、--threshold-rule)與它的兩道權限檢查,在 GCP 預算警示與停損開關的實測紀錄 的〈建立限定一個專案的預算〉。

決定一個預算會不會在對的時候通知對的人,有三件事:

  • 範圍:預算用 --filter-projects 限定要計算的專案。gcloud 說明寫著不給這個旗標時涵蓋所有專案,通知裡的 costAmount 就是帳單帳戶下所有專案的合計:別的專案的花費也會讓它越過門檻,而這個專案自己的超支被合計稀釋。
  • 抵免:gcloud 說明寫著預設把帳戶上的抵免扣掉之後才算花費。帳戶上有大額抵免(新創方案、試用額度)時,costAmount 在抵免用完之前都接近 0,門檻在抵免被燒光的整段期間都不會觸發。抵免本身也是要保護的錢時,建立預算時加 --credit-types-treatment=exclude-all-credits,花費以抵免前的金額計算,代價是免費方案的用量也會以原價算進去(推論:FREE_TIER 是預算接受的抵免類型,這次沒有量);只想讓部分抵免照常扣掉時,改用 include-specified-credits 搭配 --filter-credit-types。試用額度用完或試用期結束之後,帳單帳戶與資源會被停用、停止還是開始計費,GCP 與 Oracle Cloud、AWS 的規則見 雲端免費額度與小型主機的收費比較 的〈試用結束後帳號與資源的處置〉。
  • 通知對象:通知預設寄給帳單帳戶的管理員與帳單使用者,建立預算時先確認這兩個角色的成員信箱有人在看。值班的人通常不在這兩個角色裡,要讓他們收到,把通知另外寄到 Cloud Monitoring 的電子郵件通知管道(Cloud Billing 文件寫明預算只支援電子郵件類型的管道);要送到簡訊、聊天工具或 webhook,改由 Pub/Sub 主題接一段轉送程式。

預算另外可以把通知送到一個 Pub/Sub 主題(topic),由預算通知觸發的處置就是從這個主題接收通知;接法在 GCP 預算警示與停損開關的實測紀錄 的〈建接收通知的主題並把預算接上〉。預算警示沒有辦法手動送出一封測試用的門檻通知,第一封真實的門檻通知寄到之前,「通知送得到收的人」這一段沒有驗證過;寄到時確認收到的是誰、多久後有人看到。

強制執行支出上限:涵蓋的服務、範圍與暫停期間

在帳單主控台的「Budgets & caps」頁面(2026 年 10 月主控台顯示的名稱)建立預算時,表單裡有一個「強制執行支出上限(適用於某些服務)」的選項:花費達到上限時,GCP 暫停表單裡選定的那個專案、那一項服務的新用量,直到這個預算期間結束(以月為期的預算是到月底,下一期自動重設)或有人手動解除為止。它是帳單功能裡不必自己寫程式就能暫停用量的機制,範圍有四個限制:

  • 一個上限只涵蓋一個專案裡的一項服務:要保護多項服務,就每項各建一個。
  • 只涵蓋清單上的服務:2026 年 10 月建立表單時列出的是 Cloud Run、Cloud Run functions、Gemini API 與 Vertex AI 四項,Compute Engine 與 GKE 不在清單上。表單的服務下拉選單就是涵蓋清單,建立之前打開看一次。
  • 持續性資源的固定費用不暫停:Cloud Billing 文件說明,與持續性資源綁在一起的固定用量不會被暫停,執行也有延遲,超出的部分照常計費。
  • 以抵免前的金額計算:Cloud Billing 文件寫明上限的花費不扣抵免與折扣,與預算的預設抵免設定相反。帳戶有抵免、而上限與預算的金額相近時,上限會先達到。

工作負載跑在清單上的服務時,這個選項不必寫程式;它暫停的是這項服務的新用量,所以對正式服務同樣是一次停機,只是範圍限於這一項服務。是否建立,依〈由預算通知觸發的處置:解除帳單連結與只停止花錢的資源〉比較的兩種損失(多付的費用,與服務停止、資料可能遺失)來判斷。一個專案同時有清單上的服務與不在清單上的服務時,兩種做法可以並用:清單上的服務各建一個支出上限,在估算費用達到上限時先暫停那一項服務;不在清單上的資源由只停止花錢的資源那一種處置接手。改用解除帳單連結的話,停掉的是整個專案,清單上的服務也一起停。

由預算通知觸發的處置:解除帳單連結與只停止花錢的資源

這一節的做法是讓預算通知觸發一段程式,由程式在花費達到預算時停止用量。停止用量有兩種做法,差別在停掉的範圍:

  • 解除帳單連結:程式呼叫 Cloud Billing API 把專案從帳單帳戶解除連結。專案沒有連結帳單帳戶時,GCP 停止專案裡所有的服務(包括免費方案的服務),費用也就停止增加。這段程式在下文稱為停損開關(kill switch)。
  • 只停止花錢的資源:程式改成停止專案裡執行中的 VM、把 MIG 的目標台數設成 0、把 GKE node pool 縮到 0 個節點。Cloud Billing 文件的〈Control resource usage with notifications〉就是這種做法,範例是逐台停止一個 zone 裡執行中的 VM。實測改用 instances().aggregatedList 跨全部 zone 列出 RUNNING 的 VM 再逐台 stop,服務帳號只給 roles/compute.instanceAdmin.v1:訊息發出後函式立刻送出停止,VM 約兩分鐘後變成 TERMINATED,帳單仍是啟用、開機磁碟仍在。資料留在磁碟與儲存裡、那部分照常計費,所以費用不會歸零;代價是程式只停它列得出來的資源,漏掉一種資源型別,或攻擊者另外開的新資源,那部分照常計費。

呼叫鏈上的 Eventarc 是 GCP 把事件(這裡是主題裡的訊息)轉成對函式的呼叫的服務;第二代的 Cloud Run functions 以 Eventarc 觸發器接主題。

1預算(帳單帳戶)── 通知 ──> Pub/Sub 主題 ── 推送 ──> Eventarc 觸發器 ── 呼叫 ──> Cloud Run functions 函式 ──> 解除帳單連結,或停止花錢的資源

兩種做法的預算、主題、觸發器與重試設定相同,差別在函式本體、服務帳號的角色與驗收的終點:改成只停止花錢的資源時,函式改成呼叫 Compute Engine 與 GKE 的 API,服務帳號改給能修改那些資源的角色(VM 與 MIG 例如 roles/compute.instanceAdmin.v1,GKE node pool 的縮放要 container 的權限,以 gcloud iam roles describe 確認),驗收的終點改成超出預算的訊息之後 gcloud compute instances list 的狀態變成 TERMINATED、MIG 的目標台數與 node pool 的節點數變成 0,每一個 zone 與資源型別各確認一次(MIG 與 node pool 的部分未實跑)。

建之前先決定要不要建、建哪一種,因為停損開關的效果是一次自己造成的全面停機:VM、資料庫、函式、Kubernetes 叢集一起停擺,停損開關本身跑在同一個專案時也會停。Cloud Billing 文件另外警告,停用帳單時資源可能被永久刪除,也不保證服務能恢復,所以這是一次可能遺失資料的停機。判斷方式是比較兩種損失:「被濫用或程式出錯而多付一筆費用」與「這個專案的服務停止、資料可能遺失」,哪一種比較大。

判斷的單位是專案,因為停損開關停的是整個專案。混合了正式服務與沙箱的專案照正式服務判斷;想對沙箱建停損開關,就先把沙箱搬到自己的專案。逐個專案套下面三條:

  • 停機時沒有人受影響、而且專案裡的資料都能從別處重建(只跑實驗的專案、個人的沙箱多半是這樣):多付的費用沒有上限,建停損開關。
  • 停機可以接受、而資料不能遺失(團隊共用的 staging、有持久資料的實驗專案):建只停止花錢的資源的那一種。
  • 停機會影響使用者或同事(正式服務):全面停機的代價通常高於一段時間的超支,改用預算警示,寄到值班的人手上(見〈預算警示:門檻依據、範圍、抵免與通知對象〉的通知對象),並把〈費用失控的來源與反應時間〉列的即時限制設上,讓帳單資料延遲期間的花費有上限。沒有人能值班時,處理時間就是自己下一次看到通知的時間,配額要依這段時間能承受的花費來設。風險集中的功能可以拆出去,例如 API 金鑰寫在網頁或 App 裡、任何人都拿得到那把金鑰的功能:拆到自己的專案再對那個專案建停損開關。停損開關停掉那個專案時,正式服務裡用這把金鑰的功能也會一起停,正式服務的其餘部分與正式服務的資料要放在另一個專案。

接停損開關的預算要另外建、範圍限定在要停用的那一個專案,金額設成這個專案本期能承受的最高花費減掉帳單資料延遲期間多出的費用。延遲費用的上限約是「每小時最多能花的費用 × 延遲的時數」,每小時最多能花多少由〈費用失控的來源與反應時間〉列的配額決定,所以先把配額壓到實際需要的量,才估得出這個上限。一個主題只接一個預算:函式不分辨通知來自哪一個預算,好幾個預算送到同一個主題時,任何一個超支都會讓它停掉同一個專案。

停損開關有三項設定,任何一項設錯,部署指令都回報成功、函式狀態也都顯示 ACTIVE,停損開關卻不會解除帳單連結:服務帳號要另外有讀取帳單狀態的 roles/browser(roles/billing.projectManager 只能建立與刪除帳單連結)、部署要加 --retry(不加時,函式在中途拋出例外而 Cloud Run 回 200,那則超出預算的通知被確認收訊、不再重送)、觸發器的服務帳號要有呼叫函式的 roles/run.invoker(缺它時每一次推送都被 403 擋下,函式一次都不執行)。三項的指令與實測時看到的錯誤,在 GCP 預算警示與停損開關的實測紀錄 的〈建服務帳號並授予兩個角色〉〈部署函式:兩個服務帳號、執行個體上限與重試〉〈授予觸發器呼叫函式的權限〉。

停損開關放在另一個專案

專案裡跑的工作負載可能被入侵時,攻擊者拿到的身分若有專案的編輯權限,也能刪掉同一個專案裡的函式與主題。主題、函式與服務帳號可以放在一個專門放帳單工具、不受停用影響的專案(Cloud Billing 文件稱為 FinOps administration project,下文稱管理專案;文件建議把帳單相關的主題集中在這種專案),被保護的專案只綁服務帳號的兩個角色。這樣放有三個效果:解除連結之後函式仍在運作,能記錄並通知人「哪個專案被停了」;被保護的專案裡被入侵的身分刪不掉它;驗收時函式不會跟著停,所以兩則測試訊息不必照順序發。代價是管理專案成了所有停損開關的共同依賴:它的帳單停了或權限被收掉,所有被保護的專案一起失去停損開關,所以監看停損開關有沒有持續收到通知的心跳監看要放在管理專案以外。分開放時要改的指令在 GCP 預算警示與停損開關的實測紀錄 的〈停損開關放在另一個專案時要改的指令〉。

驗收:真實通知確認預算到 Pub/Sub 主題的一段,測試訊息確認主題之後的一段

停損開關平常不動作,壞掉與「還沒輪到它動作」在任何狀態欄上都長得一樣;〈由預算通知觸發的處置:解除帳單連結與只停止花錢的資源〉最後一段列的三項設定,實測時各自讓它在狀態顯示正常時失效過一次。所以部署成功不算完成,驗收看的是呼叫鏈最後一段的結果:專案的帳單狀態。這是平常不動作的防護機制的部署驗證的一個應用。

驗收分成兩段,因為測試訊息是從主題送進去的:主題之後的每一段(觸發器、呼叫權限、函式、角色、重試)都會被行使到,主題之前的那一段(預算的範圍、金額、抵免設定、發布到主題的權限)不會。主題之前的那一段,用第一則真實的預算通知確認:通知裡的 costAmount 對照帳單報表上這個專案本期的費用,接近 0 而報表不是,就是抵免設定或範圍設錯了。主題之後的那一段,用一則低於預算、一則超出預算的測試訊息確認,兩則走的是函式的兩條分支;只有超出預算那一則會行使解除連結要用到的兩個權限,結果看 billingEnabled 有沒有變成 false。

這次驗收會實際讓專案停擺。專案有停用之後不能遺失的資料時,改用拆開驗證:在可拋棄的專案上用一份換了名稱與主題的複本完整觸發一次,在正式專案上只用 Policy Troubleshooter 確認服務帳號的權限;拆開驗證仍沒有行使正式專案上那一次解除連結,驗收紀錄要寫明缺了這一段。部署之後別人調整了角色或鎖定了帳單連結時,部署的人不會知道要重驗,所以另設一個心跳監看(dead man’s switch):函式的紀錄長時間沒有出現就通知,讓函式停止收到通知這件事本身也成為訊號。兩段的訊息、查詢指令、拆開驗證的複本與心跳監看的設法,在 GCP 預算警示與停損開關的實測紀錄 的〈兩段驗收:第一則真實通知,再發兩則測試訊息〉與〈心跳監看:函式停止收到通知時告警〉。

解除連結之後恢復服務的順序

實際因為超支而停用之後,costAmount 是本期累計值,整個預算期間都不會降回預算以下;只重新連結的話,下一則預算通知(一天會來好幾則)會讓函式再解除一次。恢復的順序依停用的原因而不同:

  • 服務自己出錯:修好程式之後,先調高預算金額或刪掉函式,再重新連結。
  • 資源被別人濫用:重新連結之前,把預算金額調成目前的 costAmount 加上清理期間能承受的金額,讓停損開關在攻擊者再開資源時仍會動作,又不會在清理途中被已經累計的花費再觸發一次;並先撤銷外洩的 API 金鑰與服務帳號金鑰。重新連結之後列出並刪除不屬於自己的資源,清單要在重新連結之後列:實測停用期間 gcloud compute routers list 回傳零筆,而重新連結之後 Cloud Router 仍在,所以停用期間的空清單不代表資源已經刪除。確認乾淨之後再把預算調回原本的金額。

指令與實測時資源在停用前後的狀態,在 GCP 預算警示與停損開關的實測紀錄 的〈超支停用之後的恢復〉。

延伸閱讀:逐步的實作、上限的理由、費用的歸因、防護機制的驗收與服務帳號的兩邊權限

  • → GCP 預算警示與停損開關的實測紀錄:建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令、實跑結果,以及每一步漏掉或換一種做法時的結果
  • → 開發環境成本控制:〈Per-team 成本上限〉說明給每個團隊與環境設上限的理由,本篇是那個上限在 GCP 上的執行方式
  • → 成本監控與告警:收到預算警示之後,靠資源標記定位是哪個團隊的哪類資源在漲;停損開關解除連結之前,要依歸因的結果選定是哪一個專案
  • → 平常不動作的防護機制的部署驗證:停損開關、稽核紀錄與告警共用的驗收方法,以及「部署成功、狀態正常」為什麼不算驗收
  • → Service Account(服務帳號):服務帳號的權限分成「它能做什麼」與「誰能以它的身分行事」兩邊,停損開關的角色與部署者的 serviceAccountUser 各屬一邊