GitHub Actions 无服务器部署:Lambda、Cloudflare Workers 与边缘函数

GitHub Actions 无服务器部署实战:AWS Lambda 的 OIDC 免密配置与 SAM、CDK、SST 三条路径、层与容器镜像打包、别名与版本切换、Cloudflare Workers 的 wrangler-action 与 KV、D1、R2 绑定、预览环境、边缘函数冷启动与体积约束、环境变量与密钥管理、灰度回滚与部署后冒烟测试


一、无服务器部署的特殊约束

1.1 与传统容器部署的差异

维度              容器部署                无服务器
部署单元          image + 编排清单        function 包 + 配置
启动延迟          秒级(冷启动可忽略)     毫秒到秒级(冷启动敏感)
状态              进程常驻                 无状态、随时回收
版本模型          滚动更新                版本 + 别名(Lambda)
配置方式          ConfigMap/Secret        环境变量 + 参数存储
体积上限          数百 MB 常见             压缩包 50MB / 解压 250MB
回滚              重新调度旧 image          切换别名指向旧版本

无服务器的「版本 + 别名」模型是它最大的优势:发布不需要重建环境,只需把别名从 v41 指到 v42,回滚同理,秒级生效且零冷启动惩罚。

1.2 流水线的通用骨架

1) 安装依赖(带缓存)
2) 静态检查与单元测试
3) 构建打包(bundle / zip / image)
4) 用 OIDC 换取云凭证
5) 部署到非生产别名并冒烟测试
6) 晋级到生产别名(或按流量比例灰度)
7) 归档制品与部署元数据

二、AWS Lambda 的凭证准备

2.1 用 OIDC 替代 AccessKey

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-lambda-deploy
          role-session-name: gha-${{ github.run_id }}
          aws-region: ap-northeast-1

      - name: Verify identity
        run: aws sts get-caller-identity

2.2 信任策略收紧

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

sub 一定要限定到仓库与分支,用 repo:my-org/* 这类通配等于把角色开放给组织内所有仓库。

2.3 部署所需的最小权限

lambda:UpdateFunctionCode / UpdateFunctionConfiguration
lambda:PublishVersion / UpdateAlias / GetAlias / GetFunction
lambda:InvokeFunction        # 冒烟测试直调函数时必需
s3:PutObject / GetObject     # 存放部署包
Resource 收窄到具体函数:
  arn:aws:lambda:ap-northeast-1:123456789012:function:my-func*

三、AWS Lambda 的三条部署路径

3.1 SAM

- uses: aws-actions/setup-sam@v2
  with:
    use-installer: true

- name: Build and deploy
  run: |
    sam build --use-container
    sam deploy \
      --stack-name my-app-prod \
      --s3-bucket my-artifacts \
      --capabilities CAPABILITY_IAM \
      --no-confirm-changeset \
      --no-fail-on-empty-changeset \
      --parameter-overrides Stage=prod

已有 template.yaml、团队熟悉 CloudFormation 时最省事;sam local 还能本地调试。代价是变更集较慢,回滚依赖 CFN 回滚而非别名切换。

3.2 CDK

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm

- name: Diff then deploy
  run: |
    npm ci
    npx cdk diff MyStack
    npx cdk deploy MyStack --require-approval never

用 TypeScript 描述基础设施,条件分支与循环能力最强,可复用构造器;代价是需要 bootstrap 环境,权限边界要单独收紧。

3.3 SST

- name: Deploy with SST
  run: npx sst deploy --stage prod
  env:
    AWS_REGION: ap-northeast-1

全栈应用、需要把前端与函数一起部署时体验最好,内置 Live Lambda 本地实时调试;抽象层较厚,对版本较敏感。

维度        SAM            CDK              SST
抽象层      中             高               最高
本地调试    好(sam local)一般             好(Live Lambda)
回滚方式    CFN 回滚        CFN 回滚         框架封装
学习成本    低             中               低
生态绑定    AWS            AWS               AWS + 前端框架

3.4 Serverless Framework 仍然可用

- name: Deploy
  run: |
    npm i -g serverless@3
    serverless deploy --stage prod --region ap-northeast-1
注意三点
  1) serverless v4 起对组织使用有授权要求,迁移前先确认许可
  2) 它本质也是 CloudFormation 的封装,回滚语义与 CFN 一致
  3) 大量插件依赖 Node 版本,锁定 node-version 避免踩坑

四、Lambda 的打包形态

4.1 压缩包与层

mkdir -p dist && cp -r src/* dist/
cd dist && zip -r ../function.zip . -x '*.test.js'

aws lambda update-function-code \
  --function-name my-func \
  --zip-file fileb://function.zip \
  --publish

依赖体积大(如 pandas)时抽到层里,可显著缩小函数包并复用缓存:

mkdir -p layer/nodejs
cd layer && npm install --omit=dev --prefix ./nodejs
zip -r layer.zip nodejs
aws lambda publish-layer-version \
  --layer-name my-deps \
  --zip-file fileb://layer.zip \
  --compatible-runtimes nodejs20.x

4.2 容器镜像

FROM public.ecr.aws/lambda/nodejs:20

COPY package*.json ${LAMBDA_TASK_ROOT}/
RUN npm ci --omit=dev
COPY src/ ${LAMBDA_TASK_ROOT}/

CMD [ "index.handler" ]
aws ecr get-login-password --region ap-northeast-1 \
  | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my-func:$GITHUB_SHA .
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my-func:$GITHUB_SHA
选择建议
  纯代码 + 依赖不多        zip 包,冷启动最快
  依赖体积大(如 pandas)   层,复用缓存
  依赖原生库 / 自定义运行时  容器镜像,上限 10GB
  注意:镜像函数冷启动通常比 zip 慢 1 秒以上

五、版本、别名与灰度回滚

5.1 发布版本并指向别名

aws lambda update-function-code --function-name my-func --zip-file fileb://function.zip
aws lambda publish-version --function-name my-func --description "sha-abc1234"

VERSION=$(aws lambda list-versions-by-function \
  --function-name my-func \
  --query 'Versions[-1].Version' --output text)

aws lambda update-alias \
  --function-name my-func --name live --function-version "$VERSION"

5.2 加权别名做灰度

- name: Canary 10 percent
  run: |
    aws lambda update-alias --function-name my-func --name live \
      --function-version "$VERSION" \
      --routing-config "{\"AdditionalVersionWeights\":{\"$PREV\":0.9}}"

- name: Smoke test canary
  run: ./scripts/smoke.sh https://api.example.com

- name: Promote to 100 percent
  run: |
    aws lambda update-alias --function-name my-func --name live \
      --function-version "$VERSION" --routing-config '{}'

5.3 回滚

aws lambda update-alias --function-name my-func --name live --function-version "$PREV"
回滚要点
  1) 别名切换是原子的,不需要重新部署代码
  2) 版本不可变,因此旧版本永远可回
  3) 给函数配置版本保留,避免历史版本无限堆积
  4) 如果改的是环境变量,回滚别名不会回滚配置,需要单独处理

六、Cloudflare Workers 部署

6.1 wrangler-action

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: deploy

6.2 wrangler.toml 与绑定

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-09-01"
compatibility_flags = ["nodejs_compat"]

[vars]
ENVIRONMENT = "production"

[[kv_namespaces]]
binding = "CACHE"
id = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"

[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "11111111-2222-3333-4444-555555555555"

[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-assets"

[observability]
enabled = true
绑定类型速记
  KV              最终一致的键值存储,读多写少
  D1              基于 SQLite 的关系库,支持迁移
  R2              对象存储,兼容 S3 API,无出口流量费
  Durable Objects 强一致的有状态单元
  Queues          消息队列

6.3 预览环境与灰度回滚

PR 场景下用 wrangler versions upload 生成预览版本而不影响线上:

- name: Upload preview version
  if: github.event_name == 'pull_request'
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    command: versions upload
npx wrangler versions deploy <new-version-id>@20% <old-version-id>@80%
npx wrangler deployments list
npx wrangler rollback
Workers 的版本模型与 Lambda 别名类似:
  1) 每次 upload 产生一个不可变版本
  2) deploy 决定哪个版本接收多少流量
  3) 回滚不需要重新构建,直接切版本

6.4 冷启动与体积约束

体积上限(免费 / 付费)
  压缩后 3 MiB / 10 MiB,这是硬约束,打包时必须做 tree-shaking

冷启动
  基于 V8 isolate,通常 5ms 以内
  但首次加载大 bundle 有解析开销,控制在 1MB 以内最佳

常见踩坑
  1) 引入 Node 内置模块需开启 nodejs_compat 并注意兼容日期
  2) 打包时误把 devDependencies 打进去,体积翻倍
  3) 大 JSON 常量直接 import 导致 bundle 膨胀,应改用 KV
# 检查打包体积,定位膨胀源
npx wrangler deploy --dry-run --outdir=dist && du -sh dist/*

七、环境变量与密钥管理

7.1 分层存放

非敏感配置     wrangler.toml 的 [vars] / Lambda 的环境变量
敏感配置        GitHub Secrets + 部署时注入
运行时可轮换     Secrets Manager / Parameter Store,启动时拉取
Cloudflare 密钥  wrangler secret put / Dashboard 加密变量
npx wrangler secret put DATABASE_URL
npx wrangler secret list

7.2 只在需要的环境注入

jobs:
  deploy-prod:
    environment:
      name: production
    steps:
      - uses: actions/checkout@v4
      - run: npx sst deploy --stage prod
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}

用 environment secret 而非仓库 secret,可以让未绑定 production 环境的 job 完全拿不到生产凭证。

7.3 与 Vercel、Netlify 的对比

平台         部署动作             预览环境            回滚
Cloudflare   wrangler deploy      版本上传 + 流量比例   版本切换
Vercel       vercel deploy --prod 每个 PR 一个预览 URL  重新提升旧部署
Netlify      netlify deploy        Deploy Preview      重新发布旧构建
AWS Lambda   别名切换             别名 + 加权路由      别名指回旧版本

四者的共同点是「不可变部署 + 原子切换」,差异在预览环境的默认行为:Vercel 与 Netlify 默认给每个 PR 生成独立 URL,Cloudflare 与 Lambda 需要自己实现。


八、部署后冒烟测试

8.1 直调函数与走公网

aws lambda invoke \
  --function-name my-func:live \
  --payload '{"path":"/health","httpMethod":"GET"}' \
  --cli-binary-format raw-in-base64-out \
  response.json
grep -q '"statusCode":200' response.json
for i in $(seq 1 10); do
  code=$(curl -s -o /dev/null -w '%{http_code}' https://api.example.com/health)
  if [ "$code" = "200" ]; then echo "healthy after $i attempts"; exit 0; fi
  sleep 3
done
echo "smoke test failed with $code"; exit 1

8.2 失败即回滚

- name: Rollback on failure
  if: failure()
  run: |
    aws lambda update-alias --function-name my-func --name live \
      --function-version "${{ env.PREV_VERSION }}"
冒烟测试设计原则
  1) 只测关键路径:健康检查、一次真实读写
  2) 设置重试与退避,无服务器冷启动可能让首次请求超时
  3) 断言具体字段而非只看状态码
  4) 失败必须触发回滚,不能只打印日志
  5) 冒烟脚本独立成文件,本地可复用

总结

无服务器部署把「环境」变成了「版本加别名」,流水线的重心随之从滚动更新编排转向不可变制品与原子切换。AWS Lambda 侧用 OIDC 换临时凭证替代长期 AccessKey,按团队熟悉度选 SAM、CDK、SST 或 Serverless Framework,打包按依赖体积在 zip、层、容器镜像之间取舍,发布时先 publish-version 再更新别名,灰度靠加权路由,回滚只需把别名指回旧版本。Cloudflare Workers 侧用 wrangler-action 部署,配置写在 wrangler.toml,KV、D1、R2 通过绑定暴露,预览环境用版本上传,灰度与回滚用 wrangler versions deploy 的流量比例。两者共同的收尾动作是部署后冒烟测试与失败自动回滚,这一环缺了,前面所有的版本与别名机制都只是摆设。

延伸阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「github-actions」更多文章

  1. 多云部署编排与基础设施漂移检测
  2. 文档站与静态站点发布流水线
  3. AI 代码审查与 PR 助手集成