FastAPI + SQLAlechmy (Postgres) でHerokuにデプロイ

published: 2021/6/20 update: 2021/9/29

Table of Contents

TL;DR

FastAPIはRestfulなAPIをPythonで構築するときに非常に便利なマイクロウェブフレームワークで、パフォーマンスにも優れています。また、型についてサポートしており、Swagger UIでAPIドキュメントが自動的に生成される点も素晴らしいです。

実際にAPIを構築するにあたって、Herokuは無料で利用でき、デプロイも簡単なのでテストサーバーを作成するときに重宝します。Herokuが無料枠でサポートしているのはPostgres SQLだけなので、もしデータベースを絡めようと思うと、必然的にPostgresを使う必要があります。

使い方を書いてある記事はあるのですが、単純にデプロイするだけ、といったことに焦点を当てた記事がなかったので書きました。

Dependencies

ORMとしてsqlalchemyを利用します。また、postgresと接続するためにpsycopg2-binaryを使います。個人的にpipenvを使っているので、Pipfileを用意します。あとはpostのデータには型がついていてほしいのでpydanticを使います。

Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[packages]
fastapi = "0.65.2"
uvicorn = "0.14.0"
sqlalchemy = "1.4.18"
psycopg2-binary = "2.8.6"
pydantic = "1.8.2"

[requires]
python_version = "3.8"

[pipenv]
allow_prereleases = true

FastAPIを試運転

依存関係をインストールします。個人的にいつもapp/下にpythonファイルとかを作成しているので、今回もそうします。

pipenv install
mkdir app
app/main.py
from fastapi import FastAPI

### Start App ###
app = FastAPI()


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

起動してみます。

uvicorn app.main:app --reload --host=0.0.0.0 --port=8002

https://localhost:8002{"message": "Hello World"}で見えていれば成功です。

local環境の構築

dockerを使ってローカルでDBに関してもテストできるようにします。

Dockerfile
FROM python:3.9.2-slim

ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

RUN pip install pipenv

COPY Pipfile /tmp
COPY Pipfile.lock /tmp
WORKDIR /tmp
RUN pipenv install --system && rm -rf /tmp/*

WORKDIR /
docker-compose.yml
version: "3.0"

services:
  api:
    container_name: "api-heroku"
    command: "uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"
    volumes:
      - ./app:/app:Z
    build: .
    restart: always
    tty: true
    ports:
      - 8002:8000

  db:
    image: postgres:11.7
    container_name: postgres-heroku
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    ports:
      - 5432:5432
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=main

モデルの定義

SQLalechemyのためにモデルを定義します。今回はTODOテーブルを作成します。 テーブルは自動で作成されてほしいので、

app/model.py
# Create Table
metadata = MetaData(Engine)
Base.metadata.create_all(bind=Engine, checkfirst=True)

で作成するようにしています。

また、_asdictメソッドがないので、自前で辞書型に変換する関数を定義しています。

app/model.py
from sqlalchemy import Column, create_engine, MetaData
from sqlalchemy.orm import scoped_session, sessionmaker

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql.functions import func
from sqlalchemy.sql.sqltypes import DateTime, Integer, String, TEXT

Engine = create_engine(
    "postgresql://postgres:postgres@postgres-heroku:5432/main",
    encoding="utf-8",
    echo=False,
)

db_session = scoped_session(
    sessionmaker(autocommit=False, autoflush=False, bind=Engine)
)

Base = declarative_base()

# declare for query
Base.query = db_session.query_property()


class Todo(Base):
    __tablename__ = "todo"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    description = Column(TEXT)
    created_at = Column(DateTime, default=func.now())


# Create Table
metadata = MetaData(Engine)
Base.metadata.create_all(bind=Engine, checkfirst=True)


# model to dict
def to_dict(model) -> dict:
    return dict((col.name, getattr(model, col.name)) for col in model.__table__.columns)

TODOに対するPOSTGETを定義します。Postの際に、必ずtitledescriptionをリクエストボディに入れてほしいので、pydanticでDataクラスを定義しています。

app/main.py
from app.model import db_session, Todo, to_dict
from fastapi import FastAPI, HTTPException, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel

### Start App ###
app = FastAPI()

### Start Session ###
db = db_session.session_factory()


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


@app.get("/todos")
async def get_todos():
    q = db.query(Todo)
    todos = [to_dict(qq) for qq in q]
    return todos


class Data(BaseModel):
    title: str
    description: str


@app.post("/todos")
async def post_todos(data: Data):
    todo = Todo(title=data.title, description=data.description)
    try:
        db.add(todo)
        db.commit()
        db.refresh(todo)
    except:
        db.rollback()
        raise HTTPException(status_code=500, detail="Cannot Create Todo")
    return JSONResponse(status_code=status.HTTP_201_CREATED, content="created!")

http://localhost:8002/docsにアクセスしてSwagger UIでGetとPostのテストをしてみてください。このへんまでのcommitです。

Herokuにデプロイ

好みのやり方でプロジェクトを作って、Postgres SQLのアドオンを有効にしてください。

ちょっとハマりどころなのが、herokuでPostgres SQLアドオンを追加すると環境変数としてDATABASE_URLが提供されるんですが、これをそのままcreate_engineに入れてもうまく行かないという点です。というのはDATABASE_URLpostgres://....みたいな感じなんですが、create_engineの引数としてはpostgresql://...みたいな感じである必要があります。

この点を考慮して、create_engineを書き直します。

def create_new_engine():
    import os

    database_url = os.environ.get("DATABASE_URL")

    if database_url is None:
        uri = "postgresql://postgres:postgres@postgres-heroku:5432/main"
        echo = True
    else:
        uri = database_url.replace("postgres", "postgresql")
        echo = False

        return create_engine(url=uri, encoding="utf-8", echo=echo)


Engine = create_new_engine()

のような感じにします。

次にProcfileを書きます。

Procfile
web: uvicorn app.main:app --reload --host=0.0.0.0 --port=${PORT:-5000}

あとはデプロイしたら終わりです。

終わりに

FastAPI、型があって嬉しいですね。 完成品はgithubにあります。

記事に間違い等ありましたら、お気軽に以下までご連絡ください

E-mail: illumination.k.27|gmail.com ("|" replaced to "@")

Twitter: @illuminationK

当HPを応援してくれる方は下のリンクからお布施をいただけると非常に励みになります。

Ofuse

Other Articles

Site Map

Table of Contents

    TL;DR

    Dependencies

      FastAPIを試運転

      local環境の構築

    モデルの定義

    Herokuにデプロイ

    終わりに


当HPを応援してくれる方は下のリンクからお布施をいただけると非常に励みになります。

Ofuse
Privacy Policy

Copyright © illumination-k 2020-2021.