3
Help us understand the problem. What are the problem?

posted at

updated at

[AWS Lambda] Pythonで外部モジュール(Pillow)を使う

AWS Lambdaで外部モジュールを追加する方法を情報まとめた

AWS Lambdaだと使えないライブラリ(モジュール)がありますね。追加が必要な場面がありますので外部モジュールの追加についてまとめます。

amazon_awslambda_logo_icon_167887.png

今回LambdaでS3にアップされた画像いじりたいのででPillow使おうとしました。
モジュールをzipでアップすればいいのは知っていたのでpip installでデータ取得できたらzipにしてOKでしょ!
と思ってテストを実行したら

[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'PIL'

とエラー。pip installだと無理なのこれ...どうするのこれ...みたいになったlambda初心者の私。
調べてみると方法が何個かあって、落とし穴的なものもあったので外部モジュールを追加する方法をまとめました。

今回はPillow使えるようにしますが、NumPyやpandasなどの他のモジュール使いたい時も手順は同じです。

概要

かなり簡単にいうと、Lambdaにモジュール入ったzipファイルをアップロードしてやればいいだけ!ではあります。

  • ローカルでpipとzipコマンドが使える環境があれば作れます。
    • windowsの人は注意。知識ない人はDockerやEC2インスタンス作ってLinux環境でやった方がスムーズ
  • 簡単な方法。ただ、Pillowだと失敗した方法(requestとかならいけた)
  • Pillowで成功した方法。whlファイルにてビルド
  • レイヤーを作成
    • レイヤーを追加
    • 一番スマートで他のLambda関数で使い回しもできるので便利
    • 他の方法で追加できた後に試すと便利かも

実行環境はMac。Intelです。

EC2インスタンス作ってやる場合には触れません。適切かわかりませんが以前書いたDjangoアプリのEC2デプロイでUbuntuのインスタンスの立てた記事は書いたのでリンクは一応貼っておきます。7だけやれば作れるはず。ただし、UbuntuではなくAmazonLinux2を必ず選択。
※ Python3.7以下で作成する場合はAmazonLinuxにて作成すること。

最初にPillowだと失敗した方法

一番簡単な方法から。最初に言いますが、この方法だとPillowは失敗します。
この方法で十分追加可能なものもあります。

少なくてもpandas, numpy, pillowはこの方法ではできない。あと、画像をいじるような類のモジュールは厳しいのではと思ってます。

テキトウなディレクトリ作ってその中身をzipにしていく。

$ mkdir lambda_sample

作ったディレクトリにダウンロードしたPillowを入れるってコマンド

$ pip install Pillow -t ./lambda_sample

lambdaで処理する関数記述したファイルlambda_function.pyを作る。今回の場合Pillow入ってるかテストするだけなんで本当になんでもいいです。

$ touch ./lambda_sample/lambda_function.py

lambda作ったときに書かれてたほぼそのままの記述だけ書いておきます。もちろん自由に変えてOK
もし、すでにコードを書いていたら↓のコードで上書きされる。その場合はコードをコピペしてください。

lambda_sample/lambda_function.py
import json
from PIL import Image

def lambda_handler(event, context):
    print(event)
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

ディレクトリ配下のファイルに権限与える。

$ chmod -R 755 ./lambda_sample/

アップロードするためにzipにする。ファイル名はなんでもいいです。

$ cd lambda_sample
$ zip -r9 ${OLDPWD}/mypackage.zip .

カレントディレクトリをzipにして名前はmypackage.zipにしました。

lambdaでzipファイルアップロード

スクリーンショット 2022-05-26 23.10.18.png

スクリーンショット 2022-05-26 23.11.24.png

よーし余裕だ!
と思ってテストの実行したら

"Unable to import module 'lambda_function': cannot import name '_imaging' from 'PIL' (/var/task/PIL/__init__.py)"

なんだこれは、、、

エラーの理由

lambdaはAmazonlinux2で実行してます。外部モジュールがAmazonlinux2用になっていないと出るエラーらしい。
実行環境は以下を参照

そして、python3.7まではAmazon Linuxでpython3.8以上がAmazon Linux 2です。
これまでやってきた方法で問題ないモジュールはあると思いますが、インストール時にOSに合わせてビルドするというPillowはダメらしい。

試しにrequestsでやったら大丈夫でした。画像扱うものはビルドするようなの多かった記憶あるのでは難しいかも?

環境に合わせたビルドで作る方法

  1. EC2でamazonlinux2のインスタンス作ってそこで作成する
  2. Dockerにてamazonlinux2のコンテナでこれまでと同じ手順で作る
  3. whlファイルから作成

結果から書くと、Pillowだけ入れるなら3.が一番楽でzipファイルの中身もスッキリしていて容量小さいです。AWSのQ&Aの回答にあった方法ですし。

1の方法と2の方法はやっていることは基本的に同じで、dockerで作るかEC2インスタンスで作るかの違いだけです。pillow以外でも使えるのでpillow以外も使うとか、他の人も使えるようにしたいというなら2の方法を使ってもいいと思います。

1の方法は個人的にモジュール作るだけのためにEC2作りたくないし、会社とかで自由にインスタンス作れないということもあると思うので2の方法を解説して、もちろん3の簡単な方法も解説します。

2. Dockerにてamazonlinux2のコンテナにてモジュール作る

ということでdocker-hubから使えそうなのを探す。aws-lambda-pythonとかいうちょうど良さそうなものを発見したので使ってみる。

https://hub.docker.com/r/amazon/aws-lambda-python

この時はlatestがlinux/amd64です。lambda関数作るときにPythonのバージョンを合わせるの忘れずに。

lambdaの実行環境arm64選択した場合は使えるのかわからないので注意。

Docker imageを作成

私の場合はLambdaはPython3.9で使おうと思っていたので、3.9のイメージを取ってきます。

$ mkdir pillow_lambda
$ touch ./pillow_lambda/Dcokerfile ./pillow_lambda/docker-compose.yml
FROM public.ecr.aws/lambda/python:3.9
RUN pip install pillow
CMD [ "app.handler" ]
$ cd pillow_lambda
$ docker build -t lambda_image .
  • lambda_imageといいイメージの名前つける
  • カレントディレクトリにあるDockerfileを使ってbuildする

イメージを実行する。

$ docker run lambda_image

あとはパッケージをコピーしてしまう。CONTAINER IDわかればコピーできるのでまず調べて

$ docker container ls
CONTAINER ID   IMAGE                        COMMAND                  CREATED              STATUS              PORTS                    NAMES
f3a49e4fef5c   lambda_image                 "/lambda-entrypoint.…"   About a minute ago   Up About a minute

docker cp <CONTAINER ID>:/var/lang/lib/python3.9/site-packages/ <コピー先ディレクトリ>してやればOK
function_packageディレクトリにでもコピーすることにします。

$ mkdir ~/function_package
$ docker cp f3a49e4fef5c:/var/lang/lib/python3.9/site-packages/ ~/function_package/

python3.9はバージョンに合わせて変える

あとはlambdaで処理する関数も作成する必要あるので作成。
下記のようにデフォルトのサンプルみたいなコードで試します。

$ cd 
$ touch ./function_package/lambda_function.py
lambda_function.py
import json
from PIL import Image

def lambda_handler(event, context):
    print(event)
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
$ cd function_package/site-packages
$ zip -r9 ${OLDPWD}/mypackage.zip .
$ cd ~/function_package
$ zip -u ~/mypackage.zip lambda_function.py

${OLDPWD}は適時出力先を変えてください。

一応解凍して確認してみてください。mypackageの中に複数のモジュールとlambda_function.pyが同じディレクトリにあればOK。階層深くなってないのを確認してください。読み込めないので。

mypackage/
 ├ lambda_function.py/
 ├ PIL/
 │ └.../
 └ .../

詳しくないけど、この方法だとbuildだけに使うものとか、不要そうなものも入ってる気がする。使う分には問題ない。

あとはアップロードするだけ。ここは失敗した方法と同じ。
lambdaでzipファイルアップロード
スクリーンショット 2022-05-26 23.11.24.png

アップロードが問題なく終わると

Lambda 関数「convert_mock」のデプロイパッケージが大きすぎて、インラインコード編集を有効にできません。ただし、関数を呼び出すことはできます。

のような表示が出てコードが見れなくなっているので少しびびった。

ただ、モジュールの作り方に問題がなければ、普通に動くし、テストタブからテストで動かすことも可能です。
関数のコードには問題なくてもモジュールが大きいとこうなるらしいのでしょうがない。(3MB以上?らしい)

3. whlファイルから作成

結果的にこの方法が一番楽でした。というか、AWS公式がこの方法使ったらいいです。と回答してた。

https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-python-package-compatible/

この例だとpandasですが、pillowでも同じです。

whlファイル入手

pypiのPillowページからDownload filesのページに。

選ぶファイルは注意。今回のlambdaの実行環境は前述した通りPython 3.9,amazonlinux2,x86_64なので該当する項目をダウンロード。

Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
  • cp39
  • manylinux_2
  • x86_64

と書いてあるのが該当する事になる。Pillowのバージョンはこの時の最新9.1.1使ってます。そこはこだわりなければ最新でいいと思います。

whlからもモジュール、zipファイル作成

zipコマンドで作るので一式を入れるディレクトリ作る。

$ mkdir my-lambda-function

この中に.whlを入れる。
以下は例です。lambda_func_nameを変更してください。自分で作ったlambda関数作った時の名前です。
※ 作ってない人はtargetに指定した名前でlambda関数作成
スクリーンショット 2022-05-27 15.10.46.png

$ pip install \
    --platform manylinux2014_x86_64 \
    --target=lambda_func_name \
    --implementation cp \
    --python 3.9 \
    --only-binary=:all: --upgrade \
    Pillow

バージョン 19.3.0 以降の pip を使用してください。失敗します。

$ cd lambda_func_name
$ touch lambda_function.py

これまでと同じように関数のコードはお好きに変更してください。今回はPillowがimportエラーになるかだけ確認するコードが以下

lambda_function.py
import json
from PIL import Image

def lambda_handler(event, context):
    print(event)
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

実行するディレクトリに注意して以下を実行。

~/my-lambda-function/lambda_func_name $ zip -r ../my-deployment-package.zip .

あとは作成されたmy-deployment-package.zipをアップロードすればいいだけ。
ここは他の方法と同じ。
lambdaでzipファイルアップロード
スクリーンショット 2022-05-26 23.11.24.png

テストを実行してエラーが出なければOK

レイヤーを追加する

デプロイパッケージが大きすぎて、インラインコード編集を有効にできません。と出る。

  • パッケージ入れるとインラインコード見えない。
  • 少し確認、変更したい時に不便
  • 他のlambda関数と共有もできるので便利。

これらはレイヤーとして登録すれば解消できます。

  • 先ほどまではlambdaの関数を含めてzipにしてた。関数のコード書いてあるファイルはモジュールの外
  • ディレクトリをpythonで作成して作ることがおすすめ。
    • pythonにするとimportがわかりやすいし、わかりやすい
    • 共通して使えるutilみたいなコードを他のLambda関数でも使いたい場合は共通処理のコードを入れることも可能

手順

基本的にはこれまでのパッケージ作成とあまり変わらないですが

  • lambda_function.pyはzipに含めない
  • pythonという名前のフォルダ作成。(必須でないですが、やりやすくわかりやすい)
  • フォルダの中にモジュールを入れる(
    • pip installのインストール先調整
  • layerをAWSの画面で作成

関数は以下のコード。ythonという名前のフォルダ作成することでfrom PIL import Imageのようにいつものpythonで使うようにimportできるので便利。

lambda_function.py
import json
from PIL import Image

def lambda_handler(event, context):
    print(event)
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

モジュールのzip作成

例えばwhl使った方法ならwhlファイルを配置したディレクトリにて

$ mkdir python
$ pip install \
    --platform manylinux2014_x86_64 \
    --target=python \
    --implementation cp \
    --python 3.9 \
    --only-binary=:all: --upgrade \
    Pillow

pythonディレクトリの中にモジュールがはいっていたらいいです。

pip installの場合

例えばrequestsだったらpip installで簡単にできるので以下のコマンドでOK

$ pip install requests -t ./python

zipファイルにする

あとはこれをzip圧縮してしまう。

$ zip -r9 layer.zip python

ファイル完成。名前はlayer.zipでなくても問題ないです。このzipファイル名はどこにも影響しないので好きなようにつけてOK

レイヤーを追加

先ほど作成したzipをアップロードします。
※ 一応、リージョンでまたいで使用することはできなかったと思うので確認してください。

サイドバーにレイヤーという項目あるので押す → レイヤーの追加 → 名前つけて作成したzipをアップロード
スクリーンショット 2022-06-06 11.54.19.png

スクリーンショット 2022-06-06 11.56.29.png

今回はx86_64を使ってますのでそちらを選択。ランタイムは使用するPython選んでおけばOK

レイヤーをセットする関数にて
スクリーンショット 2022-06-06 12.11.19.png

layersを押して → レイヤーの追加 → カスタムレイヤー
作成したレイヤーを選択して追加。
スクリーンショット 2022-06-06 12.13.26.png

以上です!

最後に

レイヤーの追加について。ECR使ってレイヤーを追加することもできるらしい。試してないけど今度トライしてみたい。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
3
Help us understand the problem. What are the problem?