Skip to main content

FastAPIアプリをGunicorn + Nginxで公開する

はじめに

概要

PythonのAPIフレームワークであるFastAPIを用い、Ubuntu環境 (VPS) にFastAPI + Gunicorn + Nginxの構成でREST APIを作る際の設定方法です。特にNginxの設定がいつも分からなくてググっているのでメモしておきます。

この記事の対象はNginxを全く触ったことないような方です。私のようなNginxが全く分からないPython使いがとりあえずFastAPIをNginxで公開できる所まで持っていこうという趣旨です。

環境

  • Ubuntu 22.04.1 LTS(ConoHa VPS)
  • nginx 1.18.0
  • certbot 1.21.0
  • Python: Miniconda環境
    • Miniconda 4.12.0
    • Python 3.10.4
    • FastAPI 0.79.0
    • uvicorn 0.18.2
    • gunicorn 20.1.0

FastAPIアプリを公開する(Uvicorn/Gunicorn)

FastAPIでAPIエンドポイントの作成

それでは今回デプロイするFastAPIアプリを作成します。

$ pip install fastapi pydantic uvicorn[standard] gunicorn

以下のコードをVPS上の適当なディレクトリにmain.pyというファイル名で保存します。

from fastapi import FastAPI

app = FastAPI(root_path="/")

@app.get("/")
def say_hello():
    return {"message": "Hello!"}

ルートにGETすると{"message": "Hello!"}というJSONを返すAPIです。

FastAPIアプリの公開(ローカル)

まずはアプリケーションサーバにUvicornを用いて127.0.0.1:8000にこのAPIを立てます。ポート番号は好きな番号で構いませんが、とりあえず8000番ポートに立ててみます。

$ cd [main.pyを保存したディレクトリのパス]
$ python -m uvicorn main:app --host 127.0.0.1 --port 8000

AnacondaやMiniconda環境の場合は、conda activate [仮想環境名]してから上を実行するか、上のpythonを、Anaconda/Miniconda環境で使用しているPythonのパスに置き換えます。このパスは、conda activate [仮想環境名]; which pythonで知ることができます。

別のターミナルを開き、VPS上で127.0.0.1:8000にcurlでGETして{"message": "Hello!"}というJSONが返ってくればAPIが立てられています。

$ curl 127.0.0.1:8000
{"message":"Hello!"}

今はアプリケーションサーバにUvicornを用いましたが、Gunicornを用いてGunicornからUvicornを触ることができます。本番環境ではGunicornを用いる方がいいようなので、以下Gunicornを用いて説明します。

Uvicornを直接用いる前述の場合は単一プロセスですが、Gunicornを用いるとUvicornを複数プロセス立ち上げることができ、またそのUvicornプロセスが落ちたとしても再度プロセスを自動で立ち上げてくれます。(詳細は公式ドキュメントを参照: Server Workers - Gunicorn with Uvicorn - FastAPI

以下のようにすることで、-wの引数で指定したプロセス数だけワーカーを持つようにGunicornが起動します。ワーカー数は適当に2にします。127.0.0.1:8000にcurlでGETすると同様に{"message": "Hello!"}が返ってきます。

python -m gunicorn main:app --bind 127.0.0.1:8000 -w 2 -k uvicorn.workers.UvicornWorker

FastAPIアプリの公開(グローバル)

これまでは127.0.0.1にAPIを立てていました。以下のように0.0.0.0を指定することで、外部からアクセスできるようになります。

# Uvicornの場合
$ python -m uvicorn main:app --host 0.0.0.0 --port 8000
# Gunicornの場合
$ python -m gunicorn main:app --bind 0.0.0.0:8000 -w 2 -k uvicorn.workers.UvicornWorker
  • 指定したポート番号のポート(ここでは8000番)をファイアウォールで開けておいてください。
  • 特権ポートと呼ばれる1023番までのポート番号を指定する場合、sudo権限が必要です。先頭にsudoを付けてください。

VPSではなく自分のローカルPCからcurl [VPSのIPアドレス]:8000を叩いてみて、同様に{"message":"Hello!"}が返ってくれば成功です。Webブラウザのアドレスバーに[VPSのIPアドレス]:8000を入力して開いてみても構いません。

Nginxの概略

以上の内容でとりあえずFastAPIアプリを公開することができますが、以下ではAPIにアクセスしてくるユーザとUvicorn/Gunicornの間にWebサーバのNginxを入れようと思います。

Nginxを入れない場合、複数のFastAPIアプリなどを公開しようとすると、アプリごとにポート番号を変える必要があります。

  • http://x.x.x.x:8000でアプリ1にアクセスできる
  • http://x.x.x.x:8001でアプリ2にアクセスできる

一方、Nginxを入れてリバースプロキシすると、このようにサブディレクトリへのアクセスを振り分けることができます。

  • http://127.0.0.1:8000でアプリ1を立ち上げる
    • http://hoge.example.com/app1/127.0.0.1:8000にリバースプロキシしてアプリ1にアクセスできる
  • http://127.0.0.1:8001でアプリ2を立ち上げる
    • http://hoge.example.com/app2/127.0.0.1:8001にリバースプロキシしてアプリ2にアクセスできる

この次の章では、先程作成したFastAPIアプリを127.0.0.1:8000で立ち上げておき、http://(VPSのIPアドレス)/appにGETするとNginxのリバースプロキシで127.0.0.1:8000に転送され、先程のアプリがレスポンスを返すようにします。この章では、その前にNginxの設定ファイルについて簡単に説明します。

Nginxのインストール

まずNginxをインストールします。

$ sudo apt install nginx

1行目でNginxを起動し、2行目でUbuntuの起動時にNginxが自動で起動するようにします。

$ sudo systemctl start nginx
$ sudo systemctl enable nginx

起動できているか、また自動起動が有効になっているかを確認します。

$ sudo systemctl status nginx

nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2022-12-25 20:43:29 JST; 25s ago
     (以下略)

Active: active (running)とあるのが今起動できていること、/lib/systemd/system/nginx.service; enabledとあるのが自動起動されていることを示します。

なお、起動しているNginxを停止したい場合はsudo systemctl stop nginx、自動起動を無効にしたい場合はsudo systemctl disable nginxです。

Nginxの設定ファイル

バーチャルホストを作るときに使うNginxの設定ファイルの構成は以下のようになっています。

  • /etc/nginx/nginx.conf
  • /etc/nginx/conf.d/*.conf
  • /etc/nginx/sites-enabled/*

バーチャルホストを追加する際は、/etc/nginx/nginx.confは編集せず、/etc/nginx/conf.d/*.conf/etc/nginx/sites-enabled/*に追加します。

/etc/nginx/nginx.confには以下のように記載されていることから、/etc/nginx/nginx.confがロードされるときに、その中で/etc/nginx/conf.d/直下にある拡張子confのファイルと/etc/nginx/sites-enabled/直下にあるファイルが読み込まれることが分かります。

http {

  (略)

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

/etc/nginx/sites-enabled/*に追加する際は、実際には/etc/nginx/sites-available/直下に設定を作成し、それを/etc/nginx/sites-enabled/にシンボリックリンクを張るようにするのが一般的です。シンボリックリンクを外せばNginxの設定から除外されるのがメリットです。

それでは/etc/nginx/conf.d/*.confetc/nginx/sites-available/*のどちらに設定を記載すればよいかですが、多くのバーチャルホストを使う場合、あるいはバーチャルホストをデプロイしたりしなかったりと切り替えたい場合は後者、そうではない場合は前者、のような考え方が一つの決め方になります。本記事では、/etc/nginx/sites-available/以下に設定を作ることにします。

FastAPIアプリを公開する(Gunicorn + Nginx)

Nginxを用いたリバースプロキシでのAPIの公開について説明します。

まず、/etc/nginx/sites-available/直下に設定ファイルを作成します。この記事では/etc/nginx/sites-available/fastapiというファイルを作ることにします。デフォルトのファイルとして/etc/nginx/sites-available/defaultが用意されていますので、これをひな形としてコピーしてから編集することにします。

$ sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/fastapi
$ sudo ln -s /etc/nginx/sites-available/fastapi /etc/nginx/sites-enabled/

2行目のシンボリックリンクを貼る作業は、シンボリックリンクを外さない限りは/etc/nginx/sites-available/に新しいファイルを作成したら最初の1回だけ行っておけば大丈夫です。

sudo nano /etc/nginx/sites-available/fastapiでこのファイルを編集します。以下を貼り付けて上書き保存します。

server {
    listen 80;

    location /app/ {
        proxy_pass http://127.0.0.1:8000/;
    }
}

VPSのIPアドレスをx.x.x.xとします。80番ポートをlistenするよ、http://x.x.x.x:80/app/に来たアクセスは127.0.0.1:8000に転送するよということですね。

なお、proxy_passのtrailing slash(末尾のスラッシュ)は付けるようにしましょう。付けないと正しくアクセスできません。地味にハマりポイントです。

保存したら、sudo nginx -tを実行してNginxの設定ファイルに構文エラーがないかどうかを確かめておきます。

エラーが表示されなければ構文に誤りはありませんので、Nginxを再起動することで今作成した設定ファイルを反映させます。設定ファイルを更新したら必ずNginxを再起動してください。再起動するまでは反映されません。

$ sudo systemctl stop nginx
$ sudo systemctl start nginx

次に、先程作成したFastAPIアプリのroot_pathを/app/に変更します。

from fastapi import FastAPI

app = FastAPI(root_path="/app/")

@app.get("/")
def say_hello():
    return {"message": "Hello!"}

このFastAPIのエンドポイントをGunicornで公開します。

$ python -m gunicorn main:app --bind 127.0.0.1:8000 -w 2 -k uvicorn.workers.UvicornWorker

ローカルのPCからhttp://x.x.x.x/app/をブラウザで開くかcurlを叩いて{"message":"Hello!"}が返ってくれば成功です。

http://x.x.x.x/で公開したい場合は、FastAPIアプリのroot_pathを/app/ではなく/にし、/etc/nginx/sites-available/fastapiのlocationも/app/ではなく/にします。

Nginxのより進んだ設定

ここまででFastAPIアプリをGunicorn + Nginxで公開することができました。

FastAPIアプリを本番公開する際には、Nginxの設定において追加でいくつか行った方がいいことがあります。以下順に説明していきます。

  1. SSL化
    1. Let’s Encript + certbotでSSL証明書の導入と自動更新
    2. SSL対応 + HTTPに来たアクセスのリダイレクト
  2. IPアドレス直打ちでのアクセスを拒否
  3. Nginxのバージョンを非表示
  4. IPv6対応
  5. アクセスログにリバースプロキシを考慮したIPアドレスを残す
  6. アクセスログにPOSTボディを出す
  7. アクセスログを別ファイル化

1. SSL化

SSL証明書の導入・自動更新の設定

この節は独自ドメインのSSL証明書を取る時の最初の1回だけ行います。

独自ドメインを取得していることを前提にします。以下、hoge.example.comという独自ドメインを使いたいとします。まず、独自ドメインを取得したドメイン会社のDNSレコード設定ページから、独自ドメインとVPSサーバのIPアドレスを紐づけてください。

sshの証明書はLet’s Encriptで取ることにします。証明書は90日おきに更新する必要がありますが、certbotを入れておくと自動で更新してくれます。

$ sudo apt install -y certbot python3-certbot-nginx

certbotの設定をします。以下を実行すると、恐らくメールアドレスを入力するようにメッセージが出ると思いますので、その通り入力してください。

$ sudo certbot --nginx -d hoge.example.com

次に、証明書の90日おきの自動更新が機能しているかどうか確かめます。以下を実行してエラーが出なければOKです。

$ sudo certbot renew --dry-run

Nginxの設定ファイルの対応

ここまでで独自ドメインのSSL化ができました。次にNginxの設定を行います。

独自ドメインを使いたいアプリの設定が記述されているファイル(/etc/nginx/sites-available/fastapiなど)に以下を貼り付けて上書き保存します。

server {
    server_name hoge.example.com;
    listen 443 ssl default_server;

    ssl_certificate /etc/letsencrypt/live/hoge.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hoge.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    (略)
}
server {
    if ($host = hoge.example.com) {
        return 301 https://$host$request_uri;
    }
    listen 80;
    server_name hoge.example.com;
    return 404;
}
  • 1個目のserverディレクティブでは、80番ポートだけでなく443番ポートもlistenするように設定します。また、SSLの証明書のパスなどを設定しています。
  • 2個目のserverディレクティブでは、80番ポートへのアクセスを443番ポートにリダイレクトしています。

なお、default_serverの挙動ですが、デフォルトでは複数の設定ファイルを読み込んでいるときはファイル名の順番に読み込み、一番最初に読み込まれた設定ファイルに記載されているserver_nameをdefault_serverとします。

上で記載したように同一のserverディレクティブ内にserver_nameを指定してlisten [port] default_serverと記載すると、このserverディレクティブ内のserver_nameをdefault_serverとします。

2. IP直打ち拒否

x.x.x.x宛のアクセスを拒否します。ユーザがWebサーバにIPアドレス直打ちでアクセスしてくることは普通考えにくいためです。

設定を反映させたい設定ファイルに以下を記述します。性質上、全てのバーチャルホストで同じ設定をしたいケースが多いと思いますので、個別の設定ファイルである/etc/nginx/sites-available/fastapiではなく、/etc/nginx/nginx.conf/etc/nginx/conf.d/default.confに以下の通り記述するのでも構いません。

server {
    server_name _;
    listen 80 default_server;
    listen 443 ssl default_server;
    return 444;
}

Nginxは、Hostヘッダがどのサーバ名ともマッチしないとき、あるいはリクエストにHostヘッダが含まれていないときはデフォルトサーバに振り分けます。これによって、他の設定ファイルのserverディレクティブ内に記載されているドメイン以外のアクセスは444エラーを返します。

server_nameのアンダーバーは「全てのサーバ」を示します。他の設定ファイルで定義されているhoge.example.comなどのドメインに該当しなかった全てのアクセスをこのserverディレクティブでキャッチするということですね。

3. Nginxのバージョンを非表示

Nginxはデフォルトでは使っているバージョンを表示します。特定のバージョンに脆弱性があり、自分が使っているバージョンが脆弱性のあるバージョンの場合、侵入者に脆弱性を知らせてしまっていますから、バージョンを非表示にするのが望ましいです。

/etc/nginx/nginx.conf内に以下の1行を記載すればOKです。

http {
    (略)
    server_tokens off;
}

あるいは、/etc/nginx/sites-available/fastapiなどの個別の設定ファイル内に以下のように記載しても構いません。

server {
    (略)
    server_tokens off;
}

4. IPv6対応

/etc/nginx/sites-available/fastapiに以下を記載します。

server {
    (略)
    # ここから下2行はIPv4対応
    listen 80 default_server;
    listen 443 ssl default_server;
    # ここから下2行はIPv6対応
    listen [::]:80 default_server;
    listen [::]:443 ssl default_server;
}

5. ログにリバースプロキシを考慮した接続元のIPアドレスを残す

リバースプロキシしているので、何も設定しないとログファイルに残るIPアドレスなどは自分のIPアドレスになってしまいます。外部からアクセスしてきたIPアドレスなどをそのまま残すには、/etc/nginx/sites-available/fastapiのlocationディレクティブの中に、proxy_set_headerで始まる5行を書きます。

server {
    (略)
    location /app/ {
        proxy_pass http://127.0.0.1:8000/;
        # ここから
        proxy_set_header Host               $host;
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host   $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP          $remote_addr;
        # ここまで
    }
}

6. ログにPOSTボディを出す

Nginxのデフォルトの設定では、ログにPOSTボディの中身は表示されません。以下のようにすると表示できます。

ただし、POSTボディが長い文字列になる場合、ログファイルが圧迫されてしまうことに注意してください。

まず、/etc/nginx/nginx.confにコメントを付した3行を記載します。

http {
    ##
    # Logging Settings
    ##

    log_format  '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for"';
    
    # 以下の3行を記載する
    log_format  format1 '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for" "$request_body"';

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

次に、/etc/nginx/sites-available/fastapi内に次の1行を追加します。これにより、/etc/nginx/nginx.confで定義したformat1の形式でログが記載されます。

server {
    location /app/ {
        (略)
        # 追加する
        access_log /var/log/nginx/access.log format1;
    }
}

7. ログを別ファイル化

デフォルトでは/etc/nginx/nginx.confaccess_logに記載の/var/log/nginx/access.logにログが作られますが、変更することもできます。

/etc/nginx/sites-available/fastapiに以下を追記します。

server {
    location /app/ {
        (略)
        # 追加する
        access_log /var/log/nginx/access_fastapi.log;
    }
}

もちろん、上で説明したようにログのフォーマットを変えることもできます。

server {
    location /app/ {
        (略)
        # 追加する
        access_log /var/log/nginx/access_fastapi.log format1;
    }
}

参考