32
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonAdvent Calendar 2022

Day 25

PythonとDockerで作るAPI FastAPIを最速で理解する

Last updated at Posted at 2022-12-25

はじめに

本記事はFastAPIでバックエンドを開発する方法について記載しています。

FastAPIは、PythonでAPIを開発するためのモダンで高速(高性能)なWebフレームワークです。

Pydanticによる型ヒントを使用したデータの検証や、OpenAPIドキュメントを自動的に生成することができます。
また非同期プログラミングをサポートし、SQLAlchemyやTortoise-ORMと互換性、依存性の注入(DI)を考慮した実装も可能です。

Pythonを用いたマイクロサービスのバックエンド開発などには、最適なソリューションです。

FastAPIを使用する理由

なぜFastAPIを使用するかについて、FastAPIの主な特徴について以下に記載しています。

シンプル

後述するチュートリアルの通りに数行でAPIを開発することができます。

デフォルトの状態で適切に最適化されていますが、オプションを使用してパラメータを調整することもできます。

高速

Webフレームワークの性能評価の指標を算出しているWeb Framework Benchmarksより、2022-07-19 Round 21をご参照ください。(本記事執筆時点で最新のRound)

性能を基準に評価する際は、上記サイトは参考になると思います。

標準化

FastAPIは、OpenAPI Specificationを標準として、JSON Schemaと完全互換性があります。

OpenAPI Specification(以下、OAS)は、Linux Foundation共同プロジェクトであるOpenAPI Initiative内のコミュニティ主導のオープン仕様です。またOpenAPI自体がJSONスキーマに基づいています。

APIの標準およびプログラミング言語に依存しないインターフェイスによって、HTTPベースのコミュニケーションを簡素化します。

APIについて学ぶ

システム開発におけるソフトウェアの結合は、RPC、SOAP Webサービス、Web APIなどテクノロジーの進化によって、抽象化が進みました。

FastAPIもAPIを実現するための手段の一つですが、正しく使いこなすためには、Representational State Transfer(以下、REST)の概念や、API設計の考え方について理解していることが重要です。

本記事はFastAPIを題材にしているので深くは掘り下げませんが、APIの概念につて以下に記載しています。

APIとは

APIとは、アプリケーションプログラミングインタフェースの略称です。

APIには、以下のような種類があり、それぞれ用途が異なります。

  • スマートフォンでカメラを起動して、写真を撮影するなどのハードウェアAPI
  • Windows OS内部などで使用されるOS API
  • 外部に公開していない内部利用を目的とした、インターナルAPI
  • 外部から利用可能なパブリックAPI(Open APIとも呼ばれ、YAMLまたはJSONで記述)

APIを設計するということは、利用するユーザーとコンポーネント間のインターフェースを定義するということです。APIはシステムにおける内部の実装を抽象化したものすぎません。

現実世界で例えると、ATMでお金を下ろす場合、どのような仕組みで動いているかについて気にする人は少ないと思います。
興味があるのは、どこにカードを入れて、どのような操作をしたらお金を預けたり、引きだしができるなど、インターフェースの仕様ではないでしょうか。

API設計の重要性

API設計がなぜ重要なのか考えてみましょう。

例えば、APIが適当に設計されている場合は、以下のような問題が発生する可能性があります。

  • APIが正しく使わなれないため、十分に活用されない
  • APIが分かりにくいことによるユーザーからの問い合わせや、クレームが増えることによる運用コストの増加
  • 設計の欠陥によって、データ漏洩などセキュリティに関わる脆弱性に繋がる

API設計は、原理原則に基づいたテクノロジーに対する理解や、ユーザー側からの視点など多岐にわたります。

APIは誰のためのものか

APIは目標を達成するための手段として、ユーザーが欲しいものを提供することが求められます。

例えば、リモコンはシンプルです。
テレビだろうがオーディオだろうが、大体は手に取ることができるサイズで、同じようなボタンの配置をしていて、何を目的としているかがはっきりと分かります。

API設計では、APIを使用するユーザーから見て、以下に示すような配慮が必要不可欠です。

  • そのAPIで何ができて、どのようにして使用するのか
  • APIをリクエストするための方法
  • APIが返すレスポンスの期待値、例外発生時のエラーメッセージ

REST API

REST API(またはRESTful API)は、RESTアーキテクチャスタイルの制約に従って策定されたAPIのことです。

2000年にRoy Fieldingによって発表された、以下論文によってその設計思想が広まりました。

RESTfulの原則は以下の通りです。

  • クライアント、サーバー、リソースからなるクライアント/サーバーアーキテクチャで、要求はHTTP経由で処理される
  • ステートレスな通信は各要求を独立し、分離する
  • クライアントは、サーバーのレスポンスをキャッシュすることができる
  • 統一インターフェースは以下の要件を満たすこと
    • 要求されるリソースは識別可能
    • クライアントは表現を用いてリソースを操作できる
    • 自己記述的メッセージによって、クライアントが情報をどのように処理すべきかを記述する十分な情報が含まれている
    • HATEOAS
  • 要求された情報は階層化システムである
  • コードオンデマンドはサーバーからクライアントに実行可能コードを送信することで、クライアントは機能を拡張できる

OpenAPI Specification

OASは、REST API記述フォーマットです。

以前はSwagger Specificationと呼ばれていたもので、2015年11月にOAIに寄贈され、2016年1月に現在の運用になっています。OpenAPIの詳細については、What Is OpenAPI?をご参照ください。

OASドキュメントはテキストファイルで、リソース、アクション、パラメータ、レスポンスを記述します。
Swagger Editorを使用して編集することができます。

OASを使用することの最大のメリットは、単純に開発を効率よく進めることができるの一言に尽きると思います。

例えば、フロントエンドはバックエンド側の実装を待たなくても、OASを使用してモックサーバを起動してフロントエンドの開発スピードを上げることができます。

OAS自体がドキュメントになるので、エクセルなどによるドキュメント管理が不要です。
またOASに準じて宣言的に書くことは、曖昧な記述がなくなるため、仕様が明確になります。

FastAPI開発

APIの概念がある程度理解できた段階で、FastAPIの開発を行ないましょう。

前提として、本記事の環境は以下の通りです。

項目 バージョン等
OS macOS
Python 3.9.4
fastapi 0.88.0
Docker 20.10.6

チュートリアル

チュートリアルの最初のステップでは、FastAPIの基本的な機能について学ぶことができます。

早速、チュートリアルを行うために、Pythonの仮想環境で試してみましょう。
Pythonの仮想環境はvenvを使用して作成します。

まずは仮想環境を作成するために、ターミナルなどを起動して、以下のコマンドを実行します。
<Dir>には任意のディレクトリ名を指定します。

$ python3 -m venv <Dir>
$ source <Dir>/bin/activate

仮想環境構築後、以下のコマンドを実行してすべてのオプションの依存関係と機能をインストールします。

$ pip3 install "fastapi[all]"

pipでインストール完了後、以下のコマンドを実行すると、必要なパッケージについてインストールされていることが確認できます。

$ pip3 list

出力例
Package           Version
----------------- ---------
anyio             3.6.2
certifi           2022.12.7
click             8.1.3
dnspython         2.2.1
email-validator   1.3.0
fastapi           0.88.0
h11               0.14.0
httpcore          0.16.3
httptools         0.5.0
httpx             0.23.1
idna              3.4
itsdangerous      2.1.2
Jinja2            3.1.2
MarkupSafe        2.1.1
orjson            3.8.3
pip               20.2.3
pydantic          1.10.2
python-dotenv     0.21.0
python-multipart  0.0.5
PyYAML            6.0
rfc3986           1.5.0
setuptools        49.2.1
six               1.16.0
sniffio           1.3.0
starlette         0.22.0
typing-extensions 4.4.0
ujson             5.6.0
uvicorn           0.20.0
uvloop            0.17.0
watchfiles        0.18.1
websockets        10.4

次にmain.pyを作成した後に、以下のコマンドを実行してuvicornで起動します。

  • main.py
from fastapi import FastAPI

app = FastAPI()


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

$ vi main.py
$ uvicorn main:app --reload

INFO:     Will watch for changes in these directories: ['/Users/python/Desktop']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [72817] using WatchFiles
INFO:     Started server process [72819]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

上記のような出力を確認後、Chrome ブラウザを起動します。
http://127.0.0.1:8000/にアクセスすると、{"message":"Hello World"}のJSON形式のメッセージが確認できます。
またデベロッパー ツールを使用すると、レスポンスヘッダからuvicornなどが確認できます。

スクリーンショット 2022-12-21 0.19.44.png

http://127.0.0.1:8000/docsにアクセスすると、自動生成されたSwagger UIで提供されているインタラクティブなドキュメントが確認できます。

スクリーンショット 2022-12-21 0.37.00.png

ここまで5分もかからないと思いますが、たったの数行でAPIの基礎となる土台を作ることができました。
引き続き、チュートリアルのユーザーガイドを参照することで、基本的なAPI開発の実装に関して学ぶことができます。

Dockerを使用したAPI開発

Dockerを使用してAPI開発を行う例について以下に記載します。

ソースをマウントしている状態になるため、コードの変更は自動的に行われることで、柔軟に開発を行うことができます。

以下ではAPIの動きを理解するために、ルールティングの設定と、セキュリティ対策としてAPIキーでバックエンドを保護する実装を行っています。

環境構築

ソースを格納しているカレントディレクトリ(.)を起点にした、本記事のサンプルコードは以下の構成です。

.
├── backend
│   ├── Dockerfile
│   └── api
│       ├── main.py
│       ├── routers
│       │   └── test.py
│       └── util
│           └── util.py
└── docker-compose.yaml

  • docker-compose.yaml
version: '3'

services:
  
  demo-api:
    build:
      context: "./backend"
      dockerfile: "Dockerfile"
    ports:
      - 8000:8000 
    volumes:
      - ./backend/api:/src
    environment:
      - ENVIRON

  • Dockerfile
FROM python:3.9-buster
ENV PYTHONUNBUFFERED=1

WORKDIR /src
COPY api ./

RUN pip3 install --upgrade pip
RUN pip3 install fastapi
RUN pip3 install "uvicorn[standard]"
RUN pip3 install requests

ENTRYPOINT ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"]

FROMで指定しているpython:3.9-busterは、Debian 10を基にしたイメージです。
またdockerhubに格納されているpythonのイメージはOfficial Imagesのpythonから確認できます。名前にslimが付いているイメージは軽量です。

  • main.py
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security.api_key import APIKeyHeader, APIKey
from starlette.status import HTTP_403_FORBIDDEN

from routers import test
from util import util

correct_key: str = util.get_apikey()
api_key_header = APIKeyHeader(name='Authorization', auto_error=False)

async def get_api_key(
    api_key_header: str = Security(api_key_header),
    ):
    if api_key_header == correct_key:
        return api_key_header
    else:
        raise HTTPException(
            status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
        )

app = FastAPI()
app.include_router(test.router, dependencies=[Depends(get_api_key)], tags=["Test"])

@app.get("/")
def read_root():
    return {"status": "ok"}
  • test.py
import os
from typing import Dict, List, TypedDict

from fastapi import APIRouter, Depends, HTTPException

ENVIRON = os.environ['ENVIRON']

router = APIRouter()


@router.get("/v1/list/fruit")
async def get_list_fruit() -> Dict:

    if ENVIRON == 'dev':
        response = {
                    'name': 'apple',
                    'price': '100',
                   }
    elif ENVIRON == 'prd':
        response = {
                    'name': 'orange',
                    'price': '100',
                   }                   
        
    return response

  • util.py
def get_apikey() -> str:
    response = "dummy"

    return response

本記事ではサンプルになるので便宜上、上記のような表現にしています。
実際に実装する場合、APIキーの値などはハードコーディングせずに、シークレット管理システムなどから取得するようにしましょう。

上記コードを配備後、以下のコマンドを実行してデプロイします。

$ export ENVIRON='dev'
$ docker-compose up -d

出力例
Docker Compose is now in the Docker CLI, try `docker compose up`

Building demo-api
[+] Building 40.3s (13/13) FINISHED                                                                                                 
 => [internal] load build definition from Dockerfile                                                                           0.0s
 => => transferring dockerfile: 306B                                                                                           0.0s
 => [internal] load .dockerignore                                                                                              0.0s
 => => transferring context: 2B                                                                                                0.0s
 => [internal] load metadata for docker.io/library/python:3.9-buster                                                           2.5s
 => [auth] library/python:pull token for registry-1.docker.io                                                                  0.0s
 => [internal] load build context                                                                                              0.0s
 => => transferring context: 3.81kB                                                                                            0.0s
 => [1/7] FROM docker.io/library/python:3.9-buster@sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833    23.9s
 => => resolve docker.io/library/python:3.9-buster@sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833     0.0s
 => => sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833 988B / 988B                                     0.0s
 => => sha256:c7c50787e2da71205402343dd1233b3ca6ebe70c7e790f6ba7d856b81b893200 50.45MB / 50.45MB                               2.7s
 => => sha256:821dc261e045ba09851c52ed28be3b9ecc9fe8c923fba05854ab0fd2fa38ceca 10.00MB / 10.00MB                               1.9s
 => => sha256:a21bc1f5190fa4f5d13dbdec6adc8d65ae2d3025411ecab367f616831dd570cb 2.22kB / 2.22kB                                 0.0s
 => => sha256:509942a964375108ec41c5663ed58a9d5f52f49336a113f29688ce599b9b0bee 8.51kB / 8.51kB                                 0.0s
 => => sha256:aff359114acb7d843de09375f87669fffb0abd1125c16f96431bc3c4173b48f8 7.86MB / 7.86MB                                 1.4s
 => => sha256:3790459d63d5fdf148a092f1857010796dfa217329491b66a015a40de92f3db9 51.87MB / 51.87MB                               4.8s
 => => sha256:c09b016fff62bb8002f99d7e639c0a2c0ce5a774796aae1e446e99deeb1ac01d 191.89MB / 191.89MB                            10.5s
 => => sha256:6aa81cf5366ed8cc72bf3e9865b2a33a023c602226b7c83b584b33fa1c443246 6.15MB / 6.15MB                                 3.4s
 => => extracting sha256:c7c50787e2da71205402343dd1233b3ca6ebe70c7e790f6ba7d856b81b893200                                      3.6s
 => => sha256:4c2f5a834e3b5c8b7a0195ef5d22bc8480e9879cd4c38b93dcd0df9bbe559ad6 19.92MB / 19.92MB                               5.0s
 => => sha256:1d79b8cadecb9bf336caef07c858791fef8942063d9da716cee84e03aa9f5e00 233B / 233B                                     5.1s
 => => sha256:146e10e712ef0ac2ba6baf3895242f97e3dd52e72f02bbdbbb21700f8362391c 2.88MB / 2.88MB                                 5.7s
 => => extracting sha256:aff359114acb7d843de09375f87669fffb0abd1125c16f96431bc3c4173b48f8                                      0.6s
 => => extracting sha256:821dc261e045ba09851c52ed28be3b9ecc9fe8c923fba05854ab0fd2fa38ceca                                      0.5s
 => => extracting sha256:3790459d63d5fdf148a092f1857010796dfa217329491b66a015a40de92f3db9                                      3.3s
 => => extracting sha256:c09b016fff62bb8002f99d7e639c0a2c0ce5a774796aae1e446e99deeb1ac01d                                      9.3s
 => => extracting sha256:6aa81cf5366ed8cc72bf3e9865b2a33a023c602226b7c83b584b33fa1c443246                                      0.4s
 => => extracting sha256:4c2f5a834e3b5c8b7a0195ef5d22bc8480e9879cd4c38b93dcd0df9bbe559ad6                                      0.9s
 => => extracting sha256:1d79b8cadecb9bf336caef07c858791fef8942063d9da716cee84e03aa9f5e00                                      0.0s
 => => extracting sha256:146e10e712ef0ac2ba6baf3895242f97e3dd52e72f02bbdbbb21700f8362391c                                      0.3s
 => [2/7] WORKDIR /src                                                                                                         0.3s
 => [3/7] COPY api ./                                                                                                          0.1s
 => [4/7] RUN pip3 install --upgrade pip                                                                                       4.7s
 => [5/7] RUN pip3 install fastapi                                                                                             3.6s
 => [6/7] RUN pip3 install "uvicorn[standard]"                                                                                 2.9s
 => [7/7] RUN pip3 install requests                                                                                            1.8s
 => exporting to image                                                                                                         0.4s
 => => exporting layers                                                                                                        0.4s
 => => writing image sha256:5f62e853a8f8569a6c75bc7b4a59f9912e855a890022b3c2e9d647808d840626                                   0.0s
 => => naming to docker.io/library/sample_demo-api                                                                             0.0s
WARNING: Image for service demo-api was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating sample_demo-api_1 ... done

以下のコマンドを実行し、バックエンドのコンテナが正常に起動できていることを確認します。

$ docker logs -f sample_demo-api_1

INFO:     Will watch for changes in these directories: ['/src']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [1] using WatchFiles
INFO:     Started server process [8]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

ルーティング確認

デプロイ完了後、Chrome ブラウザからhttp://localhost:8000/にアクセスします。

レスポンスのメッセージ及びStatus Code200 OKが返ってきていることが確認できます。

スクリーンショット 2022-12-23 0.49.18.png

次にルーティング設定している、http://localhost:8000/v1/list/fruitにアクセスします。
レスポンスのメッセージ及びStatus Code403 Forbidden が返ってきていることが確認できます。

スクリーンショット 2022-12-23 0.49.34.png

APIキーで保護しているため、このままではアクセスすることはできません。
HTTPリクエストのヘッダに、APIキーを付与してリクエストを送る必要があります。

app.include_router()関数を使用することで、APIのエンドポイントが追加できることが分かります。
ルーティングの仕様など、他のWebフレームワークからインスピレーションを受けていることが代替ツールから受けたインスピレーションと比較に記載されています。:point_up:

APIキーによるバックエンドの保護

http://127.0.0.1:8000/docsにアクセスし、Swaggerの画面からPIキーを設定してみましょう。

認証を行うために、下三角の箇所を押します。

スクリーンショット 2022-12-23 0.59.13.png

「Try it out」を押します。

スクリーンショット 2022-12-23 0.59.29.png

「Excute」を押すことで、リクエストを送ることができますが、この状態では先ほど同じ結果になるので、右上の鍵マークのアイコンを押します。

スクリーンショット 2022-12-23 1.00.14.png

「Available authorizations」の画面が表示されるので、「Value」にはAPIキーの値を入力して、「Authorize」を押します。

スクリーンショット 2022-12-23 1.00.31.png

「Close」を押します。

スクリーンショット 2022-12-23 1.00.35.png

再度、「Excute」を押します。
今度は、APIのレスポンスが確認できます。

スクリーンショット 2022-12-23 1.00.45.png

APIキーを保護する仕組みを実現しているのは、dependencies=[Depends(get_api_key)]による箇所です。
依存性の注入の詳細については、Dependenciesをご参照ください。

環境変数を変えてデプロイする

docker-compose.yamlenvironmentで環境変数を設定しています。

環境変数を変えてデプロイしてみましょう。
以下のコマンドを実行して、一度コンテナを削除します。

$ docker rmi -f sample_demo-api

Untagged: sample_demo-api:latest

環境変数ENVIRONprdに設定してデプロイします。

$ export ENVIRON='prd'
$ docker-compose up -d

dockerの仕組みとしてCACHEDと表示されている通りに、2回目以降はキャッシュを利用してビルドしていることが確認できます。

出力例
Docker Compose is now in the Docker CLI, try `docker compose up`

Building demo-api
[+] Building 2.1s (13/13) FINISHED                                                                                                  
 => [internal] load build definition from Dockerfile                                                                           0.0s
 => => transferring dockerfile: 37B                                                                                            0.0s
 => [internal] load .dockerignore                                                                                              0.0s
 => => transferring context: 2B                                                                                                0.0s
 => [internal] load metadata for docker.io/library/python:3.9-buster                                                           1.8s
 => [auth] library/python:pull token for registry-1.docker.io                                                                  0.0s
 => [1/7] FROM docker.io/library/python:3.9-buster@sha256:5e28891402c02291f65c6652a8abddedcb5af15933e923c07c2670f836243833     0.0s
 => [internal] load build context                                                                                              0.0s
 => => transferring context: 485B                                                                                              0.0s
 => CACHED [2/7] WORKDIR /src                                                                                                  0.0s
 => CACHED [3/7] COPY api ./                                                                                                   0.0s
 => CACHED [4/7] RUN pip3 install --upgrade pip                                                                                0.0s
 => CACHED [5/7] RUN pip3 install fastapi                                                                                      0.0s
 => CACHED [6/7] RUN pip3 install "uvicorn[standard]"                                                                          0.0s
 => CACHED [7/7] RUN pip3 install requests                                                                                     0.0s
 => exporting to image                                                                                                         0.0s
 => => exporting layers                                                                                                        0.0s
 => => writing image sha256:5f62e853a8f8569a6c75bc7b4a59f9912e855a890022b3c2e9d647808d840626                                   0.0s
 => => naming to docker.io/library/sample_demo-api                                                                             0.0s

今回はENVIRONprdになるため、responseは先ほどとは別の結果が返ってきているのが確認できます。

スクリーンショット 2022-12-23 1.02.04.png

ビルド時のキャッシュを削除する場合は、docker builder pruneコマンドを実行します。
なお、ビルド時にキャッシュを利用しないようにするためには、docker-compose build --no-cacheを実行します。

開発に役立つツール

PostmanはAPI開発の必需品です。

いくつかの機能がありますが、基本機能であるCollections機能は無料で利用できるので個人開発に重宝します。
複数人のチームでシナリオを共有する場合などは、有料プランの検討が必要になってくると思います。

Postmanの公式サイトからデスクトップアプリがダウンロードできます。

  • 実行例

スクリーンショット 2022-12-23 1.26.56.png

おわりに

歴史、設計、そしてこれからの通りに、FastAPIの歴史はまだ旅路の途中です。

2018年12月にリリースされているので他のWebフレームワークと比べると、まだ歴史が浅く、不足している機能もあると思いますが、Netflixなどでも使用されています。

Stack Overflow 2021 Developer Surveyでは、3番目に人気のあるWebフレームワークとなっています。

参考

RESTful API

FastAPI

32
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?