2. Cloud Run服务部署与容器化实践

Cloud Run 的核心约定非常明确:它运行的是标准 OCI 容器镜像,而不是源码或某个特定框架的构建产物。因此,把应用部署到 Cloud Run 的完整链路可以概括为四段:写 Dockerfile 把应用打成镜像、构建并推送到 Artifact Registry、创建或更新 Cloud Run 服务、再通过修订版本与流量拆分完成灰度发布和回滚。理解这条链路,比记住某几条 gcloud 命令更重要。

一、Dockerfile:决定镜像能否在 Cloud Run 上跑起来

Cloud Run 对容器有三条硬性约束:容器必须监听由环境变量 PORT 指定的端口(默认注入 8080);进程必须在前台运行,不能自我 daemon 化;容器启动后要尽快开始接受请求。Dockerfile 的写法直接决定这三点是否满足。

以一个 Node.js Express 应用为例,推荐使用多阶段构建,把依赖安装与运行镜像分离:

# ---- 构建阶段 ----
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

# ---- 运行阶段 ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
# Cloud Run 会注入 PORT,默认 8080
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]

应用侧必须读取 process.env.PORT,而不是把端口写死:

const port = process.env.PORT || 8080;
app.listen(port, '0.0.0.0', () => console.log(`listening on ${port}`));

这里有两个常见坑。第一,监听地址必须是 0.0.0.0 而非 127.0.0.1,否则 Cloud Run 的健康检查无法从容器外部访问。第二,镜像要尽量小:使用 slim 基础镜像、npm ci --omit=dev 去掉开发依赖、用 .dockerignore 排除 node_modules 和 .git,都能显著缩短冷启动时间。Cloud Run 按请求计费,冷启动越短,尾延迟越低。

二、构建与推送:Artifact Registry 是镜像的落脚点

Cloud Run 从 Artifact Registry 拉取镜像。先创建仓库,再构建推送。使用 Cloud Build 可以在云端完成构建,无需本地 Docker:

gcloud artifacts repositories create run-repo \
  --repository-format=docker \
  --location=asia-east1

gcloud builds submit \
  --tag asia-east1-docker.pkg.dev/PROJECT_ID/run-repo/hello-api:v1

镜像标签策略值得提前规划。用 v1、v2 这类语义化标签便于人工识别,但 Cloud Run 真正依赖的是镜像的 digest(sha256:...)。当同一个标签被覆盖推送时,Cloud Run 不会自动重新拉取,必须显式更新服务。因此生产环境建议同时保留语义标签与 digest 记录,回滚时才能精确指向某个不可变镜像。

三、部署服务:把镜像变成可访问的 HTTPS 端点

部署命令的核心参数包括区域、内存、CPU、并发、超时和最小实例数:

gcloud run deploy hello-api \
  --image asia-east1-docker.pkg.dev/PROJECT_ID/run-repo/hello-api:v1 \
  --region asia-east1 \
  --platform managed \
  --allow-unauthenticated \
  --memory 512Mi --cpu 1 \
  --concurrency 80 \
  --timeout 30s \
  --min-instances 0 --max-instances 10 \
  --set-env-vars NODE_ENV=production

几个参数需要结合业务判断。--concurrency 表示单个实例可同时处理的请求数,Node.js 这类事件循环模型可以设到 80 甚至更高;如果是 CPU 密集型的图像处理,则应降到 1~4,避免请求互相抢占。--min-instances 0 允许缩容到零,成本最低但会有冷启动;对延迟敏感的接口可设为 1 保持一个常驻实例。--timeout 上限为 3600 秒,但长连接场景要配合客户端超时一起考虑。

部署成功后,命令会输出服务 URL。此时可用 gcloud run services describe hello-api --region asia-east1 查看当前流量分配、最新修订版本和镜像 digest,作为后续版本管理的基线。

四、修订版本与流量拆分:灰度与回滚的抓手

Cloud Run 每次部署都会生成一个不可变的修订版本(Revision),命名形如 hello-api-00002-abc。流量拆分就是在这几个修订版本之间按百分比分配请求。典型做法是先发 10% 流量到新版本观察,再逐步放大:

# 部署新版本但不接收流量
gcloud run deploy hello-api \
  --image .../hello-api:v2 --region asia-east1 --no-traffic

# 将 10% 流量切到新修订版本
gcloud run services update-traffic hello-api \
  --region asia-east1 \
  --to-revisions hello-api-00002-abc=10

# 验证通过后全量切换
gcloud run services update-traffic hello-api \
  --region asia-east1 --to-latest

回滚同样简单:把流量指回上一个修订版本即可,无需重新构建镜像。这正是“镜像不可变 + 修订版本不可变”带来的好处。需要注意的是,流量拆分按请求比例生效,同一用户的连续请求可能落到不同版本,因此新版本必须保持 API 兼容;若做不到,应改用标签路由(tag)让特定 URL 指向特定修订版本,先做内部验证。

应用源码 server.js package.json Dockerfile 监听 $PORT / 0.0.0.0 Cloud Build 构建 多阶段构建 npm ci --omit=dev 产出 OCI 镜像 Artifact Registry run-repo hello-api:v1 sha256:9f3c… Cloud Run hello-api HTTPS 端点 asia-east1 修订版本与流量拆分 hello-api-00001 镜像 v1 流量 90% hello-api-00002 镜像 v2 流量 10% v1 90% v2 按请求比例分配 回滚:流量指回 00001,无需重建镜像 关键约束:容器监听 $PORT(默认 8080)· 绑定 0.0.0.0 · 前台运行 · 镜像不可变 · 修订版本不可变 灰度路径:--no-traffic 部署 → 10% 切流验证 → --to-latest 全量 → 异常时 update-traffic 回滚

五、一次完整的灰度发布示例

假设 hello-api 已稳定运行 v1,现在要发布 v2。推荐的操作序列与观察点如下:

步骤命令要点观察与判断
构建推送 v2gcloud builds submit --tag .../hello-api:v2构建日志无报错,镜像 digest 生成
无流量部署deploy --no-traffic新修订版本创建,流量仍 100% 在 v1
标签验证update-traffic --set-tags canary=hello-api-00002通过 canary---hello-api-xxx.run.app 直连新版本自测
切 10% 流量update-traffic --to-revisions ...=10观察错误率、P95 延迟、日志中版本标识
全量或回滚--to-latest 或指回 00001指标正常则全量;异常则回滚,秒级生效
Cloud Run 的修订版本是不可变的,这意味着“回滚”本质上只是把流量重新指向旧修订版本,不需要重新构建或重新部署。把这一点用好,灰度发布的风险会大幅降低。
不要把密钥写进 Dockerfile 或镜像层。环境变量、Secret Manager 挂载、服务账号权限都应在部署阶段注入。镜像一旦推送,其中的任何内容都可能被有权限的人拉取。

把这条链路走通之后,Cloud Run 的日常操作就归结为三件事:改代码、推镜像、切流量。Dockerfile 保证镜像可运行,Artifact Registry 保证镜像可追溯,修订版本与流量拆分保证发布可控可回滚。三者配合,才构成一个完整的容器化部署实践。