Vue.jsとFlaskのDocker環境を構築してみた

ウェブクルー AdventCalendar 14日目の記事です。
昨日は@DotaKobayashiさんの[ReactNativeで電車遅延のメールを送信する](https://qiita.com/DotaKobayashi/items/3cc0c3e2f9f0bd30ffac)でした。

本日は@kouchanneが担当します！

# 背景
社内向けの管理ツールを作る際に、あまり作り込まずにシンプルにアプリを構築する必要がありました。
試行錯誤しながらDocker環境で動くSAPのアプリ環境を構築したので、簡単に最低限の環境が構築できる手順をご紹介したいと思います。
※セキュリティとか細かい設定の部分はまだ課題があるので、今回はHello Worldが動く最低限の実装です。

# 環境
* Docker(OSにあわせて準備してください)
* docker-compose
* node.js & npm
* Python3(今回の作業そのものではいらないかもですが…)

# 作ってみる
**今回作ったサンプルは[ココ](https://github.com/kouchanne/SAPAplication)にあげてあります。**
## おおまかな構成
![アドベントカレンダー.png](https://qiita-image-store.s3.amazonaws.com/0/206268/8c00df90-fc16-ec00-0031-644d6e6d29f1.png)
**※今回のサンプルではとりあえず動けばいいのでログファイルのマウントはやっていません。**


**フロントエンド(nginxコンテナ)**
**バックエンド(Pythonコンテナ)**
**データベース(mysqlコンテナ)**

それぞれコンテナを作成し、サーバーですべてのコンテナを起動する形です。
 
## ファイルの作成
```
SPAAplication/
   ┣ frontend/
           ┣ vue/
　　　　　　 ┣ nginx/
            　　└ work/
   ┣ backend/
           ┣ python/
   ┣ mysql/
   ┣ docker-compose.yml
```

## フロントエンド(nginxコンテナ)
### Vue.jsプロジェクトの作成
* [vue-cli](https://jp.vuejs.org/v2/guide/installation.html#CLI)を使ってVue.jsプロジェクトの作成します。
* 作成したプロジェクトに移動し、 `npm run dev` をして `http://localhost:8080/` にとりあえずサンプルが表示されればOK
* 試しに `npm run build` してみて `dist` ファイルが作成されるか確認してみましょう 

```shell
npm install -g vue-cli #vue-cliのインストール 

vue init webpack vue #ProjectNameとかいろいろ聞かれるので適当に答えます
```

### nginx.confの作成
まずはnginxで使うconfファイルを作成します。
私は `./frontend/vue/nginx/work` の中に作成しました。

```nginx:nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for"';

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

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    upstream uwsgi {
        server backend:3031;
    }

    server {
        listen 80;
        charset utf-8;

        location / {
             alias /home/www/contents/;
        }

        location /api {
            include uwsgi_params;
            uwsgi_pass uwsgi;
        }
    }
}
```
全体像としては上のような感じです。
最低限の処理(とりあえず動いた)しか入れていないので、必要なものは追加してください。

```nginx
location / {
             alias /home/www/contents/;
        }
```
この部分で`/`でアクセスしてきた場合 `/hom/www/contents/` の中身を見るように指定しています。
ここにVue.jsをbuildした際に生成される`dist`ファイルを配置します。

```nginx
location /api {
            include uwsgi_params;
            uwsgi_pass uwsgi;
        }
```
今回同じサーバー内でAPIの処理も判断させるので、リクエストが/api/ だった場合はPythonコンテナへ処理を投げるリバースプロキシの役割を持たせます。




### Dockerfileの作成
Vue.jsでbuildされる静的ファイルの生成は、dockerの[multistage-build](https://docs.docker.com/develop/develop-images/multistage-build/#before-multi-stage-builds)を使います。
この方法はVue.jsの[公式](https://jp.vuejs.org/v2/cookbook/dockerize-vuejs-app.html)に書いてあります。
※ciツールなどで静的ファイルを生成する方法もありますが、それだと別途処理を書く必要があるのと管理が複雑になるのでできるだけdockerのみで完結できるようにしました。

```dockerfile
##### ビルド環境 #####
FROM node:9.11.1-alpine as build-stage
WORKDIR /app
# vue.jsのProjectをコピーする
COPY ./app ./   
RUN npm install
# npm run build で distファイルが生成されるscriptがpackage.jsonにある前提
RUN npm run build

##### 本番環境 #####
FROM nginx:1.13.12-alpine as production-stage

# contentsを配置するディレクトリを作成する
COPY nginx/work/ /home/work
RUN mkdir -p /var/log/nginx/log\
    && mkdir /home/www\
    && mkdir /home/www/contents

# ビルド環境で構築されたdistディレクトリをnignxの該当のディレクトリに配置する
COPY --from=build-stage /app/dist /home/www/contents

# nginx.confファイルを配置する
WORKDIR /home/work
RUN rm -f /etc/nginx/conf.d/*.conf\
    && rm -f /etc/nginx/nginx.conf\
    && cp -i *.conf /etc/nginx

EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;","-c","/etc/nginx/nginx.conf"]
```
上記のDockerfileにより、Vue.jsプロジェクトのビルドとnginxコンテナの立ち上げができるようになります。

※`--from=[コンテナ名] `で上のvue.jsbuildコンテナで生成されたものを使うことができるようになります。



## バックエンド(Pythonコンテナ)
続いて、バックエンドのコンテナを作成していきます。
今回は比較的にシンプルにWebアプリを作成することができるPythonのFlaskを使います。

### Flaskアプリケーションの作成
Pythonアプリケーションを動かすには[uwsgi](https://uwsgi-docs.readthedocs.io/en/latest/)を使います。

#### uwsgi.iniの作成
```ini:uwsig.ini
[uwsgi]
wsgi-file = /var/www/run.py
callable = app
master = true
processes = 1
socket = :3031
chmod-socket = 666
vacuum = true
die-on-term = true
py-autoreload = 1
```

これは、`uwsig`を動かす際のオプションが書いてあります。 
一番下の`py-autoreload` はpythonファイルが更新されると自動的に実行ファイルも更新されるような仕組みです。 Dockerで動かす際は、コンテナ内のファイルを直接編集することはないので0が良いと思います。
※ローカルで開発する際などは、1にしてソースファイルをvolumeマウントしておけば編集しながら都度確認をすることができます。

#### requirements.txtの作成
pythonでは追加のモジュールなどの管理を[pip](https://www.python.jp/install/windows/pip.html)を使って行います。
`pip install hoge` でinstallすることができますが、複数ある場合モジュールごとにinstallしてると行けていないので`requirements.txt` にインストールしたいものをかいて上げることでまとめてinstallができます。

```
Flask == 1.0.2
uwsgi == 2.0.17
mysql-connector-python-rf == 2.2.2
```
今回は Flaskとuwsgiとmysqlコントローラーを使いたいので上記のように作成しました。
`==` と書いているのはversionを指定しています。
※[その他の指定方法](https://note.nkmk.me/python-pip-install-requirements/)

#### run.pyの作成
Flaskはとてもシンプルなフレームワークなので run.py　一つでアプリを作成することができます。(Djangoなどは最初からパッケージが用意されていますが、Flaskはボリュームが増えてきたら追加していく形です)

```py:run.py
from flask import Flask                           
app = Flask(__name__)                             

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

if __name__ == '__main__':                        
    app.run(host="0.0.0.0", port=5000, debug=True)
```

先程、`nginx.conf` で `/api/` の場合はアプリケーションサーバーに処理を降ると書いたので
@app.route('/api/hogehoge') で呼ぶことができます


## データベース(MySqlコンテナ)
今回は特にMySqlに特別な設定等を加える必要がなかったので、Dockerfileは作らずに、DockerHubにあがっている[Dockerイメージ](https://hub.docker.com/_/mysql/)を使いました。

## docker-compose.ymlの作成
最後にこれらのコンテナを起動すための`docker-compose`を作成します。
コンテナをそれぞれ順番に起動すれば起動できますが、コマンドの引数が多くなってしまったり、
環境ごとに引数を変えたいなどの場合は、docker-composeを環境ごとに用意すれば構築ができてしまうという利点からdocker-composeを使います。

```yml:docker-compose
version: "3"

services:
  backend:
    build: ./backend
    image: backend
    container_name: backend_container
    links:
      - mysql
    ports:
      - "3031:3031"
    environment:
      TZ: "Asia/Tokyo"
    # volumes:
    #   - ./backend/python:/var/www/

  frontend:
    build: ./frontend
    image: frontend
    container_name: frontend_container
    links:
      - backend
    ports:
      - "80:80"
    environment:
      TZ: "Asia/Tokyo"
    # volumes:
    #   - ./web/vue/dist:/home/www/contents:ro

  mysql:
    image: mysql:5.7
    container_name: mysql_container
    volumes:
      - ./mysql/work:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: root
      MYSQL_ROOT_PASSWORD: passwd
```
上記の`docker-compose`をプロジェクトのルートにセットします。
ちなみに`linkes`を使ってコンテナを指定してあげることで、同じネットワークに属することができるようになります。


# 動かしてみる
**作ってみる**の手順を終えたらプロジェクトを立ち上げる準備は完了です。

## Dockerfileのビルドとコンテナの立ち上げ
実際に作成した`Dockerfile`をビルドして、起動してみましょう。

```shell:実行手順
docker-compose build
docker-compose up
```

ルートディレクトリで上のコマンドを実行するだけです。

## 動作確認
そしたら、コンテナが立ち上がるのでブラウザで `localhost` にアクセスしてみましょう！！！！！
Vue.jsのウェルカムページが表示されるはずです！！！！！！

次にAPI側にアクセスできるかは `localhost/api` にアクセスしてみます！！！！！
Hello World!が表示されるはずです！！！！！！


これで一通りの環境構築が完了しました。
あとは、Flaskで `@app.route('/api/hogehoge')`にAPIを作って、Vue.js上で axios などで `/api/hogehoge`で呼び出して上げれば使うことができます。

また、host名はコンテナ名で解決することができるので、Flaskのmysqlのhost名に`mysql`を指定して上げればmysqlコンテナに接続することができます。

# さいごに
今回は、最低限のSAPの環境構築方法を紹介しました。
Vue.jsの部分が他の言語になろうが、Pythonの部分が他の言語になろうが基本的な部分は同じなので
導入したい言語で応用が可能だと思いますので、参考にしていただければ幸いです。

また、今回Dockerの右も左もわからない状態で手探りで作ったため、まだまだ足りない部分があると思います。
もっと他にいい方法があるや今回書いている内容で間違っている点があれば教えていただければ幸いです。