构建 AWS Lambda Python Docker 镜像
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.
我们将以 public.ecr.aws/lambda/python:3.9 以基础构建一个 Lambda 镜像。首先准备一个 Lambda 代码
假如我们要安装第三方的 Python 依赖(这通常是必须的,否则也就没有必要为 Lambda 选择 Docker) 放在
接下来基本的
我们用命令
环境变量 LAMBDA_TASK_ROOT 为 /var/task, 即 Lambda 的工作目录,它的入口为
下面我们通过覆盖
我们尽量收集多的一些 Lambda 启动的信息
关键信息整理如下:
如果我们只基于一个 Python 基础镜像来构建自己的 Lambda 镜像, 只要达到以上 #5 的要求就行,所创建的镜像可同时运行于本地及 AWS 环境。实际上要达成 /var/runtime/bootstrap 的启动方式会比较复杂,所以后面未采此种方法
原因是因为本地启动容器时不存
如果我们在
AWS_LAMBDA_RUNTIME_API 为 127.0.0.1:9001
不过即使我们在本地启动 docker 时加上该环境变量也无济于事
通常是检测是否有 AWS_LAMBDA_RUNTIME_API 环境变量来选择入口,本地测试时没有该环境变量,所以进入 RIE
注:由于选择的是 python:3.9-slim 基础镜像,所以要进行 AWS 操作的话,应该在
ENTRYPOINT 可用命令,参数数组的形式
那么是否就无法在本地测试不带 RIE 的镜像呢?问题总有解决它的办法, 下载 aws-lambda-rie 在宿主机上,映射卷再替换 entrypoint
最后用一样的命令测试
先建立一个自己的 entrypoint 脚本
$@ 表示入口命令与参数,即 ENTRYPOINT + CMD
再用命令把它改为可执行(想在 Dockerfile 中 RUN 下面的命令也行)
为了保持更清晰的结构,没有把上面的多个 RUN 用 && 合并,所以会产生更多的 Docker 层。
这样打出来的包既能用
无须担心,不会影响到 Lambda 的执行。
而且这个 Docker 镜像部署到 AWS 上也是可执行的。
链接:
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
对于使用了大量依赖的 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.py1import sys
2def handler(event, context):
3 return 'Hello from AWS Lambda using Python' + sys.version + '!'假如我们要安装第三方的 Python 依赖(这通常是必须的,否则也就没有必要为 Lambda 选择 Docker) 放在
requirements.txt, 内容为1bounded-executor接下来基本的
Dockerfile 内容为1FROM public.ecr.aws/lambda/python:3.9
2
3COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/
4
5RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" --no-cache-dir
6
7CMD [ "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 '{}'同时在 Docker 容器中会打印
"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)]!"
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"这是一个标准的 Lambda 的后台输出
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 镜像
我们可以用 docker inspect 来查看所生成的镜像以及容器,基础镜像的 Dockerfile 可见 aws/aws-lambda-base-images.$ docker inspect public.ecr.aws/lambda/python:3.9我们得如下值得关注的内容
1"Env": [
2 "LANG=en_US.UTF-8",
3 "TZ=:/etc/localtime",
4 "PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin",
5 "LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib",
6 "LAMBDA_TASK_ROOT=/var/task",
7 "LAMBDA_RUNTIME_DIR=/var/runtime"
8],
9"Cmd": null,
10"Image": "",
11"Volumes": null,
12"WorkingDir": "/var/task",
13"Entrypoint": [
14 "/lambda-entrypoint.sh"
15],环境变量 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 也能看到该完整命令1docker ps --no-trunc --format "table {{.Image}}\t{{.Command}}"
2IMAGE COMMAND
3lambda-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 启动的信息
1sh-4.2# pwd
2/var/task
3sh-4.2# ls
4app.py bounded_executor bounded_executor-0.0.5.dist-info __pycache__ requirements.txt
5sh-4.2# python -c 'import sys; print("\n".join(sys.path))'
6
7/var/lang/lib/python39.zip
8/var/lang/lib/python3.9
9/var/lang/lib/python3.9/lib-dynload
10/var/lang/lib/python3.9/site-packages
11sh-4.2# cat /lambda-entrypoint.sh
12#!/bin/sh
13# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
14
15if [ $# -ne 1 ]; then
16 echo "entrypoint requires the handler name to be the first argument" 1>&2
17 exit 142
18fi
19export _HANDLER="$1"
20
21RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
22if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
23 exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
24else
25 exec $RUNTIME_ENTRYPOINT
26fi
27sh-4.2# bash -x /lambda-entrypoint.sh app.handler
28+ '[' 1 -ne 1 ']'
29+ export _HANDLER=app.handler
30+ _HANDLER=app.handler
31+ RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
32+ '[' -z '' ']'
33+ exec /usr/local/bin/aws-lambda-rie /var/runtime/bootstrap
34INFO[0000] exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)
35^CINFO[0001] Received signal signal=interrupt
36INFO[0001] Shutting down...
37WARN[0001] Reset initiated: SandboxTerminated
38sh-4.2# cat /var/runtime/bootstrap
39#!/bin/bash
40# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
41
42export AWS_EXECUTION_ENV=AWS_Lambda_python3.9
43
44if [ -z "$AWS_LAMBDA_EXEC_WRAPPER" ]; then
45 exec /var/lang/bin/python3.9 /var/runtime/bootstrap.py
46else
47 wrapper="$AWS_LAMBDA_EXEC_WRAPPER"
48 if [ ! -f "$wrapper" ]; then
49 echo "$wrapper: does not exist"
50 exit 127
51 fi
52 if [ ! -x "$wrapper" ]; then
53 echo "$wrapper: is not an executable"
54 exit 126
55 fi
56 exec -- "$wrapper" /var/lang/bin/python3.9 /var/runtime/bootstrap.py
57fi
58sh-4.2# echo $LAMBDA_RUNTIME_DIR
59/var/runtime
60sh-4.2# ls /var/runtime
61awslambdaric __pycache__
62awslambdaric-1.2.2.dist-info python_dateutil-2.8.2.dist-info
63bin runtime_client.cpython-39-x86_64-linux-gnu.so
64bootstrap runtime-release
65bootstrap.py s3transfer
66boto3 s3transfer-0.5.0.dist-info
67boto3-1.18.55.dist-info simplejson
68botocore simplejson-3.17.2-py3.9.egg-info
69botocore-1.21.55.dist-info six-1.16.0.dist-info
70dateutil six.py
71jmespath urllib3
72jmespath-0.10.0.dist-info urllib3-1.26.6.dist-info
73layer_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
起决定性的脚本是1export _HANDLER=app.handler<br/><br/> 2RUNTIME_ENTRYPOINT=/var/runtime/bootstrap 3if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then 4 exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT 5else 6 exec $RUNTIME_ENTRYPOINT 7fi
如果我们只基于一个 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但部署后 AWS 上是没问题的
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_LAMBDA_RUNTIME_API 环境变量值,而在 AWS 环境中是有的。如果我们在
app.py 中加输出 os.environ 可以看到以下所有的环境变量 1'_AWS_XRAY_DAEMON_PORT': '2000'
2'_AWS_XRAY_DAEMON_ADDRESS': '169.254.79.129'
3'_HANDLER': 'app.handler'
4'AWS_LAMBDA_FUNCTION_VERSION': '$LATEST'
5'PATH': '/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
6'AWS_LAMBDA_LOG_STREAM_NAME': '2021/11/08/[$LATEST]2ee4dc9a1a334fae8096f73424ad06ac'
7'AWS_LAMBDA_INITIALIZATION_TYPE': 'on-demand'
8'PYTHON_GET_PIP_URL': 'https://github.com/pypa/get-pip/raw/3cb8888cc2869620f57d5d2da64da38f516078c7/public/get-pip.py'
9'LAMBDA_TASK_ROOT': '/var/task'
10'AWS_REGION': 'us-east-1'
11'AWS_LAMBDA_RUNTIME_API': '127.0.0.1:9001'
12'AWS_LAMBDA_FUNCTION_NAME': 'test-lambda'
13'PYTHON_PIP_VERSION': '21.2.4'
14'PYTHON_GET_PIP_SHA256': 'c518250e91a70d7b20cceb15272209a4ded2a0c263ae5776f129e0d9b5674309'
15'PYTHON_SETUPTOOLS_VERSION': '57.5.0'
16'AWS_XRAY_DAEMON_ADDRESS': '169.254.79.129:2000'
17'AWS_SESSION_TOKEN': '......'
18'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': '128'
19'GPG_KEY': 'A035C8C19219BA821ECEA86B64E628F8D684696D'
20'AWS_LAMBDA_LOG_GROUP_NAME': '/aws/lambda/test-lambda'
21'AWS_EXECUTION_ENV': 'AWS_Lambda_Image'
22'AWS_XRAY_CONTEXT_MISSING': 'LOG_ERROR'
23'AWS_ACCESS_KEY_ID': '......'
24'LAMBDA_RUNTIME_DIR': '/var/runtime'
25'AWS_DEFAULT_REGION': 'us-east-1'
26'LANG': 'C.UTF-8'
27'PYTHON_VERSION': '3.10.0'
28'AWS_SECRET_ACCESS_KEY': '......'
29'_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通过对前面的实践,我才逐步了解到 AWS Lambda 给我们提供了两个运行接口
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
- Runtime interface emulator, 即 RIE, 本地测试用的执行入口
- Runtime interface clients, 即 RIC, 实际 Lambda 部署到 AWS 环境的执行入口
通常是检测是否有 AWS_LAMBDA_RUNTIME_API 环境变量来选择入口,本地测试时没有该环境变量,所以进入 RIE
创建不含 RIE 的 Docker 镜像
如果不考虑本地测试的 RIE, Dockerfile 内容如下 1FROM python:3.9-slim
2
3ENV LAMBDA_TASK_ROOT=/var/task
4RUN mkdir -p ${LAMBDA_TASK_ROOT}
5COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/
6
7WORKDIR ${LAMBDA_TASK_ROOT}
8
9RUN pip install awslambdaric --target ${LAMBDA_TASK_ROOT} --no-cache-dir && \
10 pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" --no-cache-dir
11
12ENTRYPOINT python -m awslambdaric app.handler注:由于选择的是 python:3.9-slim 基础镜像,所以要进行 AWS 操作的话,应该在
requirements.txt 文件中加上 boto3 依赖。ENTRYPOINT 可用命令,参数数组的形式
1ENTRYPOINT [ "python", "-m", "awslambdaric" ]
2CMD [ "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 后面整串
lambda-docker python -m awslambdaric app.handler
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#!/bin/sh
2if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
3 exec aws-lambda-rie python -m awslambdaric $@
4else
5 exec python -m awslambdaric $@
6fi $@ 表示入口命令与参数,即 ENTRYPOINT + CMD
再用命令把它改为可执行(想在 Dockerfile 中 RUN 下面的命令也行)
$ chmod +x entry_script.sh接下来就是如何把 RIE 包打入 Docker 镜像内部, 见如下完整的 Dockerfile 内容
1FROM python:3.9-slim
2
3ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin
4
5RUN chmod +x /usr/local/bin/aws-lambda-rie
6
7ENV LAMBDA_TASK_ROOT=/var/task
8RUN mkdir -p ${LAMBDA_TASK_ROOT}
9COPY app.py requirements.txt ${LAMBDA_TASK_ROOT}/
10COPY entry_script.sh /
11
12WORKDIR ${LAMBDA_TASK_ROOT}
13
14RUN pip install awslambdaric --target ${LAMBDA_TASK_ROOT} --no-cache-dir && \
15 pip install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" --no-cache-dir
16
17ENTRYPOINT [ "/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
[版权声明]
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。