フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

Lambda Layerを使ってデプロイを高速化する

TL;DR

  • Lambda Layerを使ってLambdaのデプロイを高速化します。
  • ServerlessとServerless Pluginを使うことで簡単に実現することができます。
  • 今回試した一例ですが、252.47秒->45.49秒で5倍高速化しました。

f:id:ikedaosushi:20190407012509p:plain

はじめに

Serverlessを用いたLambda Layerの活用については以前、Lambda LayerでHeadless Chromeを使う方法についてまとめたときに書いたのですが、Serverless Pluginなどを使うことで、より簡単にできることがわかったのでここにまとめたいと思います。

blog.ikedaosushi.com

環境

  • Mac OS X(10.14.3)
  • AWS Lambda(Runtime: Python3.7)
  • Serverless Framework: 1.40.0
  • serverless-python-requirements: 4.3.0

Pythonライブラリのパッケージングにはserverless-python-requirementsというServerless Pluginを用います。このプラグインもとても便利なのでもし使っていない方がいたらチェックしてみてください。

www.npmjs.com

課題: Lambdaでライブラリを使うとデプロイが鈍速に…

LambdaでPythonライブラリを使うためには、functionのコードとパッケージをまとめてzip化してデプロイする必要があります。 容量が小さいライブラリを使っている場合は特に問題ないのですが、統計や機械学習で良く使うscipy、pandasなどのライブラリを使うと容量が50MB以上になり、 デプロイに恐ろしく時間がかかる ようになってしまいます。

ちなみにどのくらいの容量なのか実験してみます。 計測は参考程度ということで time コマンドで1度だけ計測しています。

初期状態の場合

まず sls create -t aws-python3 した状態のままデプロイしてみます。 初回はCloudFromationの作成など発生するので2回目で計測しています。

Uploading service live-scheduler-lambda.zip file to S3 (10.28 KB)...
45.49 real         3.50 user         1.41 sysc

10.28KB/45.49秒でした。

重いライブラリがある場合

Pythonライブラリの中でも容量が大きい、scipy, pandas, matplotlibを追加してデプロイしてみます。 scipy, pandasはC拡張されているライブラリなので、次のように dockerizePip: true にすることでDocker上でsetupを行っています。

serverless.yml

custom:
  pythonRequirements:
    usePipenv: true
    dockerizePip: true
Uploading service live-scheduler-lambda.zip file to S3 (74.33 MB)...
252.47 real        70.49 user         4.38 sys

74.33MB/252.47秒と1回のデプロイに4分以上かかりました。 これはかなりストレスフルですね…。

Lambda Layerを使って解決

この問題を昨年発表されたLambda Layerを使って解決します!そもそものLambda Layerの仕組みについてはクラスメソッドさんのブログを見ていただくのが良いかもしれません。

ディレクトリ構成

まずディレクトリ構成ですが、次のようになります。 lambda がfunctionを含むディレクトリで layer_requirements がLambda Layer用のディレクトリです。インストールしたいライブラリが記載されているPipfileファイルを用意してください。(ここはrequirements.txtでも大丈夫です。)

├── lambda
│   ├── handler.py
│   └── serverless.yml
└── layer_requirements
    ├── Pipfile
    └── serverless.yml

Layer側

まずLayer側の設定を見てみましょう。

layer_requirements/serverless.yml

service: python-requirements-layer

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    usePipenv: true # Pipfileを使いたくない場合はここを削除
    dockerizePip: true
    layer: true # ここをtrueに

provider:
  name: aws
  runtime: python3.7
  region: ap-northeast-1

resources:
  Outputs:
    PythonRequirementsLambdaLayerExport: # function側から使いたい名前にする
      Value:
        Ref: PythonRequirementsLambdaLayer # これはpythonRequirementsで設定されるので固定値

1つ目のポイントは custom/pythonRequirementslayer をtrueにすることです。 もう1つのポイントは resources/Outputs でfunction側からlayerを使えるようにする設定です。 ちょっと紛らわしいのですが、Outputsの名前をserverlessから呼び出されるので(後述します)、ここを呼び出したい名前にします。 Value/Ref で参照するのはpythonRequirementsで設定される固定の PythonRequirementsLambdaLayer になります。

デプロイは普通に行います。

sls deploy

時間はさっきと同じくらいかかるのですが、こちらはライブラリを更新しない限りデプロイ不要なので、デプロイの度に、という問題は発生しません。

AWS Consoleから確認するとLayerが追加されていることがわかります。

f:id:ikedaosushi:20190407123734p:plain

function側

それではfunction側の設定を見てみましょう。

service: sample-lambda

custom:
  default_stage: dev
  stage: ${opt:stage, self:custom.default_stage}
  requirements_service: python-requirements-layer # layer側で設定したservice
  requirements_export: PythonRequirementsLambdaLayerExport # layer側で設定したOutputsの名前
  requirements_layer: ${cf:${self:custom.requirements_service}-${self:custom.stage}.${self:custom.requirements_export}}
  # python-requirements-layer-dev.PythonRequirementsLambdaLayerExport を呼び出します

provider:
  name: aws
  runtime: python3.7
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    layers:
      - ${self:custom.requirements_layer} # ここを設定

最終的には functions/${function_name}/layers に先程作成したLayerを指定すればよいのですが、やや煩雑になるので custom 内で上手く設定しています。 ${cf:~~~} はCloudFormationのOutputを呼び出すServerlessの関数で詳しくはドキュメントを参照してください。

serverless.com

こちらもデプロイはそのまま行います。

sls deploy
Uploading service live-scheduler-lambda.zip file to S3 (10.19 KB)...
39.42 real         3.14 user         0.59 sys

10.19KB/39.42秒で、ライブラリがない状態と同じになりました! AWS Consoleから確認するとfunctionに対してLayerが追加されていることがわかります。

f:id:ikedaosushi:20190407123953p:plain

テスト

念の為、ちゃんとライブラリが読み込めているか確認してみます。

import json
import pandas, scipy, matplotlib

def hello(event, context):
    body = {
        "pandas.__version__": pandas.__version__,
        "matplotlib.__version__": matplotlib.__version__,
        "scipy.__version__": scipy.__version__
    }

    return {
        "statusCode": 200,
        "body": json.dumps(body)
    }

関数を呼び出してみましょう。

sls invoke -f hello
#
{
    "statusCode": 200,
    "body": {
        "pandas.__version__": "0.24.2",
        "matplotlib.__version__": "3.0.3",
        "scipy.__version__": "1.2.1"
    }
}

正しく読み込めていますね。

まとめ

  • Lambda Layerを使ってServerlessのデプロイを高速化する方法をまとめました。
  • デプロイを高速化することで開発のイテレーションを加速させましょう💪