Docker で AWS Lambda の Python 用 Layer を作成する

:::note warn
この記事の内容は古くて、例えば ARM Mac (Apple Silicon) ではうまくいかなかったりします。とはいえ Docker コンテナをビルドしたり起動したりする際に `platform` オプションを指定すれば出来るとは思いますが、未確認です。
:::

AWS Lambda の Python ランタイムでよく使うモジュールを Lambda Layers に登録しようと思ったが、EC2 インスタンスを立ち上げてパッケージを作るのが面倒なので Docker でできるようにした。

## Lambda Layers とは何か
- ファイルを zip で固めたものをアップロードして「レイヤー」として登録することができる
- Lambda Function にレイヤーを追加すると、ランタイムの `/opt` 以下に固めたファイルが展開されて実行時に利用できるになる
- 言語ごとに指定された名前のディレクトリの中にモジュールを入れておくと、Lambda Function から読み込めるようにパスを通してくれる
    - 例えば Python ランタイムの場合、レイヤー zip 内の `python` という名前のディレクトリに Python モジュールを入れておくと Lambda Function 側でそのモジュールを `import` で読み込むことができる

詳しくは： [AWS Lambda レイヤー - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html)

## Layer を作成するための Docker イメージを作る
Python 3.8 ランタイムは Amazon Linux 2 で動いているらしい [^runtime] ので、本来であれば Amazon Linux 2 の EC2 インスタンスを立ててその中でレイヤーを作るべきである (別に Linux ならなんでもいいかもしれないが、少なくとも macOS で作ったレイヤーは AWS Lambda では動かない [^macos] ) のだが、実は Amazon Linux は Docker Hub で公式イメージが配布されている [^dockerhub] ので、これをベースにして Python 3.8 をインストールした Docker イメージを作ればわざわざ EC2 インスタンスを立てる必要がない。

[^runtime]: [AWS Lambda ランタイム - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html)
[^macos]: [Mac だけど Python で AWS Lambda したい！ - Qiita](https://qiita.com/hoto17296/items/a579c333a8690c96a56c)
[^dockerhub]: [amazonlinux - Docker Hub](https://hub.docker.com/_/amazonlinux)

``` Dockerfile:Dockerfile
FROM amazonlinux:2

ARG PYTHON_VERSION=3.8.7

RUN yum update -y && yum install -y tar gzip make gcc openssl-devel bzip2-devel libffi-devel \
  && curl https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz | tar xz \
  && cd Python-${PYTHON_VERSION} && ./configure && make && make install \
  && cd - && rm -rf Python-${PYTHON_VERSION}

ADD entrypoint.sh /

RUN yum install -y zip \
  && mkdir /python \
  && chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
```

Entrypoint には pip install して zip で固めるスクリプトを登録しておく。

``` sh:entrypoint.sh
#!/bin/bash -eu

SRC=/python
DIST=/dist/layer.zip

pip3 install -t ${SRC} $@
rm -f ${DIST}
zip -q -r ${DIST} ${SRC}
```

イメージをビルドする。

``` console
$ docker build . -t bundle-pip-modules-for-aws-lambda-layers:python3.8
```

イメージ名が長ったらしいが別にここは何でもいい。

## Docker イメージを使って Layer を作成する
例えば Pandas が使える Lambda Layer を作成する場合。

``` console
$ docker run --rm \
    -v $(pwd):/dist \
    bundle-pip-modules-for-aws-lambda-layers:python3.8 \
        pandas
```

成功すればカレントディレクトリに `layer.zip` が作成される。

上の例では直接 `pandas` を指定したが、イメージ名以降の引数は `pip3 install` で指定できる引数を全て渡せるので、例えば requirements.txt をマウントした上で `-r` オプションを指定することで多くのモジュールをまとめて Layer にすることもできる。

``` console
$ echo 'pandas' > requirements.txt
$ docker run --rm \
    -v $(pwd)/requirements.txt:/requirements.txt \
    -v $(pwd):/dist \
    bundle-pip-modules-for-aws-lambda-layers:python3.8 \
        -r requirements.txt
```

## Layer を登録する
作成された `layer.zip` をアップロードする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16369/7da77194-5c1e-58f7-82cb-63d566dc371a.png)

AWSCLI からアップロードする場合はこう。

``` console
$ aws lambda publish-layer-version \
    --layer-name pandas \
    --compatible-runtimes python3.8 \
    --zip-file fileb://layer.zip
```

## Layer を使用する
一番簡単な例として、Pandas の読み込み以外は何もしない Lambda Function を作ってみる。

``` python:lambda_function.py
import pandas

def lambda_handler(event, context):
    pass
```

作った Lambda Function に登録した Layer を追加する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16369/ca8e7fee-1123-34b1-2b64-4b68cd9c96b3.png)

この Lambda Function を実行してみてエラーが出なければ OK 。