Flask+MySQLで作るAPIをDocker化するまで

## これは？
APIを作リたい。Flask+MySQLで作りたい。そしてDocker化したい。
この手順をメモとして残す。個人的にDocker化するところが良く分かっていないので、ここを集中的に。

## やること
1. Flask単体で簡単なAPIを作る。
2. 1をDocker化する。
3. MySQLを用意する。
4. FlaskとMySQLの連携をdocker-composeで実行できるようにする。

### 1. Flask単体で簡単なAPIを作る。
まずなんでもいいから単純なAPIを作る。今回は、会員リスト(仮)から条件にあうデータだけをリストにして返すものを作ることに決めた。

下のファイルから、prefectureがリクエストのパラメータと一致するmail_addressのリストを返却する機能を作る。

```{personal_info.csv}
mail_address,sex,age,name,prefecture
hoge1@gmail.com,male,18,itirou,tokyo
hoge2@gmail.com,male,23,zirou,osaka
hoge3@gmail.com,male,31,saburou,tokyo
hoge4@gmail.com,female,29,itiko,tokyo
hoge5@gmail.com,mail,11,shirou,osaka
hoge6@gmail.com,female,42,fumiko,tokyo
```

Flaskを使ったコードは[こちら](https://github.com/ys201810/docker_test/blob/master/flask_test3/pure_flask/resource/app.py)。

超簡単で、app.pyを実行して、`http://127.0.0.1:5000/`に?pref=xxxをつけると、pref=xxxに一致するmail_addressがリストで返却される。

<img width="353" alt="スクリーンショット 2020-01-02 20.43.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549365/d0fd8533-7afd-38de-55ee-deb6d1ee3e1b.png"> <img width="347" alt="スクリーンショット 2020-01-02 20.43.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549365/eedc28d1-5042-d19a-c338-fd66ab5a2d6d.png"> <img width="335" alt="スクリーンショット 2020-01-02 20.43.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549365/db8cc3eb-2046-56ea-7612-77cb7ac23349.png">

次は、このAPIをDocker化する。

### 2. 1をDocker化する。
「Docker化する」とは、Dockerfileを作ってdocker buildしてDocker imageを作り、docker runでコンテナ内でapp.pyを実行してAPIリクエストが投げられる状態を作ることを目指す。

Dockerfileとは、作りたいコンテナに対し、"ベースとなるイメージを指定し、作成するコンテナの設定を記述して、コンテナ内でコマンドを実行する"動作を記述するもの。

#### 2-1 ベースとなるイメージを指定
pythonでflaskを実行できれば良いので、pythonのイメージをベースにする。

#### 2-2 コンテナの設定を記述
flaskのコードを動かすためには、以下が必要。

- ローカルのソースコードやデータをコンテナに配置する。
- 必要なライブラリをinstallする。

#### 2-3 コンテナ内でコマンドを実行する。
作成したapp.pyを実行する。

これら2-1~2-3を意識して作ったDockerfileが以下。

```{Dockerfile}
# 2-1 ベースとなるイメージを指定
FROM python:3.6  
# 2-2 コンテナの設定を記述
ARG work_dir=/work  # Dockerfile内で扱う変数の作成 
ADD pure_flask $work_dir/pure_flask  # コードを/work/にコピー(ディレクトリをコピーする時は、右側にディレクトリ名を記述しないといけないので注意)

WORKDIR $work_dir/pure_flask  #  cd work_directoryのイメージ

RUN pip install -r requirements.txt  # requirements.txtで必要なライブラリをインストール
# 2-3 コンテナ内でコマンドを実行
CMD ["python", "/work/pure_flask/app.py"]   # CMDは基本的にDockerfile内に1つだけ。
```

requirements.txtは超簡単にflaskだけ記述。

```{requirements.txt}
flask
```

この状態のDockerfileがあるディレクトリで`docker build -t flask:ver1 .`を実行する。
上記コマンドの.はカレントディレクトリにあるDockerfileを使ってイメージをbuildすることを指す。

次に、`docker run -it -d -p 5000:5000 flask:ver1`を実行。
-dはバックグラウンド実行を、-p 5000:5000はローカルの5000番ポートと、コンテナの5000番ポートのポートフォワーディングを指定することを指してます。

この状態でローカルマシンでlocalhost:5000でブラウザで確認すると、APIのreturnが確認できる。

<img width="345" alt="スクリーンショット 2020-01-03 11.01.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549365/1db436c3-6451-b340-9058-4baf79fb9553.png">


ちなみにこの状態で`docker ps`と`docker images`は以下のようになる。
もしdocker psで何も表示されない場合はエラーでこけてる可能性があるため、docker ps -aで表示したコンテナIDを使って、`docker logs [コンテナID]`をしてあげると理由がわかるかも。

```
$ docker ps
> CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
> dc371c597cef        flask:ver1          "python /work/pure_f…"   9 minutes ago       Up 9 minutes        0.0.0.0:5000->5000/tcp   quizzical_margulis
$ docker images
> REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
> flask               ver1                b4cda0e56563        9 minutes ago       923MB
> python              3.6                 138869855e44        5 days ago          913MB
```

無事、1. Flask単体で簡単なAPIを作ると同じ動作がDocker上でも確認できた。

次は、MySQLを使うための準備をする。

### 3. MySQLを用意する。
MySQLもDockerで用意する。今回は、Docker-compose.yamlの利用も同時にする。

Docker-compose.yamlは、Dockerfileと似ているが、複数のコンテナの連携などを記述するためにある。
DBは接続されることが前提のため、Docker-composeで記述するのが自然だと思う。

#### 3-1 ベースとなるイメージを指定
MySQLが使いたいため、MySQLのイメージをベースにする。

#### 3-2 コンテナの設定を記述
MySQLの設定ファイルやデータベースの初期化をする。

#### 3-3 コンテナ内でコマンドを実行する。
MySQLのプロセスを起動する。

Dockerfileは以下。

```
# 3-1 ベースとなるイメージの指定
FROM mysql:5.7

# 3-2 コンテナの設定を記述
COPY conf.d/mysql.cnf /etc/mysql/conf.d/mysql.cnf  # 文字コードの設定のセット
COPY initdb.d/init.sql /docker-entrypoint-initdb.d/init.sql  # 初期化用のSQLファイルのセット
```

Dockerfileの中でcopyしているconfigファイルとinit.sqlはそれぞれ以下。

```{mysql.cnf}
[mysqld]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8
```

```{init.sql}
CREATE TABLE `personal_info` (
    mail_address VARCHAR(100) NOT NULL,
    sex VARCHAR(6) NOT NULL,
    age INT NOT NULL,
    name VARCHAR(50) NOT NULL,
    prefecture VARCHAR(50) NOT NULL,
    createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (mail_address)
);

INSERT INTO `personal_info` (mail_address, sex, age, name, prefecture, createdAt, updatedAt)
VALUES
('hoge1_1@gmail.com', 'male',   18, 'ichirou1' , 'tokyo', current_timestamp(), current_timestamp()),
('hoge2@gmail.com', 'male',   23, 'zirou2', 'osaka', current_timestamp(), current_timestamp()),
('hoge3_3@gmail.com', 'male',   31, 'saburou', 'tokyo', current_timestamp(), current_timestamp()),
('hoge4@gmail.com', 'female', 29, 'itiko', 'tokyo', current_timestamp(), current_timestamp()),
('hoge5_5@gmail.com', 'mail',   11, 'shirou', 'osaka', current_timestamp(), current_timestamp()),
('hoge6@gmail.com', 'female', 42, 'fumiko', 'tokyo', current_timestamp(), current_timestamp());
```

docker-compose.yamlは以下。

```
version: '3'  # docker-compose.yamlの記述のバージョン
services:  
  db:  
    build: mysql  # mysqlディレクトリ配下のDockerfileを指定
    container_name: db_server  # Dockerコンテナの名前
    ports:
      - '3306:3306'  # ポートフォワーディング指定
    environment:  # 環境変数の設定
      MYSQL_ROOT_PASSWORD: pass  # MySQLのrootユーザーのパスワード
      MYSQL_DATABASE: testdb     # MySQLのスキーマ
      TZ: Asia/Tokyo             # タイムゾーンの指定
    volumes:  # MySQLのデータの永続化をするためのボリュームマウント
      - db-data/:/var/lib/mysql
    # 3-3 コンテナ内でコマンドを実行する
    command: mysqld  # mysqldコマンドの実行
volumes:
  db-data:
```

上記ができたら、`docker-compose up -d`でdbコンテナを起動する。
その後、`docker exec -it [コンテナID] /bin/bash`でログインし、`mysql -u root -p`からパスワードを入力してテーブルを確認し、init.sqlの内容が入っていればOK。

#### [備考]mysql周りのtips
MySQLのデータを永続化させるためのvolumeにデータが保存されない場合、Dockerホストでdocker volume lsでvolumeを確認して削除してからdocker-compose up -dすると良さげ。

init.sqlは/docker-entrypoint-initdb.d/に固定でコピーする。

次は、flaskとMySQLを接続させる。

### 4. FlaskとMySQLの連携をdocker-composeで実行できるようにする。
2と4で作成した二つのDockerコンテナを結合させる。
結合させるために必要なことは、以下。

- flaskもdocker-compose.yamlに入れる。
- flaskからMySQLへの接続が可能なようにする(ネットワークを)。
- flaskからMySQLへの接続が可能なようにする(コードを)。

#### 4-1 flaskもdocker-compose.yamlに入れる
flaskのコンテナをMySQLのdocker-compose.yamlに追記したものは以下。

```
version: '3'
services:
  api:
    build: python
    container_name: api_server
    ports:
      - "5000:5000"
    tty: yes
    environment:
      TZ: Asia/Tokyo
      FLASK_APP: app.py
    depends_on:  # apiサーバーはdbサーバーが立ってから起動
      - db
    networks:  # apiとdbを接続するための共通ネットワーク指定
      - app_net
  db:
    build: mysql
    container_name: db_server
    ports:
      - '3306:3306'
    environment:
      MYSQL_ROOT_PASSWORD: pass
      MYSQL_DATABASE: testdb
      TZ: Asia/Tokyo
    volumes:
      - ./db-data:/var/lib/mysql
    command: mysqld
    networks:
      - app_net
volumes:
  db-data:
networks:
  app_net:
    driver: bridge
```

services直下の階層に、dbと同じ階層としてapiを記述。

#### 4-2 flaskからMySQLへの接続が可能なようにする(ネットワークを)。
コンテナを二つ起動し、その間で接続をする場合、二つのコンテナを同一のネットワークで扱う必要がある。
4-1のdocker-compose.yaml内にそれぞれのserviceの中に以下を追記する。

```
networks:
      - app_net
```

これにより、各コンテナがどのネットワークを利用するかを指定できる。二つを同じネットワークにしていることを意味する。
そして、docker-compose.yamlのトップレベルの階層に以下を記述する。

```
networks:
  app_net:
    driver: bridge
```

これは、dockerのネットワークの作成を意味していて、driverをbridge指定で作る指定。
これでapiとdbのコンテナが同一ネットワーク上となるため、apiからdbへの接続が可能となる。

#### 4-3 - flaskからMySQLへの接続が可能なようにする(コードを)。
flask側のコードとモジュールの修正。
pythonからMySQLに接続する方法は色々ありそうだが、今回はmysqlclientを使った。

`pip install mysqlclient`でmysqlclientをインストールし、以下のようなコードで接続して使う。

```
import MySQLdb

conn = MySQLdb.connect(user='root', passwd='pass', host='db_server', db='testdb')
cur = conn.cursor()
sql = "select * from personal_info;"
cur.execute(sql)
rows = cur.fetchall()
```

このrowsには、タプルが返却され、レコード数の長さのタプルが取得できる。

```{rowsのイメージ.}
(
 (1レコード目の1カラム目の値, 1レコード目の2カラム目の値, ...), 
 (2レコード目の1カラム目の値, 2レコード目の2カラム目の値, ...),
 (3レコード目の1カラム目の値, 3レコード目の2カラム目の値, ...)
)
```

これをpythonで受け取り、リストに格納してjsonで返却するようなイメージ。

修正後のソースは[ここ](https://github.com/ys201810/docker_test/tree/master/flask_test3/mysql_flask)。
ディレクトリ構成とか、Dockerfileの位置などもこれを参照してもらえれば。

あとは、pythonで扱いたいモジュールが増えたのでrequirements.txtにmysqlclientを追記する。

これで完了。
`docker-compose up -d`で全て起動したら、ローカルで`http://0.0.0.0:5000/?pref=osaka`などを入れると、結果が確認できる。

以上。