スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Google Cloud Run で社内フリードリンク在庫判定ボットを作ってみた。

はじめまして、データプロダクト開発チームの@yuu_itoです。

一緒に仕事をしている@toohskとフリードリンクの在庫判定をするSlackボットを作成しましたので紹介します。

フリードリンクとフリードリンク在庫判定ボットとは

Quipperでは福利厚生の一環として、社内でフリードリンクが提供されています。

フリードリンクの冷蔵庫はオフィスエリアの一角にあり定期的に充填されています。

既にWebカメラを搭載したRaspberry Piで定期的に様子を撮影しており、 わざわざ席を立って在庫の状況を見にいかなくても、 Slackの専用チャンネルより確認できます。(thanks to @yoshimaru46)

Raspberry Piから撮影される冷蔵庫の画像例

しかし、空っぽの写真だけが延々とチャンネルに流れていても意味がない (フリードリンクは大変人気で、入荷日以外は在庫が空状態で続くことも多いのです...)ので 在庫が空になり、その後飲み物が入荷したときだけ通知すればみんなが嬉しいのでは? と考えました。

システム構成

今回用意したシステムは以下の通りです。

コンポーネント図 処理の流れは以下の通りです。

  1. 冷蔵庫を撮影する。
  2. Slackのタイムラインに撮影した画像を投稿する。
  3. Slack Real Time Messaging APIからGoogle Apps Script(GAS) にSlackのチャンネルに流れているメッセージを送信する。
  4. 送られてくるメッセージから対象の画像のみを画像分類ロジック(Google Cloud Runで実装) に送信する。
  5. 事前に学習した画像分類モデルで推論し結果を返す。
  6. GASのキャッシュ機能(CacheService)を用いて直前の画像分類結果と比較し、 『在庫なし』から『在庫あり』へ状態が変化した場合のみ、Slackチャンネルに投稿(@here)する。
  7. Quipperメンバーに通知が飛ぶ。

Google Cloud Run について

実はボット開発開始時はGoogle Cloud Functionsというサーバーレスアーキテクチャのサービスを用いて、画像分類モデルの推論を動かしていました。 しかし、より新しいサービスとしてGoogle Cloud Runが発表されており業務利用時の検証を兼ねて移行してみました。

移行した主な理由はCloud Runがコンテナベースのアーキテクチャであることでした。それというのもCloud Functionsはランタイム(Python, Node.js, Go)からしか選べず、他の言語はもちろんOSなどのミドルウェアを選ぶことはできません。

一方、Cloud RunはDockerコンテナとして用意できればなんでも選べるため、 例えば

  • 最新バージョンのPythonをビルドしたコンテナイメージ
  • OS依存のライブラリを利用するコンテナイメージ

などを実行環境として採用することができます。 今後、ボットに複雑な学習や推論をさせてみたり、業務で新規開発を検討した場合に新しいミドルウェアやライブラリを候補にできるのはメリットだと思い、 このボットを人柱に 移行しました。

執筆時点ではベータ版ではありますが、すでにTokyoリージョンでの利用もできたのも理由のひとつです。

Google Cloud Functions から Cloud Run に移行するためにしたこと

1. Dockerfileの追加

基本的には公式ドキュメントが丁寧に用意されています。 Cloud Functionと違い、Cloud Runはコンテナ上で動作するため、Dockerfileを用意します。

Cloud Functions 利用時には意識しなかったアプリケーションサーバの起動コマンドをDockerfileに記載します。

さらに今回のボットでは、コンテナをビルドする際に必要な Slack、GCPなどのAPIを利用するためのキーファイルや Pypiからパッケージをインストールするための requirements.txt を コピーする処理をDockerfileに追記します。

## Dockerfile

FROM python:3.7
...
COPY requirements.txt .  # 利用するパッケージ
COPY models models  # 分類モデルを含んだディレクトリ
COPY google-serviceaccount.json .  # キーファイル
...
RUN pip install -r requirements.txt  # 利用するパッケージのインストール

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 main:app

2. Pythonコードの修正

Cloud Runでは単一のアプリケーションとして動作するようにエンドポイントを追加する必要があります。 しかし、追加するコードはわずかでエンドポイントとなるメソッドを用意し、もともとCloud Functionsで使っていたメソッドを呼び出すだけでした(簡単!)

## main.py

import flask
from flask import Flask, request
...

app = Flask(__name__)  # Cloud Run 用に追加

# Cloud Functions の時点で用意していたメソッド
def predict_from_http(request):
    """ requestから画像データを取得、分類結果を返す
    Args:
        request (flask.Request): HTTP request object.
    Returns:
    """
    ...

    return flask.make_response(
                flask.jsonify(predict_info),
                200)

# 以下 Cloud Run 用に追加したメソッド
@app.route('/', methods=['POST'])
def endpoint():
    return predict_from_http(request)

画像分類モデルについて

今回使ったモデルはシンプルなCNNモデルで構築しました。 学習データはこれまでWebカメラで撮影した冷蔵庫のデータ(600枚程度)を手動でラベリングしました。 (在庫が空の場合はempty,1つでもボトルが残っている場合はnot_empty) 学習の処理はGoogle Colaboratory上で行っています。

モデル構築、検証時の内容も面白い話があるのですが、 書き出すとボリュームが出てくるので今回は割愛します。 (反響あれば、次回以降で記事にするかも?!)

動作結果

入荷された場合の通知

ハマったところ

学習した分類モデルのファイルやPythonパッケージのインストール、キーファイルの配置が漏れていたために ビルド、コンテナの登録まで問題に気づけず、実行時にエラーとなっていました。 思い返すと、Cloud Functions利用時は配下のファイルを全部まとめてzipしアップロードしてくれていたので 意識していませんでした。

問題に気づいた後は、開発時にローカルのDocker環境でビルド、 実行して試すことでCloud Runへ登録する前に気付けくことができるようになりました。

そして実はCloud RunよりもGASのほうが悩まされ、時間がかかっていました。 主にキャッシュの動作確認や、変数のtypoによるundefined errorが多発してしまいました。

キャッシュについてはCloudRunとの連携部のエンドポイントとは別に キャッシュされている情報を確認する口を用意しました。(動作結果の画像を参照)

TypoについてはJSで書かず、TypeScriptで書けばtslintなどで事前に確認できたなと反省しています...

まとめ

  • Cloud RunとCloud Functions
    • Cloud Functions
      • さくっと始めやすい。
      • テストは難しい。
      • ランタイムに依存することで自由度は低め。
    • Cloud Run
      • Dockerの理解があると始めやすい。
      • ローカルで動作確認ができるため、検証が容易。
      • コンテナにビルドできればなんでも使える!

在庫判定ボットで快適なオフィス生活に!(していきたい)