エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

GKEでStreamlitをホスティングして社内用アプリを作った話

はじめに

ブンブンハローテックブログ。エムスリー AI・機械学習チームでエンジニア兼YouTuberをやっています河合と笹川です*1。本記事は、AIチームが社内向けに提供を初めたビジュアライズアプリケーションに関する解説の記事です。

GKE上のStreamlitサーバのホスティング設定と、機械学習エンジニアが社内向けの可視化を行う際の一例として、参考となれば幸いです。

 

 

Background

一般的に「機械学習エンジニアが社内向けの可視化アプリケーションを作る」といったケースでは、以下のようなシステム利用が考えられるかと思います。

  • HTML、xlsx、Googleスプレッドシートなどを作成、配布する
  • S3、GCSのようなストレージの静的サイトホスティングサービスを利用する
  • Looker、Datalore、Metabase、Redash、SupersetなどのBIサービスを利用する
  • Lambda、ECS、GAE、Firebase、もしくは外部のホスティングサービスを利用する
  • HTTPサーバ用インスタンス、社内サービス向けのオンプレサーバを作成する
  • 既存のインフラの中に間借りする形でホスティングする

分析結果や定常的なデータのグラフを使った可視化など、Jupyter NotebookをHTMLで出力して、共有する形で終える事も多々あります。 また、実際に静的サイトのホスティングサービスで問題ない場面も多く存在します。

可視化アプリケーションと言えど、インフラコスト、管理コストが最小限で済むようファイル配布で済むのであればそうしたい所です。

 

一方で、可視化の目的に依っては、
既存のデータ取得、加工、機械学習モデルの推論などを実行するPythonスクリプトを使いまわして作成したい
社内外のQA、本番環境のデータソースにセキュリティやアクセス権限、ユーザ制限周りを考慮した上でアクセスしたい
といった状況も考えられます。

可視化アプリケーションのユーザ側もソフトウェアエンジニアリングの教養がある場合は、コストを削減できるPythonスクリプトを動作させる方法論として、Google Colabなども候補に挙がるでしょう。 ソフトウェアエンジニア以外にも見せたい場合など、可視化をリッチにしたければBIサービスを利用する方向にもなるかと思います。 実際にLooker、Datalore、Re:dash等では、Jupyter NotebookやPythonスクリプトを利用する事も可能です。

さらにリッチな実装を考える場合として、HTTPサーバをホスティングする方法が考えられます。 この場合、クラウドの何れかのサービスを利用したり、社内サーバ、既存のインフラを利用する事になります。

 

つまり、どの形式で可視化結果を共有するかは、状況や目的次第という事です。

f:id:vaaaaaanquish:20200929200859p:plain
何事も状況と目的次第
 

今回GKEでStreamlitをホスティングするに至った経緯としては、以下のようなポイントがあります。

  • 可視化したいデータが一定時間ごとに変化するため、その場でBigQuery、BigTable、社内のAPIなどにアクセスしたい
  • 既にセキュリティとデータソースアクセスに関する権限をクリアしたGKEクラスタ、Dockerコンテナが存在する
  • APIアクセスなどに実装済みの既存のPythonスクリプトを使いたい (新規にコードを書くのを防ぎmoduleとして使い回したい)
  • 「Streamlitを使いたい!」という私の願望

中でもセキュリティ面の観点で、本番データへのアクセス、社内での制限付きAPIの運用がk8sクラスタ上にて実績を持っている所が大きなポイントです。 また、terraformを整備する形で、CI/CDによる可視化結果を確認できるHTTPサーバをデプロイするノウハウを既に持っている事も、Streamlit採用の一因としてあります。 (私がStreamlitを使いたかったという気持ちの問題もあります)

 

Streamlitとは

Streamlitは、Streamlit社が2018年より公開している可視化のためのPythonモジュールです。 github.com

2019年にStreamlitのco-founderであるAdrien Treuille氏の以下ブログ記事がRedditなどで話題になった事をきっかけに、データ分析を生業とする業界で使われ始めたように記憶しています。 towardsdatascience.com

近年では機械学習モジュールとして代表的なAllenNLPやspacyがuniverseプロジェクトとしてStreamlitをサポートする他、日本でもユーザが増え、多くの技術ブログが公開されています。 手前味噌になりますが、私も定期的に使うだけでなく、公式フォーラムに参加したりvimとの連携設定を公開する等しています。

Spacy Universe Project: Overview · spaCy Universe

 

Streamlitは、しばしば2018年以前から公開されているPlotly DashやBokehといったモジュールと比較されます。 DashやBokehがエンタープライズに向かう側面があるのに対して、Streamlitはラピッドプロトタイピングに焦点を当ている点が大きな違いとなっています。 APIは簡素で扱いやすく、Jupyter Notebookのような可視化や関数の結果のcacheを少ないPythonコードで作成できます。反面、コンポーネントの拡張やエンドポイントの追加、グリッドデザイン、ユーザ認証の仕組みを追加する事をデフォルトではサポートしていません。

適材適所とはなりますが、今回の私の事例では、可視化結果の共有について、継続的でなく短期間だけ行いたいという目的が、Streamlitのラピッドプロトタイピングの思想と合っていたとも言えます。 (もちろん私自身が使いたい気持ちも強かったです)

 

Streamlitの特徴

簡単なExampleとはなってしまいますが、Streamlitを用いてpandas DataFrameをいくつかの方法で可視化する例を以下に示します。

import streamlit as st
import pandas as pd

# DataFrameを生成するcache機能付きの関数を定義
@st.cache
def get_df(s):
    return pd.DataFrame({'id': ['1', '2', '3'], 'name': ['X', 'Y', s]})

# text formを生成
s = st.text_input('input s')
df = get_df(s)

# DataFrameをテーブルで表示
st.markdown('# Table')
st.table(df)

# histgramを表示
st.markdown('# Histgram')
df['name'].hist()
st.pyplot()

Streamlitの特徴的な機能として、st.cachepyplotのhook があります。

st.cacheは内部的には、methodの入力値をパラメータとしてpickleでdumpする実装となっています。pickleのI/Oの時間より小さい処理、例えば検索APIにqueryを投げたり、DBからデータを取得したりであれば、表示までの時間を短縮する事ができます。また、st.cacheを使う事で負荷やコスト面に優しい形で可視化を行う事が出来る場合もあるでしょう。

pyplotのhockは、Jupyter Notebookのhockとほぼ同等の機能です。matplotlibを経由していれば、pandasで統計量を算出して表示したり、機械学習モデルの推論結果、WordCloudなど様々な結果を表示する事ができます。

 

上記スクリプトを streamlit run コマンドの引数に指定した時、localhostに以下のような画面を返すサーバがホスティングされます。

f:id:vaaaaaanquish:20200929182159p:plain:w400
Streamlit Example

テキストフォーム入力に応じた可視化結果が表示されていることが分かります。また、再度フォームを入力しなおして実行してみると、cacheが効いている事も確認できます。 もちろん、この他にもボタンやサイドバーを設置し、actionをhockする事ができます。

 

一方、Streamlitは、ラピッドプロトタイピングを目的とする思想から、動的なサーバとのやり取り、複雑なUI設計は苦手です。 Streamlitのフロントとサーバとのやり取りにはwebpackが走っていますが、デフォルトではサーバとフロントの状態変化を検知するもので、同期的な可視化処理に対応していません。 同期的な可視化を行う場合は、React側のComponentを独自に実装する必要があったり、Grid Layoutで2カラムで見たい等のデザイン変更には、plotlyを経由するなどして少々複雑な拡張実装が必要になります。

実例を出すと、StreamlitコミュニティのモデレータであるFanilo Andrianasolo氏がplotly.expressを動作させるためのReact Component作成例を公開しています。 dev.to

また、Streamlitコミュニティでも多く貢献しているMarc Skov Madsen氏がawesome-streamlitというRepositoryを作成しており、私もGrid LayoutやCookieを使ったcache保存の実装など多くを参考にしています。 github.com

こういった課題に関しては、Streamlitコミュニティで活発に議論が成されているので、以下で検索してみるのもオススメです。 discuss.streamlit.io

 

アプリケーション、インフラ構成

本題として、今回作成した社内用アプリケーションについて触れていきます。

今回は、BigQuery、BigTable、内部向けAPIからデータを取得、pandas DataFrameに落とし込み、統計情報を計算、matplotlibで可視化する形にしています。

f:id:vaaaaaanquish:20200929211624p:plain:w400
アプリケーション構成

上記アプリケーションを社内向けにホスティングする際のインフラの構成としては、上述の通り、すでに運用されているGKEのクラスタ上にデプロイすることとしました。 すでに、アプリケーションがDocker上で動くように開発されていたこともあり、特に選択肢としては違和感がないかと思います。 また、既存のクラスタは社内向けのサービス公開用にセキュリティ的な設定もなされていますし、今回の用途にぴったりでした。

前述の通り、今回作成した可視化アプリでは、社内で利用されているBigQueryや、既存のBigTable、別アプリケーションの内部向けAPIエンドポイントを参照するものとなっています。 この辺りのリソースへのアクセス制御は、必要なリソースに対して、権限を付与したService Accountを、Workload Identityを利用して、 アプリと紐付けることで行っており、セキュリティリスクのあるアカウントキーの発行、管理をGCPサイドに任せています。

cloud.google.com

Kubernetesリソースの設定は以下のような感じになりました (説明に必要な箇所のみ抽出、適宜省略、変更しています)。

apiVersion: v1
kind: Service
metadata:
  blahblah
  blahblah
  blahblah
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: visualizer
  name: visualizer
  namespace: sample
spec:
  selector:
    matchLabels:
      app: visualizer
  template:
    metadata:
      labels:
        app: visualizer
    spec:
      containers:
      - command:
        - streamlit
        - run
        - /app/feed_checker_app.py
        - --server.port=8000
        - --server.enableCORS=false
        env:
        - name: BIGQUERY_RELATED_CONFIG
          value: bigquery_related_config
        - name: BIGTABLE_RELATED_CONFIG
          value: bigtable_related_config
        - name: INTERNAL_API_RELATED_CONFIG
          value: internal_api_related_config
        image: app_image
        livenessProbe:
          failureThreshold: 6
          httpGet:
            path: /healthz
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
        name: feed-visualizer
        ports:
        - containerPort: 8000
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
        resources:
          limits:
            memory: 2Gi
          requests:
            cpu: 100m
            memory: 1Gi
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  blahblah
  blahblah
  blahblah
---  
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: visualizer
  namespace: sample
spec:
  connectionDraining:
    drainingTimeoutSec: 40
  securityPolicy:
    name: policy-name
  timeoutSec: 36000

上記は、特に変わったことはしておらず、アプリケーションデプロイの際の標準的な設定です。 Streamlit特有の設定としては、/healthz というパスに平常時に200を返すエンドポイントがある *2 ので、これをliveness, readinessに利用しています。

ただし、最下部に定義した、BackendConfig に重要なポイントがあります。 BackendConfigはGKEで利用可能なCustom Resouce Definition (CRD) であり、Cloud Armor、Cloud CDN、LBのtimeoutなどの設定を記述すると、設定内容が各種GCPリソースと連携され動作します。

ここで、今回のアプリケーションでは、LBのtimeoutを 36000 に設定してあります (ちなみにdefaultは30)。 このように設定した理由は、Streamlitが、 以下などに挙げられているような問題により、WebSocketの通信の関係で、LBのタイムアウト時に、画面がリセットされてしまい、これまで触っていた可視化の設定が元に戻ってしまうという問題があるためです (ローカル開発時のDockerでは再現しなかったので調査に少し手間取りました)。 Publicな場所に公開するアプリでこのような設定は問題がありますが、社内向けかつ限定的なメンバーしか閲覧しないことから、今回は、このような設定で一時的に逃げることにしました。

LBに関する議論やドキュメントは以下を参考にしています。 discuss.streamlit.io cloud.google.com

この問題については、機を見て、本体にcontributeするのもいいかもしれません。 この辺りは、Streamlitのまだ枯れきっていない部分ですが、 ほぼフロントエンドコードを記述する必要がなく、低いコストで高品質な可視化が実現でき、素早く社内に公開できるメリットは、欠点を補って余りあると思います。

 

おわりに

今回は、データ可視化アプリケーションの選択とStreamlitによるアプリケーション作成、クイックに社内向けに公開するノウハウについて紹介しました。

 

We're hiring

エムスリーでは、機械学習のタスクを実装し、結果を可視化して高速に改善することで、医療を前進させるプロダクトを開発するエンジニアを募集しています! 我こそは! という方はぜひ以下よりご応募ください!

jobs.m3.com

*1:まだ1本しか出ていませんが2本目も企画中ですので是非チャンネル登録お願いします https://youtu.be/eTZkl7EdT4A

*2:実装箇所は、この辺り https://github.com/streamlit/streamlit/blob/6754e97e52e783f64f6ed49c1cdc75b881ade892/lib/streamlit/server/server.py#L330-L332