15
24

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 3 years have passed since last update.

FastAPI + uvicorn + NginxでWebページを表示(Jinja2によるTemplates機能 )

Last updated at Posted at 2020-08-15

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に以下のように追記
pyproject.toml(追加分)
[tool.poetry.dependencies]
# 以下の2つを追加
jinja2 = "*"
aiofiles = "*"

トータルでは例えば以下のようになる:

pyproject.toml
[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の中身の修正・追加

  • 基本は公式の例を参考にすれば良い
  • ここではマルチページ化したり、Rest-API機能などと機能分離して作り込んでいく可能性も考えてここを参考にサブアプリのような形でWebページを作ってみる

ファイル構成(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

  • 以下のように内容を修正
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は空ファイル

routers/subpage.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で呼び出される
layout.html
<!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>
  • jQueryjQuery-UIBoostrapの読み込み
  • titleはパラメータとして受け取っている
  • headcontents部分の中身は個別に別ファイルで記入していく
  • テスト用にlayout.cssをstaticディレクトリからJinja2のurl_forを使って読み込んでいる

subpage/index.html

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.htmlextendsし、FastAPI側から必要なパラメータを受け取ることで完全なHTMLになる
  • テスト用にstaticディレクトリからtest.csstest.jsを読み込んでいる

staticディレクトリ

  • テスト用に置いただけでどれも実質中身が無いので詳細は省略
  • 後で実行時にちゃんと中身を読み込めていることを確認していく

3. 実行(その1)

以下を実行

# パッケージ追加、ソース修正・追加を行ったのでビルドし直す
docker-compose build

# サービスの起動
docker-compose up -d

ローカル環境で実行している場合、 http://localhost を見てみる

スクリーンショット 2020-08-16 00-28-56.png

スクリーンショット 2020-08-16 00-34-01.png

ぱっと見ではうまくいってそうな気配がするが、よく見ると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>
  • srchrefのurlがこの場合http://localhost/<url>となっていて欲しいのに、↑のようにhttp://backend/<url>となってしまう
    • HTMLファイル上でurl_forを使おうとすると上記の問題が起こる
      • なお、FastAPIのコード中main.pyrouters/***.pyなど)でurl_forを使う分には特に問題無い

この辺りはproxyの問題が絡むため、uvicornの起動オプションとnginxの設定を修正して対応する必要がある

4. 設定の修正と実行(その2)

4-1. uvicorn

  • uvicorn公式のdeploymentsettingの部分を頑張って読むと、--proxy-headersオプションの設定が必要な様子

結論から言うとDockerfileのCMDを以下のように変更してuvicornの起動オプションを修正

Dockerfile(修正分)
# 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 /内の項目を追加:
conf.d/app.conf
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が呼び出されるようになる

image.png

  • ↑staticから呼び出したファイルのURLが正しくなっている

まとめ

  • FastAPI + uvicorn + Nginx (docker-compose)の構成で、Flaskでやるような雰囲気のWeb機能を作成
  • Template機能などを使ったWeb機能作る上ではFlaskの方が挙動が簡単だし、文献も多いので楽だと感じた
    • FastAPIはRestAPI機能などに特化したものでWeb機能は得意でないのかもしれない。ただ、非同期処理などを使いこなせればパフォーマンス面でのポテンシャルはあるかもしれない。
  • SSL化するところもやった(そして結構面倒だった)ので後で更に続きを書く予定

参考

15
24
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
15
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?