💻

無料でFastAPIで作ったAPIをデプロイする

2023/03/21に公開

はじめに

今回、はじめてZennで記事を書いてみることにしました。
記事を書くことに慣れていないため、おかしな点があるかもしれませんがご了承下さい。

初めての記事の題材に、FastAPIをDeta Spaceにデプロイするというテーマを選んだ理由は、実際に自分自身で使用していて、日本語の記事があまり見つからなかったためです。
少しでもFastAPIをデプロイしたいという方のお役に立てるよう、記事を書くことにしました。

実装やデプロイを完了した後に記事を執筆しているため、キャプチャがない部分や省略した箇所がありますが、ご了承ください。

FastAPIとは

個人的に一言でまとめると簡単、早い、軽いです。
コードを書くと自動的にAPIドキュメントが生成される点が特に優れていると感じています。
詳しくはFastAPIの公式サイトに特徴がまとまっているので参照してください。

Deta Spaceとは

以前はDeta Cloudという名称のサービスでしたが、現在はDeta Spaceに名称が変更されています。

以下のクイックスターターガイドが用意されているのでFastAPI(Python)以外も簡単にデプロイできると思います。

  • Next
  • Nuxt
  • Svelte
  • Node.js
  • Python
  • Go
  • Rust

Deta Spaceの設定

はじめにDeta Spaceの設定から行います。

アカウント登録

公式サイト右上の「Sign Up」からアカウント登録を行います。

CLIのセットアップ

Deta Spaceでは専用のCLIを使ってデプロイを行います。
下記からCLIのインストールとアクセストークンを用いた認証を行います。

Setting up the CLI

Collectionの作成

RDBでいうCollectionがデータベース、Baseがテーブルという役割になります。
CollectionsNew Coollectionから新規のCollectionを作成します。
今回はusersという名前で作成します。

作成したCollectionのData Keyを作成

作成したCollectionを選び、右上のCollection SettingsCreate new data keyでData keyを作成できます。
作成したData keyはDBアクセスに使うので保存しておきます。

今回デプロイするFastAPIについて

この記事ではPythonのバージョンは3.9を使用しています。

ローカル環境では、Pyenvを使用して仮想環境を構築し、Uvicornを利用して実行します。
本記事では仮想環境でのセットアップや動かし方については説明しませんが、必要に応じて別途調べて設定してください。

今回サンプルとして作成したAPIはusersというテーブルへの基本的なCRUD機能です。

エンドポイントは以下の5つです。

No HTTP URI 名前
1 GET /users 全件取得
2 POST /users 新規作成
3 GET /users/{key} 1件取得
4 DELETE /users/{key} 削除
5 PATCH /users/{key} 更新

フォルダ構成

APIの実装はmain.pyファイルにまとめることもできますが、今回はより拡張性を持たせるために、apisディレクトリにファイルを分割しています。

root/
├ .space/
├ .venv/
├ apis/
│ ├ __init__.py
│ └ users.py
├ .spaceignore
├ database.py
├ main.py
├ requirements.txt
└ Spacefile

各ファイルの説明と今回実装したソースコード

.space/

初回のspace new実施後に自動的に作成されます。

.venv/

ローカルの仮想環境です。

apis/

今回はusersだけですが、users以外のエンドポイントを用意する場合はファイルを作成します。

apis/users.py

この記事のCRUDのメインとなるファイルです。
型定義やDB処理を別ファイルに切り出すことで、よりスッキリした実装が可能です。

DBへのアクセスはdatabase.pyで定義したget_db_usersをDIを利用してアクセスしています。
他のサービスでDBにMySQLを使う際にDIを使っていたため同じように実装しましたが、必ずしもDIを行わなくても良いと思います。

DetaのSDKの使い方については公式ドキュメントをご参照ください。

from datetime import datetime
from typing import Sequence

from deta import _Base
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field

from database import get_db_users


class UserReadResponse(BaseModel):
    key: str = Field(..., description="キー")
    name: str = Field(..., description="名前")
    created_at: str = Field(..., description="作成日時")
    updated_at: str = Field(..., description="更新日時")


class UserCreateRequest(BaseModel):
    name: str = Field(..., description="名前")


class UserCreateResponse(BaseModel):
    key: str = Field(..., description="キー")
    name: str = Field(..., description="名前")
    created_at: str = Field(..., description="作成日時")
    updated_at: str = Field(..., description="更新日時")


class UserUpdateRequest(BaseModel):
    name: str = Field(..., description="名前")


router = APIRouter()


@router.get("")
def list_users(
    db_users: _Base = Depends(db_users),
) -> Sequence[UserReadResponse]:
    response = db_users.fetch()
    users: Sequence[UserReadResponse] = response.items

    return users


@router.post("")
def create_user(
    user_body: UserCreateRequest, db_users: _Base = Depends(db_users)
) -> UserCreateResponse:
    user_body_dict = user_body.dict()
    now = str(datetime.now())

    user_body_dict["created_at"] = now
    user_body_dict["updated_at"] = now

    user: UserCreateResponse = db_users.put(user_body_dict)

    return user


@router.get("/{key}")
def get_user(key: str, db_users: _Base = Depends(db_users)) -> UserReadResponse:
    user: UserReadResponse = db_users.get(key)
    if not user:
        raise HTTPException(status.HTTP_404_NOT_FOUND)

    return user


@router.delete("/{key}")
def delete_user(key: str, db_users: _Base = Depends(db_users)) -> None:
    db_users.delete(key)


@router.patch("/{key}")
def update_user(
    key: str, user_body: UserUpdateRequest, db_users: _Base = Depends(db_users)
) -> None:
    user_body_dict = user_body.dict()
    now = str(datetime.now())

    user_body_dict["updated_at"] = now

    db_users.update(user_body_dict, key)

database.py

"data_key"Collectionsの任意のCollectionを選び、Collection SettingsでData Keyを作成できるので、作成したKeyに変更します。

from deta import Deta, _Base


def get_deta() -> Deta:
    deta = Deta("data_key")

    return deta


def get_db_users() -> _Base:
    deta = get_deta()
    db_users = deta.Base("users")

    yield db_users

main.py

from fastapi import FastAPI

from apis import users

app = FastAPI()

app.include_router(users.router, prefix="/users", tags=["Users"])

requirements.txt

deta
fastapi
uvicorn

※uvicornはローカルでテストする時のみ使用するためデプロイ時には不要

.spaceignore

このファイルに追加したファイル、ディレクトリはプッシュ時のアップロードから除外することができます。
.gitignoreと似た働きをします。
__pycache__は追加しなくてもデフォルトで除外されるため、今回はローカルの仮想環境として作成される.venvを追加しています。

.venv

Spacefile

Deta Spaceで動作させるためのアプリの構成を定義するファイルです。
.space/と同様に、初回のspace new実施後に自動的に作成されます。

srcにはmain.pyのパスを書きます。
デプロイするフォルダの直下以外にmain.pyがある場合はパスを修正する必要があります。

# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
v: 0
micros:
  - name: ExampleFastAPI
    src: .
    engine: "python3.9"

FastAPIをDeta Spaceへデプロイするメリット・デメリット

メリット

Deta Space独自の書き方を覚える必要がありますが、完全無料でDBを用いたREST APIをデプロイできるところが非常に魅力的です。
負荷テストなどはしていないのでどのぐらいの規模まで使用できるのかは分かりませんが、レスポンスも早くちょっとしたAPIを作りたい場合非常に便利だと感じました。

デメリット

まだサービスが提供されたばかりで仕様の変更やユーザー毎の制限が変わることが懸念されます。
Deta Cloud時代に試しでデプロイをしてしばらくたってから、ファイルを修正してデプロイ仕様としたところ、仕様がガラッと変わっていてデプロイできないということが発生しました。
デプロイ先としてDeta Spaceで本当に大丈夫なのか?と不安になってしまう点は一番のデメリットかと思います。
あとは日本語のドキュメントが存在しないという点もデメリットですが、公式ドキュメントが充実しているのでその辺はなんとかなるかなと思います。

Deta Spaceに欲しい機能

  • CDを組みたい
    GitHubにプッシュされたら自動でデプロイされたら便利だと感じました。
    CLIでアクセスコードを使ってログインをする必要があるので自動デプロイができませんでした。
    アクセスコードを引数にしてログイン後デプロイができたら最高です。

  • ブラウザ上での操作性
    特にCollectionですが操作性が直感的ではないのでもう少しUI/UXが改善したらなと思います。
    あとは私が見つけられていないだけなのかもしれませんが、誤って作成したCollectionやBaseを削除する方法が見つかりません。
    もしかしたら削除はできないのかも?

  • 日本語対応
    URLを見ると多言語化対応を見据えた構成になっているので、いずれされるのだろうと予想しています。
    やはり日本語化されているだけで手を出すハードルがいっきに下がるので、多言語化対応される時はぜひ日本語も追加してほしいところです。

終わりに

Deta Spaceはフロントエンドのデプロイも可能なので、次はフロントエンドも開発してデプロイして見たいと思います。
VueやReactの無料デプロイはNetlifyやVercelが主流かと思いますが、Deta Spaceでバックエンドもフロントエンドもデプロイできて、さらにDBも賄えるならDeta Spaceという選択肢もありだと思っています。

記事を執筆することにハードルを感じていましたが、自分自身のアウトプットも兼ねてどんどん書いていきたいと思います。

Discussion