見出し画像

RenderとVercelを使ってFastAPIで書かれたPythonプロジェクトをサービスとしてデプロイする(前編)

Web開発者っぽいDevOps記事を何故か2日に1回書いているCEOです。
CxO的なことをしたい人、募集中です。

ちょっとした用事があってPythonで書いたコードをサービスにせねばならなくなったのです。
AWSやGCPでVMを作ってもいいのだけど、先日書いた記事にようにGitHub Actionsにお願いするにはちょっと難しい。

具体的にはDiscordボットみたいな常時起動型の処理と、Webhookを受け付けるような処理が融合したサービス。
スクレイピングとかドキュメント変換みたいにバッチ起動する感じではなく、常に上がってない小規模開発だとすると、SaaSやIaaSにお任せしたいところ。

NextJSでフロントエンド中心だったらVercelで終わるんだけど、今回はPythonのFastAPIで…ということでVercelには難しい。

RenderとVercelでつくるToDo管理アプリ

Render.comはそんな用途に向いているらしい。これ以外にも色々あるんだけどSEO的に不利な名前が多いですね、なんでこんな名前にするのかな。
フロントエンドはNextJSを使ってVercelにデプロイしてみます。

今回はDiscordボットではなく、いったんToDo管理アプリを作ってみます。

完成品はこちら https://fast-todo-pi.vercel.app/

Free版なのでひっそり止まってるかもです。

ベースになるのはMicrosoftの「VSCode Remote Try Python」というリモート環境にあるpythonを使うプロジェクト。

Create this template→Create a new repositoryで新しいリポジトリを作ります。


このとき、注意。
- .gitignoreの設定はpython
- Publicになってますが普通はprivate

またこの先はCodespacesを使っています。
もちろんVSCodeでも構いませんが、大変さが全然違いますのでCodespaceオススメです。

高速なネットワークに接続されたエディタが使えるだけでなく、docker環境でのLinuxや、そのネットワークに接続されたトンネルも動いちゃいます。

まずは FastAPI関連を一式インストールします。

pip install "fastapi[all]"

もとからある Flaskで書かれた app.py を…

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return app.send_static_file("index.html")

FastAPIに書き直していきます

from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug")

uvicornをポート8000で起動して{"message":"Hello World"}を返すだけのコードです。
とりあえず起動してみます

python3 app.py

すると右下にポート8000番につなげるポップアップが表示されるのでブラウザで開くを選択します。
トンネルが自動で掘られて、結果が表示できます。

やったぜ世界。まだ何も始まってません。

アクセスしたURLに /docsを付けてアクセスすると、APIドキュメントも生成されています。テスト実行もできます。

データベースを使ったToDoアプリを作ってみる

コンソール側で Ctrl+C で終了して、今度はデータベース利用アプリを作ってみます。

AlembicSQLAlchemyをこの環境にインストールします。

pip install alembic SQLAlchemy

テスト用のToDoアプリなので、ファイルデータベース「Sqlite」を使います。

データベースの管理情報を初期化します。

alembic init migrations

alembic.iniが生成されるので、63行目のsqlalchemy.urlの行を編集します。

# sqlalchemy.url = driver://user:pass@localhost/dbname
sqlalchemy.url = sqlite:///sample.sqlite

settings.pyをプロジェクトのルートに作成しデータベースの接続情報などを記載します。

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///sample.sqlite"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 

Base = declarative_base()

記載します、って言ってますけどほぼ自動補完で書かれていきます。


models.pyでデータベースのテーブルを定義します。
- idは数値でプライマリーキー
- titleは文字列型

from sqlalchemy import Column, Integer, String, DateTime
from datetime import datetime
from settings import Base


class TodoModel(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True)
    title = Column(String)
    created_date = Column(DateTime, default=datetime.utcnow)

migrations/env.pyを編集、定義したモデルをAlembicに伝えます。

既にあるimport文の下に以下を追加

from settings import Base
from models import *


24行目のtarget_metadataグローバル変数を以下のように編集。

target_metadata = Base.metadata


Ctrl+Sで保存して、Pythonコードで定義したモデルを元に、テーブルやカラムをデータベースに作成するコードを生成します。

alembic revision --autogenerate -m "create todo table"

実行すると、migrations/versions/XXXXXXXXXXXX_create_todo_table.pyが生成されます。以下の様に、DB操作コードが魔法のように生成できます。

以下のコマンドを実行してデータベースに上記のコードを適用させます。

alembic upgrade head

エンドポイントの作成

app.pyを完成させます

from fastapi import FastAPI, Depends
import uvicorn

from schemas import PostTodo
from models import TodoModel
from settings import SessionLocal

from sqlalchemy.orm import Session


app = FastAPI()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.get("/")
async def root():
    return {"message": "Hello World"}


# データベースからToDo一覧を取得するAPI
@app.get("/todo")
def get_todo(
        db: Session = Depends(get_db)
    ):
    # query関数でmodels.pyで定義したモデルを指定し、.all()関数ですべてのレコードを取得
    return db.query(TodoModel).all()

# ToDoを作成するAPI
@app.post("/todo")
def post_todo(
        todo: PostTodo, 
        db: Session = Depends(get_db)
    ):
    # 受け取ったtitleからモデルを作成
    db_model = TodoModel(title = todo.title)
    # データベースに登録(インサート)
    db.add(db_model)
    # 変更内容を確定
    db.commit()

    return {"message": "success"}

# ToDoを削除するAPI
@app.delete("/todo/{id}")
def delete_todo(
        id: int,
        db: Session = Depends(get_db)
    ):
    delete_todo = db.query(TodoModel).filter(TodoModel.id==id).one()
    db.delete(delete_todo)
    db.commit()

    return {"message": "success"}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug")

ブラウザからデータを受け取る型を定義する schemas.py 作成します。

from typing import List, Optional
from pydantic import BaseModel


class PostTodo(BaseModel):
    title: str

動作確認とAPIドキュメントの自動生成

FastAPIを起動します。

python3 app.py 

ブラウザを開き、https://○○○○.app.github.dev/docsにアクセスします。するとFastAPIの特徴である自動生成されたAPIドキュメントを開くことができます。


APIの動作確認

このページでAPIを試すことができます。
まずはToDoを作成してみます。Request body内の "string"を書き換え、ToDoのタイトルとして記入します。ExceuteをクリックするとAPIを実行できます。


GET /todo で追加したToDoがゲットできることを確認します。

sample.sqlite というファイルに格納されていきます(バイナリファイルなので読めませんがアップデートはかかっています)。
こんな感じのデータです。

[
  {
    "title": "string",
    "id": 1,
    "created_date": "2024-08-13T15:30:58.360510"
  },
  {
    "title": "牛乳を買ってくる",
    "id": 2,
    "created_date": "2024-08-13T15:31:39.294597"
  },
  {
    "title": "牛乳を買ってくる",
    "id": 3,
    "created_date": "2024-08-13T15:31:42.791192"
  }
]

最後にIDを指定して 2番目を削除してみましょう。

再度 GETしてみたら減ってました。

ここまでの手順は参考にさせていただきました。
素晴らしいですさすがNTTデータさん。惜しみない拍手を送ります👏👏👏


requirements.txt

上記の手順はFastAPIをデプロイするところまで行きませんので、続けていきます。
まずは requirements.txt を以下のように更新します。

requirements.txt 

fastapi
uvicorn
SQLAlchemy
python-dotenv

自分のリポジトリにコミットします

Codespacesの左上の「ソース管理」で、コミット名をつけてコミット。

忘れちゃいけないアップデートは「変更の同期」。

これでGitHubのリポジトリに格納されます。

Render.comへのデプロイ

一応無料でできます。アカウントを作成して、
「Web Service」を作成。

作ったFastAPIコードのリポジトリを選択

Build Command: pip install -r requirements.txt
Start Command: uvicorn app:app --host 0.0.0.0 --port $PORT

$PORT は Render.com が自動的に設定する環境変数です。

【自動設定】: Render.com は、サービスをデプロイする際に自動的に PORT 環境変数を設定します。この変数には、アプリケーションがリッスンすべきポート番号が含まれています。

【動的ポート割り当て】クラウドプラットフォームでは、セキュリティとリソース管理の観点から、固定のポート番号を使用するのではなく、動的にポートを割り当てることが一般的です。$PORT を使用することで、Render.com が割り当てたポートでアプリケーションを起動できます。

【使用方法】スタートコマンドで $PORT を指定することで、Render.com が設定したポートでアプリケーションが起動します。
例:uvicorn app:app --host 0.0.0.0 --port $PORT

【ローカル開発との互換性】

ローカル環境では PORT 環境変数が設定されていない可能性があります。
そのため、アプリケーションコード内で以下のようにデフォルト値を設定することが良い習慣です:

import os
port = int(os.getenv("PORT", 8000))

プラットフォームが自動的に管理するため、開発者が明示的に設定する必要がありません。

このように、$PORT を使用することで、Render.com の環境に柔軟に対応でき、ローカル開発環境とクラウド環境の両方で同じコードベースを使用できます。これは、クラウドネイティブな開発practices の一部であり、アプリケーションのポータビリティと柔軟性を向上させます。

あとは「Deploy」するだけ

1-2分待つと出来上がりです。
左上にアクセスできる onrender.com のURLが作られていますのでアクセスしてみましょう。

やったぜ!
https://???.onrender.com/docs でAPIドキュメントが表示されています。

なお render.yml を書くことでこの設定も GitHubから指定できます。

services:
  - type: web
    name: fastapi-todo-api
    env: python
    buildCommand: pip install -r requirements.txt
    startCommand: uvicorn app:app --host 0.0.0.0 --port $PORT
    envVars:
      - key: PYTHON_VERSION
        value: 3.9.0

GitHub側に新規の更新を加えると Render側で自動で更新が走ります。
ただし、今回はFree版サーバーなので放置すると停止します。

フロントエンド側の開発(手抜き準備編)

生成AI時代の開発者なんで、ここから先はClaudeで手抜きします。
ここまでのファイルをzipにしてダウンロードします。

Claude 3.5 Sonnetに、 

  • models.py

  • schemas.py

  • settings.py

  • app.py

をドロップしてわたします。

「このToDoアプリのフロントエンドのnext.jsコードを作ってください」

あとはどんな言語でも作れちゃうんですが、例えば NextJSの場合はこんな感じの構成になるはず。

my-nextjs-todo-app/
├── pages/
│   └── index.js
├── components/
│   ├── TodoForm.js
│   └── TodoList.js
├── package.json
└── next.config.js

<後編に続きます>

リポジトリへのリンクもこちら

RenderとVercelを使ってFastAPIで書かれたPythonプロジェクトをサービスとしてデプロイする(後編)


いいなと思ったら応援しよう!

しらいはかせ(AI研究/Hacker作家)
チップとデール!チップがデール!ありがとうございましたー!!