以前、php-fpm+Nginxの環境をDockerで作りましたが、今回はGunicorn+Nginxで環境を作ります。
Gunicorn
Gunicorn(Green Unicorn)はWSGI HTTPサーバです。GunicornはRubyのUnicornプロジェクトから移行してきたpre-fork workerモデル(HTTPリクエストを受け取った時に、事前に用意した子プロセスで処理するモデル)です。
同じレイヤーのアプリでPython系では、これ以外に(使ったことはないですが、)uWSGIというものもあります。
今回はFlaskアプリを作りますが、GunicornがFlaskアプリを実行するという形になります。
Nginx
NginxはHTTPサーバ、リバースプロキシサーバ、メールプロキシサーバ、TCP/UDPプロキシサーバのサービスを提供するOSSです。
同じレイヤーのアプリでApacheがありますが、ApacheよりNginxの方が高速に動かすことができます。
クライアントからのHTTPリクエストは、Nginxが受け、それをGunicornに中継します。
システム構成
外部からはNginxしかアクセスできないようにしています。
以前、Nginxとphp-fpm間の通信では、TCPソケット通信しか試さなかったので、今回は
NginxとGunicorn間は、TCPソケット通信とUNIXドメインソケット通信の二通り試しました。
非常に勉強になる他者様の記事を参考に書いていきます。
TCPソケット通信
正しくはINETドメインと呼ぶようです。
こちらは「異なるマシンで動作しているプロセス間の通信を行うためのソケット」です。
ソケットは、OSI参照モデルのセッション層のレイヤーにあたるもので、※通信はIPアドレスとポート番号によって行います。(※HTTPではありません。)
そのため、ネットワーク上でマシンを超えたプロセス間通信が行えます。また、他にもUNIXドメインソケットとは違い、1台のプロキシサーバから複数台のWebアプリケーションサーバに通信を割り当て、負荷分散をさせたり、WebサーバとWebアプリケーションサーバを別のマシンに置いても使えるというメリットがあります。
また、これは所感ですが、TCPソケットの場合、コンテナにポートを開けることで、Nginxを介さずに動作確認できるのもあり、エラーが起きたときに原因の確認がしやすいです。
UNIXドメインソケット
こちらは「同じマシン内で動作しているプロセス間の通信を行うためのソケット」です。
マシン内にbindファイルを用意し、WebサーバとWebアプリケーションサーバをこのファイルを介して通信をします。そのため、この通信は同じマシン内でしか行なえません。代わりに、ファイルの書き込み・読み込みで通信ができるため、TCPソケットによる通信より速いです。また注意点として、この通信をサポートしているサービスが限られています。詳しくは、他者様の記事
実装
今回はDocker周り、Nginx周りとgunicorn・flask周りの部分を残します。
まずはcomposeファイルから
version: '3'
services:
web:
build:
context: ./web
ports:
- "80:80"
- "443:443"
volumes:
- ./web/public:/etc/nginx/public
- ../ssl/certs/:/etc/pki/tls/certs/
- ../ssl/private/:/etc/pki/tls/private/
- ./gunicorn_socket:/tmp/gunicorn_socket
depends_on:
- app
container_name: web
restart: always
networks:
- network
app:
build:
context: ./app
volumes:
- ./app:/var/www/
- ./gunicorn_socket:/tmp/gunicorn_socket
depends_on:
- db
container_name: app
ports:
- 9876:9876
networks:
- network
volumes:
db_data: {}
networks:
network:
driver: bridge
WebコンテナがNginxのコンテナ、AppコンテナがGunicorn+Flaskのコンテナです。
volumes:
- ./gunicorn_socket:/tmp/gunicorn_socket
この部分で、UNIXドメインソケットの通信で使うソケットファイルを出力するディレクトリをマウントします。
TCPソケットを使う場合はここの記述は不要です。
ports:
- 9876:9876
Appコンテナは9876ポートを開けています。
これはAppコンテナ内で9876ポートで立てているGunicornに外部からアクセスできるようにしているからです。
こちらがなくとも、WebコンテナはAppコンテナと通信できます。UNIXドメインソケットを使う場合はここの記述不要です。
本番環境で使う場合も不要なポートを開けるのは避けた方がいいので、ここの記載はやめた方がいいです。
Nginxコンテナ
Dockerfileは下記です。
FROM nginx:latest
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./server.conf /etc/nginx/conf.d/server.conf
CMD ["nginx", "-g", "daemon off;"]
build時に、コピーするファイルは下記です。
user nginx;
worker_processes auto;
pid /var/run/nginx.pid;
events{
worker_connections 512;
multi_accept on;
use epoll;
}
http {
charset UTF-8;
server_tokens off;
include /etc/nginx/mime.types;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
ssl_protocols TLSv1.1 TLSv1.2;
default_type text/html;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
include /etc/nginx/conf.d/server.conf;
}
upstream app {
# UNIXドメインソケットを使う場合
server unix:/tmp/gunicorn_socket/gunicorn_flask.sock fail_timeout=0;
# TCPソケットを使う場合
server app:9876 fail_timeout=0;
}
server {
listen 80;
server_name localhost;
root /var/www/public;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
try_files $uri @flask;
}
location @flask {
proxy_pass_request_headers on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_pass http://app;
}
}
upstreamという部分で、UNIXドメインソケットか、TCPソケットを使うかを選択します。
※使わない方をコメントアウトして使ってください。
Gunicorn+Flaskコンテナ
Dockerfileは下記です。
FROM python:3.8
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN apt update -y
RUN mkdir -p /var/www
COPY ./requirements.txt /var/www
RUN pip install -r /var/www/requirements.txt
COPY ./flask_app.py /var/www
COPY ./gunicorn.py /var/www
WORKDIR /var/www
CMD ["gunicorn", "flask_app:app", "--config", "/var/www/gunicorn.py" ]
Flask==1.1.2
gunicorn==20.0.4
GunicornとFlaskはpipでinstallします。
今回、Gunicornはsystemdサービスのものは使いません。(ネットではsystemdサービスを使う方法が多いですが、)
やることは同じなので、お好きな方を選択して使ってください。
あとは、flaskアプリのファイルとgunicornのconfigファイルを用意します。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World'
import os
# UNIXドメインソケット
socket_path = 'unix:/tmp/gunicorn_socket/gunicorn_flask.sock'
# TCPソケット
socket_path = '0.0.0.0:' + str(os.getenv('PORT', 9876))
bind = socket_path
# Debugging
reload = True
# Logging
accesslog = '-'
#loglevel = 'info'
loglevel = 'debug'
logfile = './log/app.log'
logconfig = None
# Proc Name
proc_name = 'Infrastructure-Practice-Flask'
# Worker Processes
workers = 2
worker_class = 'sync'
gunicorn.pyのsocket_pathでどっちの通信を受け付ける設定にするかを指定します。こちらも両方書いているので、どちらかをコメントアウトして使ってください。
また、補足ですが、コンテナを使うとエラーが起きた時に原因がわかりにくいことが多いです。
$ docker logs -f ${コンテナ名}
でコンテナのログを見れます。
また、Gunicornを使ってのFlaskアプリの起動は
$ gunicorn flask_app:app --config /var/www/gunicorn.py
で行っていますので、コンテナの起動に失敗している方は一度コンテナを使わない環境で動作確認された方が解決が早いかもしれません。
以上の設定で下記のコマンドでbuild+コンテナ起動をさせますと
$ docker-compose build
$ docker-compose up -d
UNIXドメインソケットを使われる方は
docker-compose.ymlのディレクトリに、gunicorn_socketというディレクトリができています。ここにgunicorn_flask.sockというファイルができており、通信できます。
エラーの確認がしやすい、負荷分散できることを考えると、個人的にはやはりTCPソケットですかね。
皆さんはどっちがいいとかありますか?