Post

AWS -- Lambda

I am setting up some webhooks using AWS Lambda these days. As you know Lambda functions can be deployed using a docker image. AWS provides a set of base images for various languages.

I am curious how these base images work underneath.

Python base image

The first log I saw when running a python Lambda image is

1
25 Jul 2023 05:20:01,256 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/asset, handler=)

So I went to the docker container and below is the content of bootstrap script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash-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.10

if [ -z "$AWS_LAMBDA_EXEC_WRAPPER" ]; then
  exec /var/lang/bin/python3.10 /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.10 /var/runtime/bootstrap.py
fi

Basically, it simply runs bootstrap.py in the same folder

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
bash-4.2# cat bootstrap.py
"""
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
"""

import json
import logging
import os
import site
import sys
import time
import traceback
import warnings
import awslambdaric.__main__ as awslambdaricmain


def is_pythonpath_set():
    return "PYTHONPATH" in os.environ


def get_opt_site_packages_directory():
    return '/opt/python/lib/python{}.{}/site-packages'.format(sys.version_info.major, sys.version_info.minor)


def get_opt_python_directory():
    return '/opt/python'


# set default sys.path for discoverability
# precedence: LAMBDA_TASK_ROOT -> /opt/python/lib/pythonN.N/site-packages -> /opt/python
def set_default_sys_path():
    if not is_pythonpath_set():
        sys.path.insert(0, get_opt_python_directory())
        sys.path.insert(0, get_opt_site_packages_directory())
#     'LAMBDA_TASK_ROOT' is function author's working directory
#     we add it first in order to mimic the default behavior of populating sys.path and make modules under 'LAMBDA_TASK_ROOT'
#     discoverable - https://docs.python.org/3/library/sys.html#sys.path
    sys.path.insert(0, os.environ['LAMBDA_TASK_ROOT'])


def add_default_site_directories():
#     Set 'LAMBDA_TASK_ROOT as site directory so that we are able to load all customer .pth files
    site.addsitedir(os.environ["LAMBDA_TASK_ROOT"])
    if not is_pythonpath_set():
        site.addsitedir(get_opt_site_packages_directory())
        site.addsitedir(get_opt_python_directory())

def set_default_pythonpath():
    if not is_pythonpath_set():
#         keep consistent with documentation: https://docs.aws.amazon.com/lambda/latest/dg/lambda-environment-variables.html
        os.environ["PYTHONPATH"] = os.environ["LAMBDA_RUNTIME_DIR"]


def main():
    set_default_sys_path()
    add_default_site_directories()
    set_default_pythonpath()
    awslambdaricmain.main([os.environ["LAMBDA_TASK_ROOT"], os.environ["_HANDLER"]])

if __name__ == '__main__':
    main()

OK. So it add a bunch of paths to sys.path including LAMBDA_TASK_ROOT. One question confused me a lot is how to install 3rd party packages inside Lambda docker image. Different posts gave different instructions. Some said all packages should be installed inside /assert folder. Even AWS documents are inconsistent among versions. So I did a quick experiment to figure out the correct behavior. I installed random package using pip install lz4.

1
2
3
bash-4.2# pip show lz4
...
Location: /var/lang/lib/python3.10/site-packages

No surprise, it is installed under the standard python3.10 folder. Now check sys.path.

1
2
3
4
5
6
7
8
9
bash-4.2# which python
/var/lang/bin/python

bash-4.2# /var/lang/bin/python
Python 3.10.12 (main, Jul 11 2023, 13:56:08) [GCC 7.3.1 20180712 (Red Hat 7.3.1-15)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/var/lang/lib/python310.zip', '/var/lang/lib/python3.10', '/var/lang/lib/python3.10/lib-dynload', '/var/lang/lib/python3.10/site-packages']

OK. No hack! Just do RUN pip install requirements.txt in the Dockerfile!

Another thing to note is the main() function above. Its implementation is here. Basically, it is an endless while loop that handle incoming requests.

This post is licensed under CC BY 4.0 by the author.