详解 AWS Lambda zip 包限制、容器镜像突破 10GB 上限的优势,以及系统依赖、构建一致性的实际收益。
当 zip 包不再够用
调用模型的函数通常很小——一个 HTTP 客户端、一个 SDK、一些 JSON 处理。真正让它变大的是 SDK 自带的东西:gRPC 绑定、protobuf 运行时、带编译扩展的分词器、加密轮子,有时还包含通过传递依赖被拉进来的数值栈。这些都可能让包大小超过 zip 的承载能力。
还有两个转向镜像的好理由。它可以让你使用系统包——你的 wheel 需要的共享库,而 pip 无法安装。同时让构建变得可重现:相同的 Dockerfile 在本地和 CI 中产生相同的产物,而如果在错误的架构或错误的 glibc 上构建 zip,则只有在部署后才会出现导入错误。
有一个决定是永久性的。AWS 文档指出,你无法在创建后更改函数的包类型——将容器函数转回 zip 意味着创建一个新函数。
从 AWS 基础镜像开始。对于你的运行时,它自带语言运行时、运行时接口客户端和运行时接口模拟器,这意味着 Dockerfile 只需要三条指令。AWS 在其"使用容器镜像部署 Python Lambda 函数"页面上发布了规范形式。
编写 Dockerfile。将依赖和代码复制到 LAMBDA_TASK_ROOT,并设置 CMD 为处理程序——而不是命令。这点容易让人混淆:这里的 CMD 是传给运行时的参数,不是要执行的进程。
FROM public.ecr.aws/lambda/python:3.12
COPY requirements.txt ${LAMBDA_TASK_ROOT}
RUN pip install -r requirements.txt
COPY lambda_function.py ${LAMBDA_TASK_ROOT}
CMD [ "lambda_function.handler" ]
编写 handler 时,要把耗时操作放在导入时而不是每次调用时。在容器镜像上这比 zip 包更重要,因为镜像更大,而且环境一旦存在后会被多次调用复用。
import json, os
import boto3
# Module scope: runs once per execution environment, during Init.
client = boto3.client("bedrock-runtime")
MODEL_ID = os.environ["MODEL_ID"]
def handler(event, context):
body = json.loads(event.get("body") or "{}")
resp = client.converse(
modelId=MODEL_ID,
messages=[{"role": "user", "content": [{"text": body["prompt"]}]}],
)
text = resp["output"]["message"]["content"][0]["text"]
return {"statusCode": 200, "body": json.dumps({"text": text})}
用 buildx 构建,两个标志都要设置。原因见下一节。
docker buildx build --platform linux/amd64 --provenance=false \
-t model-caller:test .
在同一区域创建 ECR 仓库、登录、打标签并推送。
aws ecr create-repository --repository-name model-caller \
--region eu-west-1 --image-scanning-configuration scanOnPush=true
aws ecr get-login-password --region eu-west-1 \
| docker login --username AWS --password-stdin \
111122223333.dkr.ecr.eu-west-1.amazonaws.com
docker tag model-caller:test \
111122223333.dkr.ecr.eu-west-1.amazonaws.com/model-caller:latest
docker push 111122223333.dkr.ecr.eu-west-1.amazonaws.com/model-caller:latest
用 --package-type Image 创建函数,然后调用它。
aws lambda create-function \
--function-name model-caller \
--package-type Image \
--code ImageUri=111122223333.dkr.ecr.eu-west-1.amazonaws.com/model-caller:latest \
--role arn:aws:iam::111122223333:role/lambda-ex \
--timeout 60 --memory-size 1769
aws lambda invoke --function-name model-caller \
--payload '{"body":"{\"prompt\":\"hello\"}"}' response.json
两个都会产生能成功构建但在 AWS 上失败的镜像,这是最糟糕的失败类型——因为错误要在下一个推送周期才能看到。
--provenance=false 是必需的,AWS 直接这么说:"要使镜像与 Lambda 兼容,必须使用 --provenance=false 选项。"最近的 buildx 版本默认附加 provenance 证明,这会把推送变成多清单索引。Lambda 接受 Docker 镜像清单 V2 Schema 2 和 OCI v1.0.0 及以上版本,AWS 明确指出 Lambda 不支持多架构容器镜像——你构建的镜像必须只针对一种架构。
--platform linux/amd64 很重要,因为你的笔记本可能不是 x86。在 Apple silicon 上不带这个标志构建的镜像是 arm64,部署到配置为 x86_64 的函数会失败。两个标志都可以设置成另一种方式——--platform linux/arm64 配合 arm64 函数架构是完全有效的组合,通常成本更低——但两者必须一致。
AWS"使用容器镜像创建 Lambda 函数"页面还有两个较小的要求。镜像必须在只读文件系统上运行,/tmp 是唯一可写的位置——可在 512 MB 到 10,240 MB 之间配置。而且你不应该添加 USER 指令:Lambda 定义了一个最低权限的默认 Linux 用户,你的代码需要的所有文件必须对它可读。
如果你使用非 AWS 基础镜像——slim Python 镜像、Alpine 或内部镜像——你必须自己安装运行时接口客户端。对于 Python,那是 pip install awslambdaric,ENTRYPOINT 设置为 python -m awslambdaric,CMD 设置为处理程序。
每次推送-查看周期都要花费几分钟。运行时接口模拟器可以消除这个等待。使用 AWS 基础镜像时它已经存在,所以运行容器就能获得本地调用端点:
docker run --platform linux/amd64 -p 9000:8080 model-caller:test
curl "http://localhost:9000/2015-03-31/functions/function/invocations" \
-d '{"body":"{\"prompt\":\"hello\"}"}'
路径不是笔误——这就是模拟器的固定端点。使用非 AWS 基础镜像时,模拟器不在镜像中,你需要单独下载,挂载进来并覆盖入口点:
mkdir -p ~/.aws-lambda-rie && \
curl -Lo ~/.aws-lambda-rie/aws-lambda-rie \
https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \
chmod +x ~/.aws-lambda-rie/aws-lambda-rie
docker run --platform linux/amd64 -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \
--entrypoint /aws-lambda/aws-lambda-rie \
model-caller:test \
/usr/local/bin/python -m awslambdaric lambda_function.handler
这种方式能捕获整类打包问题——缺失的共享库、为错误架构构建的 wheel、无法解析的处理程序路径——只需几秒而不是一个部署周期。它无法捕获的是 IAM、VPC 路由或真正的冷启动问题,所以它是对已部署测试的补充而非替代。
容器函数有 zip 函数没有的状态,而且三种状态都有运维影响。
更新后变为 Pending。AWS 在函数能服务调用前会优化镜像,函数在此期间保持 Pending 状态。在此窗口期间无法调用函数,所以如果在 update-function-code 后立即调用的部署流水线需要等待变为 Active。
数周无流量后变为 Inactive。如果函数多周未被调用,AWS 会回收优化版本。下一次调用会被拒绝,函数返回 Pending 状态同时镜像被重新优化,之后才能正常工作。对于不常用的函数——内部工具、月度任务——这表现为神秘的首次调用失败。设置定时暖调用可以避免这个问题。
镜像消失后变为 Failed。Lambda 会定期从 ECR 重新获取镜像。如果镜像已被删除或其权限被撤销,函数会转为 Failed 状态,每次调用都会失败。对于有激进生命周期策略的 ECR(使未标记镜像过期)来说这是一个真实风险:Lambda 在部署时将标签解析为 digest,所以"未标记"的镜像仍然可以是生产环境中的那个。
这个 digest 解析还有一个值得明确说明的后果。向同一标签推送新镜像不会更新函数。你必须用镜像 URI 调用 update-function-code,即使标签没有变化;如果想要一个版本供别名指向(这是预配置并发所需要的),还要加上 --publish。而且由于镜像很大,要考虑内存大小:更多内存意味着 Init 阶段相应地更多 CPU,而容器函数把额外启动时间花在了 Init 阶段。
Lambda Memory Size and AI Workload Performance
Why the Model API Call, Not Lambda Init, Dominates a Cold Path
Lambda Provisioned Concurrency for AI Endpoints