技術メモなど

ほぼ自分用の技術メモです。

既存のHerokuで動かしているFlask + GunicornのアプリにNGINXのリバースプロキシを追加する方法 (Heroku Buildpack)

概要

  • Heroku Buildpack: NGINX を使えばHerokuでNGINXを導入できるのでHerokuにデプロイしているアプリケーションにリバースプロキシを設置できる。
  • アプリケーションのWebサーバがGunicornの場合、GunicornをUNIXソケットで待ち受けるように設定変更が必要。
  • 基本的に Heroku Buildpack: NGINX の手順通りやれば動いた。サンプルにはWebサーバにRubyUnicornを使う手順が記載されているが、適宜PythonのGunicornに置き換えれば大丈夫。アプリケーションはFlaskで動かしているが、DjangoでもGunicornを使うなら動くのではないかと思う。

きっかけ

旭川市新型コロナウイルスまとめサイトGooglePage Speed Insights で計測したらモバイルページの点数が低かったので、リバースプロキシ入れてキャッシュ時間をコントロールすれば改善できると思ったから。しかしオチを先に書くと、自分の書いたnginx.confがしょぼいのか導入前後でスコアは変わらなかった。(たぶんHerokuってログに記録される送信元IPアドレスがローカルIPアドレスなところを見るとデフォルトでリバースプロキシ越しに最適化されてアクセスされるように設計されている?)

ちなみに最初DockerでGunicorn動かすコンテナとNGINX動かすコンテナ2つ動かせばいけるのかなと思ったのだけど、自分が調べた範囲ではHerokuだと無理っぽい。なので開発環境ではDocker ComposeでGunicorn + Flaskが動くApplicationコンテナとNGINXが動くWeb Serverコンテナを使い、Heroku上の本番環境ではHeroku BuildpackのNGINXを使うようにしている。

手順

GunicornをUNIXソケットで待ち受けるよう変更

アプリのルートディレクトリに次のような gunicorn.conf.py を配置し、Gunicorn起動時に -c オプションで gunicorn.conf.py を指定すれば、通常のTCPソケットで待ち受けるモードからUNIXソケットで待ち受けるモードに変更できる。

def when_ready(server):
    open("/tmp/app-initialized", "w").close()


bind = "unix:///tmp/nginx.socket"

UNIXソケット通信で使うファイル名はHeroku Buildpacks: NGINXの仕様に合わせておく。

Requirements (Proxy Mode)

Your webserver listens to the socket at /tmp/nginx.socket.

You touch /tmp/app-initialized when you are ready for traffic.

You can start your web server with a shell command.

heroku/heroku-buildpack-nginx - Buildpacks - Heroku Elements

リバースプロキシで動かすためのNGINXの設定ファイルを作成

アプリのルートディレクトリに config ディレクトリを作成し、その中に nginx.conf.erb を作成する。Heroku Buildpack: NGINXの Githubリポジトリnginx.conf.erb のサンプル があるのでこれをベースにお好みで設定する。

daemon off;
worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>;

events {
    use epoll;
    accept_mutex on;
    worker_connections <%= ENV['NGINX_WORKER_CONNECTIONS'] || 1024 %>;
}

http {
    gzip on;
    gzip_comp_level 2;
    gzip_min_length 512;
    gzip_proxied any;

    server_tokens off;
    log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';
    access_log <%= ENV['NGINX_ACCESS_LOG_PATH'] || 'logs/nginx/access.log' %> l2met;
    error_log <%= ENV['NGINX_ERROR_LOG_PATH'] || 'logs/nginx/error.log' %>;

    include mime.types;
    default_type application/octet-stream;
    sendfile on;

    tcp_nopush on;
    keepalive_timeout 65;

    upstream app_server {
        server unix:///tmp/nginx.socket fail_timeout=0;
    }

    server {
        listen <%= ENV["PORT"] %>;

        location / {
            proxy_redirect off;
            proxy_pass http://app_server;
        }

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

ちなみに nginx.conf.erb の設置パスに注意。Heroku Buildpacks: NGINXのページには次の通りSolo Modeの要求事項としてしか記載されていないが、Proxy Modeでも config/nginx.conf.erb に設置しないと読み込んでくれない。

Requirements (Solo Mode)

Add a custom nginx config to your app source code at config/nginx.conf.erb.

heroku/heroku-buildpack-nginx - Buildpacks - Heroku Elements

Heroku Buildpacks: NGINXのインストール

Heroku Toolbeltはインストール済みとする。ターミナルから次のコマンドでHeroku Buildpacks: NGINXを追加する。

heroku buildpacks:add heroku-community/nginx

Procfile の修正

Heroku Buildpacks: NGINXを使うには、アプリのルートディレクトリにある Procfile を修正する。

まず、動かしたいFlaskアプリがルートディレクトリにある次のような hello.py だとする。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'


if __name__ == "__main__":
    app.run()

これを普通にHerokuでWebサーバをGunicornで動かすには、以下のような Procfile になっていると思う。

gunicorn hello:app --log-file -

ここから、Procfile を次の通り変更する。

web: bin/start-nginx gunicorn hello:app -c gunicorn.conf.py --log-file=-

あとはHerokuの本番リポジトリに変更をPushすれば、NGINXのリバースプロキシ経由でGunicornにアクセスされるようになる。

感想

現状 Page Speed Insights はNGINX追加前後でスコアは変わらなかった。 旭川市新型コロナウイルスまとめサイト はUDフォントを使いたくてWebフォントを使っているし、FontAwsomeも使っているため、これでだいぶスコアが落ちてしまっているから仕方ないかもしれない。キャッシュを効かせるとしたら大きめのメディアファイルにだと思うのだけど、容量が大きいのがグラフの画像ファイルで、これは一応準リアルタイムで配信したいので、キャッシュ時間を10分に設定してみたがあまり効果なさそう。

参考