docker+nginx+uwsgi+Flask+scikit-learnで機械学習予測用のWebAPIつくるまで(環境設定編)

# 動機
- 自分は普段業務アプリケーション開発をやっているので、サーバー構築の知識を付けたかった。
- Dockerの使い方を知る
- 機械学習予測用のサーバー構成を考えてみたかった。

---

# 要件
- 機械学習の予測結果をWebAPI形式で返したい
- Python、scikit-learn、tensorflowなどをそのまま使いたい
- 機械学習の予測はCPUコストが高いので、複数のアプリケーションサーバで分散させたい

---
# 構成
だいたい↓こんな感じの構成をDockerを使ってやってみようという考え。

```:イメージ
 --------                        -----------------
| Client | ---> ( Web API) ---> | Proxy Server    |
|        |                      |    (nginx)      |
 --------                        -----------------
                                         |
                                         V
                                  --------------------
                                  |                  |
                          ------------------    --------------
                         | App Server1      |  | App Server 2 |
                         |     (uwsgi)      |  |  (uwsgi)     |
                          ------------------    --------------
                                  |
                                  V
                          ------------------
                         | App Framework    |
                         |      (Flask)     |
                          ------------------
                                  |
                         ------------------------------
                        |   Application                |
                        | (Python,scikit-learn etc...  |
                         ------------------------------
```
- Pythonやscikit-learnで学習したモデルをそのままWebアプリにしたいので、フレームワークはPythonのFlask
- アプリケーションサーバにはuWSGI（参考記事が多かったので…）
- ロードバランサの役割としてNginxを前におく（これまた参考記事が多かったので）

---
# 準備
- HOSTマシンは普段使っているUbuntu16.04LTSマシンにDockerをインストール
- 作業ディレクトリを作成

このようなファイル構成になる予定

```[text:ファイル構成]
/myserver/
     myserver.sh           # 起動や停止のコマンドが書かれたシェル
     appserver/
         Dockerfile        # APPサーバのDockerfile
         app/              # コンテナの/var/www/appと共有するディレクトリ
             uwsgi.ini     # uWsgiの設定ファイル
             main.py       # Webアプリ本体
     proxyserver/
         Dockerile         # WebサーバのDockerfile
         conf.d/           # コンテナの/etc/nginx/conf.dと共有するディレクトリ
             default.conf  # サーバー設定ファイル
```

---
# アプリケーションサーバ（uwsgi）
## Dockerfileの作成
- アプリケーションサーバのベースイメージはUbuntu
- `python3`,`python3-pip`,`flask`などをインストール

```Docker:/appserver/Dockerfile
FROM ubuntu

# Python、pipをインストール
RUN apt update
RUN apt install --yes python3 python3-pip
RUN apt clean
RUN ln -s /usr/bin/python3 /usr/bin/python
RUN ln -s /usr/bin/pip3 /usr/bin/pip

# FlaskとuWSGIをインストール
RUN pip install flask
RUN pip install uwsgi

# アプリの配置場所作成 
RUN mkdir /var/www/app

#サーバ起動用のユーザーを作成
RUN useradd -r -s /bin/false uwsgiusr
USER uwsgiusr

# ポートは8080
EXPOSE 8080
# サーバー起動コマンド
CMD ["uwsgi","--ini","/var/www/app/uwsgi.ini"]
```
---

## uWSGIサーバー設定ファイルを作成
```ini:/appserver/app/uwsgi.ini
[uwsgi]
master = true
base = /var/www/app
chdir = /var/www/app
module = main
http-socket = :8080
callable = app
processes = 1
threads = 2
uid = uwsgiusr
gid = uwsgiusr
```
---

## Flaskサンプルアプリ作成
今回はサンプルだが、ここに機械学習予測を書く予定。

```py:/appserver/app/main.py
from flask import Flask

app = Flask(__name__)

@app.route('/sample')
def sample():
    return "サンプルだよ！"

```
---

# リバースプロキシサーバー(nginx)
# Dockerfileの作成

- ベースは`alpine`という軽量Linux

```Dockerfile:/proxyserver/Dockerfile
FROM alpine

RUN apk update

# nginxのインストール
RUN apk add nginx

# 事前にフォルダ作ってないとこけたのでフォルダ作成
RUN mkdir /run/nginx

# これは要らないかもしれないけど、デーモン起動をオフに
RUN echo "daemon off;" >> /etc/nginx/nginx.conf

# ポートは80番
EXPOSE 80

# Nginx起動コマンド
CMD ["nginx","-c","/etc/nginx/nginx.conf"]

```

---

## nginxサーバ設定ファイルを作成

- ２つのアプリケーションサーバ`appsrv1`と`appsrv2`がある。
- リクエストされると、上記のアプリケーションサーバーに振り分けられる

```conf:/proxyserver/conf.d/default.conf
upstream my_servers {
	server appsrv1:8080;
	server appsrv2:8080;
}
server {
	listen 80 default_server;
	listen [::]:80 default_server;

	location / {
		include uwsgi_params;
		proxy_pass http://my_servers;

	}
}

```
---

# コンテナをビルド〜起動まで
## ビルド・起動・停止用のシェル作成
- 複数コンテナを動かすために`Docker compose`なるものがあるようだが、今回はシェルを作成した。
- アプリケーションサーバ　→ プロクシサーバの順番に起動しないと、`--link`できない。
- `--rm`オプションをつけるとコンテナ停止後に自動でコンテナ削除してくれる。（最初これを知らなくて、ずっと`docker rm`コマンドで削除してた・・・）
- プロキシサーバは`-p`オプションでホストのポート番号8080からコンテナのポート番号80へフォワードする
- `-v`オプションでホストのアプリ配置ディレクトリとコンテナのアプリ配置ディレクトを共有する。このオプション、絶対パス指定ではダメらしいので、とりあえず`pwd`コマンドで作業ディレクトリのパスを記述

```bash:myserver.sh
#!/bin/bash

# アプリケーションサーバのコンテナ名とイメージ名
APPSRVNAME1=appsrv1
APPSRVNAME2=appsrv2
APPSRVIMAGE=myappsrv_img

# プロキシサーバのコンテナ名とイメージ名
PROXYSRVNAME=proxysrv
PROXYSRVIMAEG=mywebsrv_img

# ポートフォワードするホストのポート番号
HOSTPORT=8080

# サーバー起動コマンド
if [ $1 = "run" ]; then

	# アプリケーションサーバ1の起動
	docker run \
		-d\
		--rm\
		--name $APPSRVNAME1\
		-v $(pwd)/appserver/app:/var/www/app\
		$APPSRVIMAGE

	# アプリケーションサーバ2の起動
	docker run \
		-d\
		--rm\
		--name $APPSRVNAME2\
		-v $(pwd)/appserver/app:/var/www/app\
		$APPSRVIMAGE

	# プロキシサーバーの起動
	docker run \
		-d\
		--rm\
		--name $PROXYSRVNAME\
		-p $HOSTPORT:80\
		--link $APPSRVNAME1:$APPSRVNAME1\
		--link $APPSRVNAME2:$APPSRVNAME2\
		-v $(pwd)/proxyserver/www:/var/www\
		-v $(pwd)/proxyserver/conf.d:/etc/nginx/conf.d $PROXYSRVIMAEG
	exit
fi

# サーバー停止
if [ $1 = "stop" ]; then
	docker stop $APPSRVNAME1
	docker stop $APPSRVNAME2
	docker stop $PROXYSRVNAME
	exit
fi

# イメージビルド
if [ $1 = "build" ]; then
	docker build -t $APPSRVIMAGE ./appserver/.
	docker build -t $PROXYSRVIMAEG ./proxyserver/.
	exit
fi

```
---

## ビルド
```bash
$ sudo ./myserver.sh build
```

## 起動
```bash
$ sudo ./myserver.sh run
```

- ビルドできたら、サーバーを起動してみる
- ブラウザから'localhost:8080/sample'にアクセスし、Flaskアプリが値を返すと成功！
- ちゃんと2台のアプリケーションサーバが動作している！

## 停止
```bash
$ sudo ./myserver.sh stop
```
---

# はまったこと・理解したこと
- root権限以外ではコンテナの80番ポート（ローナンバーのポート）が使えない事をしらず、小一時間はまったてた
- Dockerは環境構築を色々試し、使い捨てができるのがいい。
- Dockerのコマンド長くて面倒なので、複数まとめてシェルにしておくのが楽(Docker Composeなるものを使うともっと楽らしい）
- nginxやuWSGIの設定ファイルの書き方で解らない部分が多いので、勉強が必要。


---
# 今後
構築したサーバー上にScikit-learnで学習した予測モデルを突っ込み、予測結果を返すようなWebAPIを作成する予定。