ZennのバッジをFastAPI+Vercelでお手軽に作る

公開:2021/01/13
更新:2021/01/13
6 min読了の目安(約5900字TECH技術記事

はじめに

FastAPIVercelを使って、Zennのバッジを提供するサービス zenn-badge を作りました。

たとえば、以下のようにバッジを作ることができます。

ganariya-liked ganariya-followers ganariya-articles

![your-text](https://zenn-badge.ganariya.vercel.app/{username}/liked)
![your-text](https://zenn-badge.ganariya.vercel.app/{username}/followers)
![your-text](https://zenn-badge.ganariya.vercel.app/{username}/articles)

記事を書き出してから「vercelのドメイン名なんかおかしいなぁ」になり色々調べていると
Zenn.badgeという、より使いやすいサービスがありました。

Zenn.badgeも参考にさせていただいて、もっと使いやすいものにしていきたいですね。

FastAPI

zenn-badgeでは、バックエンドにFastAPIというフレームワークを使っています。
開発スタートはかなり最近ですが、ものすごい勢いでスター数が増加しています。

以下のような特徴があります。

  • Python3.5以降に追加されたType Hintsを積極的に利用
    • 変数に型を設定するとそのまま自動バリデーションに
    • IDEの恩恵も受けることができて一石二鳥
  • Swagger-UIがとくに設定することなく自動で作成される
  • 非同期処理がasync/awaitで意識せずに書ける
    • FastAPIはASGI
    • バックグラウンドで走らせるタスクもとても短く書ける
  • ドキュメントが分かりやすい
    • 使い方・チュートリアルが丁寧に書かれている
    • FastAPI以外のPythonの特性についても取り扱っている
      • Pydanticへのモデルに辞書を渡すときの**dictによるキーワード変数展開
      • ファイル・モジュールの分割方法
  • GraphQL / WebSocketなどへの公式サポート

公式のアプリケーション例は以下のコードです。
Flaskのような文法で書くことができます。
ここで、def read_item(item_id: int, q: str = None)と引数に型を設定しています。
実はこれだけで自動で引数に関するバリデーションを行い、かつSwaggerUIを作成・型の表示を行ってくれます。

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

これから新しくバックエンドAPIをミニマムに始めたい場合は、FlaskだけでなくFastAPIを視野に入れると良いと思います。
かなり少ない勉強量に対して大きなメリットがあります。

vercel

vercelは、サーバレスでPython/Go/Nodejsなどを動かすことができるサービスです。
サーバの構築をほぼ意識することなく、自動でビルド・デプロイ・運用が行われます。
しかも、類似サービスであるHerokuと比較して、かなり無料枠が大きいです。
(ほぼ無料枠ということを意識することなく、自由に使える)

また、GitHubにプッシュするだけで自動でビルド・デプロイをしてくれるのも嬉しい点です。
ドメインも都度作成・更新してくれます。

ただ、そもぞもvercelの前身はZEIT社のNowというサービスでした。
そのため、インターネットの記事が混在していたり、CLIもnow-cli->vercel-cliになっています。
このあたりの分かりづらい点と、Pythonに関するケースが少ないことが課題かなと感じました。
Next.js, Nuxt.jsは公式のテンプレートもあってかなり分かりやすいのかな、という印象です。

zenn-badgeを参考に、vercelのデプロイについてまとめようと思います。

vercelにデプロイする前

vercelにデプロイする前は、開発をGitHub上で行います。
というのも、以下の画像のようにリポジトリをvercel上で指定するだけで自動デプロイが設定されます。

そのため、それまでは普通にGitHubで開発できます。
ただし、vercelでFastAPIが動くように意識して開発する必要があります。

vercelでPython/FastAPIを動作させる

vercelで運用したいサービスの設定を行うために、now.jsonもしくはvercel.jsonを作成します。
おそらくNow時代の名残でnow.jsonも使用できますが、公式ドキュメントで扱っているのはvercel.jsonです。
いくつか書き方も異なるようです。

今回は参考にしたリポジトリをもとに、now.jsonで設定を行っています。

{
  "version": 2,
  "public": false,
  "builds": [
    {
      "src": "zenn_badge/app.py",
      "use": "@now/python"
    }
  ],
  "routes": [
    {
      "headers": {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
        "Access-Control-Allow-Headers": "X-Requested-With, Content-Type, Accept"
      },
      "src": "/.*",
      "dest": "zenn_badge/app.py"
    }
  ]
}

publicではログなどを公開状態にするかを指定できます。
今回はオフにしています。

buildsでは、ビルドするソースコードの指定と、useでビルド時に利用するモジュールを指定します。
今回はPythonOnlyなので@now/python(正式には、現在は@vercel/python)を利用しています。
どうやらbuildsに関するドキュメントを参考にすると、現在は推奨されておらず、かわりにfunctionsを指定すべきらしいですね。(しかしfunctionsでは/apiディレクトリにコードを置く必要があり、今回のリポジトリでは違う設計になっているため、かわりにbuildsを用います。)

routesはやってきたクエリのルーティングを行います。
今回はすべての通信をすべてzenn_badge/app.pyに投げています。
これは、FastAPIのルーティングを利用しているため、エントリポイントのapp.pyにすべてを任せているためです。
このroutesも現在は推奨されていないようです。

このようにnow.jsonを設定することでzenn_badge/app.pyをビルドし、すべての通信をzenn_badge/app.pyに投げています。

ライブラリのインストール

builds@now/pythonを指定しているため、vercelでGitHubのプロジェクトをインポートすると自動でライブラリもインストールされます。
このとき、リポジトリのトップディレクトリにrequirements.txtもしくはPipfile.lockを置いてください。
これらのどちらかを置いておけば自動でインストールが実行されます。

Pythonのバージョン

注意点として、Pythonのバージョンは3.6のようです。
そのため、3.8から導入されたFinalなどは使えないことに注意してください。

FastAPI

これまでのnow.jsonの設定で、zenn_badge/app.pyにFlaskやFastAPIを書けば動作するようになりました。
あとはコードを書いていくだけです。

今回のエントリポイントであるzenn_badge/app.pyのソースコードの出だしは以下のようになっています。

import pybadges

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

from .zenn import scrape_user
from .user import User
from .logo import LOGO

app = FastAPI()
BASE_COLOR: str = '#3FA8FF'


def make_badge(username: str, left_text: str, right_text: str) -> str:
    url = f'https://zenn.dev/{username}'
    return pybadges.badge(left_text=left_text, right_text=right_text, right_color=BASE_COLOR, embed_logo=True, logo=LOGO, left_link=url, right_link=url)


@app.get("/")
def main() -> str:
    return "Zenn Badges"

もともとは絶対ディレクトリ指定でfrom zenn_badge.user import Userのようにインポートしていたのですがデプロイしたところエラーを吐きました。
どうやらマウントされたトップモジュール名が__vercel__dir__のような感じになっていて(名前を正確に覚えておらず申し訳ないです)、絶対指定をしようとするとそもそもパスが開発環境と異なっていました。

そのため、今回は相対ディレクトリを利用することにしました。
このあたりの仕様について、もっとしらべていきたいですね。

課題

以下のような課題に引き続き取り組みたいと思っています。

  • 画像やデータのキャッシュ
    • DBを外部に用意するなど
  • /パスではバッジのテキストの自動作成を行えるReactを提供する
  • now.jsonからvercel.jsonに以降し、推奨設定に変更
  • liked, followersなど全体を踏まえた きれいなSVGの作成(GitHub-trophyみたいな)
  • チーム開発(プルリク・イシューなどもらえると嬉しいです)

まだまだサーバレスやバックエンドが苦手なので、もっと色々作る中で勉強して、そして改善していきたいです!
vercelとnetlify, AWSに早いところ慣れたいです。

この記事に贈られたバッジ