DockerでReactとMariaDB使ってWebサービスの雛形を作る

# 目的

* いろいろソフト入れて環境構築しているとわけがわからなくなってくる。
   * Dockerに設定全部書いて簡単に構築できるようにする。
* Pythonであれこれ処理したものをデータベースに保存したい。
   * PandasでMariaDBに接続しよう。
* フロントエンドはReactを使いたい。
   * Webpack + Babelを導入する。
* Webサーバーはどうする？
   * 最近流行のNginxを使おう。

# Dockerの導入まで

* GCPでCentOS7のインスタンスを作成する。
* dockerとdocker-composeをインストールする。
* dockerをサービスに登録し、sudoなしでdockerコマンドを実行できるようにする。

```bash
sudo yum install -y yum-utils device-mapper-persistent-data lvm2 && \
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && \
sudo yum makecache fast && \
sudo yum install -y docker-ce && \
sudo usermod -aG docker $USER && \
sudo systemctl enable docker

sudo curl -L https://github.com/docker/compose/releases/download/1.20.0/docker-compose-`uname \
-s`-`uname -m` -o /usr/local/bin/docker-compose && \
sudo chmod +x /usr/local/bin/docker-compose
# バージョン確認して再起動
docker-compose --version
sudo reboot
```

# コンテナの構成

以下の3つのコンテナを作る。

* MariaDB
* Node
* Nginx

1. ローカルにPython実行環境とMariaDBのクライアントをインストールする。
2. MariaDBを初期設定の後、Pythonを使ってデータを流し込む。
3. Nodeの初期設定はイメージ作成後にnpm installで実行する。
4. Nginxの設定書いてからDocker-compose upで起動。

# Python3のインストール

```bash
sudo yum install -y https://centos7.iuscommunity.org/ius-release.rpm && \
yum search python36 && \
sudo yum install -y python36u python36u-libs python36u-devel python36u-pip && \
sudo ln -s /usr/bin/python3.6 /usr/bin/python3 && \
sudo ln -s /usr/bin/pip3.6 /usr/bin/pip3 && \
python3 --version && pip3 --version
sudo pip3 install pip --upgrade && \
sudo pip3 install sqlalchemy && \
sudo pip3 install PyMySQL && \
sudo pip3 install pandas
```


# MariaDBクライアントのインストール

```bash
sudo yum install -y mariadb
```

# MariaDBコンテナの作成

```bash
mkdir docker && cd docker
mkdir container && mkdir container/mariadb && mkdir container/mariadb/init

cat << EOF > container/mariadb/multibyte.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
EOF

# Dockerfileの作成とDocker-composeに設定を追記
cat << EOF > container/mariadb/Dockerfile
FROM mariadb

EXPOSE 3306
EOF

cat << EOF > docker-compose.yaml
version: '3'
services:
  mariadb:
    build:
      context: ./container/mariadb
    container_name: mariadb
    image: mariadb
    volumes:
      - ./mariadb_data:/var/lib/mysql:z
      - ./container/mariadb/multibyte.cnf:/etc/mysql/conf.d/multibyte.cnf
      - ./container/mariadb/init:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=rootpass
      - MYSQL_DATABASE=app
      - MYSQL_USER=username
      - MYSQL_PASSWORD=secret
EOF

docker-compose up
```

# MariaDBの動作確認

別のターミナルから……

```bash
cd ~/docker

# テストデータの準備
cat << EOF > test_data.csv
ID,Name,Birthdate,Sex,Occupation,Salary
ID-0001,Abe,1985/1/1,M,Engineer,8422213
ID-0002,Saito,1970/2/11,F,Professor,8222588
ID-0003,Yamada,1975/3/21,M,Doctor,9845288
ID-0004,田中,1980/4/22,F,Sales,8505218
ID-0005,Okamoto,1995/5/25,M,Student,218103
EOF

cat << EOF > test_data.py
import pandas as pd
import sqlalchemy as sa

df = pd.read_csv("test_data.csv")
url = 'mysql+pymysql://username:secret@127.0.0.1/app?charset=utf8'
engine = sa.create_engine(url, echo=True)
df.to_sql('table1', engine, index=False, if_exists='replace')
EOF

python3 test_data.py test_data.csv && rm test_data.csv && rm test_data.py
mysql -u root -prootpass -h 127.0.0.1
> use app
> SELECT * FROM table1;
# UTF8出力チェック
> exit
```
一度docker-composeを止める

# nodeコンテナの作成

```bash
mkdir container/node && mkdir app && mkdir app/src && mkdir app/dist && mkdir app/node_modules

cat << EOF > container/node/Dockerfile
FROM node:9

WORKDIR app
RUN npm install -g npm
EOF

cat << EOF >> docker-compose.yaml
  node:
    build:
      context: ./container/node
    container_name: my-node
    image: my-node
    volumes:
      - ./app:/app:z
    ports:
      - "80:8080"
EOF

docker-compose build
```

# npm install

完成package.json用意してのnpm updateは重くて挙動が怪しいので少しずつ入れる。
dev系はサーバーでは不要なものもあるがローカルと設定を共通にするために入れておく。

```bash
docker-compose run node npm init -y
docker-compose run node npm i -D webpack webpack-cli webpack-dev-server babel-core babel-loader babel-preset-env babel-preset-react babel-polyfill
docker-compose run node npm i -D uglifyjs-webpack-plugin sass-loader node-sass style-loader css-loader style-loader css-loader url-loader
docker-compose run node npm i -S react react-dom redux react-redux
docker-compose run node npm i -D redux-devtools react-hot-loader
docker-compose run node npm i -D eslint eslint-loader eslint-plugin-node eslint-plugin-react babel-eslint
docker-compose run node npm i -D node-mariadb
```

# 設定ファイルの準備

webpack4で設定が簡単になったというが、十分複雑だ。

```bash
cat << EOF > update_package_json.py
import json
import collections

decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
with open('app/package.json') as read_file:
    data = decoder.decode(read_file.read())
    data["name"] = "react-test"
    data["version"] = "0.1.0"
    data["author"] = "tibigame"
    data["description"] = "react-test"
    data["scripts"]["build"] = "webpack"
    data["scripts"]["start"] = "babel-node ./src/server.js"
    data["scripts"]["dev"] = "webpack-dev-server --hot"
    data["scripts"]["watch"] = "webpack -d --watch --progress"
    fw = open('app/package2.json','w')
    json.dump(data, fw, indent=4)
EOF

python3 update_package_json.py && rm -rf app/package.json && mv app/package2.json app/package.json

cat << EOF > app/webpack.config.js
module.exports = {
  mode: 'production',
  entry: [
    'babel-polyfill',
    './src/index.js',
  ],
  output: {
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: [
          {
            loader: 'babel-loader'
          }
        ],
        exclude: /node_modules/
      },
      {
        test: /\.scss/,
        use: [
          'style-loader',
          {loader: 'css-loader', options: {url: false}},
        ],
      }
    ]
  }
};
EOF

cat << EOF > app/.babelrc
{
  "presets": [
    "react",
    ["env", {
      "targets": {
        "browsers": ["last 2 versions"]
      },
      "modules": false
      }
    ]
  ],
  "plugins": [
    "transform-class-properties",
    "transform-object-rest-spread",
    "react-hot-loader/babel"
  ]
}
EOF
```

# ReactとSCSSの確認ファイルの準備

```bash
cat << EOF > app/dist/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <style>
    body {
      background: #eee;
    }
    #app {
      display: flex;
      justify-content: center;
      align-items: center;
      text-align: center;
    }
  </style>
  <script defer src="main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
EOF

cat << EOF > app/src/style.scss
$red: #FF3333;
h1 {
  color: $red;
}
EOF

cat << EOF > app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import "./style.scss"

import {TestComponent} from './test_component';

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello React!</h1>
        <TestComponent name="My Counter for Babel" />
      </div>
    );
  }
}
console.log('テスト');
ReactDOM.render(<App/>, document.querySelector('#app'));
EOF

cat << EOF > app/src/test_component.js
import React from 'react';

export class TestComponent extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }

  handleClick() {
    console.log('クリックされました');
    this.setState({
      count: this.state.count + 1
    });
  }

  render() {
    return (
      <div>
        <h2>{this.props.name}</h2>
        <div>{this.state.count}</div>
        <button onClick={this.handleClick.bind(this)}>Add +1</button>
      </div>
    );
  }
}
EOF
```

# Nginxコンテナの作成

ここの設定はよく理解していないが、制限かけておけば開発中でも問題ないだろう。

```bash
mkdir container/nginx
sudo yum install -y httpd-tools
htpasswd -c ./container/nginx/.htpasswd http_user
password

cat << EOF > container/nginx/Dockerfile
FROM nginx
EOF

vim container/nginx/default.conf
--------
server {
  listen 8080;
  server_name localhost;
  location /api/ {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
  location / {
    root /www/app;
  }
  # IPアドレスによる制限
  allow xxx.xxx.xxx.xxx;
  deny all;
  # BASIC認証による制限
  auth_basic "Restricted";
  auth_basic_user_file /etc/nginx/conf.d/.htpasswd;
}
--------

cat << EOF > docker-compose.yaml
version: '3'
services:
  node:
    build:
      context: ./container/node
    container_name: my-node
    image: my-node
    command: npm run build
    volumes:
      - ./app:/app:z

  mariadb:
    build:
      context: ./container/mariadb
    container_name: mariadb
    image: mariadb
    volumes:
      - ./mariadb_data:/var/lib/mysql:z
      - ./container/mariadb/multibyte.cnf:/etc/mysql/conf.d/multibyte.cnf
      - ./container/mariadb/init:/docker-entrypoint-initdb.d
    expose:
      - 3306
    environment:
      - MYSQL_ROOT_PASSWORD=rootpass
      - MYSQL_DATABASE=app
      - MYSQL_USER=username
      - MYSQL_PASSWORD=secret

  nginx:
    build:
      context: ./container/nginx
    image: nginx
    container_name: nginx
    ports:
      - '80:8080'
    volumes:
      - ./container/nginx:/etc/nginx/conf.d:ro
      - ./app/dist:/www/app:ro
    links:
      - 'node'
      - 'mariadb'
EOF

docker-compose run node npm run build
docker-compose up
```

# ブラウザでの動作確認

![a.jpg](https://qiita-image-store.s3.amazonaws.com/0/133885/49dd0aa6-e934-229b-e282-79f9690b309a.jpeg)

とりあえずReactのコンポーネントはちゃんとビルドされてNginxも機能しているようだ。
が、データベースとの接続は一筋縄ではいかなかった。

# TODO

* なんでもNode.jsはデータベースだけを扱ってwabpackの高度なビルドはしないとかだといいが、全部やろうとするとサーバーサイドレンダリングとかでダークサイドに落ちてしまう。
* 軽く触った感じでは深入りせずにエンジニアはeslintやmocha、sassの書き方に注力したほうがいい。
* Reactの世界とデータベースの間は薄いけど闇が深い。
 * ここをつなぐのはRailsかDjangoか…いや、もっと薄い皮でいいのでREST APIを試してみよう。