Edited at

docker-composeでgunicorn+nginx+flaskを動かしてみた話

※ この記事は エキサイト Advent Calendar 2018 20 日目の記事です ※


ごあいさつ

こんにちわ。

また...この季節がやってきましたね

去年もこうしてアドベントカレンダーを書き、先輩に遊びに連れて行ってもらうことによりぼっちマスを乗り越えました、新卒二年目のエンジニアの本田です!そんな先輩も今年に彼女ができてしまったようで、本当に一人になってしまいました...

そんなことはさておき、去年まではウーマンエキサイトアプリを担当していましたが、今年から新設部署に移動になりまして、今はブロックチェーンや機械学習の開発を行っております!

そこで開発した投稿監視AIの環境をOpenStack上に立てたのですが、どうにかコンテナ化できないかと、すこし頑張ってdockerについて調べつつ奮闘している(現在進行形)記事となります。


コンテナ化したい環境

コンテナ化したい環境は事前にまとめておきました。

gunicorn + Flask + nginx + Systemdで動かしてみた

こちらの記事を毎度恒例、ざっくり三行でまとめると


  • Flask(pythonの軽量ウェブフレームワーク)をgunicornと呼ばれるフレームワークを包み込むWeb Server Gateway Interfaceで起動する


  • gunicornはリバースプロキシができるnginxを推奨しています。


  • gunicornをsystemdでサービス化してやることで、プロセス管理やデーモン化(gunicornのワーカー設定)できます。


今回はgunicorn + nginx + Flaskまでをdockerで頑張ってみました。


dockerとは

もう最近ではk8sなどの登場でみんな浸透していると思いますが、dockerとは"コンテナ"とよばれる単位で仮想環境を構築するものです。

利点や欠点などは、Casleyさんの記事、「Dockerのメリット・デメリット」を引用すると、



  • 構成が仮想化よりも単純なため、高密度化が可能

  • 他の仮想化技術に比べ、オーバーヘッドが少ない

  • 新しいマシン(コンテナ)の起動が、仮想マシン(VM)より高速


となり、最近ではオンプレからこういったコンテナ化が騒がれている?そうです。

dockerはプロセスごとにコンテナを立てるので、今回gunicornとnginxの二つのコンテナ構成(下図)になります。

flask-gunicorn-nginx/

├nginx/
│ ├ Dockerfile
│ └ default.conf
└ web/
├ Dockerfile
└ requirements.txt


まずは、Flaskでhello worldまで

とにかく参考にさせてもらったのは、「Djangoの環境をDocker化する(Docker + Django + Gunicorn + nginx)その1」です。

まず、コンテナを立てるためにdockerfileを書きます。

FROM python:3.6

ENV PYTHONUNBUFFERED 1
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
ADD requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
ADD . /usr/src/app/

pythonの3.6をFROMでとってきており、今回これがベースコンテナとなります。

参考にさせてもらった記事どおり、今回は/usr/src/app下にrequirements.txtを用意して、pip installします。


requirements.txt

gunicorn==19.9.0

Flask==1.0.2

まずはここまで書いて、次はコンテナを立てるためにdockerfileをビルドします。

cd flask-gunicorn-nginx

cd web
docker build .

ビルドに成功したらコンテナのID(英数字の羅列)が出てくるので

docker run -it コンテナID /bin/bashでこのコンテナのバッシュを起動します。



↑の感じで立てたコンテナに入ります。





ちゃんとFlaskもgunicornも入っていることが確認できました!

ということで、書いた記事のサンプルコードを早速実行してみる


flask_sample.py

from flask import Flask

app = Flask(__name__)

@app.route('/hello')
def hello():
hello = "Hello world"
return hello

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


さらに、Dockerfileにこのファイルをusr/src/app下追加して実行するコマンドを書きます。

# sampleファイルをadd

ADD flask_sample.py ./usr/src/app/
# 実行
CMD ["python", "flask_sample.py"]

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

が出るのがわかっているので、

そして, docker build . && docker run -p 5000:5000 -it コンテナID

とポートを指定しつつrunすると、URLが出てくるのでたたくと、

ハマる


ハマったポイント①:Flaskのサーバーはデフォルトだと公開されてない

公式:http://flask.pocoo.org/docs/0.12/quickstart/


Externally Visible Server

If you run the server you will notice that the server is only accessible from your own computer, not from any other in the network. This is the default because in debugging mode a user of the application can execute arbitrary Python code on your computer.

If you have the debugger disabled or trust the users on your network, you can make the server publicly available simply by adding --host=0.0.0.0 to the command line:

外部公開用サーバーについて

(Flaskの)デフォルトはデバック用で、自分のネットワークのみアクセス可能な状態になっています。

もし、あなたが信頼できるネットワークに公開したければ、 host=0.0.0.0をつけて




というわけで追加したらちゃんとHello world見えました。


gunicornで起動

CMD ["python", "flask_sample.py"]で実行できるのは理解したので、gunicornで起動してみる

# 実行

CMD ["gunicorn", "flask_sample:app"]

実行すると前回同様http://127.0.0.1:8000で起動されるので、URLをたたくと

ハマりました


ハマったポイント②:gunicorn起動にbindすべし

http://docs.gunicorn.org/en/stable/settings.html#bind

bind指定しないと8000が開くということで、上記のflaskで設定した5000で開けるために、バインドして起動します。

# 実行

CMD ["gunicorn", "flask_sample:app", "-b", "0.0.0.0:5000"]

と指定して、起動すると動きました。


nginxの設定

gunicorn側の使い勝手はよくわかったので、次はnginxを設定していきます。

FROM nginx:latest

COPY default.conf /etc/nginx/conf.d/default.conf

一番新しいnginxを公式からpullしてきて、default.confは自前ので置き換えてます。

さて、そこで一つ疑問が、

...nginxとgunicornのコンテナ別々だけど、ソケット通信できるの?


ハマったポイント③:ソケット通信どうするのか

書いた記事には, gunicornで起動する際にsocketをbindすることで、生成 -> そのsocketをリバースプロキシでアクセスするという流れだったのですが、

ソケットってそもそも同サーバー内で各プロセス間のやり取りにおいて使うイメージ。(あんまり理解してないし、ソケット通信のメリットもあまり良く知っていない)

やるとしたら、

docker-composeでローカルにボリュームがマウントできるので、/tmp下をマウントして、gunicornで/tmpしたにsocket作成。nginxのコンテナも/tmp下にディレクトリ作って同じところにマウントすれば、一応できそう(これ以上案が思い浮かばないです

かなり、:thinking: になったので、簡単のため、ソケットは行わずに、ポートだけ指定してそこに飛ばすように変更しました。というわけで書いたのが下のdefault.conf


default.conf

upstream hello_app {

server web:8000;
}

server {
listen 80;

root /usr/src/app/;
server_name hello_world.excite.co.jp;

access_log /var/log/nginx/access.log main;

location / {
try_files $uri @flask;
}

location @flask {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off;

proxy_pass http://hello_app;
}

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}


nginxでhello_world.excite.co.jpを叩くと、location /にマッチして、次に@flaskにまっちしてサーバー群(hello_app)に飛んで、web:8000 にアクセスするっていう感じになります。

どうやら、docker-composeでnetworkの設定をすると、コンテナ名を指定したら名前解決して別コンテナのネットワークにアクセスできるとのこと(すごい

公式:http://docs.docker.jp/compose/networking.html#specifying-custom-networks

これを参考にdocker-composeを書いてみる。


docker-compose.yml

version: '3'

services:

web:
build: ./web
networks:
- nginx_network
ports:
- 8000:5000

nginx:
build: ./nginx
ports:
- 5000:80
depends_on:
- web
networks:
- nginx_network

networks:
nginx_network:
driver: bridge


versionは3で、services以下から2つのコンテナ("web", "nginx")を指定。

networksに"nginx_network"って名前をつけて、driver設定でbridgeにする。

公式曰く


一般的には単一ホスト上であれば bridge でしょうし、 Swarm 上であれば overlay でしょう。


なので、今回は単一なので、bridgeにしています。

あとは依存関係でgunicorn側のコンテナが上がってなかったらnginxのコンテナは挙げないみたいなdepend_on(ここはAnsibleっぽい)をつけてるくらいです。

まとめると、フロントあたり(macのPC)で5000番で受けたものを80番にポートフォワーディングして、

default.confで80番をlistenしているので、次はweb(gunicornのコンテナ)の8000にアクセスされる。

web:8000では、docker-compose.ymlが5000番にポートフォワーディングしているので、アクセスされるとgunicornの5000番バインドに引っかかり、gunicornがリクエストを受け取りFlaskのアプリケーションの中身が実行されて返却される。

という流れです。

最終的な構成図は

flask-gunicorn-nginx/

├nginx/
│ ├ Dockerfile
│ └ default.conf
├docker-compose.yml
└ web/
├ Dockerfile
└ requirements.txt

こんな感じです

docker-compose buildしてみると、

通っております...!!!

ここで、



このエラーが出たあなた。

docker-compose.ymlを【空の状態】でビルドするとこのようなエラーになります。今すぐ[ctr + s]を押して保存しましょう。

そうです。私は30分これにハマりました。

ビルドできたらdocker-compose up -dで起動して

http://hello_world.excite.co.jp:8000/hello

を叩くと....

できました!!!!


まとめ・感想


  • docker-composeでnginxとgunicornの連携を行いました。

  • docker-composeでネットワーク設定するとコンテナ名で名前解決できました。

  • 来年はもっと高度なことを書きたい(k8sとか)


参考


最後に

つくったものはgitにおいておきます。

明日は【ゲーム製作超入門】、気になります!

ではまた!


2018年01月06日追記

上記の画像を見てると、8000番ポートでアクセスしてますね...

よくdocker-compose.ymlをみてると、gunicorn側で8000番ポートを5000番(gunicornのbindポート)にポートフォワーディングしているので、実際はnginx通ってないです。(確認不足申し訳ないです


変更内容


docker-compose.yml

version: '3'

services:

web:
build: ./web
networks:
- nginx_network
expose:
- 5000

nginx:
build: ./nginx
ports:
- 8080:80
depends_on:
- web
networks:
- nginx_network

networks:
nginx_network:
driver: bridge


と、gunicorn側のコンテナの設定は、外部公開するportsではなく内部公開するexposeを使うように修正しました。これで8000番叩いてもgunicorn側には届かない。

次に、default.confは


default.conf

upstream hello_app {

server web:5000;
}

これでクライアント(8080)→nginx(80)→upstreamを経てgunicorn側のip(5000)→hello worldの表示です。

あとは

http://localhost:8080/hello

か、

http://hello_world.excite.co.jp:8080/hello

でhello worldが確認できればおkです。

申し訳ないです!


謎ポイント

docker psでnginx側のコンテナをしらべて

docker run -it nginxのコンテナ名 bin/bash

で入ってnginx -tをすると、

2019/01/06 15:04:59 [emerg] 7#7: host not found in upstream "web:5000" in /etc/nginx/conf.d/default.conf:2

nginx: [emerg] host not found in upstream "web:5000" in /etc/nginx/conf.d/default.conf:2
nginx: configuration file /etc/nginx/nginx.conf test failed

と出るのですが、これはgunicorn側のコンテナを立ち上げる前に、nginxのコンテナが立ち上がってhost名画解決できないということなのでしょうか。

強い人がいれば教えてください...涙

(ただ、これでもコンテナが落ちてない&nginxとgunicornの通信もできているので謎が深まります)