0. はじめに
- 「FastAPI + uvicorn + nginxをdocker-composeで構築」で作ったものを元にして、Jinja2によるTemplates機能などを使ってWebページの雛形を作る
- 前にFlaskなどを試しに使っていたことがあり、TemplateやStaticファイルの配信を同じような感じで出来るか調べたかった
※続編: FastAPI + uvicorn + NginxでWebページを表示(SSL/HTTPS化 )
実践:Webページ機能追加
※前提:「FastAPI + uvicorn + nginxをdocker-composeで構築」の状態からスタートする
1. Packageの追加
- Template機能用に
Jinja2
が、Staticファイルの配信にaiofiles
の追加インストールが必要- 参考:公式ドキュメント
-
pyproject.toml
に以下のように追記
[tool.poetry.dependencies]
# 以下の2つを追加
jinja2 = "*"
aiofiles = "*"
トータルでは例えば以下のようになる:
[tool.poetry]
name = "test_fastapi_app"
version = "0.1.0"
description = "just for test"
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.8"
uvicorn = "*"
fastapi = "*"
jinja2 = "*"
aiofiles = "*"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
2. app
の中身の修正・追加
ファイル構成(app
)
$ tree
.
├── app
│ ├── Dockerfile
│ ├── app
│ │ ├── __init__.py
│ │ ├── main.py
│ │ ├── routers
│ │ │ ├── __init__.py
│ │ │ └── subpage.py
│ │ ├── static
│ │ │ ├── layout.css
│ │ │ └── subpage
│ │ │ ├── test.css
│ │ │ └── test.js
│ │ └── templates
│ │ ├── layout.html
│ │ └── subpage
│ │ └── index.html
│ ├── poetry.lock
│ └── pyproject.toml
├── docker-compose.yml
└── web
app/app
内のファイルを変更・追加しているので以下詳細を見ていく
main.py
- 以下のように内容を修正
"""
app main
"""
import pathlib
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse
from .routers import subpage
# pathlib.Pathを使って、staticディレクトリの絶対パスを取得
PATH_STATIC = str(pathlib.Path(__file__).resolve().parent / "static")
def create_app():
"""
create app
- 少し複雑化してきたので関数化している
"""
_app = FastAPI()
# routersモジュールのサブアプリ`subpage`をURL"/subpage/"以下にマウントする
_app.include_router(
subpage.router,
prefix="/subpage",
tags=["subpage"],
responses={404: {"description": "not found"}},
)
# static
# URL`/static"以下にstaticファイルをマウントする
_app.mount(
"/static",
StaticFiles(directory=PATH_STATIC, html=False),
name="static",
)
return _app
app = create_app()
@app.get('/')
async def redirect_subpage():
"""redirect webpage"""
return RedirectResponse( # subpageのサブアプリで作ったWebページにリダイレクトさせている
"/subpage",
)
若干行数が増えたが、やっていることは主に
-
static
の追加 - サブアプリ:
subpage
のマウントとリダイレクト
routers/subpage.py
※同じ場所にある__init__.py
は空ファイル
"""
test subpage
"""
import pathlib
from fastapi import (
APIRouter,
Request,
)
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
# templatesディレクトリの絶対パスを取得
PATH_TEMPLATES = str(
pathlib.Path(__file__).resolve() \
.parent.parent / "templates"
)
# Jinja2のobject生成
templates = Jinja2Templates(directory=PATH_TEMPLATES)
# サブアプリ
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
async def site_root(
request: Request,
):
"""test subpage"""
title = "test subpage"
return templates.TemplateResponse(
"subpage/index.html", # `templates`ディレクトリにおける相対パス
context={ # 変数をdict形式で渡すことが出来る
"request": request,
"title": title,
}
)
- Jinja2のTemplate機能を使い、
templates
ディレクトリ下のファイルを指定してHTMLレスポンスとして返している - Flaskなどのように、パラメータを渡すことが出来る
templates
ディレクトリ
layout.html
- 複数のhtmlファイルで共通して使うような想定
- 他のファイルから
extends
で呼び出される
- 他のファイルから
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="apple-mobile-web-app-capable" content="yes">
{% if title %}
<title>{{ title }}</title>
{% else %}
<title>Template</title>
{% endif %}
<!-- jQuery & Bootstrap4 -->
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<script
src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
<!-- jQuery UI -->
<script
src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"
integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU="
crossorigin="anonymous"></script>
<link
rel="stylesheet"
type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.min.css">
<!-- CUSTOM STYLE -->
<link rel="stylesheet" type="text/css" href="{{url_for('static', path='/layout.css')}}">
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
-
jQuery
、jQuery-UI
、Boostrap
の読み込み -
title
はパラメータとして受け取っている -
head
とcontents
部分の中身は個別に別ファイルで記入していく - テスト用に
layout.css
をstaticディレクトリからJinja2のurl_for
を使って読み込んでいる
subpage/index.html
{% extends "layout.html" %}
{% block head %}
<link
rel="stylesheet"
type="text/css"
href="{{ url_for('static', path='/subpage/test.css') }}">
<script
type="text/javascript"
src="{{ url_for('static', path='subpage/test.js') }}"></script>
{% endblock %}
{% block content %}
<h2>Test Subpage</h2>
<br>
<h3>
Hello, World.
</h3>
{% endblock %}
-
layout.html
をextends
し、FastAPI側から必要なパラメータを受け取ることで完全なHTMLになる - テスト用にstaticディレクトリから
test.css
とtest.js
を読み込んでいる
static
ディレクトリ
- テスト用に置いただけでどれも実質中身が無いので詳細は省略
- 後で実行時にちゃんと中身を読み込めていることを確認していく
3. 実行(その1)
以下を実行
# パッケージ追加、ソース修正・追加を行ったのでビルドし直す
docker-compose build
# サービスの起動
docker-compose up -d
ローカル環境で実行している場合、 http://localhost を見てみる
ぱっと見ではうまくいってそうな気配がするが、よく見るとHTMLからstatic
のファイルを読み込めていない:
<link rel="stylesheet" type="text/css" href="http://backend/static/layout.css">
<link
rel="stylesheet"
type="text/css"
href="http://backend/static/subpage/test.css">
<script
type="text/javascript"
src="http://backend/static/subpage/test.js"></script>
-
src
やhref
のurlがこの場合http://localhost/<url>
となっていて欲しいのに、↑のようにhttp://backend/<url>
となってしまう-
HTMLファイル上で
url_for
を使おうとすると上記の問題が起こる- なお、FastAPIのコード中(
main.py
やrouters/***.py
など)でurl_for
を使う分には特に問題無い
- なお、FastAPIのコード中(
-
HTMLファイル上で
この辺りはproxyの問題が絡むため、uvicorn
の起動オプションとnginx
の設定を修正して対応する必要がある
4. 設定の修正と実行(その2)
4-1. uvicorn
- uvicorn公式のdeploymentとsettingの部分を頑張って読むと、
--proxy-headers
オプションの設定が必要な様子
結論から言うとDockerfileのCMD
を以下のように変更してuvicorn
の起動オプションを修正
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--proxy-headers", "--forwarded-allow-ips", "*"]
4-2. Nginx
次にNginxの設定ファイル(web/conf.d/app.conf
)を修正する
(uvicornのdeploymentにNginxの例があるので参考にする)
$ tree
.
├── app
├── docker-compose.yml
└── web
└── conf.d
└── app.conf
- ↑の
app.conf
を以下のように修正する-
location /
内の項目を追加:
-
upstream backend {
server app:8000;
}
server {
listen 80;
# server_name localhost;
# index index.html index.htm;
location / {
# 以下の5項目を追加
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://backend;
}
# log
# access_log /var/log/nginx/access.log;
# error_log /var/log/nginx/error.log;
}
# server_tokens off;
ここまでやるとHTMLファイル上でurl_for
をしたものも正しくhttp://localhost/<url>
といった感じで期待通りのURLが呼び出されるようになる
- ↑staticから呼び出したファイルのURLが正しくなっている
まとめ
- FastAPI + uvicorn + Nginx (docker-compose)の構成で、Flaskでやるような雰囲気のWeb機能を作成
- Template機能などを使ったWeb機能作る上ではFlaskの方が挙動が簡単だし、文献も多いので楽だと感じた
- FastAPIはRestAPI機能などに特化したものでWeb機能は得意でないのかもしれない。ただ、非同期処理などを使いこなせればパフォーマンス面でのポテンシャルはあるかもしれない。
- SSL化するところもやった(そして結構面倒だった)
ので後で更に続きを書く予定
参考
- FastAPI公式ドキュメント
-
starlette公式ドキュメント
- FastAPIはstarletteの拡張になっている(らしい)ので、適宜starletteの仕様を確認する必要がある
- uvicorn公式ドキュメント
- https://note.com/yusugomori/n/n9f2c0422dfcd