【GCP】Cloud RunでPython FastAPI・Streamlitをデプロイ

プログラミング

はじめに

Cloud Runはサーバーレスなコンテナ実行環境を提供するGCPのサービスです。

Cloud Runを使うことによって、Dockerイメージを構築するだけでアプリのデプロイを簡単におこなうことができます。

この記事では、pythonのアプリケーションをCloud Runにデプロイする方法について述べます。

gcloudを使用するので、インストールできていない方は公式ドキュメントにしたがって、インストールしましょう。

この記事で使用したソースコードは以下のGitHubで利用できます。

GitHub - Joichiro433/Blog-cloud-run-python
Contribute to Joichiro433/Blog-cloud-run-python development by creating an account on GitHub.

システム構成

今回はフロントエンドを Streamlit 、バックエンドを FastAPI とし、2つのサービスをCloud Runにデプロイします。

フロントエンドのStreamlitは誰でもアクセス可能な一般公開としますが、バックエンドのFastAPIはStreamlit側からしかアクセスできない限定公開とします。

アプリの内容は、画面に入力した名前にレスポンスとして挨拶を返す、とても単純なものとしました。

使用するソースコード一式

今回必要なファイル一式のディレクトリ構造は以下の通りです。

.
├─ backend/
│   ├─ dockerfile
│   ├─ main.py
│   └─ requirements.txt
└─ frontend/
    ├─ dockerfile
    ├─ main.py
    └─ requirements.txt


backend

backend/dockerfile
FROM python:3.9.12

RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install --no-cache-dir -r  requirements.txt

EXPOSE 8080

# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True

WORKDIR /app
COPY main.py .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Cloud Runは8080ポートに対応しているので、portを8080に設定しています。

backend/main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/hello")
def read_root(name: str):
    return {'message': f'Hello {name}!'}

挨拶を返す非常に簡単なAPIです。

backend/requirements.txt
fastapi==0.75.2
uvicorn==0.17.6

frontend

frontend/dockerfile
FROM python:3.9.12

RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install --no-cache-dir -r  requirements.txt

EXPOSE 8080

# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True

WORKDIR /app
COPY main.py .
CMD streamlit run main.py \
    --browser.serverAddress="0.0.0.0" \
    --server.port="8080"
frontend/main.py
import os

import requests
from requests.compat import urljoin
import streamlit as st
import google.auth.transport.requests
import google.oauth2.id_token

URL = os.environ.get("BACKEND_URL")


auth_req = google.auth.transport.requests.Request()
target_audience = URL
id_token = google.oauth2.id_token.fetch_id_token(auth_req, target_audience)
headers = {"Authorization": f"Bearer {id_token}"}

st.title('Greeting')

form = st.form(key='my-form')
name = form.text_input('Enter your name')
submit = form.form_submit_button('Submit')

st.write('Press submit to have your name')

if submit:
    params = {'name': name}
    resp = requests.get(urljoin(URL, 'hello'), params=params, headers=headers)
    st.json(resp.json())

リクエストを投げるFastAPIのURLは、Cloud Runにデプロイして初めて払い出されるので、環境変数として読み込むようにしています。

frontend/requirements.txt
streamlit==1.9.0
google.auth==2.6.6

GCR(コンテナレジストリ)にDockerイメージをpushする

1. gcloud の設定をおこなう
PROJECT_ID={作成したプロジェクトのID}
# プロジェクトIDの設定
gcloud config set project ${PROJECT_ID}
# リージョンの設定
gcloud config set run/region asia-northeast1

PROJECT-ID には、作成したプロジェクトのIDを記載します。

2. Dockerイメージを作成する
# fastapiのDockerイメージを作成
$ cd backend/
$ docker build -t asia.gcr.io/${PROJECT_ID}/fastapi-image:v1 .

# streamlitのDockerイメージを作成
$ cd ../frontend/
$ docker build -t asia.gcr.io/${PROJECT_ID}/streamlit-image:v1 .

Dockerイメージを確認して、下記のように表示されれば構築成功です。

$ docker images

REPOSITORY                                  TAG       IMAGE ID       CREATED         SIZE
asia.gcr.io/xxxxxx-xxxxxx/streamlit-image   v1        xxxxxxxxxxxx   x seconds ago   1.33GB
asia.gcr.io/xxxxxx-xxxxxx/fastapi-image     v1        xxxxxxxxxxxx   x seconds ago   968MB
3. GCRにpushする
# fastapiのイメージをpush
$ docker push asia.gcr.io/${PROJECT_ID}/fastapi-image:v1

# streamlitのイメージをpush
$ docker push asia.gcr.io/${PROJECT_ID}/streamlit-image:v1

以上でGCRへのDockerイメージのpushは完了です。

pushしたDockerイメージはGCPコンソールで確認できます。

Cloud RunにFastAPIをデプロイ

FastAPIはアクセスが制限された限定公開のサービスとしてデプロイします。

$ gcloud iam service-accounts create fastapi-identity

$ gcloud run deploy fastapi-cloudrun \
    --image asia.gcr.io/${PROJECT_ID}/fastapi-image:v1 \
    --service-account fastapi-identity \
    --no-allow-unauthenticated

# 出力
Service URL: https://fastapi-cloudrun-xxxxxxxxxxx-an.a.run.app

成功すると、FastAPIのURLが最後に出力されます。以上でCloud Runへのデプロイは完了です。簡単ですね。

限定公開のサービスとしてデプロイしたので、Webブラウザでは 出力されたURLにアクセスすることはできません。試しにアクセスしてみると、403エラーが返却されます。

実際に動作しているかテストするには、gcloudでトークンを取得し、Authorization ヘッダーを挿入してリクエストを投げます。

# トークンを取得
$ TOKEN=$(gcloud auth print-identity-token)

# リクエストを投げる
$ curl -H "Authorization: Bearer ${TOKEN}" \
    "https://fastapi-cloudrun-xxxxxxxxxxx-an.a.run.app/hello?name=Alice"

# 出力
{"message":"Hello Alice!"}

このようにメッセージが返却されれば成功です。

(curl に指定するURLは、Cloud Runへデプロイした際に払い出されたService URLを指定してください。)

Cloud RunにStreamlitをデプロイ

Streamlitは誰でもアクセスできる一般公開のサービスとしてデプロイします。

以下ではCloud RunにデプロイしたFastAPIを利用できるようにアクセス権の付与と、FastAPIのURLを環境変数として与えています。

$ gcloud iam service-accounts create streamlit-identity

$ gcloud run services add-iam-policy-binding fastapi-cloudrun \
    --member serviceAccount:streamlit-identity@${PROJECT_ID}.iam.gserviceaccount.com \
    --role roles/run.invoker

$ gcloud run deploy streamlit-cloudrun \
    --image asia.gcr.io/${PROJECT_ID}/streamlit-image:v1 \
    --service-account streamlit-identity \
    --set-env-vars BACKEND_URL=https://fastapi-cloudrun-xxxxxxxxxxx-an.a.run.app \
    --allow-unauthenticated

# 出力
Service URL: https://streamlit-cloudrun-xxxxxxxxxxx-an.a.run.app

成功すると、StreamlitのURLが最後に出力されます。以上でCloud Runへのデプロイは完了です。

出力されたURLにブラウザでアクセスしてみます。下図のように画面が表示され、FastAPIからのレスポンスも確認できました。

Cloud Runで公開しているサービスはGCPコンソールで確認できます。


以上のようにCloud RunへFastAPIとStreamlitをデプロイすることができました。

おまけ Dockerイメージのサイズを小さくする

dockerのmulti-stage buildを用いることで、dockerイメージの容量を小さくすることができます。

ホストのpythonパッケージ管理はpoetryを使用しているものとして、dockerfileの例を示します。

fastapi dockerfile
# building stage
FROM python:3.10 as builder

WORKDIR /opt

COPY ./pyproject.toml ./poetry.lock* /opt/

RUN pip install poetry && \
    poetry config virtualenvs.create false && \
    poetry install --no-dev && \
    rm -rf ~/.cache


# production stage
FROM python:3.10-slim

COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/uvicorn

COPY ./app /app
WORKDIR /app

EXPOSE 8080

# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

streamlit dockerfile

# building stage
FROM python:3.10 as builder

WORKDIR /opt

COPY ./pyproject.toml ./poetry.lock* /opt/

RUN pip install poetry && \
    poetry config virtualenvs.create false && \
    poetry install --no-dev && \
    rm -rf ~/.cache


# production stage
FROM python:3.10-slim

COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/local/bin/streamlit /usr/local/bin/streamlit

COPY ./app /app
WORKDIR /app

EXPOSE 8080

# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True

CMD streamlit run main.py \
    --browser.serverAddress="0.0.0.0" \
    --server.port="8080"

以下のようにDockerイメージの容量を小さくすることができます。

REPOSITORY                                  TAG       IMAGE ID       CREATED            SIZE
asia.gcr.io/xxxxxx-xxxxxx/streamlit-image   v1        xxxxxxxxxxxx   x minutes ago      579MB
asia.gcr.io/xxxxxx-xxxxxx/fastapi-image     v1        xxxxxxxxxxxx   x minutes ago      224MB

参考にした記事

タイトルとURLをコピーしました