Lambdaのpython3.7ランタイムで外部モジュール使おうとしたらハマった話

# 【この記事の目的】
AWS Lambdaで外部モジュールを利用したpython3.7のファイルを実行しようするとエラーがでました。
で、そのエラーに超絶ハマったので同じ被害者を産まないための備忘録として書きます。

# 【対象者となる人】
- Lambdaで外部モジュールを利用したpythonを実行しようとしてもうまく行かない人
- `[ERROR] Runtime.ImportModuleError: Unable to import module 'xxx': cannot import name '_imaging' from 'PIL' (/var/task/PIL/__init__.py)` にハマってる人
- `Unable to import module 'xxx': cannot import name '_imaging'` にハマってる人
- ↑２つのログで対象となっているモジュールだけ違ってるような人
- ちなみに私はpython初心者です

# 【結論：なぜ失敗するのか】
端的にまとめると、利用しようとしている外部モジュールがLambdaで実行する為の形式(amazonlinux2用)になっていなかったことが原因だった模様。

# 【やろうとしてたこと / 実行ファイル】
背景として「S3にpdfがアップロードされたらjpgに変換」という処理をやりたかったのですが、「python初心者 + Lambda x pythonも初めて」というひよこクラブ入会したて状態だったのでまずは↓にあるようなsampleを試して見ようと思い、大分簡略化したファイルでテストしてみました。

しかし、Lambdaに登録して実行したら見事にエラーが出てしまい、そこで足踏みをする事態になっていました。

[試してた公式資料]
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example-deployment-pkg.html#with-s3-example-deployment-pkg-python

[簡略化したファイル]

```python:test.py
# coding: utf-8
import boto3
from PIL import Image

import PIL.Image

s3_client = boto3.client('s3')

def handler(event, context):
    print('[MAIN] python:test function run ======================')
    # ここにメイン処理をかく
    print('[MAIN] python:test function done ======================')
    return event
```

```tex:requirements.txt
boto3
Pillow
gm
```

[エラー1]

```
[ERROR] Runtime.ImportModuleError: Unable to import module 'xxx': cannot import name '_imaging' from 'PIL' (/var/task/PIL/__init__.py)
```

[エラー2]

```
Unable to import module 'xxx': cannot import name '_imaging'
```


# 【参考にした資料】
http://www.perrygeo.com/running-python-with-compiled-code-on-aws-lambda.html


# 【解消できた手順】
原因はこの記事上部にある結論を見ていただければわかりますので、こちらでは解消できた手順をさっくりとまとめていきます。

## □　まずは実行ディレクトリの構成

```
/
├── requirements.txt  // pip3 installする外部モジュールが書いてあるファイル
└── test.py           // 実行するpythonファイル（内容は前項に書いてあるとおり）
```

このままLambdaに渡しても怒られちゃうので、外部モジュールを準備していきます。

## □　実行する環境と同じ状況を用意する
Lambda:Python３.7ランタイムのOSはAmazonLinuxで実行されます。([参考](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html))
なので、AMIをAmazonLinuxにしたEC2を用意して実行してもいいのですが、わざわざそのためにEC2立ち上げるのもコストがかかるのでDockerで代用します。

まずは↓の2つのファイルを用意します。

```bash
$ tree
.
├── Dockerfile
├── docker-compose.yml
├── test.py
└── requirements.txt
```

```dockerfile:Dockerfile
FROM amazonlinux:latest

RUN yum update -y
RUN yum install python3 -y 
RUN pip3 install virtualenv
```

```yaml:docker-compose.yml
version: '2'
services:
  app:
    build: .
    volumes:
      - '.:/var/task'
    working_dir: /var/task
    command: >
     bash -c 'virtualenv env &&
     source env/bin/activate &&
     pip3 install -r requirements.txt -t .'
```

## □ あとは実行するだけで準備OK
用意ができたらまずはbuildしてupします。

```bash

$ docker-compose build
Building app
Step 1/4 : FROM amazonlinux:latest
 ---> b94321659aca
Step 2/4 : RUN yum update -y
 ---> Using cache
 ---> a3a4818e5f14
Step 3/4 : RUN yum install python3 -y
 ---> Using cache
 ---> edf9db770858
Step 4/4 : RUN pip3 install virtualenv
 ---> Using cache
 ---> 3d06a508a1b1
Successfully built 3d06a508a1b1
Successfully tagged test_app:latest


$ docker-compose up
Recreating test_app_1 ... done
Attaching to test_app_1
app_1  | Using base prefix '/usr'
app_1  |   No LICENSE.txt / LICENSE found in source
app_1  | New python executable in /var/task/env/bin/python3
app_1  | Also creating executable in /var/task/env/bin/python
app_1  | Installing setuptools, pip, wheel...
app_1  | done.

(中略)

app_1  | WARNING: Target directory /var/task/__pycache__ already exists. Specify --upgrade to force replacement.
test_app_1 exited with code 0
```

## □ テストしてみる

本当にエラーが解消したかどうかをローカルPCで確認してみます。
**※ 実行するときはdocker-compose upをしたのと同じ階層でやること**

```bash
$ docker run -v "$PWD":/var/task lambci/lambda:python3.7 test.handler '{"Id":"1"}'
START RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72 Version: $LATEST
[MAIN] test function run ======================
[MAIN] test function done ======================
{"Id":"1"}
END RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72
REPORT RequestId: 52fdfc07-2182-154f-163f-5f0f9a621d72  Duration: 8.90 ms       Billed Duration: 100 ms Memory Size: 1536 MB      Max Memory Used: 38 MB
```
→無事成功していることが確認できたました！ :tada: 

## Lambdaにアップロードするには...

外部モジュールを利用する場合はzipにする必要があるので、一工夫加えてzipファイルができるようにする。

```dockerfile:Dockerfile
FROM amazonlinux:latest

RUN yum update -y
RUN yum install python3 -y 
RUN pip3 install virtualenv
RUN yum install zip -y
```

```yaml:docker-compose.yml
version: '2'
services:
  app:
    build: .
    volumes:
      - '.:/var/task'
    working_dir: /var/task
    command: >
     bash -c 'virtualenv env &&
     source env/bin/activate &&
     pip3 install -r requirements.txt -t . &&
     zip -9 bundle.zip test.py &&
     zip -r9 /var/task/bundle.zip *'
```

↑に直したあとに再度 `docker-compose up` すると↓のようにzipファイルができます。

```bash

$ ls -ltrh bundle.zip 
-rw-r--r--  1 xxx  xxx Users    73M  9 19 17:48 bundle.zip
```
これをLambdaにアップロードすれば無事実行できます！

# 【まとめ】

Lambdaで外部モジュールを利用する時は同じ環境で開発したものをzip化するように気をつけましょう！