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