构建 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.

构建一个简单的 Lamda 镜像

最简单的构建一个 Lambda 用的 Python Docker 镜像的方式就是从 AWS 提供的基础镜像开始,Amazon 有一个公开的像 Dockhub 类似的镜像仓库,叫做 Amazon ECR Public Gallery。下面是两个 Lambda 镜像相关的链接

  1. https://gallery.ecr.aws/?operatingSystems=Linux&searchTerm=lambda: 包括所有 Lambda 所支持语的运行时相关镜像
  2. https://gallery.ecr.aws/lambda/python: Python 相关的 Lambda 基础镜像

我们将以 public.ecr.aws/lambda/python:3.9 以基础构建一个 Lambda 镜像。首先准备一个 Lambda 代码  app.py

假如我们要安装第三方的 Python 依赖(这通常是必须的,否则也就没有必要为 Lambda 选择 Docker) 放在 requirements.txt, 内容为

接下来基本的 Dockerfile 内容为

我们用命令

$ 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

我们得如下值得关注的内容

环境变量 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 也能看到该完整命令

下面我们通过覆盖 --entrypoint 参数进到该容器

$ docker run -it -p 9080:8080 --entrypoint /bin/sh lambda-docker-demo
sh-4.2# 

手动执行 /lambda-entrypoint.sh app.handler 后,同样可以在本地测试通过。

我们尽量收集多的一些 Lambda 启动的信息

关键信息整理如下:

  1. 第三方依赖我们用 pip install --target /var/task 目录下,不指定 --target  安装到  site-packages 中也行
  2. 从执行入口 /lambda-entrypoint.sh app.handler,在本地测试时,最后执行的代码是
    export _HANDLER=app.handler
    exec /usr/local/bin/aws-lambda-rie /var/runtime/bootstrap
  3. /usr/local/bin/aws-lambda-rie 是一个二进制文件,专为测试用的
  4. 在 /var/runtime 是 Lambda 的一些必需的模块,主要是 boto3,不用太关注
  5. 通过后面的回溯,本地测试时没有 AWS_LAMBDA_RUNTIME_API,执行的是 exec /usr/local/bin/aws-lambda-rie /var/runtime/bootstarp, 部署到 AWS  环境后有了 AWS_LAMBDA_RUNTIME_API, 执行的是 /var/runtime/bootstrap
    起决定性的脚本是

如果我们只基于一个 Python 基础镜像来构建自己的 Lambda 镜像, 只要达到以上 #5 的要求就行,所创建的镜像可同时运行于本地及 AWS 环境。实际上要达成 /var/runtime/bootstrap 的启动方式会比较复杂,所以后面未采此种方法 awslambdaricaws-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 可以看到以下所有的环境变量

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  给我们提供了两个运行接口

  1. Runtime interface emulator, 即 RIE, 本地测试用的执行入口
  2. Runtime interface clients, 即 RIC, 实际 Lambda 部署到 AWS 环境的执行入口

通常是检测是否有 AWS_LAMBDA_RUNTIME_API 环境变量来选择入口,本地测试时没有该环境变量,所以进入 RIE

创建不含 RIE 的 Docker 镜像

如果不考虑本地测试的 RIE,  Dockerfile 内容如下

注:由于选择的是 python:3.9-slim 基础镜像,所以要进行 AWS 操作的话,应该在 requirements.txt 文件中加上 boto3 依赖。

ENTRYPOINT 可用命令,参数数组的形式

那么是否就无法在本地测试不带 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, 内容为

$@ 表示入口命令与参数,即 ENTRYPOINT + CMD

再用命令把它改为可执行(想在 Dockerfile 中 RUN 下面的命令也行)

$ chmod +x entry_script.sh

接下来就是如何把 RIE 包打入 Docker 镜像内部, 见如下完整的 Dockerfile 内容

为了保持更清晰的结构,没有把上面的多个 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 上也是可执行的。

链接:

  1. Testing Lambda container images locally
  2. Deploy Python Lambda functions with container images
  3. Creating Lambda container images

本文链接 https://yanbin.blog/build-aws-lambda-python-docker-image/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] Lambda 在启动 Python 代码前有所动作,这就把我们拉回到了我之前的 构建 AWS Lambda Python Docker 镜像 文中一个相关问题,AWS 是通过 awslambdaric 启动 Lambda […]