これは、なにをしたくて書いたもの?
AWS Lambda関数のデプロイパッケージにはzipファイルとコンテナイメージがあります。
このうちzipファイルしか使ったことがなかったので、コンテナイメージを使ってみたいと思います。で、ローカルで動かしてみます。
AWS Lambdaのデプロイパッケージ
AWS Lambdaのデプロイパッケージには、zipファイルとコンテナイメージがあります。
Lambda の主要な概念を理解する / デプロイパッケージ
AWS Lambda関数をコンテナイメージで作成するには、以下の3種類の方法があるようです。
- AWS Lambda関数用のAWSベースイメージを使用する
- AWSのOS専用ベースイメージを使用する
- 非AWSベースイメージを使用する
- Docker Image Manifest V2 Schema 2またはOpen Container Initiative(OCI)仕様に準拠するイメージである必要がある
- ランタイムインターフェイスクライアントをイメージに含める必要がある
コンテナイメージを使用した Lambda 関数の作成 - AWS Lambda
各言語向けにも、コンテナイメージを使ったAWS Lambda関数の構築方法が書かれています。
Node.js Lambda 関数をコンテナイメージとともにデプロイする - AWS Lambda
コンテナイメージを使用して、トランスパイルされた TypeScript コードを Lambda にデプロイする - AWS Lambda
コンテナイメージで Python Lambda 関数をデプロイする - AWS Lambda
コンテナイメージで Ruby Lambda 関数をデプロイする - AWS Lambda
コンテナイメージを使用した Java Lambda 関数のデプロイ - AWS Lambda
コンテナイメージを使用して Go Lambda 関数をデプロイする - AWS Lambda
コンテナイメージを使用して.NET の Lambda 関数をデプロイする - AWS Lambda
今回はPythonを使おうと思います。
コンテナイメージを使ったAWS Lambda関数をローカルで動かすには?
ローカルでAWSに関するサービスをエミュレーションででも動かそうと思うと、LocalStackを使うことが個人的には多いです。
なのですが、コンテナイメージを使うにはLocalStackの有償版を使う必要があります。

AWS Service Feature Coverage | Docs
どうしようかな?と思ったのですが、AWS SAMではコンテナイメージであってもローカルで動かせそうな感じだったので今回はこちらを
使ってみることにしました。
あと、そもそも各言語のコンテナイメージに関するページに、curlを使った確認方法が書かれているのでこちらでも試してみましょう。
今回はコンテナイメージを使ったAWS Lambda関数のビルド、動作確認を中心にするので、あまり凝ったことはやりません。
環境
今回の環境はこちら。
$ python3 --version Python 3.12.3 $ pip3 --version pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12)
Docker。
$ docker version Client: Docker Engine - Community Version: 27.3.1 API version: 1.47 Go version: go1.22.7 Git commit: ce12230 Built: Fri Sep 20 11:40:59 2024 OS/Arch: linux/amd64 Context: default Server: Docker Engine - Community Engine: Version: 27.3.1 API version: 1.47 (minimum version 1.24) Go version: go1.22.7 Git commit: 41ca978 Built: Fri Sep 20 11:40:59 2024 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.7.24 GitCommit: 88bf19b2105c8b17560993bee28a01ddc2f97182 runc: Version: 1.2.2 GitCommit: v1.2.2-0-g7cb3632 docker-init: Version: 0.19.0 GitCommit: de40ad0
AWS SAM。
$ sam --version SAM CLI, version 1.131.0
$ aws --version aws-cli/2.22.12 Python/3.12.6 Linux/6.8.0-49-generic exe/x86_64.ubuntu.24
クレデンシャルはダミーの値を環境変数で設定しておきます。
$ export AWS_ACCESS_KEY_ID=dummy $ export AWS_SECRET_ACCESS_KEY=dummy $ export AWS_DEFAULT_REGION=ap-northeast-1
AWS Lambda関数をコンテナイメージで作成してcurlで動作確認する
最初はPython向けのガイドに従っていきましょう。
コンテナイメージで Python Lambda 関数をデプロイする - AWS Lambda
Pythonは3.12を使うので、ベースイメージもPython 3.12のものになります。なお、Python 3.12からベースイメージがAmazon Linux 2023に
なるようです(3.11以前はAmazon Linux 2)。
Python 3.12 以降のベースイメージは、Amazon Linux 2023 の最小コンテナイメージに基づいています。Python 3.8~3.11 のベースイメージは、Amazon Linux 2 のイメージに基づいています。AL2023 ベースのイメージには、デプロイのフットプリントが小さいことや、glibc などのライブラリのバージョンが更新されていることなど、Amazon Linux 2 に比べていくつかの利点があります。
コンテナイメージで Python Lambda 関数をデプロイする / Python の AWS ベースイメージ
ベースイメージにパッケージを追加したい場合は、microdnfを使うようです。
AL2023 ベースのイメージでは、Amazon Linux 2 のデフォルトのパッケージマネージャである yum の代わりに microdnf (dnf としてシンボリックリンク) がパッケージマネージャとして使用されています。microdnf は dnf のスタンドアロン実装です。AL2023 ベースのイメージに含まれるパッケージのリストについては、「Comparing packages installed on Amazon Linux 2023 Container Images」の「Minimal Container」列を参照してください。AL2023 と Amazon Linux 2 の違いの詳細については、AWS コンピューティングブログの「Introducing the Amazon Linux 2023 runtime for AWS Lambda」を参照してください。
また、このあたりも注意ですね。
コードで import ステートメントを使用すると、Python ランタイムはモジュールまたはパッケージが見つかるまで検索パス内のディレクトリを検索します。デフォルトでは、ランタイムは {LAMBDA_TASK_ROOT} ディレクトリを先に検索します。ランタイムに含まれるライブラリのバージョンをイメージに含める場合、そのバージョンが、ランタイムに含まれるバージョンよりも優先されます。
ランタイムに含まれるライブラリーとpipを使ってインストールしたライブラリーは、ベースイメージのバージョンによって優先順位が
違うようです。Python 3.11以降のベースイメージであれば、pipでインストールするとランタイムに含まれるライブラリーをオーバーライドする
ことになるようです。
Python 3.11 以降: ランタイムに含まれるライブラリと pip でインストールされるライブラリは /var/lang/lib/python3.11/site-packages ディレクトリにインストールされます。このディレクトリは、検索パス内で /var/runtime よりも優先されます。pip を使用して新しいバージョンをインストールすることで、SDK をオーバーライドできます。pip を使用して、ランタイムに含まれる SDK とその依存関係が、インストールする任意のパッケージと互換性があることを確認できます。
Python 3.8-3.10: ランタイムに含まれるライブラリは /var/runtime ディレクトリにインストールされます。pip でインストールされるライブラリは /var/lang/lib/python3.x/site-packages ディレクトリにインストールされます。/var/runtime ディレクトリは検索パス内で /var/lang/lib/python3.x/site-packages より優先されます。
3.10以前だと関係が逆になっているのでちょっと注意でしょうか…。
ランタイムインターフェイスクライアントと非AWSベースイメージを使った例も書かれていますが、今回はAWS Lambda関数用の
AWSベースイメージを使用していきます。
ドキュメントのサンプルでは依存関係がないので、適当にPyYAMLを使っておきましょう。
$ pip3 install pyyaml
インストールされたライブラリー一覧。
$ pip3 list Package Version ------- ------- pip 24.0 PyYAML 6.0.2
requirements.txtも必要になるので、作成しておきます。
$ pip3 freeze > requirements.txt
こんなAWS Lambda関数を作成。
app.py
import yaml def lambda_handler(event, context): print(f"event = {event}") print(f"context = {context}") return yaml.dump({"message": event["message"]})
Dockerfileを作成。
Dockerfile
FROM public.ecr.aws/lambda/python:3.12 COPY requirements.txt ${LAMBDA_TASK_ROOT} RUN pip install -r requirements.txt --no-cache-dir COPY app.py ${LAMBDA_TASK_ROOT} CMD [ "app.lambda_handler" ]
コンテナイメージを作成。
$ docker image build --platform linux/amd64 -t hello-lambda:0.0.1 .
ドキュメントを見ると、--platform linux/amd64を指定した方がよさそうですね。ARM向けにコンテナイメージを作成する場合は、
--platform linux/arm64を指定します。
作成したコンテナイメージを使って、AWS Lambda関数を起動。
$ docker container run -it --rm --platform linux/amd64 --name lambda-function -p 9000:8080 hello-lambda:0.0.1 08 Dec 2024 07:22:24,604 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)
確認してみます。
$ curl localhost:9000/2015-03-31/functions/function/invocations -d '{"message": "Hello Lambda"}'
"message: Hello Lambda\n"
なんか最後に改行がついてますが…まあいいでしょう。ペイロードは-dでそのままJSONを送ればよさそうです。
コンテナ側のログを見ると、ちゃんとペイロードなどを認識しています。
event = {'message': 'Hello Lambda'}
context = LambdaContext([aws_request_id=0c90c2f1-4aee-4d35-b5d9-e463f29e610b,log_group_name=/aws/lambda/Functions,log_stream_name=$LATEST,function_name=test_function,memory_limit_in_mb=3008,function_version=$LATEST,invoked_function_arn=arn:aws:lambda:us-east-1:012345678912:function:test_function,client_context=None,identity=CognitoIdentity([cognito_identity_id=None,cognito_identity_pool_id=None])])
パスは/2015-03-31/functions/function/invocationsなんですね。
ところで、ドキュメントではローカルポートを9000に割り当てているのですが、なにか理由があるのかな?と思って8080ポートに割り当てて
みましたが、特に問題なかったです。
$ docker container run -it --rm --platform linux/amd64 --name lambda-function -p 8080:8080 hello-lambda:0.0.1
結果。
$ curl localhost:8080/2015-03-31/functions/function/invocations -d '{"message": "Hello Lambda"}'
"message: Hello Lambda\n"
ローカルポートにバインドせずに、コンテナのIPに直接アクセスしてもかまいません。
$ docker container run -it --rm --platform linux/amd64 --name lambda-function hello-lambda:0.0.1
$ curl 172.17.0.2:8080/2015-03-31/functions/function/invocations -d '{"message": "Hello Lambda"}'
"message: Hello Lambda\n"
このパートはこんなところでしょうか。
AWS SAMでコンテナイメージで作成して動作確認します
続いては、AWS SAMを使ってみます。AWS SAMでコンテナイメージを使ったAWS Lambda関数の構築について記載があるのは、このあたり
でしょうか。
AWS SAM を使用した構築の概要 - AWS Serverless Application Model
AWS SAM を使用したデフォルトのビルド - AWS Serverless Application Model
まずはAWS SAMプロジェクトを作成してみます。
$ sam init
インタラクティブに入力した場合。
You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
Choose an AWS Quick Start application template
1 - Hello World Example
2 - Data processing
3 - Hello World Example with Powertools for AWS Lambda
4 - Multi-step workflow
5 - Scheduled task
6 - Standalone function
7 - Serverless API
8 - Infrastructure event management
9 - Lambda Response Streaming
10 - Serverless Connector Hello World Example
11 - Multi-step workflow with Connectors
12 - GraphQLApi Hello World Example
13 - Full Stack
14 - Lambda EFS example
15 - DynamoDB Example
16 - Machine Learning
Template: 1
Use the most popular runtime and package type? (python3.13 and zip) [y/N]: n
Which runtime would you like to use?
1 - dotnet8
2 - dotnet6
3 - go (provided.al2)
4 - go (provided.al2023)
5 - graalvm.java11 (provided.al2)
6 - graalvm.java17 (provided.al2)
7 - java21
8 - java17
9 - java11
10 - java8.al2
11 - nodejs22.x
12 - nodejs20.x
13 - nodejs18.x
14 - nodejs16.x
15 - python3.9
16 - python3.8
17 - python3.13
18 - python3.12
19 - python3.11
20 - python3.10
21 - ruby3.3
22 - ruby3.2
23 - rust (provided.al2)
24 - rust (provided.al2023)
Runtime: 18
What package type would you like to use?
1 - Zip
2 - Image
Package type: 2
Based on your selections, the only dependency manager available is pip.
We will proceed copying the template using pip.
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]: n
Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: n
Would you like to set Structured Logging in JSON format on your Lambda functions? [y/N]: y
Structured Logging in JSON format might incur an additional cost. View https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-pricing for more details
Project name [sam-app]: hello-sam-docker
-----------------------
Generating application:
-----------------------
Name: hello-sam-docker
Base Image: amazon/python3.12-base
Architectures: x86_64
Dependency Manager: pip
Output Directory: .
Configuration file: hello-sam-docker/samconfig.toml
Next steps can be found in the README file at hello-sam-docker/README.md
Commands you can use next
=========================
[*] Create pipeline: cd hello-sam-docker && sam pipeline init --bootstrap
[*] Validate SAM template: cd hello-sam-docker && sam validate
[*] Test Function in the Cloud: cd hello-sam-docker && sam sync --stack-name {stack-name} --watch
これをsam initのオプションだけで行った場合はこちら。
$ sam init --name hello-sam-docker --base-image amazon/python3.12-base --app-template hello-world-lambda-image --package-type Image --no-tracing --no-application-insights --structured-logging
コンテナイメージを使う場合は、ベースイメージを--base-imageで指定するようです。
コンテナイメージのテンプレートは、そんなに多くはなさそうですね。
$ curl -s https://raw.githubusercontent.com/aws/aws-sam-cli-app-templates/master/manifest-v2.json | jq '."amazon/python3.12-base"'
[
{
"directory": "python3.12/hello-img",
"displayName": "Hello World Lambda Image Example",
"dependencyManager": "pip",
"appTemplate": "hello-world-lambda-image",
"packageType": "Image",
"useCaseName": "Hello World Example"
},
{
"directory": "python3.12/apigw-scikit",
"displayName": "Scikit-learn Machine Learning Inference API",
"dependencyManager": "pip",
"appTemplate": "ml-apigw-scikit-learn",
"packageType": "Image",
"useCaseName": "Machine Learning"
},
{
"directory": "python3.12/apigw-xgboost",
"displayName": "XGBoost Machine Learning Inference API",
"dependencyManager": "pip",
"appTemplate": "ml-apigw-xgboost",
"packageType": "Image",
"useCaseName": "Machine Learning"
}
]
作成したAWS SAMプロジェクト内に移動。
$ cd hello-sam-docker
ディレクトリツリー。
$ tree -a
.
├── .gitignore
├── README.md
├── __init__.py
├── events
│ └── event.json
├── hello_world
│ ├── Dockerfile
│ ├── __init__.py
│ ├── app.py
│ └── requirements.txt
├── samconfig.toml
├── template.yaml
└── tests
├── __init__.py
└── unit
├── __init__.py
└── test_handler.py
5 directories, 13 files
気になるファイルを見てみましょう。
AWS Lambda関数。
hello_world/app.py
import json def lambda_handler(event, context): """Sample pure Lambda function Parameters ---------- event: dict, required API Gateway Lambda Proxy Input Format Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format context: object, required Lambda Context runtime methods and attributes Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html Returns ------ API Gateway Lambda Proxy Output Format: dict Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html """ return { "statusCode": 200, "body": json.dumps( { "message": "hello world", } ), }
これはふつうのテンプレートですね。
requirements.txtdはrequestsをインストールするようになっています。
hello_world/requirements.txt
requests
Dockerfile。
hello_world/Dockerfile
FROM public.ecr.aws/lambda/python:3.12 COPY app.py requirements.txt ./ RUN python3.12 -m pip install -r requirements.txt -t . # Command can be overwritten by providing a different command in the template directly. CMD ["app.lambda_handler"]
AWS SAMのテンプレートファイル。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > python3.12 Sample SAM Template for hello-sam-docker # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 # You can add LoggingConfig parameters such as the Logformat, Log Group, and SystemLogLevel or ApplicationLogLevel. Learn more here https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-loggingconfig. LoggingConfig: LogFormat: JSON Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: PackageType: Image Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Metadata: Dockerfile: Dockerfile DockerContext: ./hello_world DockerTag: python3.12-v1 Outputs: # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function # Find out more about other implicit resources you can reference within SAM # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api HelloWorldApi: Description: API Gateway endpoint URL for Prod stage for Hello World function Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" HelloWorldFunction: Description: Hello World Lambda Function ARN Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: Implicit IAM Role created for Hello World function Value: !GetAtt HelloWorldFunctionRole.Arn
AWS Lambda関数の部分を見てみましょう。
HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: PackageType: Image Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Metadata: Dockerfile: Dockerfile DockerContext: ./hello_world DockerTag: python3.12-v1
PackageTypeがImageになっていること、MetadataにDocker関係の情報が記述されているところがポイントでしょうか。
このあたりはこちらに書かれています。
AWS SAM を使用したデフォルトのビルド / コンテナイメージの構築
リソースの方には特に書かれていないようです。
AWS::Serverless::Function - AWS Serverless Application Model
テンプレートから生成されたAWS Lambda関数は特にパラメーターを受け取らないので、少しだけ変更しておきました。
return { "statusCode": 200, "body": json.dumps( { "message": json.loads(event["body"])["message"], } ), }
HTTPメソッドはpostにしておきます。
Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: post
では、ビルドします。
$ sam build
zipファイルパッケージをビルドしている時とは、様子が変わりました。
Building codeuri: /path/to/hello-sam-docker runtime: None architecture: x86_64 functions: HelloWorldFunction Building image for HelloWorldFunction function Setting DockerBuildArgs for HelloWorldFunction function
コンテナイメージができたみたいです。
Successfully tagged helloworldfunction:python3.12-v1
ちなみにタグは2つできました。
$ docker image ls | grep helloworldfunction helloworldfunction rapid-x86_64 810fef517975 16 minutes ago 519MB helloworldfunction python3.12-v1 92efcda2a0ac 16 minutes ago 501MB
こちらに沿って動かしてみましょう。
sam local コマンドを使用したテストの概要 - AWS Serverless Application Model
sam local invokeから。
sam local invoke を使用したテストの概要 - AWS Serverless Application Model
ペイロードは--event(または-e)オプションでファイルとして渡すのですが、標準入力から与える場合はこうなるようです。
$ echo '{"body": "{\"message\": \"Hello World!!\"}"}' | sam local invoke HelloWorldFunction --event -
HTTPボディを文字列で渡すところが面倒ですね…。
結果。
Reading invoke payload from stdin (you can also pass it from file with --event)
No current session found, using default AWS::AccountId
Invoking Container created from helloworldfunction:python3.12-v1
Building image.................
Using local image: helloworldfunction:rapid-x86_64.
START RequestId: f16edd4a-3c8c-4792-bbb1-9a63cae96adb Version: $LATEST
END RequestId: 75e1ad44-a96d-4a53-a713-7e1d82f3a58c
REPORT RequestId: 75e1ad44-a96d-4a53-a713-7e1d82f3a58c Init Duration: 0.09 ms Duration: 51.82 ms Billed Duration: 52 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello World!!\"}"}
体感的にはまあまあ重く、5秒くらいかかります。
次はsam local start-api。
sam local start-api を使用したテストの概要 - AWS Serverless Application Model
実行。
$ sam local start-api
Amazon API Gatewayのエミュレーターが起動します。
No current session found, using default AWS::AccountId Initializing the lambda functions containers. Building image................. Using local image: helloworldfunction:rapid-x86_64. Containers Initialization is done. Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [POST] You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. If you used sam build before running local commands, you will need to re-run sam build for the changes to be picked up. You only need to restart SAM CLI if you update your AWS SAM template 2024-12-08 17:14:05 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:3000 2024-12-08 17:14:05 Press CTRL+C to quit
確認。
$ curl localhost:3000/hello -d '{"message": "Hello World!!"}'
{"message": "Hello World!!"}
OKですね。
最後はsam local start-lambdaです。
を使用したテストの概要 sam local start-lambda - AWS Serverless Application Model
AWS Lambda環境を起動。
$ sam local start-lambda
これでエンドポイントをhttp://127.0.0.1:3001として、AWS CLIでAWS Lambda関数を呼び出すことができます。
No current session found, using default AWS::AccountId Initializing the lambda functions containers. Building image................. Using local image: helloworldfunction:rapid-x86_64. Containers Initialization is done. Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint. 2024-12-08 17:15:10 WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:3001 2024-12-08 17:15:10 Press CTRL+C to quit
呼び出してみます。
$ aws --endpoint-url http://localhost:3001 lambda invoke --function-name HelloWorldFunction --cli-binary-format raw-in-base64-out --payload '{"body": "{\"message\": \"Hello World!!\"}"}' result.json
{
"StatusCode": 200
}
OKですね。
$ cat result.json
{"statusCode": 200, "body": "{\"message\": \"Hello World!!\"}"}
おわりに
デプロイパッケージをコンテナイメージとしたAWS Lambda関数をローカルで動かす方法を、いろいろと確認してみました。
LocalStackが使えなかったのでちょっと困ったのですが、Dockerコンテナ単体で確認する方法、AWS SAMを使う方法などの選択肢を
確認できたり、そもそもコンテナイメージをデプロイパッケージにする方法を見てきていなかったのでこの機会に確認できてよかったですね。
ただ、AWS SAMを使うとちょっと重たいのがやや難点でしょうか…。