Docker コンテナ上に Bumblebee による画像分類 REST API を構築する

## はじめに

前回ローカル環境に Phoenix と Bumblebee による AI REST API を実装しました

https://qiita.com/RyoWakabayashi/items/04f220e64db006bf1400

今回はこれを Docker コンテナで動かします

実装の全量はこちら

https://github.com/RyoWakabayashi/elixir-learning/tree/main/sagemaker/serve

## 実行ファイルの作成

前回作成した Phoenix プロジェクト api ディレクトリー内に serve というファイル名で Phoenix を起動するシェルを作成します

```bash
#!/bin/bash

mix phx.server
```

この `serve` というファイル名も SageMaker の仕様に沿っています

実行できるように権限を付けておきます

```bash
chmod +x serve
```

`chmod` がパーミッション（権限）変更コマンドで、 `+x` 指定で誰でも実行できるようにしています

## serving.ex の変更

コンテナで実行する場合、コンテナ内のストレージに保存したファイルはビルド毎に破棄されてしまいます

Bumblebee でダウンロードするモデルのキャッシュディレクトリーはコンテナの外にしておいた方が便利です

lib/api_web/serving.ex をキャッシュディレクトリーを指定するように変更します

```diff_elixir
...
  @resnet_id "microsoft/resnet-50"
+ @cache_dir "/opt/ml/model"

  def start_link(_opts) do
    {:ok, model} =
-     Bumblebee.load_model({:hf, @resnet_id})
+     Bumblebee.load_model({:hf, @resnet_id, cache_dir: @cache_dir})

    {:ok, featurizer} =
-     Bumblebee.load_featurizer({:hf, @resnet_id})
+     Bumblebee.load_featurizer({:hf, @resnet_id, cache_dir: @cache_dir})
...
```

## 依存モジュールとビルド結果の削除

api ディレクトリー配下でローカル実行時に作成されたディレクトリーを削除します

ローカルとコンテナ内では OS が違うため、そのまま同じものを使いまわせないためです

```bash
rm -rf _build
```

```bash
rm -rf deps
```

## Docker ファイルの作成

api の親ディレクトリーに Dockerfile を作成します

```Dockerfile
FROM elixir:1.14.2

RUN mix local.hex --force \
  && mix local.rebar --force

COPY ./api /app

WORKDIR /app

RUN mix deps.get

RUN mix compile.phoenix

RUN chmod +x /app/serve

ENV PATH="/app:${PATH}"

EXPOSE 8080

CMD ["serve"]
```

- Elixir 1.14.2 のコンテナをベースにしています

- api ディレクトリー配下の Phoenix プロジェクトを動かすようにしています

- コンテナ内でも serve に実行権限を追加しています

- SageMaker　では `serve` がコンテナ起動時に実行されるため、 PATH に `/app` を追加して `serve` だけで実行できるようにします

- ポート番号 8080 を外部に公開します

## docker-compose.yml の作成

Dockerfile と同じディレクトリーに docker-compose.yml を作成します

```yml
---

version: '3.2'
services:
  web:
    container_name: phoenix
    build: .
    ports:
      - '8080:8080'
    command: serve
    environment:
      - MIX_ENV=dev
    volumes:
      - ./tmp:/tmp
      - ./models:/opt/ml/model
```

- コンテナ内の 8080 を localhost の 8080 に紐づけています

- コンテナ起動時には serve を実行するようにしています

- 環境変数 MIX_ENV に dev を指定しています

- ローカルの ./tmp をコンテナ内の /tmp にマウントします

  Bumblebee は /tmp にまずモデルファイルをダウンロードするため、ここはキャッシュディレクトリーと同じファイルシステムである必要があります

  ./tmp がなければ作成しておいてください

- ローカルの ./models をコンテナ内の /opt/ml/model にマウントします

  ここが Bumblebee のキャッシュディレクトリーになります

  ./models がなければ作成しておいてください

## コンテナの起動

コンテナを起動し、しばらくすると Phoenix が起動します

```bash
$ docker-compose up --build
...
Attaching to phoenix
phoenix  | Compiling 11 files (.ex)
phoenix  | Generated api app
phoenix  | [info] TfrtCpuClient created.
phoenix  | [info] Running ApiWeb.Endpoint with cowboy 2.9.0 at 0.0.0.0:8080 (http)
phoenix  | [info] Access ApiWeb.Endpoint at http://localhost:8080
```

## 動作確認

別のターミナルを開きます

まず ping を確認します

```bash
$ curl http://localhost:8080/ping
{}%
```

空の JSON が返ってきます

次に推論を確認します

`@sample.jpg` の部分は手元の JPEG ファイルのパスに変えてください

jq をインストールしている場合は jq にパイプすると見やすいです

https://stedolan.github.io/jq/

```bash
$ curl -XPOST http://localhost:8080/invocations \
  --data-binary @sample.jpg \
  --header "Content-Type:image/jpeg" | jq
{
  "predictions": [
    {
      "label": "notebook, notebook computer",
      "score": 0.6154251098632812
    },
    {
      "label": "laptop, laptop computer",
      "score": 0.11529479920864105
    },
    {
      "label": "bow tie, bow-tie, bowtie",
      "score": 0.03906528279185295
    },
    {
      "label": "projector",
      "score": 0.0267447829246521
    },
    {
      "label": "dining table, board",
      "score": 0.024912187829613686
    }
  ]
}
```

## まとめ

SageMaker　で推論実行できるコンテナの条件が揃いました

- serve で Web サーバーを起動する
- ポート番号 は 8080
- /ping で常に正常応答する
- /invocations で推論を実行する

次回、このコンテナを SageMaker にデプロイし、 AI のマイクロサービスとして動作させます