site logo

Marico's space

将 Serverless 从 GCP 迁移至 AWS:Cloud Run、Functions 与 Events

编程技术 2026-10-29 11:28:50 4

最近折腾了一次从 GCP(Google Cloud,谷歌云)到 AWS(Amazon Web Services,亚马逊云)的 Serverless(无服务器)迁移,踩了几个默认值不对齐的坑。这篇把 Cloud Run、Cloud Run functions、App Engine、Eventarc、Cloud Tasks、Scheduler 和 Workflows 分别迁到 AWS 后该选什么、哪些行为会变、账单哪里会涨,以及一套可落地的迁移顺序讲清楚。

所谓“Serverless”,就是把你写的代码交给云平台托管。不用选服务器,不用打补丁,也不用让机器一直运行。云平台在有请求或事件时启动代码,没人用了再把它停掉。

在 GCP 上,Serverless 通常包括 Cloud Run、Cloud Run functions 和 App Engine,也包括把它们串起来的 Eventarc、Pub/Sub、Cloud Tasks、Cloud Scheduler 与 Workflows。

团队迁移这些应用,原因往往很直接:公司其他系统都在 AWS,数据已经搬过去了,或者应用需要靠近 AWS 上的其他服务。

Serverless 看着像最容易迁的一类,因为压根没有服务器要复制。但两家云的默认行为并不一样,真正的活儿都藏在这里。Cloud Run 可以让多个请求共享一个应用实例,Lambda 通常一个实例只处理一个请求;Cloud Run 请求最长能跑一小时,Lambda 15 分钟就结束。听起来只是小数点级别的差异,却足以让原本正常跑在 GCP 上的应用直接出问题。

“Serverless 实际上是一种思维模式:做技术决策时,要优先考虑它能否让你更专注于业务真正有差异化的部分。”

— Ben Kehoe,AWS Serverless Hero,演讲 Serverless as a mindset

这也是判断迁移是否成功的一个标准。目标不是把每个 GCP 服务一对一照搬过去,而是迁移完以后少维护一些东西。这篇主要解决这些问题:

  • 每个 GCP Serverless 服务在 AWS 上对应什么
  • 行为差异在哪里,哪些地方最容易出问题
  • 事件、队列和定时任务怎么迁
  • 账单会发生什么变化
  • 怎样制定一套简单的分步迁移计划

快速对照表

下面先列出 GCP Serverless 服务与 AWS 最接近的替代方案,具体差异会在后文逐项展开。

Serverless service map from Google Cloud to AWS: Cloud Run services to ECS Express Mode on Fargate or Lambda, Cloud Run jobs to ECS tasks or AWS Batch, Cloud Run functions to AWS Lambda, App Engine to Elastic Beanstalk or ECS, Eventarc and Pub/Sub push to EventBridge and SNS plus SQS, and Workflows to Step Functions

GCP AWS 中最接近的方案 需要重点留意
Cloud Run services Amazon ECS Express Mode,或 AWS Lambda Cloud Run 的一个实例可以同时处理多个请求,Lambda 不行
Cloud Run jobs Fargate 上的 ECS tasks,或 AWS Batch 任务编号与重试机制不同
Cloud Run functions AWS Lambda 15 分钟执行上限,以及新的代码入口
App Engine standard Lambda,或 Elastic Beanstalk 旧的内置服务必须替换
App Engine flexible Elastic Beanstalk,或 Fargate 上的 ECS app.yaml 中的配置要换成新配置
API Gateway / Cloud Endpoints Amazon API Gateway 登录方式和限制不同
Eventarc Amazon EventBridge 事件结构不同,读取事件的代码要改
Pub/Sub push SNS 和 SQS,或 EventBridge 重试要通过队列配置
Cloud Tasks Amazon SQS 加 Lambda SQS 只负责保存任务,必须有消费者读取
Cloud Scheduler EventBridge Scheduler 每个调度都要指定自己的时区和目标
Workflows AWS Step Functions 工作流需要重写,不能直接转换
Secret Manager AWS Secrets Manager,或 Parameter Store 访问权限来自应用的 IAM role

那 App Runner 呢? 它看起来是 Cloud Run 最明显的对应物,但 AWS 已于 2026 年 4 月 30 日停止向新客户开放 App Runner。对于新应用,AWS 推荐改用 ECS Express Mode。

Cloud Run:迁容器还是函数

Cloud Run 把应用跑在容器里。流量上涨时,它会增加实例;流量消失后,它会回收实例。迁到 AWS,主要有两个合理选择:

  • ECS Express Mode 是最接近 Cloud Run 的方案。你提供容器和两个 IAM roles,AWS 会配置其余部分,包括 Fargate 上的 service、负载均衡器、伸缩与网络。容器本身不用改。和 Cloud Run 一样,一个应用实例可以同时处理多个请求。
  • AWS Lambda 只在请求到来时运行代码,按请求计费,适合小型 API 和突发流量。但普通 Lambda 实例一次只处理一个请求。Lambda 也支持容器镜像,但代码必须遵循 Lambda 的请求格式。

How requests reach your code: on Cloud Run, up to 80 or more requests share one container instance; on standard AWS Lambda, each request gets its own execution environment, so 80 requests need 80 environments

下面把主要差异放在一起看。Lambda 的数字来自 Lambda quotas page:

项目 Cloud Run ECS Express Mode Lambda
每个实例处理的请求数 最多 1,000,常见配置是 80 取决于应用自身能处理多少 1
最长请求时间 60 分钟 由负载均衡器决定 15 分钟
缩容到零 支持 不支持,至少运行一个实例 支持
保持温热 最小实例数 最小任务数 预置并发
最大请求体 32 MiB 受负载均衡器限制 6 MB
部署内容 容器 容器 Zip 文件或容器

由这张表会引出两个问题。

第一是数据库连接。在 Cloud Run 上,100 个请求可能只共享两个应用实例和少量数据库连接;到了 Lambda,100 个请求意味着 100 个实例,每个实例都会建立自己的连接,数据库很容易被连接数拖垮。此时可以使用 RDS Proxy 复用连接,或者干脆把应用继续放在 ECS。

第二是执行时长。凡是可能超过 15 分钟的任务,都不能原样放到 Lambda 上。

然后是冷启动。所谓冷启动,就是 Lambda 启动新实例时产生的短暂等待。很多团队对这点有些过度焦虑:

“不会增加用户可感知延迟的冷启动,通常不值得专门操心。”

— Yan Cui,AWS Serverless Hero,Running Serverless at Scale

如果用户确实能感觉到,可以开启 provisioned concurrency,提前保留可用实例;Java、Python 和 .NET 还可以使用 SnapStart。

经验法则:如果 Cloud Run 服务是一个完整 Web 应用,包含框架、数据库或长请求,就把原容器迁到 ECS Express Mode;如果它只是流量波动明显的小型 API 或 webhook,就迁到 Lambda。

还有一种折中方案:Lambda Managed Instances。它让 Lambda 运行在由 AWS 托管的 EC2 机器上,一个实例可以同时处理多个请求,适合稳定且持续繁忙的流量。前提是你的代码能安全地并行处理多个请求。

Cloud Run jobs

Cloud Run jobs 会持续运行容器直到任务结束,经常还会同时启动多个 task。迁到 AWS 后,可以把同一个容器作为 Fargate 上的 ECS task 运行,再由 EventBridge Scheduler 或 Step Functions 触发。如果是大规模任务,可以拆分后使用 AWS Batch 的 array jobs。

这里有一个容易漏掉的改动:Cloud Run 通过 CLOUD_RUN_TASK_INDEX 告诉每个任务自己的编号,AWS Batch 对应的环境变量是 AWS_BATCH_JOB_ARRAY_INDEX。

Functions:从 Cloud Run functions 迁到 Lambda

Cloud Run functions,过去叫 Cloud Functions,与 AWS Lambda 的对应关系最直接。两者都是一小段代码,在请求或事件到达时运行。真正需要改的是代码外围:

项目 Cloud Run functions Lambda
入口 Functions Framework(@functions_framework.http) handler(event, context)
访问地址 内置 Function URL 或 API Gateway
事件触发 Eventarc 或 Pub/Sub SQS、SNS、EventBridge 等
内存与 CPU 分别设置 设置内存,CPU 随内存增长
最长运行时间 HTTP 请求 60 分钟 15 分钟
本地文件 保存在内存中 /tmp,最大 10 GB

大部分工作都集中在入口和触发器,业务逻辑本身通常不用大改。建议把业务逻辑单独放在一个文件里,然后分别为两个云平台写一层很薄的入口。这样两边仍能复用同一套逻辑和测试。

App Engine

App Engine 是 GCP 最早推出的 Serverless 服务。迁移方式取决于你使用的是哪一种运行时:

  • App Engine flexible 本身就运行容器,可以迁到 Fargate 上的 ECS。如果团队更喜欢“推代码就得到一个应用”的模式,也可以使用 Elastic Beanstalk。
  • App Engine standard 中基于较新运行时的应用已经是普通 Web 应用,把它们装进容器,再按 Cloud Run 的方式处理即可。
  • 旧版 App Engine 应用 使用了 Memcache、Task Queues、Users 和 Datastore 等内置服务,每一项都要在 AWS 上重新安家,分别对应 ElastiCache、SQS、Amazon Cognito 和 DynamoDB。这是真正需要改代码的部分,应该优先盘点。

App Engine 还有几个很容易被忽略的小配置文件:cron.yaml 要转成 EventBridge Scheduler 调度,dispatch.yaml 要转成负载均衡器规则,queue.yaml 则要转成 SQS 队列。

事件、队列与定时任务

Serverless 应用通常通过事件互相通信,比如“订单已创建”“文件已上传”。也正是这些小差异,最容易制造隐蔽 Bug:

  • Eventarc 迁到 EventBridge。 Eventarc 使用 CloudEvents 格式发送事件,EventBridge 使用自己的结构,包括 source、detail-type 和 detail。所有读取事件的函数都要做一点调整。
  • Pub/Sub push 迁到 SNS 和 SQS。 Pub/Sub push 会调用你的 Web 地址,失败后重试。在 AWS 上,可以在每个消费者前面放一个 SQS 队列,再让 Lambda 批量读取队列。持续处理失败的消息进入 dead-letter queue,也就是专门保存无法处理消息的旁路队列。
  • Cloud Tasks 迁到 SQS。 Cloud Tasks 会按指定速率逐个调用 Web 地址。SQS 只保存任务,不主动调用任何服务,所以还需要消费者读取。可以让 Lambda 消费,并通过 maximum concurrency 控制处理速率。
  • Cloud Scheduler 迁到 EventBridge Scheduler。 EventBridge Scheduler 支持 cron、时区和一次性调度,可以直接启动 Lambda、ECS task 和 Step Functions。
  • Workflows 迁到 Step Functions。 GCP Workflows 使用 YAML 编写步骤,Step Functions 使用 JSON。两者都包含步骤、重试、分支和等待,但每个工作流都得手工重写。

所有 handler 都要保证重复执行时安全。 两个云平台都可能重复投递同一条消息。AWS 上,如果一个批次中有消息失败,默认会重试整个批次,除非你明确报告哪些消息失败。如果 handler 会发邮件或给银行卡扣款,应保存业务 ID,确保同一件事不会执行两次。

API、访问控制与身份认证

谁能调用哪个服务,两边遵循的基本思路相同,主要是名称发生了变化:

GCP AWS
服务绑定 service account Lambda function 或 ECS task 绑定 IAM role
在服务上使用 roles/run.invoker Lambda resource policy,或在 function URL 上使用 IAM 登录
服务之间使用 token 调用 IAM 签名调用,或 API Gateway 私有 API
“Allow unauthenticated” 公开 function URL 或公开 API
Identity-Aware Proxy 负载均衡器登录加 Amazon Cognito
API Gateway 中的 API keys Amazon API Gateway 中的usage plans 和 API keys

每个函数或服务都应拥有独立的 IAM role,并且只授予实际需要的权限。即使旧应用共用同一个 service account,也不要在 AWS 上照搬成一个权限过大的万能角色。

最容易踩坑的网络问题

  • 访问私有数据库。 Cloud Run 通过 connector 或 Direct VPC egress 接入私有网络。Lambda 可以加入 VPC,也就是 AWS 上的私有网络;ECS task 本来就在 VPC 内运行。
  • 从 VPC 访问互联网。 位于 VPC 内的 Lambda 无法自行访问互联网,需要 NAT gateway。NAT gateway 会同时产生小时费和流量费。对于 S3、DynamoDB 等 AWS 服务,应优先使用 VPC endpoint,通常更便宜。
  • 自定义域名。 Cloud Run domain mapping 迁到 AWS 后,通常需要 ACM 证书,再搭配负载均衡器、CloudFront 或 API Gateway 自定义域名。
  • 回调 GCP。 迁移期间,有些应用仍然要调用 GCP。这不仅更慢,还会产生 GCP 数据流出费用。最好优先清理掉这些跨云调用。

一套简单的选择方法

Serverless decision flow: if it runs longer than 15 minutes, use ECS on Fargate or AWS Batch; if it is a web app with a connection pool or steady traffic, use ECS Express Mode; if it is a small API, webhook or event handler, use Lambda; if it is a scheduled job, use EventBridge Scheduler with Lambda or an ECS task; if it chains several steps, use Step Functions

  1. 运行时间会超过 15 分钟吗? 使用 Fargate 上的 ECS 或 AWS Batch。
  2. 它是带框架、数据库或稳定流量的完整 Web 应用吗? 使用 ECS Express Mode,并继续部署原来的容器。
  3. 它是小型 API、webhook 或事件处理器吗? 使用 Lambda。
  4. 它按计划运行吗? 使用 EventBridge Scheduler 启动 Lambda function 或 ECS task。
  5. 它需要依次执行多个步骤,并包含重试与等待吗? 使用 Step Functions。

账单会发生哪些变化

两个云平台都会按代码运行时间和请求次数收费,但最终金额取决于具体计费项:

项目 GCP AWS
按请求计费 Cloud Run:CPU 时间、内存时间、请求数 Lambda:内存时间与请求数
常驻容器 Cloud Run 实例计费 Fargate:按秒计 CPU 与内存费用,另加负载均衡器
保持温热 最小实例数,空闲价格较低 预置并发,开启期间持续收费
API API Gateway 按调用次数计费 API Gateway 按调用次数计费;HTTP API 比 REST API 便宜
事件与队列 Eventarc、Pub/Sub、Cloud Tasks EventBridge、SNS、SQS,均按请求计费
折扣 承诺使用折扣 Compute Savings Plans,覆盖 Lambda 和 Fargate

最容易让团队意外的有三项:

  • 负载均衡器。 ECS Express Mode 会在应用前面放一个负载均衡器,即使没有流量也会产生费用。多个应用可以共用一个。
  • NAT gateway。 VPC 内的 Lambda 如果要访问互联网,就要经过 NAT。大量出站流量很容易成为账单上的意外支出。
  • 等待时间。 Lambda 在等待其他服务时仍会计费。如果一个应用的大部分时间都花在等待上,放在 Fargate 上可能更便宜。

先让新环境运行几周,再购买 Savings Plan。只有真实使用数据,才适合作为长期承诺的依据。

代码层面会发生什么

先看一个 GCP HTTP 函数与 AWS Lambda 的对应写法:

# GCP: Cloud Run function (Python)
import functions_framework @functions_framework.http
def hello(request): name = request.args.get("name", "world") return {"message": f"Hello, {name}"} # AWS: Lambda behind a function URL or HTTP API (Python)
import json def handler(event, context): params = event.get("queryStringParameters") or {} name = params.get("name", "world") return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"message": f"Hello, {name}"}), }
Enter fullscreen mode Exit fullscreen mode

再来看读取 Pub/Sub 消息的函数,以及 AWS 中读取 SQS 的对应实现:

# GCP: triggered by a Pub/Sub topic through Eventarc
import base64, json
import functions_framework @functions_framework.cloud_event
def on_order(cloud_event): data = base64.b64decode(cloud_event.data["message"]["data"]) process(json.loads(data)) # AWS: triggered by an SQS queue, with partial batch failures
import json def handler(event, context): failed = [] for record in event["Records"]: try: process(json.loads(record["body"])) except Exception: failed.append({"itemIdentifier": record["messageId"]}) return {"batchItemFailures": failed}
Enter fullscreen mode Exit fullscreen mode

最后是一个 Cloud Run service 的 Terraform 定义,以及 AWS Lambda 的对应写法:

# GCP: a Cloud Run service
resource "google_cloud_run_v2_service" "api" { name = "api" location = "us-central1" template { containers { image = "us-docker.pkg.dev/shop-prod/apps/api:1.4.2" resources { limits = { cpu = "1", memory = "512Mi" } } } scaling { min_instance_count = 1 } max_instance_request_concurrency = 80 service_account = google_service_account.api.email }
} # AWS: the same API as a Lambda function from a container image
resource "aws_lambda_function" "api" { function_name = "api" package_type = "Image" image_uri = "123456789012.dkr.ecr.us-east-1.amazonaws.com/api:1.4.2" memory_size = 1024 # CPU grows with memory timeout = 30 role = aws_iam_role.api.arn # was the service account
} resource "aws_lambda_function_url" "api" { function_name = aws_lambda_function.api.function_name authorization_type = "AWS_IAM" # was roles/run.invoker
}
Enter fullscreen mode Exit fullscreen mode

这些代码只展示基本结构,不是可以直接复制的完整配置。真正落地前,仍然要核对当前文档与实际限制。

迁移后才会冒出来的坑

“失败无可避免,所有东西最终都会失败。”

— Werner Vogels,Amazon CTO,10 Lessons from 10 Years of AWS

迁移后经常出问题的是这些不起眼的细节:

  • 调用链中的超时。 API Gateway REST API 默认最多等待29 秒。它后面的 Lambda 即使还能继续运行,调用方也已经超时了。要从用户请求一路检查到数据库的超时设置。
  • 大文件上传。 Lambda 普通请求最大只接受 6 MB。原来能在 Cloud Run 上传的大文件,应改为直接上传到 S3,再使用预签名 URL。
  • 保存在内存里的状态。 当每个请求都有独立的应用实例时,内存缓存、计数器之类的状态表现会和以前不同。
  • 日志与告警。 Cloud Logging 的查询与告警要重建成 CloudWatch Logs Insights 查询和 CloudWatch alarms。切流量之前,先把团队真正依赖的告警补齐。
  • 密钥。 projects/.../secrets/... 这样的 Secret Manager 路径会变成 Secrets Manager 或 Parameter Store 中的名称。代码里所有引用都要全局搜索。
  • 配额。 新 AWS 账号的 Lambda 并发配额通常较低。正式切流量前几周,就应申请提升。

按步骤完成迁移

  1. 列清所有资源。 找出每一个 Cloud Run service、job、function、App Engine 应用、触发器、队列、调度和工作流,同时记录流量、运行时间、成本与负责人。
  2. 为每项资源选择 AWS 服务。 根据前面的问题做决定,并把选择理由写下来。
  3. 先搭基础环境。 准备 AWS 账号、网络、容器镜像仓库 ECR、IAM roles、日志、告警