MLモデル実行APIを GCP Run + Docker + Flask で実装してみた (M1 Mac)


## はじめに
今回はハッカソンでAI(自然言語処理)を使ったモバイルアプリを開発することになり、私はUIとサーバー（未経験）を担当したのでサーバーサイドでの備忘録を残したいと思います。

### ゴール
製作物のゴールは以下のイメージです。

![スクリーンショット 2022-06-29 15.48.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2058516/ce3b3aeb-4dd6-7f55-65ae-b364f7e3d597.png)



”ML班が作ってくれた学習済みモデルコードを実行できるコンテナイメージをGCP Runにデプロイし運用、モバイル側からエンドポイントを叩いてレスポンスを取得”という流れです。

## flaskアプリの作成(app.py)

app.pyはシンプルな構成で作りました。詳細は割愛ですが、GCPはデプロイ時に環境変数(PORT = 8080)を自動で入れてくれるので'PORT'を使っています。

```py: app.py
from re import U
from flask import Flask, jsonify
import os
import main
import asyncio
import scrapeNews
import json
import codecs

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

@app.route('/collect')
async def collect():
    await scrapeNews.main()
    json_data = json.load(open("articleList.json", "r"))
    articleList = []
    for news in json_data:
        title = news["title"]
        text = news["text"]
        url = news["url"]
        score = await main.predict(title, text)
        articleList.append({"title":title,"url":url, "score": score})
    articleListJson = json.dumps(articleList,ensure_ascii=False )
    fileName = "article_with_score_list.json"
    file = codecs.open(fileName, 'w','utf-8')
    file.write(articleListJson)
    return articleListJson
    

@app.route('/')
async def provide():
    if os.path.isfile("article_with_score_list.json"):
        json_data = json.load(open("article_with_score_list.json", "r", encoding='utf-8'))
        return jsonify(json_data)
    else:
        await collect()
        json_data = json.load(open("article_with_score_list.json", "r", encoding='utf-8'))
        return jsonify(json_data)


if __name__ == "__main__":
    app.run(debug=True, threaded=True, host="0.0.0.0", port=int(os.environ.get('PORT', 8080)))

```


## Docker
### Dockerでコンテナのビルド

Pytorch, transformer などML用のライブラリを使うことになったのですが、alpineや-slimなど軽いイメージではうまく動作しなかったので普通（？）のPythonイメージを使いました。以下が構成とDockerfile。


```
/HackServer
	- requirement.txt
	- docker-compose.yml
	- Dockerfile
	- opt
	   - model
	   - app.py
	   - main.py

```


```tex: Docerfile
FROM python:3.9

# mecabの導入
RUN apt-get -y update && \
  apt-get -y upgrade && \
  apt-get install -y mecab && \
  apt-get install -y libmecab-dev && \
  apt-get install -y mecab-ipadic-utf8 && \
  apt-get install -y git && \
  apt-get install -y make && \
  apt-get install -y curl && \
  apt-get install -y xz-utils && \
  apt-get install -y file && \
  apt-get install -y sudo

ADD . /app
WORKDIR /app

RUN apt-get install -y vim less
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools

#Rustのコンパイラがないと怒られてしまうので以下でインストール
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile default --component rls rust-analysis
ENV PATH="/root/.cargo/bin:$PATH"
RUN pip install -r requirements.txt
RUN pip install gunicorn
RUN sudo cp /etc/mecabrc /usr/local/etc/

WORKDIR /app/opt
VOLUME ./opt:/app

#非常に処理が重いのでworkers:4にした
#$PORTはGCP Runが用意してくれる(8080)のでそれとコンテナをバインドする
CMD exec gunicorn --bind :$PORT --workers 4 --threads 8 app:app
CMD python app.py

```

### ローカル用イメージの作成
以下のコマンドでイメージを作ります。
今回チーム名がオムライスに関連してたので、作成するイメージ名を"omlette-server"にしています
```
$ docker build -t omlette-server .    

```
### ローカルで実行

flaskで8080をポートにしているので、ホスト側の8080とコンテナ側の8080をバインドします。
```
$ docker run -e PORT=8080 -p 8080:8080 omlette-server
```
うまくできたら次はGCP Runにデプロイする準備です
## GCP
### イメージにタグをつける
GCP Runにイメージをデプロイするには Container registry (今はArtifact registoryが推奨だそうです)にプッシュしなければなりません。プッシュする前に前章で作成したイメージにタグ（latestやv1など）をつけます。

```
$ docker tag omlette-server asia.gcr.io/[プロジェクトID]/omlette-server:latest
```
### プッシュする
```
$ docker push asia.gcr.io/hackuomletteserver/omlette-server:latest   
```
あとはコンソールでイメージ登録して終わり！！と思ったのですが、コンソールでデプロイする時にGCPでエラーが出てしまいました…
```txt
Failed to start and then listen on the port defined by the PORT environment variable. Logs for this revision might contain more information
```
どうやらM1の影響らしいです…（[参考にさせていただいた記事](https://qiita.com/toyoday/items/020770209cb7adf8a91d)）
<br>GCP Run の公式にもありましたがMチップMacではローカルでビルドするとうまくいかないよう。
<br>
GoogleはCloud Buildを使えと言っていますが、課金リスクは避けたいし認証まわりがめんどくさそうだったので、上記の記事の方同様にDocker のbuildxコマンドでイメージを作成し直すことに。
```text
$ docker buildx build --platform linux/amd64 -t omlette-server-x64 .
```
区別しやすくするため、イメージ名を変更しています。

### デプロイ設定
私はメモリの大きさやCPU数、セッション時間など細かく設定したかったのでわかりやすいコンソールで行いました（いつかコマンドでサクサクできるエンジニアになりたい…）
![スクリーンショット 2022-06-28 17.52.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2058516/19b1008d-d4d5-f9d0-b24a-1186d68f033f.png)

### 終わりに
今回普段使わないものばかり触れていた、かつ時間がなかったので荒作業感が否めませんでした…<br>何かアドバイス等あればご教示いただけると幸いです。
#### 参考にさせていただいた他の記事
https://zenn.dev/daikichidaze/articles/225ea11abd6b8e

https://outputable.com/post/start-cloudrun/