※ この記事は エキサイト 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します。
gunicorn==19.9.0
Flask==1.0.2
まずはここまで書いて、次はコンテナを立てるためにdockerfileをビルドします。
cd flask-gunicorn-nginx
cd web
docker build .
ビルドに成功したらコンテナのID(英数字の羅列)が出てくるので
docker run -it コンテナID /bin/bash
でこのコンテナのバッシュを起動します。
ということで、書いた記事のサンプルコードを早速実行してみる
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下にディレクトリ作って同じところにマウントすれば、一応できそう(これ以上案が思い浮かばないです
かなり、 になったので、簡単のため、ソケットは行わずに、ポートだけ指定してそこに飛ばすように変更しました。というわけで書いたのが下の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を書いてみる。
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とか)
#参考
- Docker Compose with NginX, Django, Gunicorn and multiple Postgres databases
- docker-compose で別の docker-compose.yml で作ったコンテナとリンクする (ネットワークを繋げる)
- 空ビルドに気づかず頑張って読んだ記事
- Djangoの環境をDocker化する(Docker + Django + Gunicorn + nginx)その1
#最後に
つくったものはgitにおいておきます。
明日は【ゲーム製作超入門】、気になります!
ではまた!
#2018年01月06日追記
上記の画像を見てると、8000番ポートでアクセスしてますね...
よくdocker-compose.ymlをみてると、gunicorn側で8000番ポートを5000番(gunicornのbindポート)にポートフォワーディングしているので、実際はnginx通ってないです。(確認不足申し訳ないです
##変更内容
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は
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の通信もできているので謎が深まります)