如何通过 billing alert 完全禁用 Cloud Function?
结论
不能通过把 Cloud Function 的 status 改为 INACTIVE 来暂停函数。status 是平台维护的只读状态,不属于可更新的配置字段,所以示例中的 patch({ updateMask: 'status' }) 无法禁用函数。
如果需要停止用户触发函数,并避免函数继续产生执行费用,比较可靠的做法是:
- 删除函数或对应的触发器;
- 需要恢复时重新部署;
- 对于 HTTP 函数,可以先撤销调用权限,或在入口层拦截请求,但这和删除函数并不相同;
- 不要把 Billing Budget Alert 当作实时且严格的费用上限。
为什么布尔开关不能完全解决问题
把开关保存在 RTDB、Firestore 或环境变量中,只能让函数启动后尽早退出:
if (disabled) {
return;
}
函数仍会被调度,并产生一次调用。读取开关也可能涉及数据库操作,因此仍可能产生以下费用:
- 函数调用和计算费用;
- 数据库读取费用;
- 日志费用;
- 网络及其他关联服务费用。
这种方式可用于业务降级,但无法完全停止触发。
为什么示例代码无效
下面这种更新方式的问题在于:
updateMask: 'status',
requestBody: {
status: 'INACTIVE',
},
在 Cloud Functions API 中,status 表示函数当前的部署或运行状态,由平台生成。用户不能通过 patch 把它设置成暂停开关,也不能用 INACTIVE 冻结已经部署的函数。
Cloud Functions 没有通用的状态,可以在保留部署的同时暂停所有触发。要可靠地阻止触发,通常需要删除函数、删除触发器,或阻止请求到达函数。
使用 Billing Alert 自动删除函数
可以让 Cloud Billing Budget 向 Pub/Sub 发布通知,再由一个管理函数调用 Cloud Functions API,删除目标函数。
负责删除其他函数的管理函数仍处于活动状态,也可能产生少量费用。这个管理函数不应删除自己,否则以后无法继续处理新的预算通知。
下面的示例展示了使用 Cloud Functions v1 API 删除第一代函数的基本方式:
const functions = require('firebase-functions');
const {google} = require('googleapis');
exports.disableFunctionOnBillingAlert = functions.pubsub
.topic('wellWavel_remove_cloudFunction_whenCostTooHigh')
.onPublish(async (message) => {
const messageData = message.json;
if (
!messageData ||
typeof messageData.costAmount !== 'number' ||
typeof messageData.budgetAmount !== 'number' ||
messageData.costAmount < messageData.budgetAmount
) {
return;
}
const auth = await google.auth.getClient({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
const cloudfunctions = google.cloudfunctions({
version: 'v1',
auth,
});
const functionName =
'projects/wellwave-cfc72/locations/us-central1/functions/incrementQuota';
try {
await cloudfunctions.projects.locations.functions.delete({
name: functionName,
});
console.log(`Deletion requested for ${functionName}.`);
} catch (error) {
// 重复的预算通知可能在函数已经删除后再次到达。
if (error.code === 404) {
console.log(`${functionName} is already deleted.`);
return;
}
console.error(`Failed to delete ${functionName}:`, error);
throw error;
}
});
删除通常是一项异步控制面操作。API 请求成功只表示平台接受了删除请求,并不代表函数会在同一瞬间彻底消失。
如果目标是第二代 Cloud Functions,应改用 Cloud Functions v2 API,并确认完整资源名称和客户端调用方式与部署代际一致,例如:
projects/PROJECT_ID/locations/REGION/functions/FUNCTION_NAME
第二代函数还涉及 Cloud Run、Eventarc 等底层资源,不能混用第一代函数的状态或更新字段。
权限配置
预算通知处理函数使用的 service account 至少要有删除和查询目标函数的权限,例如:
cloudfunctions.functions.delete
cloudfunctions.functions.get
可以选用合适的预定义角色,也可以创建只包含必要权限的自定义角色。不要为了省事直接授予项目级 Owner。
还要防止普通用户向预算 Pub/Sub topic 发布伪造消息,否则攻击者可能利用这条链路删除函数。应严格限制 topic 的发布、订阅和管理权限。
HTTP 函数的替代方案
如果目标是 HTTP 函数,而且调用者必须通过 IAM 验证,可以撤销公开调用权限,例如移除 allUsers 的 invoker 权限。这样能保留函数部署,同时阻止未授权用户调用。
需要注意:
- 这并不是暂停函数;
- 已获得授权的身份仍可调用;
- 如果函数前面还有 Firebase Hosting、API Gateway、Load Balancer 或其他入口,也要检查相应的入口配置;
- 权限变更可能需要一段时间才能完全生效。
对于公开的 HTTP 接口,还可以在 API Gateway、负载均衡器或 Cloud Armor 层拦截请求。这比等请求进入函数后再检查布尔值更早,但入口层服务仍可能产生费用。
事件触发函数的替代方案
对于由 Pub/Sub、Eventarc、Firestore、RTDB 或 Cloud Storage 触发的函数,可以删除或停用相应的触发链路。具体操作取决于函数代际和触发类型。
不要随意删除多个业务共用的 Pub/Sub topic、数据库或存储桶。应优先删除只供该函数使用的订阅或 Eventarc trigger。部分触发资源由 Cloud Functions 自动管理,不适合单独修改。遇到这种情况,直接删除函数通常更稳妥。
Billing Alert 不是硬性费用上限
Budget Alert 用来发送通知,不是实时断路器。费用从产生、汇总到通知发出可能有延迟,所以即使累计金额已经达到预算,费用仍可能继续增加。还要考虑以下情况:
- Pub/Sub 通知可能重复投递;
- 通知可能乱序到达或出现延迟;
- 删除函数前已经进入队列的事件可能仍在处理;
costAmount >= budgetAmount只能根据通知中的累计金额判断;- 一个预算可能覆盖多个项目或服务,未必能准确对应某个函数;
- 预算周期重置后,函数不会自动恢复,除非另外设计重新部署流程。
因此,自动删除逻辑必须支持幂等处理,并把 404 Not Found 视为函数已经删除的正常结果。
如果风险来自恶意用户或突发流量,还应配合身份验证、配额、速率限制、App Check、API Gateway 或其他入口保护。预算通知更适合充当最后一道止损措施,不能作为唯一的实时防护机制。
备注:内容仅供参考。
