無計画なAdvent Calendar駆動開発は時間が足らなくてつらい。natsuumeです。
この記事はOpt Technologies AdventCalendarの12日目の記事です。
11日は@shoyaokayamaさんで「LINEThingsとM5CoreInkを使ってO2Oマーケティングを体験してみる」でした。
はじめに
AWS Re:InventにてAWS Lambdaがコンテナイメージサポートという発表がありました。
しかも最大10GBまでのコンテナイメージが可能だそうです。
となればとりあえず利用方法として下記のような例が思いつきます。
- AWS Lambda上で形態素解析器を動かす
- AWS Lambda上で深層学習の推論を動かす
今回は深層学習の推論エンドポイントとして使う方は時間の都合上試していませんが、こちらも後でやってみたいところです。
趣味で深層学習試しても推論エンドポイントずっと立てて公開するのは金銭的にちょっと……という感じだったので、多少実行時間かかるとしてもサーバレスで推論エンドポイント立てられるのは夢がありますね。
深層学習モデルをLambdaに乗せる参考例
- AWS Lambdaがコンテナイメージをサポートしたので、Detectron2 を使って画像認識(Object Detection)を行うAPI を作る
- AWS Lambda + Docker + TensorFlowを使ってサクッと推論APIをつくる
また、Lambda上で形態素解析器等を手軽に動かせるようになれば、S3に溜まっていくテキストデータを加工・整形して蓄積するというフローも楽に作れるようになりそうです。
というわけで、今回はAWS Lambda上で形態素解析器(今回はJUMAN++)を動かすのを試していきます。
なお先にネタバレすると当初はKNPも入れる予定でしたが、KNPを入れたらdocker imageのサイズが10GBを超過したため諦めました。
全体構成
今回は最小構成でLambdaにテキストを投げたら解析結果をそのまま返すLambdaを作ります。
使用した全コードはgithubに下記公開してあります。
https://github.com/Natsuume/lambda-container-sample
cdk
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
を使っています。
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
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のはまりどころ)
などの入力に対する多少の変形が必要です。
結果
無事に入力文字列に対して解析結果を返すLambdaが完成しました。
前述のタイムアウトと関連しますが、実行時間は初回のみ多少時間がかかります。
実行回数 | 実行時間(ms) |
---|---|
1回目 | 11547 |
2回目 | 395 |
2回目以降はよろしくやってくれるのか、問題ない速度で動きます。
ハマったポイント
entrypoint
cdkのDockerImageFunctionの設定でかなり時間を費やしました。
DockerImageCode.fromImageAsset
が引数にとるAssetImageCodePropsのentrypointの挙動が個人的にはかなりわかりにくかったです。
AssetImageCodePropsは下記のプロパティを持ちます。
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)
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日も私予定ですが、間違いなく遅刻します。