🍺

StreamlitとGoのAPIサーバーをgRPCで繋げてみた

2025/03/10に公開

はじめに

こんにちは。よこやんです。
株式会社バニッシュ・スタンダードという会社でサーバーサイドエンジニアをやっています。
前回はテキスト生成AIのRAGをGo言語とベクトルDBで実装してみたという業務外で勉強したことをブログのネタにさせていただきました。

今回は少し毛色を変えてwebアプリケーションネタでブログを書きたいと思います。
私は普段の業務ではバックエンドはGo言語、フロントではReact、(あとはレガシー部分ではRails)を使った開発を行っています。
また趣味で個人開発なんかも行っているのですが、最近、、

みたいなことを考えていて、もっと簡単にいい感じのフロントが作れるものはないかなと考え、以前から気になっていたStreamlitを試してみようと思いました。
Pythonなのでデータ分析系ライブラリ使えるし、フロントもサクッと作れるし、しっかり作り込みたい部分はGo側で対処すればいいし、そして両者をgRPCで繋げるようにすればコンテナ2つだけで色々できるんじゃないかな、と

今回は上記のような思いつきを実装してみました。もし何かの参考になれば幸いです。

Streamlitとは

https://streamlit.io/
StreamlitとはPythonで簡単にWebアプリケーションを作成できるライブラリで以下のような特徴があります。

  • Pythonの知識だけでWebアプリを開発できる
  • HTMLやCSSなどの知識を必要とせずにデータの可視化が可能
  • ユーザーが入力したデータをリアルタイムで表示できる
  • グラフィカルなカスタムWebアプリケーションを簡単に作成できる

詳細については公式サイトを見ていただきたいですが、かなり少ないコード量でぱっと見で綺麗なwebアプリケーションが作れます。
ただし自動でよしなにやってくれる部分が多いので、痒いところには手が届きにくいというデメリットはあります。
とはいえプロトタイプ開発や社内用ツールのようなUIにそこまで拘らなくて良いものであればこれで十分かもしれません。
また、Pythonなので当然PandasPyTorchなどのデータ分析・機械学習などで有名なライブラリも使えます。
他言語でもデータ分析・機械学習系のライブラリは存在しますが、ドキュメントの豊富さや信頼性を考えるとやはりPythonでのライブラリを使うのが一番良いかと思います。
一方で動的型付けでインタプリタ方式のPythonであること自体がボトルネックとなる可能性もあり、大量のアクセスを並列に処理する、などの場合はRuby on Railsなどと同様に工夫が必要になってくると思われます。

実際にStreamlitはデータサイエンスの分野でクローズドな範囲で使われることが多いようで、「データ分析はガッツリやりたいけど、それを操作・表示させるUI/UXはそこまで拘らなくて良い。広く使ってもらうものではなく、限られた数のクライアントや社内管理システム用」みたいなシーンでの利用が多いみたいです。

gRPCとは

gRPCについては以前弊社のメンバーが詳細を書いてくれた記事があるのでこちらをご参照ください。
https://zenn.dev/vs_blog/articles/c73f2957b328ad
簡単に説明すると
gRPCは、Googleが開発した高性能なオープンソースのRPC(Remote Procedure Call)フレームワークです。以下が主なポイントです:

  • 高速通信: HTTP/2を採用しており、低レイテンシや双方向ストリーミングを実現しています。
  • 効率的なデータシリアライゼーション: Protocol Buffers(Protobuf)を用いることで、データのシリアライズ・デシリアライズが高速かつ軽量に行えます。
  • 多言語対応: C++, Java, Python, Goなど、さまざまなプログラミング言語をサポートしており、異なる環境間での連携が容易です。
  • シンプルなサービス定義: Protobufを使ってサービスやメッセージを定義するため、明確で統一されたAPI設計が可能です。

これらの特徴から、gRPCはマイクロサービスアーキテクチャや分散システムの構築において、効率的かつスケーラブルな通信手段として広く利用されています。

今回はこれを使って【Pythonで作ったStreamlitアプリ】と【Goで作ったAPIサーバー】を繋いでみます。

実装内容の紹介

今回作るサンプルはこちらです。
https://github.com/xiao1203/streamlit_and_go

docker-composeにてgoのAPIサーバー、Streamlitアプリの二つのコンテナを作成し、そのコンテナ間通信をgRPCで行うようにしています。
両コンテナの共通コードであるProtobufの管理についてはgitのサブモジュールを用いても良かったのですがちょっと大袈裟になりそうだったのでシンプルにcommonディレクトリを作って、その中で管理するようにしました。
構成は以下のようになります。

$ tree
.
├── README.md
├── common
│   └── hello.proto
├── docker-compose.yml
├── go_server
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   ├── hello
│   │   ├── hello.pb.go
│   │   └── hello_grpc.pb.go
│   └── server.go
└── streamlit_client
    ├── Dockerfile
    ├── proto_files
    │   ├── hello_pb2.py
    │   └── hello_pb2_grpc.py
    ├── requirements.txt
    └── streamlit_client.py

ひとまずは単純に

  1. Stremalit側で文字列入力
  2. GoのAPIサーバーで文字列を受け取って加工して返却
  3. Streamlitアプリ側でレスポンスを受け取り表示

というものを作ってみました。
詳細な実装手順については下記にまとめていますが、

実装の詳細手順

前提

  • プロジェクト名はstreamlit_and_goとします。もし本記事を参考に実装してみる場合はここを変更して適宜読み替えてください。
  • Goモジュール名はgithub.com/xiao1203/streamlit_and_goとします。
    基本的にはgithub.com/[ユーザー名]/[Project名]となるところなので変更する際は適宜読み替えてください。

Step 1

プロジェクト名を決めてディレクトリ作成

$ mkdir streamlit_and_go
$ cd streamlit_and_go

Step 2

必要なディレクトリ、ファイルを作成
(ディレクトリなどは変更可能)

$ touch docker-compose.yml
$ mkdir common
$ mkdir go_server
$ touch go_server/server.go
$ touch go_server/Dockerfile
$ mkdir go_server/hello
$ mkdir streamlit_client
$ mkdir streamlit_client/proto_files
$ touch streamlit_client/Dockerfile
$ touch streamlit_client/streamlit_client.py
$ touch streamlit_client/requirements.txt

Step 3

プロトコル定義ファイルを作成します。
ひとまずhello worldを出力するための設定を書くと
commonディレクトリ以下にhello.protoファイルを作成し
以下のように記載

syntax = "proto3";

package streamlit_and_go;

option go_package = "github.com/xiao1203/streamlit_and_go/go_server/hello";

service Greeter {
  // クライアントからのリクエストに対して挨拶を返す
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

こちらを使いGoとPythonそれぞれのコード生成に利用します。

Step 4

Goのコードを生成します
初回のみGo用のgRPCコード生成ツール(protoc-gen-go と protoc-gen-go-grpc)をインストールします。

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

commonディレクトリに移動し、以下のコマンドでコードを生成します。

$ cd common
common$ protoc --go_out=paths=source_relative:../go_server/hello --go-grpc_out=paths=source_relative:../go_server/hello hello.proto

生成されたファイルは、Goサーバー側でインポートして使用します。

Step 5

Pythonのコードを生成します

初回のみPython用のgRPCツールをインストールします。

pip install grpcio grpcio-tools

コードを生成します。
(commonディレクトリにいるままだったら、以下のコードをそのままで良いです。もし移動していたらディレクトリの指定を調整してください)

$ python -m grpc_tools.protoc -I. --python_out=./../streamlit_client/proto_files --grpc_python_out=./../streamlit_client/proto_files hello.proto

Step 6

Goサーバーの実装を行います。
go_serverへ移動してください。

cd ../go_server

モジュールを初期化を実施します。

go mod init github.com/xiao1203/streamlit_and_go

go_server/server.go に、以下のコードを記述します。
※ pb としてインポートするパッケージは、先ほど生成したGo用コードの配置場所に合わせて修正してください。

package main

import (
	"context"
	"log"
	"net"

	pb "github.com/xiao1203/streamlit_and_go/hello"

	"google.golang.org/grpc"
)

// サーバー実装(Greeterサービス)
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHelloメソッドの実装
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	log.Printf("Received request for name: %v", in.GetName())
	return &pb.HelloResponse{Message: "Hello " + in.GetName()}, nil
}

func main() {
	// ポート50051で待ち受け
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	// gRPCサーバーの生成
	s := grpc.NewServer()
	// Greeterサービスの登録
	pb.RegisterGreeterServer(s, &server{})

	log.Printf("Server is listening on %v", lis.Addr())
	// サーバーの起動
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

続いてgo_server/Dockerfileに、Goサーバーをビルドして実行するための設定を記述します。

FROM golang:1.23

WORKDIR /app

# 必要な依存関係のインストール
COPY go.mod go.sum ./
RUN go mod download

# ソースコードのコピー
COPY . .

# hot reload 用ツール air のインストール
RUN go install github.com/air-verse/air@latest

# ポート50051を公開
EXPOSE 50051

# air によりホットリロードを実施
CMD ["air"]

Step 7

Streamlitアプリの実装を行います。

streamlit_client/streamlit_client.py に、以下のコードを記述します。
ここでは、ユーザーが名前を入力すると、gRPCを通してGoサーバーにリクエストを送り、返ってきた挨拶メッセージを表示します。

import streamlit as st
import grpc

# 生成済みのprotoファイルをインポート
from proto_files import hello_pb2, hello_pb2_grpc


def run():
    st.header("gRPCでGoサーバーと通信するStreamlitクライアント")
    name = st.text_input("あなたの名前を入力してください:")

    if st.button("挨拶を送る"):
        if name:
            try:
                # Goサーバーがdocker-composeのネットワーク上で利用可能なホスト名 "go_server" を指定
                with grpc.insecure_channel("go_server:50051") as channel:
                    stub = hello_pb2_grpc.GreeterStub(channel)
                    response = stub.SayHello(hello_pb2.HelloRequest(name=name))
                    st.success(f"サーバーからの応答: {response.message}")
            except grpc.RpcError as e:
                st.error(f"gRPCエラー: {e}")
        else:
            st.warning("名前を入力してください。")

    if st.button("Send balloons!"):
        st.balloons()


if __name__ == "__main__":
    st.title("StreamlitとGoのAPIサーバーをgRPCで繋いでみた👋")
    run()

streamlit_client/Dockerfileを作成し、Streamlitアプリを実行するためのイメージをビルドします。

FROM python:3.9-slim

WORKDIR /app

# 必要なPythonパッケージのインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションのソースコードをコピー
COPY . .

# ポート8501を公開(Streamlitのデフォルトポート)
EXPOSE 8501

# Streamlitアプリを起動
CMD ["streamlit", "run", "streamlit_client.py", "--server.address=0.0.0.0"]

また、streamlit_client/requirements.txt には必要なライブラリを記述します。

streamlit
grpcio
grpcio-tools

Step 8

プロジェクトルートのdocker-compose.ymlにてGo製APIサーバーとStreamlitクライアントを連携させます。

services:
  go_server:
    build: ./go_server
    ports:
      - '50051:50051'
    volumes:
      - ./go_server:/app

  streamlit_client:
    build: ./streamlit_client
    ports:
      - '8501:8501'
    volumes:
      - ./streamlit_client:/app
    depends_on:
      - go_server

Step9

Dockerイメージのビルドとサービスの起動を行います。

$ docker compose build
$ docker compose up

出来上がったwebアプリケーションはこのように動作します。

いかがでしょう?
書いた量としてはかなり少ないですが、最低限のものができています。
今回は触れませんがStreamlitはさらにサイドメニューの実装データ分析用のチャート表示が標準でかなり充実しています。
https://docs.streamlit.io/develop/api-reference
マルチページ機能も備わっており、pagesというディレクトリを作ってその中にスクリプトを置くことで簡単にページ切り替えを行うことができます。
この辺りのルールに従って実装すれば簡単、という設計思想はRailsっぽさを感じますね。構成は全然違いますが。。

ただ今回作ってみて、Streamlitアプリ側の役割とGoのサーバー側の役割を明確にしておかないと混乱するだろうな、と思いました。単純なデータ管理程度であればStreamlitアプリ側でもできるので、「Goの方にはドメイン設計」「データ分析はStreamlitアプリ側で対応」みたいなルール付けをしないと、設計部分で混乱を引き起こしそうです。
「Streamlit + 何か」というのをあまり見かけないなと思ったのはこの辺りが理由ですかね?
とはいえ触った感じかなり楽しそうなので今後も勉強していきたいと思います。

また、今後の展開としてコンテナにDBを追加してGo側ではデータ管理を担当させたり、gRPCの双方向ストリーミングを活用してGo側からStreamlitアプリ側のデータ分析系の処理を呼んでみるなどの対応も試してみたいと思いました。

最後に

皆さん、この記事を最後まで読んでいただき、ありがとうございます。私たちのプロジェクトや技術的な挑戦に興味を持っていただけたなら、さらに嬉しい限りです。そして、この機会に弊社「株式会社バニッシュ・スタンダード」では現在エンジニア、デザイナーを積極的に探していることをお伝えしたいと思います。

私たちは会社のミッションである「つまらない常識を革めるプロダクトを開発し、おもしろく生きる人で世界をいっぱいにする。」を実現するため、この理念に共感してくれる新たな仲間を求めています。

私たちと一緒に、技術の最前線で働き、新しい未来を創造していきませんか?あなたの才能と情熱を、私たちのチームで発揮してください。興味のある方は、ぜひお問い合わせください。私たちはあなたからの応募を心からお待ちしています。

https://recruit.v-standard.com/

株式会社バニッシュ・スタンダード

Discussion