Serverless Frameworkのプラグインを利用した外部モジュールの管理

はじめに

こんにちは、中山です。

Serverless Frameworkを利用したPythonベースのLambda関数を作成する際に、標準で同梱されている以外の外部モジュールを使いたい場合、みなさんはそのモジュールをどのように管理されているでしょうか。方法は色々と考えられます。単純にpipコマンドでインストールしたモジュールをデプロイメントパッケージに含める方法や、Amazon Linuxでモジュールをインストールしておく方法などです。それぞれメリット/デメリットがありますが、今回はUnitedIncome/serverless-python-requirementsというServerless Frameworkのプラグインで外部モジュールを管理する方法についてご紹介したいと思います。

このプラグインを利用するメリットはどういった点でしょうか。色々と考えられますが、私は以下の2点が大きいと思います。

  1. Serverless Frameworkのプラグインとして実装されているので sls コマンドとシームレスに連携できる
  2. lambci/docker-lambdaを利用した非Pure Pythonなモジュールにも対応している

まず1点目について。ソースコードを確認すると分かりますが、デプロイメントパッケージを作成する前にモジュールをインストールする処理がHookに定義されています。そのため、 sls deploy すると自動的にモジュールがインストールされ、デプロイメントパッケージに同梱することが可能です。この実装のメリットは、モジュールのインストールを意識することなく通常のServerless Frameworkのデプロイフローと同じ感覚で扱えるという点だと思います。つまり、pipコマンドを明示的に叩いたりする手間が省けます。もちろん、sls requirementsというコマンドも定義されているので明示的に実行することも可能です。

続いて2点目について。恐らくLambda関数を利用した経験のある方なら一度はハマるであろうネイティブライブラリ問題にも対応しています。非Pure Pythonで実装されているモジュールを、例えばMac上でインストールしてもLambda関数はLinuxベースのコンテナが利用されているため、そのままでは使えない場合があります。エラーメッセージ(「invalid ELF header」)が表示され悲しみにくれた経験がある方も多いでしょう。この問題に対して、今まではAmazon LinuxあるいはAmazon LinuxのDockerイメージを利用する方法などがよく使われていたかと思います。このプラグインではlambci/docker-imageというLambda関数の実行環境とほぼ同等のDockerイメージを利用しています。 custom プロパティに dockerizePip: true と定義するだけで利用可能です。

ただし、注意点があります。こちらのドキュメントに記載されているように、lambci/docker-lambdaはAWSが公式にサポートしているものではありません。個人の方がOSSで公開しているものです。大抵の環境で問題なく動作するかと思いますが、この点は注意しておいた方がよいと思います。

なお、本エントリを執筆する上で検証に利用した主要な各種ツールのバージョンは以下の通りです。バージョンによって結果が変更される可能性があるので、その点ご了承ください。

  • Serverless Framework: 1.5.1
  • Serverless Python Requirements: 1.2.0

使ってみる

説明が長くなりました。早速使ってみましょう。

インストール

インストールは簡単です。Serverless Frameworkで管理しているディレクトリ上で以下のコマンドを実行するだけです。コマンドを実行すると、 node_modules というディレクトリに各種モジュールがインストールされます。

$ npm install --save serverless-python-requirements

Pure Pythonなモジュール

まずは最初にPure Pythonで実装されたモジュールを使ってみます。今回はほぼ標準モジュール的な扱いであるpytzを利用して、現在の時刻をUTCからJSTに変換して表示させてみます。以下のファイルを用意してください。

  • serverless.yml
frameworkVersion: ">=1.5.0"

service: test

provider:
  name: aws
  runtime: python2.7
  cfLogs: true

plugins:
  - serverless-python-requirements

functions:
  hello:
    handler: handler.hello

plugins プロパティで利用するプラグインを指定しています。

  • handler.py
from datetime import datetime
import requirements
import pytz


def hello(event, context):
    return str(datetime.now(pytz.utc).astimezone(pytz.timezone('Asia/Tokyo')))

ハイライトした箇所に注目してください。 import requirementsrequirements.pyをインポートしています。中身は単に sys.path にモジュールへのパスを追加しているだけです。この文は利用したいモジュールより前に書く必要がある点に注意してください。

  • requirements.txt
pytz

ファイルを用意したらデプロイしてみます。

$ sls deploy -v
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - test-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::CloudFormation::Stack - test-dev
Serverless: Stack create finished...
Serverless: Packaging Python requirements helper...
Serverless: Packaging required Python packages...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (1.49 MB)...
Serverless: Updating Stack...
<snip>

ハイライトしている箇所で requirements.txt に記載したモジュールをデプロイメントパッケージに含めているのが確認できます。この時点でディレクトリは以下のようになっています。

$ tree -I 'node_modules' -L 2 -a
.
├── .npmignore
├── .requirements
│   ├── pytz
│   └── pytz-2016.10.dist-info
├── .serverless
│   ├── cloudformation-template-create-stack.json
│   ├── cloudformation-template-update-stack.json
│   └── test.zip
├── handler.py
├── requirements.py
├── requirements.pyc
├── requirements.txt
└── serverless.yml

4 directories, 9 files

.requirements ディレクトリに requirements.txt で指定したpytzがインストールされていることが確認できます。また、 requirements.py がコンパイルされ .pyc ファイルが生成されています。デプロイメントパッケージ( .serverless/test.zip ) の中身を見るとpytzが同梱されていることが確認できます。

$ unzip -l .serverless/test.zip
Archive:  .serverless/test.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      192  01-21-2017 10:51   .npmignore
    19398  01-21-2017 10:51   .requirements/pytz-2016.10.dist-info/DESCRIPTION.rst
        4  01-21-2017 10:51   .requirements/pytz-2016.10.dist-info/INSTALLER
<snip>

最後に実行してみましょう。

$ sls invoke -f hello -l
"2017-01-21 19:52:49.224927+09:00"
--------------------------------------------------------------------
START RequestId: bfe1f06f-dfc7-11e6-8d16-b56e041c9162 Version: $LATEST
END RequestId: bfe1f06f-dfc7-11e6-8d16-b56e041c9162
REPORT RequestId: bfe1f06f-dfc7-11e6-8d16-b56e041c9162  Duration: 0.53 ms       Billed Duration: 100 ms         Memory Size: 1024 MB    Max Memory Used: 17 MB

正常に実行されているようです。Lambda関数のモジュールパスがどうなっているか確認すると、 sys.path にモジュールへのパスが追加されていることが確認できます。

[
    "/var/task",
    "/var/runtime/awslambda",
    "/var/runtime",
    "/usr/lib/python27.zip",
    "/usr/lib64/python2.7",
    "/usr/lib64/python2.7/plat-linux2",
    "/usr/lib64/python2.7/lib-tk",
    "/usr/lib64/python2.7/lib-old",
    "/usr/lib64/python2.7/lib-dynload",
    "/usr/local/lib64/python2.7/site-packages",
    "/usr/local/lib/python2.7/site-packages",
    "/usr/lib64/python2.7/site-packages",
    "/usr/lib/python2.7/site-packages",
    "/usr/lib64/python2.7/dist-packages",
    "/usr/lib/python2.7/dist-packages",
    "/var/task/.requirements"
]

非Pure Pythonなモジュール

続いて非Pure Pythonなモジュールを利用してみます。今回は有名な画像処理ライブラリであるPillowを利用します。当然ですが、このServerless Frameworkプラグインは内部的にDockerを利用しているためその環境は事前に用意しておく必要があります。

  • serverless.yml
frameworkVersion: ">=1.5.0"

service: test

provider:
  name: aws
  runtime: python2.7
  cfLogs: true

plugins:
  - serverless-python-requirements

custom:
  dockerizePip: true

functions:
  hello:
    handler: handler.hello

custom プロパティでlambci/docker-lambdaを利用することを指定しています。

  • handler.py
import requirements
from PIL import Image
import urllib2


def hello(event, context):
    url = 'http://3.bp.blogspot.com/-fQOCSaHHl_8/U7O61b5i-uI/AAAAAAAAiTo/3AOyCEbtMIA/s800/tatemono_hakubutsukan.png'
    image_path = '/tmp/test.png'

    with open(image_path, 'w') as f:
        f.write(urllib2.urlopen(url).read())

    return Image.open(image_path).filename

今回は簡単に画像ファイルをopenしてファイル名を表示するだけです。

  • requirements.txt
Pillow

ファイルが用意できたらデプロイしてみましょう。なお、lambci/docker-lambdaのイメージサイズはかなりでかいです。Pythonの場合1.61GBあります(Amazon LinuxのDockerイメージは約300MB)。まだイメージをローカルにpullしていない場合、回線の状況によってはそもそもイメージのダウンロードに時間が掛ると思います。また、CIツール/サービスによってこの部分がネックになる可能性がある点も注意が必要です。あと、進捗が端末に表示されないので処理が止まっているのか進んでいるのか分かりにくい。。。 sls deploy でイメージをpullするのではなく、事前に以下のコマンドでイメージをダウンロードしておいた方がよいと思います。

$ docker pull lambci/lambda:build-python2.7
build-python2.7: Pulling from lambci/lambda
20dfd86accb2: Pull complete
184cd63a224a: Pull complete
6a7c292ef725: Pull complete
488c82c53ac3: Pull complete
c3005cb5a993: Pull complete
Digest: sha256:0324acc6bf7dcff814776ab4f2fad5a278aebaf51bc214bc952b90c651e94f8e
Status: Downloaded newer image for lambci/lambda:build-python2.7

デプロイは以前と同じく以下のコマンドで実行可能です。

$ sls deploy -v
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - test-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::CloudFormation::Stack - test-dev
Serverless: Stack create finished...
Serverless: Packaging Python requirements helper...
Serverless: Packaging required Python packages...
<snip>

デプロイが完了したら実行してみましょう。

$ sls invoke -f hello -l
"/tmp/test.png"
--------------------------------------------------------------------
START RequestId: 8bde2661-df9d-11e6-af55-4574a61f712c Version: $LATEST
END RequestId: 8bde2661-df9d-11e6-af55-4574a61f712c
REPORT RequestId: 8bde2661-df9d-11e6-af55-4574a61f712c  Duration: 526.71 ms     Billed Duration: 600 ms         Memory Size: 1024 MB    Max Memory Used: 21 MB

正常に実行されているようです。やりましたね。

まとめ

いかがだったでしょうか。

Serverless Frameworkのプラグインを利用した外部モジュールの管理方法についてご紹介しました。外部モジュールを含んだデプロイメントパッケージの管理方法は色々とあり、それぞれメリット/デメリットがあります。今回ご紹介させていただいた方法を含めて、自分の環境に合った方法を見つけると良いのではないでしょうか。

本エントリがみなさんの参考になれば幸いに思います。