PAYATHON 2026

如何通过 billing alert 完全禁用 Cloud Function?

数据小许

结论

不能通过把 Cloud Function 的 status 改为 INACTIVE 来暂停函数。status 是平台维护的只读状态,不属于可更新的配置字段,所以示例中的 patch({ updateMask: 'status' }) 无法禁用函数。

如果需要停止用户触发函数,并避免函数继续产生执行费用,比较可靠的做法是:

  1. 删除函数或对应的触发器;
  2. 需要恢复时重新部署;
  3. 对于 HTTP 函数,可以先撤销调用权限,或在入口层拦截请求,但这和删除函数并不相同;
  4. 不要把 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 或其他入口保护。预算通知更适合充当最后一道止损措施,不能作为唯一的实时防护机制。

备注:内容仅供参考。