GCP 无服务器事件驱动实战:使用 Terraform 构建 Pub/Sub 推送至 Cloud Run 的微型调度中心

1. 背景与架构痛点

在企业级数据湖建设中,我们经常遇到这样的需求:上游系统会将异构的 CSV 数据文件频繁推送到 Google Cloud Storage (GCS)。我们需要实时监听到文件上传事件,并将数据经过清洗后加载到 BigQuery。

起初,我们尝试使用一个 24 小时在线的 Dataflow Streaming Pipeline (基于 Beam YAML) 来同时处理 10 种以上不同 Schema 的表写入。但残酷的现实是:

  • DAG 爆炸:由于表结构异构,静态图的体积迅速膨胀,导致提交失败或解析超时。
  • 跨语言开销 :Beam YAML 的 WriteToBigQuery 依赖 Java Expansion Service。在同一节点初始化大量不同的 Schema 写入通道会导致 OOM。
  • 资源浪费:流式任务闲置时依然占用 Compute Engine 资源。

优雅的解决方案:事件驱动与职责解耦

为了解决这一痛点,我们将架构从"单体重型流处理"重构为 "微型事件调度 + 轻量级批处理" 。

我们引入了 Cloud Run 作为 API 接收端和调度器,配合 Pub/Sub Push Subscription 实现了 Scale to Zero (缩容至零) 的全自动事件驱动机制。


(上图为架构示意图:GCS 产生 Event -> 触发 Pub/Sub -> 推送 OIDC 鉴权请求 -> Cloud Run 提取参数并拉起对应 Schema 的 Dataflow Batch Job)

本篇博客将详细演示如何使用 Terraform 和 Python FastAPI 在 GCP 上完整构建这套 Pub/Sub to Cloud Run 的底层通知架构。


2. 编写微服务:Python FastAPI 接收端

首先,我们需要一个轻量级的 Web 容器来接收来自 Pub/Sub 的 HTTP POST 请求。由于 GCP Pub/Sub 推送的数据默认是 Base64 编码的,我们需要在代码中进行解码。

requirements.txt

text 复制代码
fastapi
uvicorn[standard]

main.py

python 复制代码
import base64
import logging
from fastapi import FastAPI, Request

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("uvicorn.error")

app = FastAPI()

@app.post("/pubsub-push")
async def pubsub_push(request: Request):
    try:
        # 1. 接收 Pub/Sub 的标准事件信封
        envelope = await request.json()
        if not envelope:
            msg = "No message received"
            logger.error(msg)
            return {"error": msg}, 400

        pubsub_message = envelope.get("message")
        
        # 2. 提取并解码 Base64 Payload
        if pubsub_message and "data" in pubsub_message:
            data = base64.b64decode(pubsub_message["data"]).decode("utf-8")
            logger.info(f"Received Pub/Sub message: {data}")
            
            # 【后续扩展】在这里解析 JSON,动态组装 Pipeline YAML,调用 API 拉起 Dataflow Job
            
            return {"status": "success", "data": data}

        return {"error": "bad format"}, 400
    except Exception as e:
        logger.error(f"Error processing message: {e}")
        return {"error": str(e)}, 500

@app.get("/")
def health_check():
    return {"status": "ok", "message": "Service is running!"}

部署非常简单,借助 GCP Buildpacks,无需写 Dockerfile 即可一键部署:

bash 复制代码
gcloud run deploy pubsub-receiver \
  --source . \
  --region europe-west2 \
  --allow-unauthenticated \
  --project YOUR_PROJECT_ID

获取到部署后的 URL(例如 https://pubsub-receiver-xxx.run.app),我们将把它配置到后续的 Terraform 中。


3. 基础设施即代码 (IaC):Terraform 编排

为了满足企业级安全合规(禁止 allUsers 公网任意调用),我们在 Terraform 中创建了一个专用的 Service Account ,授予其 run.invoker 权限,并在 Pub/Sub 订阅中开启了 OIDC Token 鉴权。

main.tf

hcl 复制代码
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

provider "google" {
  project = "YOUR_PROJECT_ID"
  region  = "europe-west2"
}

variable "cloud_run_url" {
  description = "Cloud Run 服务的 URL (加上 /pubsub-push 路径)"
  type        = string
}

# 1. 创建专用于 Pub/Sub 触发的 Service Account
resource "google_service_account" "pubsub_invoker" {
  account_id   = "pubsub-invoker-sa"
  display_name = "Pub/Sub Invoker Service Account"
}

# 2. 授权该 Service Account 拥有调用 Cloud Run 服务的权限
resource "google_cloud_run_service_iam_member" "invoker" {
  service  = "pubsub-receiver"
  location = "europe-west2"
  role     = "roles/run.invoker"
  member   = "serviceAccount:${google_service_account.pubsub_invoker.email}"
}

# 3. 创建 Pub/Sub 主题
resource "google_pubsub_topic" "poc_topic" {
  name = "cloudrun-trigger-topic"
}

# 4. 创建 Push 模式的订阅,并挂载 OIDC 安全令牌
resource "google_pubsub_subscription" "poc_push_sub" {
  name  = "cloudrun-push-sub"
  topic = google_pubsub_topic.poc_topic.name

  push_config {
    push_endpoint = var.cloud_run_url

    oidc_token {
      service_account_email = google_service_account.pubsub_invoker.email
    }
  }
}

执行部署:

bash 复制代码
terraform init
terraform apply -auto-approve -var="cloud_run_url=https://pubsub-receiver-xxx.run.app/pubsub-push"

4. 测试与验证

现在,整个基础设施已经全部就绪。没有任何文件上传时,Cloud Run 实例数为 0,且不对外暴露任何不安全的公开端点。

我们通过 gcloud 发送一条模拟的 GCS 文件上传通知(JSON 格式):

bash 复制代码
gcloud pubsub topics publish cloudrun-trigger-topic \
  --message='{"event": "file_upload", "bucket": "my-data-lake", "filename": "trade_data_2026.csv"}' \
  --project=YOUR_PROJECT_ID

去 GCP Console 中打开 Cloud Run -> pubsub-receiver -> Logs,你可以立刻看到极速响应的日志输出:

json 复制代码
{
  "textPayload": "INFO: Received Pub/Sub message: {\"event\": \"file_upload\", \"bucket\": \"my-data-lake\", \"filename\": \"trade_data_2026.csv\"}",
  "resource": {
    "type": "cloud_run_revision",
    "labels": {
      "service_name": "pubsub-receiver"
    }
  }
}

5. 结论与展望

通过这套 Pub/Sub Push -> OIDC -> Cloud Run 架构,我们完美隔离了控制平面(调度触发)与数据平面(Dataflow 计算)。

在这个坚实且安全的底座之上,我们下一步只需在 main.py 中引入 apache-beam 的 yaml_transform 包,根据提取出的 filename 动态挂载对应的 Schema 映射,并在内存中提交 Dataflow Batch Job。这样既彻底消除了 Streaming 任务的空转成本,又完美规避了异构 Schema 造成的 DAG 爆炸灾难。

相关推荐
罗小爬EX11 分钟前
Helm 从概念到实战:以一个普通 Web 应用为例
云原生·helm
分布式存储与RustFS8 小时前
镜像仓库的层别只堆本地盘:用 RustFS 给 Harbor 当 S3 后端
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
分布式存储与RustFS11 小时前
工作流脚本和依赖 JAR 散在各节点本地盘:用 DolphinScheduler 资源中心接到 RustFS
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
Eyfcom11 小时前
〖物流云原生实战 01〗K8s Operator 在物流定时任务中的实践:从“裸 CronJob”到“声明式任务编排”
云原生·物流管理系统·物流云原生实战
阿里云云原生1 天前
阿里云智能体白皮书开源进展
云原生
阿里云云原生1 天前
万联智策基于 AgentCore 构建数字员工团队,800+智能体规模化落地
云原生
分布式存储与RustFS1 天前
群晖 NAS 被 C3RB3R 锁了:本地备份一起被加密,RustFS 对象锁为什么删不掉
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
江华森1 天前
4 台华为云 ECS 从零到一:我用 AI Agent 搭了一套 K3s 多节点集群,全自动、免运维、翻车实录
云原生
时间售卖铺1 天前
Chrony与SSH实战:精准时间同步与安全远程登录
运维·安全·云原生·ssh
分布式存储与RustFS1 天前
OpenObserve 的日志别堆本地盘:用 RustFS 当它的对象存储底座
运维·云原生·开源·对象存储·分布式存储·s3·性能基准