AWS 的 Lambda 在 2020-12-01 开始支持用 Docker 镜像存放代码,见 New for AWS Lambda - Container Image Support。AWS Lambda 最初的对发布包的限制是 50M, 解压后(因为执行前需要解压缩)不能超过 250M,对于压缩比小于 1/5 的包来说,要突破 50M 部署包的限制就要用 2018-11-29 推出的层(layer), 即把 Lambda 的依赖可以组织为层,每个 Lambda 可引用最多 5 个层,但最终 Lambda 加上层所解压后的大小仍然有 250 M 的限制。
对于使用了大量依赖的 Lambda,比如 Python 中用了 Pandas 之类的数学分析包,250M 的大小是不够的,所以才有了 Docker 镜像化的 Lambda, 镜像的大小限制一下蹦到 10G,要构建出一个 10G Lambda 用的 Linux 镜像, 那绝对是个巨兽,至少目前是超越我的想像力,除非往里面塞入大量的业务数据。关于 Lambda 有哪些限制,请参阅 Lambda quotas。
介绍完 Lambda 引入 Docker 镜像的背景后,本文接下来只关注如何构建一个 Python Lambda 镜像,对于如何部署 Docker 化的 Lambda, 不在本文的范围之内。主要的参考文档为 AWS Lambda 官方的 Deploy Python Lambda functions with container images.
构建一个简单的 Lamda 镜像
最简单的构建一个 Lambda 用的 Python Docker 镜像的方式就是从 AWS 提供的基础镜像开始,Amazon 有一个公开的像 Dockhub 类似的镜像仓库,叫做 Amazon ECR Public Gallery。下面是两个 Lambda 镜像相关的链接
- https://gallery.ecr.aws/?operatingSystems=Linux&searchTerm=lambda: 包括所有 Lambda 所支持语的运行时相关镜像
- https://gallery.ecr.aws/lambda/python: Python 相关的 Lambda 基础镜像
我们将以 public.ecr.aws/lambda/python:3.9 以基础构建一个 Lambda 镜像。首先准备一个 Lambda 代码 app.py
1 2 3 |
import sys def handler(event, context): return 'Hello from AWS Lambda using Python' + sys.version + '!' |
假如我们要安装第三方的 Python 依赖(这通常是必须的,否则也就没有必要为 Lambda 选择 Docker) 放在 requirements.txt
, 内容为
1 |
bounded-executor |
接下来基本的 Dockerfile
内容为
1 2 3 4 5 6 7 |
FROM public.ecr.aws/lambda/python:3.9 COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/ RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" --no-cache-dir CMD [ "app.handler" ] |
我们用命令
$ docker build -t lambda-docker-demo .
构建好后,在部署到 AWS 之前我们可进行本地测试,先运行容器
$ docker run -p 9080:8080 lambda-docker-demo
time="2021-11-08T16:27:43.262" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)"
测试
$ curl -XPOST "http://localhost:9080/2015-03-31/functions/function/invocations" -d '{}'
"Hello from AWS Lambda using Python3.9.6 (default, Oct 25 2021, 08:38:55) \n[GCC 7.3.1 20180712 (Red Hat 7.3.1-13)]!"
同时在 Docker 容器中会打印
time="2021-11-08T16:28:29.09" level=info msg="extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory"
time="2021-11-08T16:28:29.09" level=warning msg="Cannot list external agents" error="open /opt/extensions: no such file or directory"
START RequestId: 025033dc-1361-42f4-8325-4abc97e3f689 Version: $LATEST
END RequestId: 025033dc-1361-42f4-8325-4abc97e3f689
REPORT RequestId: 025033dc-1361-42f4-8325-4abc97e3f689 Init Duration: 0.77 ms Duration: 78.07 msBilled Duration: 79 ms Memory Size: 3008 MB Max Memory Used: 3008 MB
这是一个标准的 Lambda 的后台输出
分析 Lambda 镜像
我们可以用 docker inspect 来查看所生成的镜像以及容器,基础镜像的 Dockerfile 可见 aws/aws-lambda-base-images.
$ docker inspect public.ecr.aws/lambda/python:3.9
我们得如下值得关注的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
"Env": [ "LANG=en_US.UTF-8", "TZ=:/etc/localtime", "PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", "LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib", "LAMBDA_TASK_ROOT=/var/task", "LAMBDA_RUNTIME_DIR=/var/runtime" ], "Cmd": null, "Image": "", "Volumes": null, "WorkingDir": "/var/task", "Entrypoint": [ "/lambda-entrypoint.sh" ], |
环境变量 LAMBDA_TASK_ROOT 为 /var/task, 即 Lambda 的工作目录,它的入口为 /lambda-entrypoint.sh
, 构建镜像 lambda-docker-demo 的 CMD
的内容将作为该 Entrypoint 的参数,即最终镜像的启动命令相当于
$ cd /var/task
$ /lambda-entrypoint.sh app.handler
如果我们用 docker ps --no-trunc
也能看到该完整命令
1 2 3 |
docker ps --no-trunc --format "table {{.Image}}\t{{.Command}}" IMAGE COMMAND lambda-docker-demo "/lambda-entrypoint.sh app.handler" |
下面我们通过覆盖 --entrypoint
参数进到该容器
$ docker run -it -p 9080:8080 --entrypoint /bin/sh lambda-docker-demo
sh-4.2#
手动执行 /lambda-entrypoint.sh app.handler
后,同样可以在本地测试通过。
我们尽量收集多的一些 Lambda 启动的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
sh-4.2# pwd /var/task sh-4.2# ls app.py bounded_executor bounded_executor-0.0.5.dist-info __pycache__ requirements.txt sh-4.2# python -c 'import sys; print("\n".join(sys.path))' /var/lang/lib/python39.zip /var/lang/lib/python3.9 /var/lang/lib/python3.9/lib-dynload /var/lang/lib/python3.9/site-packages sh-4.2# cat /lambda-entrypoint.sh #!/bin/sh # Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. if [ $# -ne 1 ]; then echo "entrypoint requires the handler name to be the first argument" 1>&2 exit 142 fi export _HANDLER="$1" RUNTIME_ENTRYPOINT=/var/runtime/bootstrap if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT else exec $RUNTIME_ENTRYPOINT fi sh-4.2# bash -x /lambda-entrypoint.sh app.handler + '[' 1 -ne 1 ']' + export _HANDLER=app.handler + _HANDLER=app.handler + RUNTIME_ENTRYPOINT=/var/runtime/bootstrap + '[' -z '' ']' + exec /usr/local/bin/aws-lambda-rie /var/runtime/bootstrap INFO[0000] exec '/var/runtime/bootstrap' (cwd=/var/task, handler=) ^CINFO[0001] Received signal signal=interrupt INFO[0001] Shutting down... WARN[0001] Reset initiated: SandboxTerminated sh-4.2# cat /var/runtime/bootstrap #!/bin/bash # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. export AWS_EXECUTION_ENV=AWS_Lambda_python3.9 if [ -z "$AWS_LAMBDA_EXEC_WRAPPER" ]; then exec /var/lang/bin/python3.9 /var/runtime/bootstrap.py else wrapper="$AWS_LAMBDA_EXEC_WRAPPER" if [ ! -f "$wrapper" ]; then echo "$wrapper: does not exist" exit 127 fi if [ ! -x "$wrapper" ]; then echo "$wrapper: is not an executable" exit 126 fi exec -- "$wrapper" /var/lang/bin/python3.9 /var/runtime/bootstrap.py fi sh-4.2# echo $LAMBDA_RUNTIME_DIR /var/runtime sh-4.2# ls /var/runtime awslambdaric __pycache__ awslambdaric-1.2.2.dist-info python_dateutil-2.8.2.dist-info bin runtime_client.cpython-39-x86_64-linux-gnu.so bootstrap runtime-release bootstrap.py s3transfer boto3 s3transfer-0.5.0.dist-info boto3-1.18.55.dist-info simplejson botocore simplejson-3.17.2-py3.9.egg-info botocore-1.21.55.dist-info six-1.16.0.dist-info dateutil six.py jmespath urllib3 jmespath-0.10.0.dist-info urllib3-1.26.6.dist-info layer_bootstrap |
关键信息整理如下:
- 第三方依赖我们用 pip install --target /var/task 目录下,不指定 --target 安装到 site-packages 中也行
- 从执行入口 /lambda-entrypoint.sh app.handler,在本地测试时,最后执行的代码是
export _HANDLER=app.handler
exec /usr/local/bin/aws-lambda-rie /var/runtime/bootstrap - /usr/local/bin/aws-lambda-rie 是一个二进制文件,专为测试用的
- 在 /var/runtime 是 Lambda 的一些必需的模块,主要是 boto3,不用太关注
- 通过后面的回溯,本地测试时没有 AWS_LAMBDA_RUNTIME_API,执行的是 exec /usr/local/bin/aws-lambda-rie /var/runtime/bootstarp, 部署到 AWS 环境后有了 AWS_LAMBDA_RUNTIME_API, 执行的是 /var/runtime/bootstrap
起决定性的脚本是
12345678export _HANDLER=app.handlerRUNTIME_ENTRYPOINT=/var/runtime/bootstrapif [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; thenexec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINTelseexec $RUNTIME_ENTRYPOINTfi
如果我们只基于一个 Python 基础镜像来构建自己的 Lambda 镜像, 只要达到以上 #5 的要求就行,所创建的镜像可同时运行于本地及 AWS 环境。实际上要达成 /var/runtime/bootstrap 的启动方式会比较复杂,所以后面未采此种方法 awslambdaric
或 aws-lambda-rie
来启动 Lambda 的。
创建 Python 为基础的 Lambda 镜像
如果按照当前版本的 AWS 官方文档 To create an image using an alternative base image 来创建一个 Lambda 镜像,在本地启动的时候会出现错误
$ docker run -p 9080:8080 lambda-docker-demo1
Traceback (most recent call last):
File "/usr/local/lib/python3.10/runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/local/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/function/awslambdaric/__main__.py", line 20, in <module>
main(sys.argv)
File "/function/awslambdaric/__main__.py", line 14, in main
lambda_runtime_api_addr = os.environ["AWS_LAMBDA_RUNTIME_API"]
File "/usr/local/lib/python3.10/os.py", line 679, in __getitem__
raise KeyError(key) from None
KeyError: 'AWS_LAMBDA_RUNTIME_API'
但部署后 AWS 上是没问题的
原因是因为本地启动容器时不存 AWS_LAMBDA_RUNTIME_API
环境变量值,而在 AWS 环境中是有的。
如果我们在 app.py
中加输出 os.environ
可以看到以下所有的环境变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
'_AWS_XRAY_DAEMON_PORT': '2000' '_AWS_XRAY_DAEMON_ADDRESS': '169.254.79.129' '_HANDLER': 'app.handler' 'AWS_LAMBDA_FUNCTION_VERSION': '$LATEST' 'PATH': '/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' 'AWS_LAMBDA_LOG_STREAM_NAME': '2021/11/08/[$LATEST]2ee4dc9a1a334fae8096f73424ad06ac' 'AWS_LAMBDA_INITIALIZATION_TYPE': 'on-demand' 'PYTHON_GET_PIP_URL': 'https://github.com/pypa/get-pip/raw/3cb8888cc2869620f57d5d2da64da38f516078c7/public/get-pip.py' 'LAMBDA_TASK_ROOT': '/var/task' 'AWS_REGION': 'us-east-1' 'AWS_LAMBDA_RUNTIME_API': '127.0.0.1:9001' 'AWS_LAMBDA_FUNCTION_NAME': 'test-lambda' 'PYTHON_PIP_VERSION': '21.2.4' 'PYTHON_GET_PIP_SHA256': 'c518250e91a70d7b20cceb15272209a4ded2a0c263ae5776f129e0d9b5674309' 'PYTHON_SETUPTOOLS_VERSION': '57.5.0' 'AWS_XRAY_DAEMON_ADDRESS': '169.254.79.129:2000' 'AWS_SESSION_TOKEN': '......' 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': '128' 'GPG_KEY': 'A035C8C19219BA821ECEA86B64E628F8D684696D' 'AWS_LAMBDA_LOG_GROUP_NAME': '/aws/lambda/test-lambda' 'AWS_EXECUTION_ENV': 'AWS_Lambda_Image' 'AWS_XRAY_CONTEXT_MISSING': 'LOG_ERROR' 'AWS_ACCESS_KEY_ID': '......' 'LAMBDA_RUNTIME_DIR': '/var/runtime' 'AWS_DEFAULT_REGION': 'us-east-1' 'LANG': 'C.UTF-8' 'PYTHON_VERSION': '3.10.0' 'AWS_SECRET_ACCESS_KEY': '......' '_X_AMZN_TRACE_ID': 'Root=1-618968c7-7f745c050fc6b61d59cf2280;Parent=790e20cb2f0228fe;Sampled=0' |
AWS_LAMBDA_RUNTIME_API 为 127.0.0.1:9001
不过即使我们在本地启动 docker 时加上该环境变量也无济于事
$ docker run -e AWS_LAMBDA_RUNTIME_API=127.0.0.1:9001 -p 9080:8080 lambda-docker-demo1
Traceback (most recent call last):
File "/usr/local/lib/python3.10/runpy.py", line 196, in _run_module_as_main
[ERROR] [123456789088] LAMBDA_RUNTIME Failed to get next invocation. No Response from endpoint
return _run_code(code, main_globals, None,
File "/usr/local/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/function/awslambdaric/__main__.py", line 20, in <module>
main(sys.argv)
File "/function/awslambdaric/__main__.py", line 16, in main
bootstrap.run(app_root, handler, lambda_runtime_api_addr)
File "/function/awslambdaric/bootstrap.py", line 399, in run
event_request = lambda_runtime_client.wait_next_invocation()
File "/function/awslambdaric/lambda_runtime_client.py", line 68, in wait_next_invocation
response_body, headers = runtime_client.next()
RuntimeError: Failed to get next
通过对前面的实践,我才逐步了解到 AWS Lambda 给我们提供了两个运行接口
- Runtime interface emulator, 即 RIE, 本地测试用的执行入口
- Runtime interface clients, 即 RIC, 实际 Lambda 部署到 AWS 环境的执行入口
通常是检测是否有 AWS_LAMBDA_RUNTIME_API 环境变量来选择入口,本地测试时没有该环境变量,所以进入 RIE
创建不含 RIE 的 Docker 镜像
如果不考虑本地测试的 RIE, Dockerfile 内容如下
1 2 3 4 5 6 7 8 9 10 11 12 |
FROM python:3.9-slim ENV LAMBDA_TASK_ROOT=/var/task RUN mkdir -p ${LAMBDA_TASK_ROOT} COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/ WORKDIR ${LAMBDA_TASK_ROOT} RUN pip install awslambdaric --target ${LAMBDA_TASK_ROOT} --no-cache-dir && \ pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" --no-cache-dir ENTRYPOINT python -m awslambdaric app.handler |
注:由于选择的是 python:3.9-slim 基础镜像,所以要进行 AWS 操作的话,应该在 requirements.txt
文件中加上 boto3
依赖。
ENTRYPOINT 可用命令,参数数组的形式
1 2 |
ENTRYPOINT [ "python", "-m", "awslambdaric" ] CMD [ "app.handler" ] |
那么是否就无法在本地测试不带 RIE 的镜像呢?问题总有解决它的办法, 下载 aws-lambda-rie 在宿主机上,映射卷再替换 entrypoint
$ mkdir -p ~/.aws-lambda-rie
$ wget -P ~/.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 -v ~/.aws-lambda-rie:/aws-lambda -p 9080:8080 --entrypoint /aws-lambda/aws-lambda-rie \
lambda-docker python -m awslambdaric app.handler
镜像名 lambda-docker 后面整串 python -m awslambdaric app.handler
是作为 entrypoint 的参数,所以启动 Lambda 的命令是
/aws-lambda/aws-lambda-rie python -m awslambdaric app.handler
最后用一样的命令测试
$ curl -XPOST "http://localhost:9080/2015-03-31/functions/function/invocations" -d '{}'
创建整合了 RIE 的 Docker 镜像
现在要构建一个整合了 RIE 的 Lambda 镜像,既能方便本地测试同时又能部署到 AWS 上运行的 Docker 镜像的方法如下:
先建立一个自己的 entrypoint 脚本 entry_script.sh
, 内容为
1 2 3 4 5 6 |
#!/bin/sh if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then exec aws-lambda-rie python -m awslambdaric $@ else exec python -m awslambdaric $@ fi |
$@ 表示入口命令与参数,即 ENTRYPOINT + CMD
再用命令把它改为可执行(想在 Dockerfile 中 RUN 下面的命令也行)
$ chmod +x entry_script.sh
接下来就是如何把 RIE 包打入 Docker 镜像内部, 见如下完整的 Dockerfile 内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
FROM python:3.9-slim ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin RUN chmod +x /usr/local/bin/aws-lambda-rie ENV LAMBDA_TASK_ROOT=/var/task RUN mkdir -p ${LAMBDA_TASK_ROOT} COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/ COPY entry_script.sh / WORKDIR ${LAMBDA_TASK_ROOT} RUN pip install awslambdaric --target ${LAMBDA_TASK_ROOT} --no-cache-dir && \ pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" --no-cache-dir ENTRYPOINT [ "/entry_script.sh" ] |
为了保持更清晰的结构,没有把上面的多个 RUN 用 && 合并,所以会产生更多的 Docker 层。
这样打出来的包既能用
$ docker run -it -p 9080:8080 lambda-docker
启动来进行本地测试,本地测试时控制台出现
IndexError: list index out of range
WARN[0009] First fatal error stored in appctx: Runtime.ExitError
WARN[0009] Process 14(python) exited: Runtime exited with error: exit status 1
ERRO[0009] Init failed InvokeID= error="Runtime exited with error: exit status 1"
WARN[0009] Reset initiated: ReserveFail
WARN[0009] Cannot list external agents error="open /opt/extensions: no such file or directory"
无须担心,不会影响到 Lambda 的执行。
而且这个 Docker 镜像部署到 AWS 上也是可执行的。
链接:
- Testing Lambda container images locally
- Deploy Python Lambda functions with container images
- Creating Lambda container images
本文链接 https://yanbin.blog/build-aws-lambda-python-docker-image/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
[…] Lambda 在启动 Python 代码前有所动作,这就把我们拉回到了我之前的 构建 AWS Lambda Python Docker 镜像 文中一个相关问题,AWS 是通过 awslambdaric 启动 Lambda […]