AWS Lambdaがコンテナイメージ対応したらしいのでLambda上でJUMAN++を動かしてみた


無計画なAdvent Calendar駆動開発は時間が足らなくてつらい。natsuumeです。

この記事は[Opt Technologies AdventCalendar](https://qiita.com/advent-calendar/2020/opt-technologies)の12日目の記事です。
11日は[@shoyaokayama](https://qiita.com/shoyaokayama)さんで「[LINEThingsとM5CoreInkを使ってO2Oマーケティングを体験してみる](https://qiita.com/shoyaokayama/items/ba65c1d024255a40ecb6)」でした。

# はじめに
AWS Re:InventにてAWS Lambdaがコンテナイメージサポートという発表がありました。
しかも最大10GBまでのコンテナイメージが可能だそうです。

となればとりあえず利用方法として下記のような例が思いつきます。

* AWS Lambda上で形態素解析器を動かす
* AWS Lambda上で深層学習の推論を動かす

今回は深層学習の推論エンドポイントとして使う方は時間の都合上試していませんが、こちらも後でやってみたいところです。  
趣味で深層学習試しても推論エンドポイントずっと立てて公開するのは金銭的にちょっと……という感じだったので、多少実行時間かかるとしてもサーバレスで推論エンドポイント立てられるのは夢がありますね。

**深層学習モデルをLambdaに乗せる参考例**  

* [AWS Lambdaがコンテナイメージをサポートしたので、Detectron2 を使って画像認識(Object Detection)を行うAPI を作る](https://qiita.com/gorogoroyasu/items/2039273e7c04365f3da8)
* [AWS Lambda + Docker + TensorFlowを使ってサクッと推論APIをつくる](https://tech.aptpod.co.jp/entry/2020/12/11/100000)

また、Lambda上で形態素解析器等を手軽に動かせるようになれば、S3に溜まっていくテキストデータを加工・整形して蓄積するというフローも楽に作れるようになりそうです。

というわけで、今回はAWS Lambda上で形態素解析器（今回はJUMAN++）を動かすのを試していきます。

なお先にネタバレすると当初はKNPも入れる予定でしたが、KNPを入れたらdocker imageのサイズが10GBを超過したため諦めました。

# 全体構成
今回は最小構成でLambdaにテキストを投げたら解析結果をそのまま返すLambdaを作ります。

使用した全コードはgithubに下記公開してあります。
https://github.com/Natsuume/lambda-container-sample

## cdk

```cdk.ts
import { DockerImageCode, DockerImageFunction } from "@aws-cdk/aws-lambda";
import * as cdk from "@aws-cdk/core";
import { Duration } from "@aws-cdk/core";

export class LambdaContainerSampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const lambda = new DockerImageFunction(this, "jumanpp-lambda", {
      code: DockerImageCode.fromImageAsset("./docker", {
        cmd: ["/var/task/jumanpp-lambda.jumanppHandler"],
        entrypoint: ["/lambda-entrypoint.sh"],
      }),
      timeout: Duration.seconds(30),
    });
  }
}
```

DockerImageFunctionはイメージのpush先がaws-cdk/assets固定ですが、このような記述だけでローカルのdockerfileをpushしてlambda上で動かせます。
簡単。

また、後述しますがタイムアウトは長めに取っておきます。

## Dockerfile
詳しくは知らないのですがAmazon Linux2はCentOS系らしいです。
つまり`apt`が使えない。つらい。

`yum`でinstallできる`cmake`のバージョンは2.x系ですがJUMAN++のインストールには3.0以上の`cmake`が必要だったり、という関係で色々インストールしたりしています。

ubuntu系ならもっと簡単にinstallできた記憶があるのでそっちベースで書いたほうが楽という可能性も無きにしもあらずですが、その辺は詳しくないので今回は公式で提供されている`public.ecr.aws/lambda/nodejs:12`を使っています。

```Dockerfile
FROM public.ecr.aws/lambda/nodejs:12

ENV JUMAN_VERSION 2.0.0-rc3
# ENV KNP_VERSION knp-4.20

RUN yum update -y
RUN yum upgrade -y
RUN yum -y groupinstall "Development Tools"
RUN yum install -y wget

# wget cmake
RUN wget https://cmake.org/files/v3.18/cmake-3.18.0.tar.gz
RUN tar -xvzf cmake-3.18.0.tar.gz
RUN rm cmake-3.18.0.tar.gz

# wget jumanpp
RUN wget https://github.com/ku-nlp/jumanpp/releases/download/v${JUMAN_VERSION}/jumanpp-${JUMAN_VERSION}.tar.xz
RUN tar xvf jumanpp-${JUMAN_VERSION}.tar.xz
RUN rm jumanpp-${JUMAN_VERSION}.tar.xz

# wget knp
# RUN wget http://nlp.ist.i.kyoto-u.ac.jp/nl-resource/knp/${KNP_VERSION}.tar.bz2
# RUN tar jxvf ${KNP_VERSION}.tar.bz2
# RUN rm ${KNP_VERSION}.tar.bz2

# install cmake
RUN yum -y install openssl-devel
RUN cd cmake-3.18.0 && \
    ./bootstrap && \
    make && \
    make install

# install juman++v2
RUN cd jumanpp-${JUMAN_VERSION} && \
    mkdir build && \
    cd build && \
    cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local && \
    make && \
    make install

# install knp
# RUN cd ${KNP_VERSION} && \
#     ./configure && \
#     make && \
#     make install

# clean
RUN cd cmake-3.18.0 && \
    make uninstall
RUN rm -rf cmake-3.18.0
RUN yum -y groupremove "Development Tools"
RUN yum remove -y wget

COPY ./jumanpp-lambda.js /var/task
CMD ["/var/task/jumanpp-lambda.jumanppHandler"]
ENTRYPOINT ["/lambda-entrypoint.sh"]

EXPOSE 8080

```

## lambda
```lambda.ts
import * as childProcess from "child_process";
import * as util from "util";

exports.jumanppHandler = async (event: { content: string }) => {
  const out = await util
    .promisify(childProcess.exec)(`echo "${event.content}" | jumanpp`)
    .then((result) => result.stdout)
    .then((output) => output.split("\n"))
    .then((output) => output.filter((s) => s.length > 0));

  return {
    result: out,
  };
};
```

今回はLambda上でJUMAN++が動くかの確認がメインのため非常に簡素なコードで済ませています。
実際に運用する際には、

* execを使用しているので危険な入力が与えられる可能性があるのであればその対策
* JUMAN++の解析が失敗するような入力を正規化する（参考:[稀によくあるKNPのはまりどころ](https://qiita.com/natsuume/items/5ccdb86f1663bba26a96)）

などの入力に対する多少の変形が必要です。

# 結果
無事に入力文字列に対して解析結果を返すLambdaが完成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34789/51656e15-de2c-8e8e-fd6e-61eff5a61e6d.png)

前述のタイムアウトと関連しますが、実行時間は初回のみ多少時間がかかります。

| 実行回数 | 実行時間(ms) |
| --- | --- |
|1回目|11547|
| 2回目 |395|

2回目以降はよろしくやってくれるのか、問題ない速度で動きます。

# ハマったポイント
## entrypoint
cdkのDockerImageFunctionの設定でかなり時間を費やしました。
`DockerImageCode.fromImageAsset`が引数にとるAssetImageCodePropsのentrypointの挙動が個人的にはかなりわかりにくかったです。

AssetImageCodePropsは下記のプロパティを持ちます。

```ts
buildArgs?: { [string]: string },
cmd?: string[],
entrypoint?: string[],
file?: string,
target?: string,
```

このentrypointについて、公式リファレンスでは
> Specify or override the ENTRYPOINT on the specified Docker image or Dockerfile.

と記載されています。
(参考: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.AssetImageCodeProps.html)

entrypointがoptionalだったためDockerfile側でENTRYPOINTを指定すれば問題ないと思ったのですが、どうもcdk側でもentrypointを指定しないとdeploy時にentrypointが空になるようでした。

# Dockerfileの置き場
`DockerImageCode.fromImageAsset`でDockerfileをプロジェクト直下においてディレクトリを`./`に指定するとcdkコマンドで死にます。
（参考：https://github.com/aws/aws-cdk/issues/3899）

```ダメなパターン.ts
    const lambda = new DockerImageFunction(this, "jumanpp-lambda", {
      code: DockerImageCode.fromImageAsset("./", { // "./"指定は死ぬ
        cmd: ["/var/task/jumanpp-lambda.jumanppHandler"],
        entrypoint: ["/lambda-entrypoint.sh"],
      }),
      timeout: Duration.seconds(30),
    });
```

今回はdockerfile用にディレクトリを掘って回避しました。

# おわりに
Lambdaでコンテナイメージが動かせるようになって、容量に限りはあるもののこのように実行ファイルもLambdaで手軽に使えるようになりました。
これによって手軽にサーバレスでできることがぐっと広がったのではないかと思います。

以上です。

なお、アドベントカレンダーの13日も私予定ですが、間違いなく遅刻します。

# 参考文献
* [CDKでAWS Lambdaのパッケージフォーマットにコンテナイメージを指定してデプロイしてみた](https://dev.classmethod.jp/articles/shuntaka-container-image-support-lambda-cdk/)
* [AWS LambdaのコンテナサポートをJavaで試してみた。](https://qiita.com/keni_w/items/112114eff2ccffed94fb)
* [爆速？！コンテナイメージからデプロイしたLambdaのコールドスタートについて検証してみた #reinvent](https://dev.classmethod.jp/articles/measure-container-image-lambda-coldstart/)