AWS Lambdaで簡単機械学習 = Lambdaコンテナ + serverless framework

:::note info
AWS LambdaとServerless Advent Calendar 2021の12日目の記事です
:::

https://qiita.com/advent-calendar/2021/lambda

## 背景

Pythonでテキストからカテゴリを推定する関数をLambdaで作った際に、mecabの辞書ライブラリやpandas, numpyがでかすぎてデプロイできずに右往左往したのですが、serverless frameworkとLambdaコンテナのおかげで簡単にデプロイができたので、感動をお伝えするためにこの記事を書いていこうと思います。

## やりたかったこと

テキストがどのカテゴリに属するか、tf-idfとコサイン類似度を使って算出するというのが、今回やりたいことになります。
テキストは、S3のPUTされ、PUTイベントをトリガーに関数を走らせるようなイメージです。

以下はカテゴリ推定方法です。

1. テキストをmecabを使ってわかち書き
2. わかち書きされたテキストをtf-idfでベクトル化
    1. ベクタライザはカテゴリに含まれる単語のリスト化したものをつ使って事前計算したものを使う
3. tf-idfベクトルをコサイン類似度を使って最もマッチするカテゴリを算出

## できなかったこと

ライブラリ郡が大すぎて、デプロイできませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120358/b9751f80-a909-81ea-cc04-73da1cf756f8.png)

Lambdaには、250MB以上のパッケージをデプロイすることはできません。
(これは、Lambda Layerも同様です)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html

特に、わかち書きのためにmecabを採用したため、日本語辞書が必要になり、ライブラリがかなり大きくなってしまいました。

これは、機械学習系のライブラリを利用していると、起りやすいことかと思います。

## どう解決したか

### :no_good:  Lambda Layerを使う

最初はLambda Layerを使ってデプロイしようと試みました。
究極、Layerを250MBづつ分けて作成すれば、デプロイすること自体は可能そうではありましたが、Layerの管理がかなり煩雑になる危険性がありました (何をLayerに含めるのか、どこまでLayerに含めるのか)
また、1つの関数に紐付けられるLambda Layerの数には5つまでという制限があり、今回は引っかかりはしませんでしたが、この点も懸念だったので、Lambda Layerを使う方法は断念しました。

### :ok_woman: Lambdaコンテナを使う

ちょうど去年のre:InventでLambdaのパッケージフォーマットにコンテナイメージがサポートされる発表がありました。
https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/

このLambdaコンテナは、LambdaでサポートしていないPHPなどのランタイムが動かせるようになるという点も大きいですが、10GBのコンテナイメージまでであればLambda上で動かすことができる点が今回のやりたかったことに非常にマッチしました。

そのため、今回はLambdaコンテナを使ったデプロイを採用しました。

## どうデプロイしたのか

Lambdaコンテナを使ってデプロイすることは決まったものの、Lambdaにデプロイするツールはいくつか存在していたため、それぞれのツールについて検討しました。

### :no_good:  Terraformを使う

既存のアプリケーションでは、Terraformを使っていたため、Terraformでデプロイすることを第1に検討しました。

しかし、TerraformだとDockerイメージをECRにpushするために新しく`.tf`ファイルを作成する必要があったり、実行順序を考える必要があるなど、他のツールと比較すると初回の構築が面倒そうでした。
そのため、SAMや他の方式を使ったほうが楽そうという結論に至り、断念しました。

### :no_good:  SAMを使う

次点で触り始めたのが、SAMでした。
SAMは以前に利用したことがあったため、まぁイケるだろうと思っていたのですが、思わぬ罠がありました。

今回の要件には「テキストは、S3のPUTされ、PUTイベントをトリガーに関数を走らせる」というものがあります。
ここがCDKでは、引っかかってしまいました。

> S3 バケット名。このバケットは、同じテンプレートに存在する必要があります。
> https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-function-s3.html

というのも、PUTされるS3が既に作成済みのS3ということが決まっており、SAMでは上記にあるように、同じテンプレートで作成したS3しかトリガーに指定できないという仕様が存在していたのです。

まさかの仕様に絶句しながらも、どうしようもないので新たなツールを探すべくAmazonの奥地にむか...(ってないです)。

### :ok_woman: serverless frameworkを使う

ということで、最後にたどり着いたのがserverless frameworkでした。
これまでserverless frameworkを触ってこなかった人間だったのですが、もっと早くに出会いたかったと思うくらい最高のツールでした。

良かった点を列挙します。

#### Lambdaコンテナのデプロイが簡単

TerraformやSAMでも、Docker Imageのデプロイが可能ですが、serverless frameworkもかなり簡単にコンテナイメージをLambdaと一気通貫でデプロイすることが可能です。


なんと、`functions.{関数名}.image`を指定するだけで、`image.name`で指定したディレクトリにあるDockerfileを勝手にビルドしてECRにpushして、そのECRにpushしたイメージを使ってLambdaを作成してくれます。

```yaml
functions:
  Predictor:
    image:
      name: predictor
```

たったこれだけの記述で至れり尽くせり感が半端ないです。

※ おまじないの記述をfunctionsの上に書く必要がありますが、簡単のため省略しています。

#### Triggerの指定が可能

これはSAMでできなかったことですが、serverless frameworkではテンプレートにない既存のバケットもトリガーとして指定することが可能です。

↓のように、`existing: true`を指定することで、新しくS3バケットを作成せずとも、トリガーだけ作成してくれます。

```yaml
functions:
  Predictor:
    image:
      name: predictor
    events:
      - s3:
          bucket: sample-bucket-name
          existing: true
          event: s3:ObjectCreated:*
          rules:
            - suffix: .csv

```

#### 環境変数の管理がSAMより優れている

SAMやCloudFormationでは、環境別の変数を入れ込もうと思うと、かなり複雑な記法を使う必要がありました。

```yaml
Parameters:
    Env:
        AllowedValues: ["dev", "stg", "prd"]
        Type: String

Conditions:
    IsProduction: { "Fn::Equals" : [ { "Ref" : "Env" }, "prd" ] }

Resources:
    Instance:
        Type: AWS::EC2::Instance
        IamInstanceProfile:
            Fn:If:
                - IsProduction
                - iamprofile
                - !Ref AWS::NoValue
```

これが、serverless fremeworkを使うと簡単な記法で記述することができ、デプロイ時にパラメータを一つ指定するだけで、簡単に環境を切り替えることが可能となります。

```yaml
custom:
  defaultStage: stg
  bucketName:
    stg: stg-bucket-name
    prd: prd-bucket-name

functions:
  Predictor:
    image:
      name: predictor
    environment:
      S3_BUCKET_NAME: ${self:custom.bucketName.${self:provider.stage}}
```

```bash
sls deploy --stage prd
```

## まとめ

機械学習をLambda上で実行させるために、色々と試行錯誤してきた過程を書いてきました。

機械学習のような巨大なライブラリを使うような関数ではLambdaコンテナを使うのが簡単でした。
また、Lambdaコンテナのデプロイに関してもSAMを使うよりserverless frameworkを使ったほうが圧倒的に簡単に書くことができました。

結論: AWS Lambdaで簡単機械学習 = Lambdaコンテナ + serverless framework

ということでした〜〜〜 :hotsprings: