LambdaでDockerコンテナイメージ使えるってマジですか？（Python3でやってみる）

# 背景

自然言語処理関係でLambdaを使いたいと思っているけど、どうもライブラリのサイズが大きく、[Lambdaのクオータ](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html)に引っかかる。
ECSでAutoScalingかな？と思っていた所に、re:Invent2020で、Lambdaでコンテナイメージが使えるという発表があったという話を聞く。

早速Developers.IO様で記事になってた。まさに大きいファイルを扱う必要があるML系処理向けらしい！
[【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました！！ #reinvent](https://dev.classmethod.jp/articles/lambda-support-oci-container-image/)

# 前提

今回は、Python3.8のランタイムが対象
Ubuntu18.04マシンで構築

# 流れ確認

流れとしては以下の感じらしい。

- 指定エンドポイントにアクセスすると、LambdaフォーマットでのJsonオブジェクトを返す様なコンテナを作成
- このコンテナ作成際にはベースイメージがあるそうだが、フォーマットを守れば自作でも可能らしい
- ローカルでテストしつつDockerfileを作成
- ECRに登録
- Lambdaの関数作成時に、そのECRのDockerImageのURIを指定する

# 用語

## [Runtime interface clients](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-images.html#runtimes-api-client)

コンテナ内部に存在し、Lambdaとプログラムコードをつなぐ役割をする。このモジュールにhandlerが渡されて処理される形の模様。
デフォルトのAWS Lambdaベースイメージには既に入っているので、自分たちで独自にDockerImageを作る場合には個別対応が必要。

## [Runtime Interface Emulater](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-images.html#runtimes-test-emulator)

ローカル環境でLambdaコンテナを試せるエミュレーター。多分、Lambdaテスト用のエンドポイントを提供するwebサーバーの様なものだと思う。公式DockerImageなら既に入っている模様。

# ローカル開発環境セットアップ

[RIE公式Github](https://github.com/aws/aws-lambda-runtime-interface-emulator) にインストールコマンドが記載されている。
全体概要がよく解らなくて苦戦していたら、[日本語説明ページ](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)があった。これをトレースしてみる。

## 作業用フォルダ作成

```bash
mkdir locallambdatest
cd locallambdatest
```

## aws-lambda-rieをダウンロード

https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
へアクセスするとダウンロードが始まるので、先に作った作業用フォルダ直下に保存。

## Dockerfile作成

公式ページそのままから、aws-lambda-rieのコピー部分、entry.shのモード変更だけ修正

<details><summary>ファイル名：Dockerfile.python3.9test</summary><div>

```dockerfile:Dockerfile.python3.9test

# Define global args
ARG FUNCTION_DIR="/home/app/"
ARG RUNTIME_VERSION="3.9"
ARG DISTRO_VERSION="3.12"

# Stage 1 - bundle base image + runtime
# Grab a fresh copy of the image and install GCC
FROM python:${RUNTIME_VERSION}-alpine${DISTRO_VERSION} AS python-alpine
# Install GCC (Alpine uses musl but we compile and link dependencies with GCC)
RUN apk add --no-cache \
    libstdc++

# Stage 2 - build function and dependencies
FROM python-alpine AS build-image
# Install aws-lambda-cpp build dependencies
RUN apk add --no-cache \
    build-base \
    libtool \
    autoconf \
    automake \
    libexecinfo-dev \
    make \
    cmake \
    libcurl
# Include global args in this stage of the build
ARG FUNCTION_DIR
ARG RUNTIME_VERSION
# Create function directory
RUN mkdir -p ${FUNCTION_DIR}
# Copy handler function
COPY app/* ${FUNCTION_DIR}
# Optional – Install the function's dependencies
# RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR}
# Install Lambda Runtime Interface Client for Python
RUN python${RUNTIME_VERSION} -m pip install awslambdaric --target ${FUNCTION_DIR}

# Stage 3 - final runtime image
# Grab a fresh copy of the Python image
FROM python-alpine
# Include global arg in this stage of the build
ARG FUNCTION_DIR
# Set working directory to function root directory
WORKDIR ${FUNCTION_DIR}
# Copy in the built dependencies
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
# (Optional) Add Lambda Runtime Interface Emulator and use a script in the ENTRYPOINT for simpler local runs
# COPY https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
COPY aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod 755 /usr/bin/aws-lambda-rie
COPY entry.sh /
RUN chmod 755 /entry.sh
ENTRYPOINT [ "/entry.sh" ]
CMD [ "app.handler" ]
```
</div></details>

## アプリソース（app.py）を作成して、appフォルダ以下へ配置

```bash

mkdir app
vim app/app.py
```

```python:app.py

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

## entry.sh 作成

日本語ページだと$1が消えているので注意。その部分を修正。

```bash:entry.sh

#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
    exec /usr/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $1
else
    exec /usr/local/bin/python -m awslambdaric $1
fi
```

## DockerImageをビルド

ここではイメージ名を「python3.9test:local」とする。

```bash
sudo docker build -t python3.9test:local -f Dockerfile.python3.9test .
```

## コンテナを起動する

ここではコンテナ名を「locallambdatest」とする。

```bash
sudo docker run -p 9000:8080 --name locallambdatest python3.9test:local
```

## コンテナへアクセスしてみる

```bash

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
"Hello from AWS Lambda using Python3.9.0 (default, Nov 25 2020, 02:36:55) \n[GCC 9.3.0]! test"
```

成功！

# 自然言語処理ライブラリをインストールしてみる

## Dockerfile.python3.9test の修正

モジュールインストール部分コメントイン＋requirements.txtの配置

```dockerfile:Dockerfile.python3.9test

COPY requirements.txt requirements.txt
RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR}
# RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR}
```

## requirements.txtを作成

作業用フォルダ直下に以下の内容で作成。ginzaを入れておく。

```text:requirements.txt
ginza==4.0.5
```

## app.py の修正

ginzaライブラリで形態素解析をして、その中身をそのまま返す。

```python
import sys
import json
import spacy
import logging
from ginza import *

logger = logging.getLogger()

def handler(event, context):
    logger.info(context)
    target_text = event['text']
    nlp = spacy.load('ja_ginza')

    doc = nlp(target_text)
    morpheme_list = []
    for sent_idx, sent in enumerate(doc.sents):
        for token_idx, tk in enumerate(sent):
            wk_morpheme = {}
            wk_morpheme['text'] = tk.text
            wk_morpheme['dep'] = tk.dep_
            wk_morpheme['pos'] = tk.pos_
            wk_morpheme['tag'] = tk.tag_
            morpheme_list.append(wk_morpheme)
    return morpheme_list
```

## 実行

```bash
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"text":"テストしてみる"}'

[{"text": "\u30c6\u30b9\u30c8", "dep": "ROOT", "pos": "VERB", "tag": "\u540d\u8a5e-\u666e\u901a\u540d\u8a5e-\u30b5\u5909\u53ef\u80fd"}, {"text": "\u3057", "dep": "advcl", "pos": "AUX", "tag": "\u52d5\u8a5e-\u975e\u81ea\u7acb\u53ef\u80fd"}, {"text": "\u3066", "dep": "mark", "pos": "SCONJ", "tag": "\u52a9\u8a5e-\u63a5\u7d9a\u52a9\u8a5e"}, {"text": "\u307f\u308b", "dep": "aux", "pos": "AUX", "tag": "\u52d5\u8a5e-\u975e\u81ea\u7acb\u53ef\u80fd"}]
```

出力が文字コードの文字列になっちゃいましたが、形態素解析出来ている模様。

# ECRへコンテナイメージを登録する

ここではレポジトリ名を「lambdacontainer-test」とする。123412341234はもちろんダミーです。実際にはECRリポジトリを作成するAWSアカウントIDEです。

```bash

aws ecr create-repository --repository-name lambdacontainer-test --image-scanning-configuration scanOnPush=true
sudo docker tag python3.9test:local 123412341234.dkr.ecr.ap-northeast-1.amazonaws.com/lambdacontainer-test:latest
aws ecr get-login-password | sudo docker login --username AWS --password-stdin 123412341234.dkr.ecr.ap-northeast-1.amazonaws.com
sudo docker push 123412341234.dkr.ecr.ap-northeast-1.amazonaws.com/lambdacontainer-test:latest
```

docker login 実行時、「Error saving credentials: error storing credentials」なんてエラーが出てきたら以下実行

```
sudo apt install gnupg2 pass
```

ここまでで、AWSコンソールのECRリポジトリリストに表示されているはず。

# Lambda関数をコンテナを使って作成する。

- AWSコンソールからLambdaのページへ行き「関数の作成」
- オプションで「コンテナイメージ」を選択
- 関数名は適当に。今回は「my-lambda-container-test」
- コンテナイメージURIは「画像を選択」（多分 Select Image の日本語訳）ボタンを押してリポジトリ(lambdacontainer-test)とタグ（latest）を選択
- 「関数の作成」を押してしばらくすると「関数の作成が正常に終了しました」のポップアップが出てくる。

# テストする

- 「テスト」ボタンを押す
- イベント名は適当に
- ```{"text":"テストしてみる"}``` をテスト用Bodyに指定
- 「作成」を押す

## 設定変更

- 「基本設定」エリアの「編集」ボタンを押す
- メモリを1GB、実行時間制限を５分ぐらいにする

## テスト実行

なんかエラー出た。ginzaが使用しているsudachiライブラリがsymlinkを実行しようとして失敗している模様。
このエラー、ローカルテスト時点で出るようにして欲しかった・・・・（公式イメージだとそうなったりするのかな？）
回避するためにはまたライブラリの設定とかが必要になってきそう（そもそもそれが可能なのかも）。
※Lambda上では、書き込み系ファイル操作は /tmp/フォルダ以下で行う必要がある。

```console
{
  "errorMessage": "[Errno 30] Read-only file system: '/home/app/sudachidict_core' -> '/home/app/sudachidict'",
  "errorType": "OSError",
  "stackTrace": [
    "  File \"/home/app/app.py\", line 12, in handler\n    nlp = spacy.load('ja_ginza')\n",
    "  File \"/home/app/spacy/__init__.py\", line 30, in load\n    return util.load_model(name, **overrides)\n",
    "  File \"/home/app/spacy/util.py\", line 170, in load_model\n    return load_model_from_package(name, **overrides)\n",
    "  File \"/home/app/spacy/util.py\", line 191, in load_model_from_package\n    return cls.load(**overrides)\n",
    "  File \"/home/app/ja_ginza/__init__.py\", line 12, in load\n    return load_model_from_init_py(__file__, **overrides)\n",
    "  File \"/home/app/spacy/util.py\", line 239, in load_model_from_init_py\n    return load_model_from_path(data_path, meta, **overrides)\n",
    "  File \"/home/app/spacy/util.py\", line 203, in load_model_from_path\n    nlp = cls(meta=meta, **overrides)\n",
    "  File \"/home/app/spacy/language.py\", line 186, in __init__\n    make_doc = factory(self, **meta.get(\"tokenizer\", {}))\n",
    "  File \"/home/app/spacy/lang/ja/__init__.py\", line 274, in create_tokenizer\n    return JapaneseTokenizer(cls, nlp, config)\n",
    "  File \"/home/app/spacy/lang/ja/__init__.py\", line 139, in __init__\n    self.tokenizer = try_sudachi_import(self.split_mode)\n",
    "  File \"/home/app/spacy/lang/ja/__init__.py\", line 38, in try_sudachi_import\n    tok = dictionary.Dictionary().create(\n",
    "  File \"/home/app/sudachipy/dictionary.py\", line 37, in __init__\n    self._read_system_dictionary(config.settings.system_dict_path())\n",
    "  File \"/home/app/sudachipy/config.py\", line 107, in system_dict_path\n    dict_path = create_default_link_for_sudachidict_core(output=f)\n",
    "  File \"/home/app/sudachipy/config.py\", line 72, in create_default_link_for_sudachidict_core\n    dict_path = set_default_dict_package('sudachidict_core', output=output)\n",
    "  File \"/home/app/sudachipy/config.py\", line 48, in set_default_dict_package\n    dst_path.symlink_to(src_path)\n",
    "  File \"/usr/local/lib/python3.9/pathlib.py\", line 1398, in symlink_to\n    self._accessor.symlink(target, self, target_is_directory)\n",
    "  File \"/usr/local/lib/python3.9/pathlib.py\", line 445, in symlink\n    return os.symlink(a, b)\n"
  ]
}
```

基本目的は達成したのと、この問題の解消はまた別問題になるので、今回はここまでに。

# 感想

今回は最後できれいな結果は出てこなかったけど、Lambdaをコンテナ指定で使うという部分は出来た。
所要時間は「2485.68 ms（連続実行時は数ms）」。
処理の最初でエラーが出ているのでほぼオーバーヘッドとみなしていいかと。warmup戦略を取れば低レイテンシーが求められる用途にも使えそう。
ただ、使用しているライブラリが書き込み系ファイル操作を行うかは十分にチェックする必要あり（コンテナ使用Lambdaでなくても）。

# 参考にさせていただいたページ

正直まだ公表されたばかりのサービスで、公式ドキュメントも説明不足を感じました。ただ、今後改善されていくと思いますし、有志の方が情報整理を行ってくれたりもすると思います。

[AWS公式基本DockerImage](https://docs.aws.amazon.com/lambda/latest/dg/python-image.html#python-image-clients)
[RIE公式Github](https://github.com/aws/aws-lambda-runtime-interface-emulator)

[【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました！！ #reinvent](https://dev.classmethod.jp/articles/lambda-support-oci-container-image/)
[CDKでAWS Lambdaのパッケージフォーマットにコンテナイメージを指定してデプロイしてみた](https://dev.classmethod.jp/articles/shuntaka-container-image-support-lambda-cdk/)