😇

Serverless Framework(Python)でLambda Layerを使う

2023/05/22に公開

概要

  • 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のインストール

この辺を参考に進めた。略

https://codelikes.com/mac-node-install/

IAM ユーザーの用意

ドキュメントを見てくれ。略

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html

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
~/pipeline/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をインストールした想定で進めます。

~/pipeline/requirements.txt
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
~/pipeline/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

~/pipeline/serverless.yml
# ...省略...
custom:
  pythonRequirements:
    dockerImage: public.ecr.aws/sam/build-python3.9:latest-arm64
    dockerizePip: true
    layer: true  # これを追加する

独自で用意するLayer設定

プロジェクトのルートディレクトリに layers というディレクトリを作成します。

$ mkdir layers/
$ vim layers/utils.py
~/pipeline/layers/utils.py
import pandas as pd


def get_pandas_version():
    return pd.__version__

functionspluginsの間にlayersを追加します。

~/pipeline/serverless.yml
# ...省略...
layers:
  sampleLayer:
    path: layers
    description: layer for ingredients
    compatibleRuntimes:
      - python3.9
# ...省略...

2つのLayerを参照できるようにLambdaの設定を追加する

命名がむずい

~/pipeline/serverless.yml
# ...省略...
functions:
  pipeline:
    handler: pipeline.handler.main
    layers:
      - Ref: PythonRequirementsLambdaLayer  # pluginのlambda layer
      - Ref: SampleLayerLambdaLayer         # 自分で用意するlambda layer
# ...省略...

https://qiita.com/ayu_kane/items/6b4a0c139e34a31051dd#設定ファイルの記述-1

とあるのですが、期待した変換が行われません。

my-layer → MyDashlayerLambdaLayer
my_layer → MyUnderscorelayerLambdaLayer
「MyLayer」とすれば「MyLayerLambdaLayer」となるので、好みやポリシーで命名規約を決めてよいと思います。

むじゅい・・・

最後にLambdaをいじります

~/pipeline/pipeline/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()),
        "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

ローカルの環境変数を用意します。

~/pipeline/conf/local/local.yml
MY_CONFIG: 'localです'

内容をgitにコミットしたくなければ、.gitignore~/pipeline/conf以下を追加してあげてください。

また、↑はlocalだけの手順ですが、devprdの環境変数も用意しないとデプロイ時エラーになるので注意です。

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も下記のように変更する。

とてもわかりやすい記事を参考にしました。

https://dev.classmethod.jp/articles/serverless-framework-conf-change/

~/pipeline/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
~/pipeline/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()
~/pipeline/serverless.yml
# ...省略...
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
~/pipeline/conf/local/local.yml
MY_CONFIG: 'localです'
~/pipeline/conf/dev/dev.yml
MY_CONFIG: 'devです'
~/pipeline/conf/prd/prd.yml
MY_CONFIG: 'prdです'
~/pipeline/requirements.txt
numpy==1.24.3
pandas==2.0.1
python-dateutil==2.8.2
pytz==2023.3
tzdata==2023.3
~/pipeline/serverless.yml
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
~/pipeline/layers/utils.py
import pandas as pd
import os


def get_pandas_version():
    print(os.environ.get('MY_CONFIG'))
    return pd.__version__
~/pipeline/pipeline/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()),
        "input": event
    }
    def get_response():
        return {
            "statusCode": 200,
            "body": json.dumps(body)
        }

    return get_response()
~/pipeline/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()

ハマった点

Mac(m1)でデプロイするとなぜか動かない

  • 原因を追っていない(よくない)
  • 「Serverless Python Requirements」の dockerImage: true にすると動くようになった
  • たぶんm1のせい
    • CPUが違うからというのは理解しているが、アーキテクチャ変更してもエラーになったのでわからない

Serverless FrameworkのLayerとPluginのLayerを「同じLambda Layer」と勘違いしてた

  • それぞれ独立したLayerの設定
  • ぼくは全部同じLayerに設定されると勘違いしてしまった
  • だからドキュメントが2つあるのか!!!!(?????)
  • あと、CloudFormationの設定むずすぎる

その他

掃除

$ sls remove --stage local

参考記事

https://www.serverless.com/framework/docs/providers/aws/guide/layers
https://www.serverless.com/plugins/serverless-python-requirements
https://dev.classmethod.jp/articles/serverless-framework-conf-change/

CBcloud Tech Blog

Discussion