コンテナイメージを使用してAWS Lambda関数を作成する

この記事は [ユニークビジョン株式会社 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/uniquevision) の7日目の記事です。

# 1. はじめに

この記事ではAWS re:Invent 2020で発表された [AWS Lambda の新機能 – コンテナイメージのサポート](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/) についてまとめます。

従来、AWS Lambdaの関数を作成するためには関数の実行に必要なものをZIP形式でパッケージ化する必要がありました。
個人的にはローカルで動作確認をする構成とZIP形式のパッケージを作る手順が存在しているのは好みではありませんでしたが、この新機能によって開発時・本番運用時の両方とも同じコンテナイメージを使用することが出来るようになりました。

また、従来のZIP形式ではデプロイパッケージのサイズが250MBに制限されており、例えばTensorFlowなどを使用した推論処理のためにAWS Lambdaを使用するのは困難でした。それがこの新機能では最大10GBのコンテナイメージに対応するということでAWS Lambdaの用途が広がったと感じています。

そこで、本記事では

1. 最小限のコンテナイメージを使用した関数
2. TensorFlowを使用して画像分類を行う関数

を作成しながら利用方法を確認します。


# 2. Hello, AWS Lambda with Conrtainer

まずは最小限の構成で動くLambda関数を作成します。
この章では

1. 作成する関数を構成する要素の紹介
2. ローカルでの動作確認
3. デプロイ

について取り扱います。

## 2.1. 構成

ディレクトリ構成は以下の通りです。

```
（プロジェクトルートディレクトリ）/
├── Dockerfile
└── app.py
```

この章で作成する関数はDockerfileが肝になります。
「[Create an image from an AWS base image for Lambda](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html#images-create-1)」の手順に従って、Lambda用のベースイメージに `app.py` をコピーするだけの簡単な内容になっています。

```Dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.8

COPY app.py ${LAMBDA_TASK_ROOT}

CMD [ "app.handler" ]
```

`app.py` が実際に実行される関数になります。
今回は冒頭の[AWS Lambda の新機能 – コンテナイメージのサポート](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)に登場する、Pythonのバージョンを返すだけの処理としています。

```python:app.py
import sys
def handler(event, context): 
    return 'Hello from AWS Lambda using Python' + sys.version + '!'
```


## 2.2. ローカルでの動作確認

ローカルでの動作確認は上で作成したDockerfileをビルドして実行するだけで良いです。
プロジェクトのルートディレクトリにて

```bash
$ docker build -t {イメージ名} .
```

を実行してイメージを作成し、

```bash
$ docker run --rm -p 9000:8080 {イメージ名}
```

で実行します。ポート番号は任意のもので良いです。
この時点で、`/2015-03-31/functions/function/invocations`というパスで関数を実行することが出来るようになっているので

```bash
$ curl -d '{}' http://localhost:9000/2015-03-31/functions/function/invocations
"Hello from AWS Lambda using Python3.8.6 (default, Nov 26 2020, 14:33:53) \n[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)]!"
```

のようにすることで動作確認を行うことが出来ます。
意図した通り、Pythonのバージョンが返っていることが分かります。

また、コンテナのログには以下の通り、本当にLambdaを実行した際のログに近い内容が出力されています。

```
START RequestId: 81e030ca-0657-40e5-a80f-c8eb23f522cd Version: $LATEST
END RequestId: 81e030ca-0657-40e5-a80f-c8eb23f522cd
REPORT RequestId: 81e030ca-0657-40e5-a80f-c8eb23f522cd  Duration: 1.20 ms       Billed Duration: 100 ms    Memory Size: 3008 MB    Max Memory Used: 3008 MB
```


## 2.3. デプロイ

デプロイの大まかな流れは以下の通りです。

1. ECRに先ほど作成したイメージをプッシュ
2. 関数を作成


### 2.3.1 イメージのプッシュ

本記事ではECRへのプッシュは主題ではないので手順を割愛します。以下のリンクの通りに進めれば困ることはないかと思います。
https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/docker-push-ecr-image.html


### 2.3.2. 関数を作成

AWSマネジメントコンソールにて関数を作成すると以下の画面が表示されます。  
関数の作成オプションで「コンテナイメージ」を選択し、関数名とコンテナイメージURIを入力すると関数が出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/e6951e25-a668-4903-6d16-0bce334c52a7.png)

コンテナイメージURIの入力は「画像を参照」ボタンを押下するとこでECRからイメージを選択することが出来ます。  
「Browse images」の日本語訳がバグっていますがこれはご愛敬ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/972e6e22-3bde-592a-cc42-8de6fffc6a34.png)

この手順で作成した関数は無事、動作することが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314315/1926bad7-a7e3-48ce-812f-184fd13d966b.png)


# 3. TensorFlowを使用して画像分類をする関数を作成する

この章では、実用的な関数の例としてS3に画像をアップロードしたことをトリガーに画像分類する関数を作成します。
従来のZIP形式でのデプロイでは「[AWS LambdaでTensorFlow 2.0を使った画像分類](https://tech.unifa-e.com/entry/2019/09/17/085400)」という記事で扱われているように多くのことを考慮して作成する必要がありますが、コンテナイメージを使用すると前章とほぼ同じ手順で関数を作成することが出来ます。

## 3.1. 構成

本章の関数のディレクトリ構成は以下の通りです。

```
├── Dockerfile
└── app
    ├── app.py
    ├── model.h5
    └── requirements.txt
```

まず、画像分類を行うため、TensorFlowの学習済みモデルが追加されています。
今回は以下のプログラムで、学習済みモデルをダウンロードしています。

```python
from tensorflow.keras.applications.mobilenet_v2 import MobileNetV2

model = MobileNetV2(weights='imagenet')
model.save('model.h5', include_optimizer=False)
```

もう一点、TensorFlowなどのパッケージをインストールする必要があるため、 `requirements.txt`を追加しています。
今回はTensorFlowでの推論とS3へのアクセスを行うため、それに必要なものを記述しています。

```text:requirements.txt
boto3
tensorflow
pillow
```

Dockerfileには `pip install`を追加しています。
一応、ビルド時にキャッシュが利用できるように `requirements.txt`だけを先にコピーしています。

```dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.8

COPY app/requirements.txt ${LAMBDA_TASK_ROOT}
RUN pip install -r requirements.txt

COPY app ${LAMBDA_TASK_ROOT}

CMD [ "app.handler" ]
```

app.pyは当然大きく内容が変わっています。処理内容は簡単に、

1. handlerの引数、`event`からアップロードされたファイルの情報を取得
2. S3から上記ファイルをダウンロード
3. 画像を読み込んで推論の前処理を実施
4. 推論

という感じです。
本体は4. の後に結果を保存したりすると思いますが、今回は標準出力に出すだけとしています。

```python:app.py
import boto3
import tensorflow as tf
from tensorflow.keras.applications.mobilenet_v2 import (decode_predictions,
                                                        preprocess_input)
from tensorflow.keras.preprocessing.image import img_to_array, load_img

s3_client = boto3.client('s3')
model = tf.keras.models.load_model('./model.h5', compile=False)


def handler(event, context):
    try:
        for record in event['Records']:
            # アップロードされた画像をダウンロード
            bucket = record['s3']['bucket']['name']
            key = record['s3']['object']['key']
            download_path = '/tmp/target_image'
            s3_client.download_file(bucket, key, download_path)

            # 画像を読み込んで前処理
            img = load_img(download_path, target_size=(224, 224))
            img = img_to_array(img)
            img = img[tf.newaxis, ...]
            img = preprocess_input(img)

            # 推論
            predict = model.predict(img)
            result = decode_predictions(predict, top=5)
            print(result)

        return 'Success'

    except Exception as e:
        print(e)
        return 'Failure'
```


## 3.2. ローカルでの動作確認

この関数はS3へのアクセスが必要となるため、以下の通り環境変数を渡してDockerを起動します。

```bash
docker run --rm \
  -p 9000:8080 \
  -e AWS_ACCESS_KEY_ID={アクセスキー} \
  -e AWS_SECRET_ACCESS_KEY={シークレットキー} \
  -e AWS_DEFAULT_REGION={リージョン} \
  {イメージ名}
```

関数の実行ではS3へのPUTを模したデータを送信します。

```bash
curl -d '{"Records": [{"s3": {"bucket": {"name": "バケット名"}, "object": {"key": "ファイル名"}}}]}' \
http://localhost:9000/2015-03-31/functions/function/invocations
```

## 3.3. デプロイ

この関数のデプロイ方法は本記事の主題からは離れるため割愛しますが、

1. 前章の通りイメージをプッシュ
2. 関数を作成
3. S3の当該バケットへのアクセス権限を設定
4. 関数のトリガーを設定

のようにすることでS3に画像をアップロードすると推論を実施する関数が作成できます。


# 4. まとめ

本記事では2つの関数を作成しながらAWS Lambdaの新機能を確認しました。
個人的には従来通りの使用感で用途が広がっており、ローカルでの動作確認もしやすいので良い機能だと感じています。

今回は手動でデプロイを行いましたが、SAMやCDKなどを使用してデプロイが行えればすぐにでも実用可能な機能かなと思います。
次はデプロイ用のツールも含めて技術検証が行えたらと考えています。