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

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

![amazon_awslambda_logo_icon_167887.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/f2d6c424-010e-6845-9656-63ee0a4e6716.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初心者の私。
調べてみると方法が何個かあって、落とし穴的なものもあったので外部モジュールを追加する方法をまとめました。

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



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

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

実行環境はMac。Intelです。

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

https://qiita.com/Bashi50/items/d5bc47eeb9668304aaa2#7-aws-ec2%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E4%BD%9C%E6%A5%AD


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

:::note
少なくても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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/41a98126-6443-e714-734a-fad168510035.png)

![スクリーンショット 2022-05-26 23.11.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/0ed463c7-a9f1-3c55-7bf0-1ae638d846a8.png)

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

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

なんだこれは、、、

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

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python.html

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

:::note
試しに`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`の方法を解説して、もちろん３の簡単な方法も解説します。

# 2. Dockerにてamazonlinux2のコンテナにてモジュール作る
ということでdocker-hubから使えそうなのを探す。`aws-lambda-python`とかいうちょうど良さそうなものを発見したので使ってみる。

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

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

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

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

```
$ mkdir pillow_lambda
$ touch ./pillow_lambda/Dockerfile ./pillow_lambda/docker-compose.yml
```

```Dockerfile
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/
```

:::note
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ファイルアップロード](#lambdaでzipファイルアップロード)
![スクリーンショット 2022-05-26 23.11.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/0ed463c7-a9f1-3c55-7bf0-1ae638d846a8.png)

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

:::note info
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のページに。

https://pypi.org/project/Pillow/#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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/ad094ea9-aa98-5f16-0ac1-98e4af8d7bec.png)

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

:::note warn
バージョン 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ファイルアップロード](#lambdaでzipファイルアップロード)
![スクリーンショット 2022-05-26 23.11.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/0ed463c7-a9f1-3c55-7bf0-1ae638d846a8.png)

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

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

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

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

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


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

- `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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/0232c65b-d1c6-bc63-06c8-74fed55814a2.png)

![スクリーンショット 2022-06-06 11.56.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/e4cf5a0e-0fac-b97d-5a25-5dd61b0d3f53.png)

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

レイヤーをセットする関数にて
![スクリーンショット 2022-06-06 12.11.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/b3f2c1da-37bc-99a9-f554-e1865e1fe389.png)

layersを押して → レイヤーの追加 → カスタムレイヤー
作成したレイヤーを選択して追加。
![スクリーンショット 2022-06-06 12.13.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260345/606eee2f-f343-0148-151b-2fe1c3c3d4be.png)


以上です！

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