2. 容器化基础与镜像构建

在 Google Cloud 上部署容器应用,第一步不是写 Kubernetes 清单,而是把应用可靠地变成一个可分发、可复现的镜像。镜像构建质量直接决定了后续部署速度、冷启动时间、漏洞扫描结果和回滚成本。本节围绕 Dockerfile 最佳实践、多阶段构建、镜像分层优化,以及用 Cloud Build 构建并推送至 Artifact Registry 展开。

一、镜像分层:理解构建缓存与体积的来源

Docker 镜像由一系列只读层叠加而成,每条 Dockerfile 指令(除少数元数据指令外)通常生成一层。层是内容寻址的,相同内容可被复用。构建时,Docker 从上到下逐条执行指令,一旦某层的内容发生变化,该层及其之后所有层的缓存都会失效,必须重新构建。这就是“把变化频率低的操作放前面、变化频率高的放后面”这一原则的根本原因。

以 Node.js 应用为例,如果先 COPY . . 再 RUN npm install,那么每次修改任意源码文件都会导致依赖重新安装。正确顺序是先复制 package.json 与 package-lock.json,执行安装,再复制其余源码。这样只有依赖清单变化时才会重装依赖。

# 反例:源码变化导致依赖重装
COPY . .
RUN npm ci

# 正例:依赖层独立缓存
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

镜像体积同样重要。基础镜像选择、是否清理包管理器缓存、是否删除构建产物,都会显著影响最终大小。例如在 Debian 系镜像中安装软件后应执行 rm -rf /var/lib/apt/lists/*,并尽量把安装与清理写在同一条 RUN 指令中,避免清理动作被留在独立层而无法减小体积。

二、多阶段构建:把编译环境与运行环境分离

多阶段构建允许在一个 Dockerfile 中定义多个 FROM 阶段,只把需要的产物复制到最终镜像。典型场景是 Go、Java、Rust 等编译型语言,或前端构建。构建阶段可以包含完整 SDK、编译器和构建工具,运行阶段只保留二进制或静态资源,从而大幅缩小镜像并减少攻击面。

# 构建阶段
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/server

# 运行阶段
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

这里使用 distroless 作为运行基础镜像,它不含 shell 和包管理器,体积小且默认以非 root 用户运行。注意 COPY --from=builder 只复制产物,构建阶段的工具链不会进入最终镜像。若应用需要时区或证书,应在运行阶段显式复制对应文件,而不是依赖构建阶段残留。

三、Cloud Build 构建并推送至 Artifact Registry

Artifact Registry 是 Google Cloud 推荐的镜像仓库,支持 Docker 及多种制品格式,并与 IAM、VPC-SC、漏洞扫描集成。使用前先创建仓库:

gcloud artifacts repositories create app-repo \
  --repository-format=docker \
  --location=asia-east1 \
  --description="应用镜像仓库"

gcloud auth configure-docker asia-east1-docker.pkg.dev

镜像地址格式为 LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:TAG。Cloud Build 通过 cloudbuild.yaml 描述构建步骤,常用内置构建器 gcr.io/cloud-builders/docker。下面示例构建镜像、打上提交短哈希标签并推送:

steps:
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - -t
      - asia-east1-docker.pkg.dev/$PROJECT_ID/app-repo/api:$SHORT_SHA
      - .
  - name: gcr.io/cloud-builders/docker
    args:
      - push
      - asia-east1-docker.pkg.dev/$PROJECT_ID/app-repo/api:$SHORT_SHA
images:
  - asia-east1-docker.pkg.dev/$PROJECT_ID/app-repo/api:$SHORT_SHA

其中 $PROJECT_ID 与 $SHORT_SHA 是 Cloud Build 内置替换变量,无需手动传入。images 字段会让 Cloud Build 在步骤完成后自动推送所列镜像,因此上面的 push 步骤也可省略,二者选其一即可。执行构建:

gcloud builds submit --config cloudbuild.yaml \
  --substitutions=_TAG=v1.0.0 .

构建完成后,可在 Artifact Registry 控制台或通过 gcloud artifacts docker images list asia-east1-docker.pkg.dev/PROJECT_ID/app-repo 查看镜像及其摘要。建议同时启用漏洞扫描,观察是否存在高危 CVE,再决定是否进入部署阶段。

镜像构建与推送链路 源码仓库 Dockerfile 应用代码 Cloud Build cloudbuild.yaml docker build / push $SHORT_SHA 标签 多阶段构建 builder 阶段 runtime 阶段 COPY --from Artifact Registry 镜像仓库 漏洞扫描 镜像分层与缓存命中 基础镜像层(不变) 依赖安装层(package.json 变化才失效) 源码复制层(频繁变化) 启动命令层(元数据) 缓存命中规则 • 某层内容变化 → 该层及之后全部重建 • 依赖清单先复制 → 依赖层可复用 • 多阶段构建 → 运行镜像不含编译工具 • 合并安装与清理 → 避免残留层 • 使用非 root 用户 → 降低运行风险 gcloud builds submit → 构建 → 打标签 → 推送 → 扫描 → 部署 镜像地址:LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY/IMAGE:TAG

四、实践中的关键判断

构建完成后,需要判断镜像是否达到可用标准。可以从三个维度检查:一是体积,运行镜像是否明显小于构建镜像,是否包含不必要的工具链;二是缓存效率,修改一行源码后重新构建,依赖层是否命中缓存;三是安全,是否以非 root 运行、是否扫描出高危漏洞。若体积异常偏大,常见原因是把构建阶段整体复制进运行阶段,或未清理包管理器缓存。

标签策略同样影响可追溯性。建议同时使用不可变标签(如提交短哈希)和可读标签(如语义版本),避免只用 latest。Cloud Build 的 $SHORT_SHA 天然适合作为不可变标识,配合 images 字段自动推送,可以形成一条从提交到镜像的清晰链路。后续在 GKE 或 Cloud Run 中引用该镜像时,使用带哈希的完整地址即可实现精确回滚。

多阶段构建中,运行阶段的基础镜像应尽量选择精简版本(如 distroless、alpine 或 slim),并显式声明 USER。若应用需要写入临时目录,应提前创建并授权,避免因只读文件系统导致启动失败。
不要把密钥、令牌或 .env 文件通过 COPY 写入镜像层。镜像层一旦推送,即使后续删除文件,历史层中仍可能保留敏感内容。应使用 Secret Manager 或构建时挂载的方式注入。

总结来看,Dockerfile 的质量取决于对分层缓存的理解,多阶段构建解决的是“构建环境与运行环境混在一起”的问题,而 Cloud Build 与 Artifact Registry 则把本地构建提升为可审计、可复现的云端流水线。三者结合,才能让容器镜像真正成为可交付、可回滚的部署单元。