Serverless Framework(Python)でLambda Layerを使う
概要
- Serverless Frameworkを使ってLambda Layerを利用するLambdaを用意したかった
- 単純に利用するだけだったらドキュメント読んで、はい、おわり。だったんだけど、Serverless Frameworkを利用経験が浅いのと、Pluginを利用したことで混乱した
- 利用したPlugin「Serverless Python Requirements」
- その時混乱したことも含め、デプロイ手順をメモする
- あと、他にもいくつか試してみた
-
--stage
オプションで環境変数とデプロイ先を変更する - Lambda関数を複数管理する
-
環境
$ sw_vers
ProductName: macOS
ProductVersion: 13.3.1
BuildVersion: 22E261
$ docker version
Client:
Cloud integration: v1.0.31
Version: 23.0.5
API version: 1.42
Go version: go1.19.8
Git commit: bc4487a
Built: Wed Apr 26 16:12:52 2023
OS/Arch: darwin/arm64
Context: default
Server: Docker Desktop 4.19.0 (106363)
Engine:
Version: 23.0.5
API version: 1.42 (minimum version 1.12)
Go version: go1.19.8
Git commit: 94d3ad6
Built: Wed Apr 26 16:17:14 2023
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.6.20
GitCommit: 2806fc1057397dbaeefbea0e4e17bddfbd388f38
runc:
Version: 1.1.5
GitCommit: v1.1.5-0-gf19387a
docker-init:
Version: 0.19.0
GitCommit: de40ad0
$ aws --version
aws-cli/2.11.20 Python/3.11.3 Darwin/22.4.0 source/arm64 prompt/off
事前準備
Serverless Frameworkのインストール
この辺を参考に進めた。略
IAM ユーザーの用意
ドキュメントを見てくれ。略
IAM ユーザーの許可ポリシー設定
検証のため一旦FullAccess
- AmazonS3FullAccess
- AWSCloudFormationFullAccess
- AWSLambda_FullAccess
- CloudWatchEventsFullAccess
- AM5時になったら叩く。ような設定をしたかったので追加
- CloudWatchLogsFullAccess
- IAMFullAccess
本当に必要なものだけはこれから整理します。
アクセスキー・シークレットキーをServerless Frameworkへ設定
取得方法はググってくれ。略
sls
って名前のprofileを作る
$ aws configure --profile sls
Serverless Frameworkのプロジェクトを用意する
pipeline
という名前のプロジェクトディレクトリができる。
$ serverless create --template aws-python3 --path pipeline
移動する
$ cd pipeline/
こいつはいらないので消す。
$ rm -f handler.py
Pluginの設定
Pluginのインストール
$ serverless plugin install -n serverless-python-requirements
Pluginの設定
serverless.yml
をいじります。
$ vim serverless.yml
service: pipeline
frameworkVersion: '3'
provider:
name: aws
runtime: python3.9
architecture: arm64
stage: local
profile: sls
region: ap-northeast-1
functions:
pipeline:
handler: pipeline.handler.main
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerImage: public.ecr.aws/sam/build-python3.9:latest-arm64
dockerizePip: true
requirements.txtを用意します。
うまく外部ライブラリをimportできるか確認するために、pandas
をインストールした想定で進めます。
numpy==1.24.3
pandas==2.0.1
python-dateutil==2.8.2
pytz==2023.3
tzdata==2023.3
Lambdaで動作するファイルを作成します。(なぜディレクトリを切っているのかは後述します)
$ mkdir pipeline
$ vim pipeline/handler.py
import json
import pandas as pd
def main(event, context):
body = {
"message": "pandas version {}.".format(pd.__version__),
"input": event
}
def get_response():
return {
"statusCode": 200,
"body": json.dumps(body)
}
return get_response()
試しにデプロイ
$ sls deploy --stage local
# ...省略...
✔ Service deployed to stack pipeline-local (151s)
functions:
pipeline: pipeline-local-pipeline (36 MB)
# ...省略...
pandas
のバージョンが表示されたおkです。
{
"statusCode": 200,
"body": "{\"message\": \"pandas version 2.0.1.\", \"input\": {\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"value3\"}}"
}
おk
Lambda Layerの設定
2つ用意します。
「Serverless Python Requirements」のLayer設定
こっちはrequirements.txt
でインストールするパッケージが同封されたLayerになります。
serverless.yml
に一行追加するだけでおk
# ...省略...
custom:
pythonRequirements:
dockerImage: public.ecr.aws/sam/build-python3.9:latest-arm64
dockerizePip: true
layer: true # これを追加する
独自で用意するLayer設定
プロジェクトのルートディレクトリに layers
というディレクトリを作成します。
$ mkdir layers/
$ vim layers/utils.py
import pandas as pd
def get_pandas_version():
return pd.__version__
functions
とplugins
の間にlayers
を追加します。
# ...省略...
layers:
sampleLayer:
path: layers
description: layer for ingredients
compatibleRuntimes:
- python3.9
# ...省略...
2つのLayerを参照できるようにLambdaの設定を追加する
命名がむずい
# ...省略...
functions:
pipeline:
handler: pipeline.handler.main
layers:
- Ref: PythonRequirementsLambdaLayer # pluginのlambda layer
- Ref: SampleLayerLambdaLayer # 自分で用意するlambda layer
# ...省略...
とあるのですが、期待した変換が行われません。
my-layer → MyDashlayerLambdaLayer
my_layer → MyUnderscorelayerLambdaLayer
「MyLayer」とすれば「MyLayerLambdaLayer」となるので、好みやポリシーで命名規約を決めてよいと思います。
むじゅい・・・
最後にLambdaをいじります
import sys
sys.path.append('/opt')
import json
from utils import get_pandas_version
def main(event, context):
body = {
"message": "pandas version {}.".format(get_pandas_version()),
"input": event
}
def get_response():
return {
"statusCode": 200,
"body": json.dumps(body)
}
return get_response()
Lambda Layerを追加したパッケージやスクリプトを参照するには、/opt/python
以下に用意されるようなので、こうします。
import sys
sys.path.append('/opt')
試しにデプロイ
$ sls deploy --stage local
# ...省略...
functions:
pipeline: pipeline-local-pipeline (117 kB)
layers:
sampleLayer: arn:aws:lambda:ap-northeast-1:hogehoge:layer:sampleLayer:1
pythonRequirements: arn:aws:lambda:ap-northeast-1:hogehoge:layer:pipeline-local-python-requirements:1
# ...省略...
- Layerが2つ設定されていたらおk
- テスト実行してみて、
pandas
のバージョンがprintされたらおk
--stage
オプションで環境変数とデプロイ先を変更する
環境変数を設定するファイルを用意する。それぞれ3つ。
- local用
- dev用
- production用
$ mkdir -p conf/{local,dev,prd}/
$ touch conf/local/local.yml
$ touch conf/dev/dev.yml
$ touch conf/prd/prd.yml
ローカルの環境変数を用意します。
MY_CONFIG: 'localです'
内容をgitにコミットしたくなければ、.gitignore
に~/pipeline/conf
以下を追加してあげてください。
また、↑はlocal
だけの手順ですが、dev
とprd
の環境変数も用意しないとデプロイ時エラーになるので注意です。
Error:
Cannot resolve serverless.yml: Variables resolution errored with:
- Cannot resolve variable at "custom.otherfile.environment.dev": Source "file" returned not supported result: "undefined",
- Cannot resolve variable at "custom.otherfile.environment.prd": Source "file" returned not supported result: "undefined"
さらに、この手順の環境変数の設定ですが、デプロイするファイルのサイズが小さい場合も含め、Lambdaの管理画面で環境変数が確認できるので、それが嫌な方はSecret Manager的なマネージドサービスを利用するのをオススメします。
続き、serverless.yml
も下記のように変更する。
とてもわかりやすい記事を参考にしました。
# ...省略...
provider:
name: aws
runtime: python3.9
architecture: arm64
stage: ${opt:stage, self:custom.defaultStage}
profile: ${self:custom.profiles.${self:provider.stage}}
region: ap-northeast-1
functions:
pipeline:
handler: pipeline.handler.main
environment:
MY_CONFIG: ${self:custom.otherfile.environment.${self:provider.stage}.MY_CONFIG}
layers:
- Ref: PythonRequirementsLambdaLayer
- Ref: SampleLayerLambdaLayer
# ...省略...
custom:
defaultStage: local
profiles: # aws profileを変更したい場合はこちらを変更する
local: sls
dev: sls
prd: sls
otherfile:
environment:
local: ${file(./conf/local/local.yml)}
dev: ${file(./conf/dev/dev.yml)}
prd: ${file(./conf/prd/prd.yml)}
# ...省略...
デプロイするときの --stage
で、local
なのかdev
なのかを選択できる。
$ sls deploy --stage dev
ログ出力でprintされていればおk
devです
Lambdaを複数管理する
複数Lambda関数を管理したい場合は、ディレクトリをわけて、serverless.yml
をネストして設定したらおkです。
$ mkdir runner
$ vim runner/handler.py
import sys
sys.path.append('/opt')
import json
from utils import get_pandas_version
def main(event, context):
body = {
"message": "pandas version {}.".format(get_pandas_version()),
"lambda_name": "2つ目",
"input": event
}
def get_response():
return {
"statusCode": 200,
"body": json.dumps(body)
}
return get_response()
# ...省略...
functions:
pipeline:
handler: pipeline.handler.main
environment:
MY_CONFIG: ${self:custom.otherfile.environment.${self:provider.stage}.MY_CONFIG}
layers:
- Ref: PythonRequirementsLambdaLayer
- Ref: SampleLayerLambdaLayer
runner:
handler: runner.handler.main
environment:
MY_CONFIG: ${self:custom.otherfile.environment.${self:provider.stage}.MY_CONFIG}
layers:
- Ref: PythonRequirementsLambdaLayer
- Ref: SampleLayerLambdaLayer
# ...省略...
試しにデプロイ(今度はprd)
$ sls deploy --stage prd
# ...省略...
✔ Service deployed to stack pipeline-prd (120s)
functions:
pipeline: pipeline-prd-pipeline (118 kB)
runner: pipeline-prd-runner (118 kB)
# ...省略...
おっkでした。
{
"statusCode": 200,
"body": "{\"message\": \"pandas version 2.0.1.\", \"lambda_name\": \"\\uff12\\u3064\\u76ee\", \"input\": {\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"value3\"}}"
}
態 オキシジェン・デストロイヤー
最終形 こんなファイルたちができました。
$ tree pipeline -L 3 -I "node_modules|__pycache__"
pipeline
├── conf
│ ├── dev
│ │ └── dev.yml
│ ├── local
│ │ └── local.yml
│ └── prd
│ └── prd.yml
├── layers
│ └── utils.py
├── package-lock.json
├── package.json
├── pipeline
│ └── handler.py
├── requirements.txt
├── runner
│ └── handler.py
└── serverless.yml
MY_CONFIG: 'localです'
MY_CONFIG: 'devです'
MY_CONFIG: 'prdです'
numpy==1.24.3
pandas==2.0.1
python-dateutil==2.8.2
pytz==2023.3
tzdata==2023.3
service: pipeline
frameworkVersion: '3'
provider:
name: aws
runtime: python3.9
architecture: arm64
stage: ${opt:stage, self:custom.defaultStage}
profile: ${self:custom.profiles.${self:provider.stage}}
region: ap-northeast-1
functions:
pipeline:
handler: pipeline.handler.main
environment:
MY_CONFIG: ${self:custom.otherfile.environment.${self:provider.stage}.MY_CONFIG}
layers:
- Ref: PythonRequirementsLambdaLayer
- Ref: SampleLayerLambdaLayer
runner:
handler: runner.handler.main
environment:
MY_CONFIG: ${self:custom.otherfile.environment.${self:provider.stage}.MY_CONFIG}
layers:
- Ref: PythonRequirementsLambdaLayer
- Ref: SampleLayerLambdaLayer
layers:
sampleLayer:
path: layers
description: layer for ingredients
compatibleRuntimes:
- python3.9
plugins:
- serverless-python-requirements
custom:
defaultStage: local
profiles:
local: sls
dev: sls
prd: sls
otherfile:
environment:
local: ${file(./conf/local/local.yml)}
dev: ${file(./conf/dev/dev.yml)}
prd: ${file(./conf/prd/prd.yml)}
pythonRequirements:
dockerImage: public.ecr.aws/sam/build-python3.9:latest-arm64
dockerizePip: true
layer: true
import pandas as pd
import os
def get_pandas_version():
print(os.environ.get('MY_CONFIG'))
return pd.__version__
import sys
sys.path.append('/opt')
import json
from utils import get_pandas_version
def main(event, context):
body = {
"message": "pandas version {}.".format(get_pandas_version()),
"input": event
}
def get_response():
return {
"statusCode": 200,
"body": json.dumps(body)
}
return get_response()
import sys
sys.path.append('/opt')
import json
from utils import get_pandas_version
def main(event, context):
body = {
"message": "pandas version {}.".format(get_pandas_version()),
"lambda_name": "2つ目",
"input": event
}
def get_response():
return {
"statusCode": 200,
"body": json.dumps(body)
}
return get_response()
ハマった点
Mac(m1)でデプロイするとなぜか動かない
- 原因を追っていない(よくない)
- 「Serverless Python Requirements」の
dockerImage: true
にすると動くようになった - たぶんm1のせい
- CPUが違うからというのは理解しているが、アーキテクチャ変更してもエラーになったのでわからない
Serverless FrameworkのLayerとPluginのLayerを「同じLambda Layer」と勘違いしてた
- それぞれ独立したLayerの設定
- ぼくは全部同じLayerに設定されると勘違いしてしまった
- 「Serverless Python Requirements」のソースコードを確認してやっと気がついた
- あれ、
serverless.yml
のlayers
キーで設定したデプロイファイルはどこで作成されるの?そんなところないじゃん → まさか・・・?( ゚д゚)ハッ!?
- だからドキュメントが2つあるのか!!!!(?????)
- あと、CloudFormationの設定むずすぎる
その他
掃除
$ sls remove --stage local
参考記事
Discussion