[Flask]Webアプリのサクッと作れる『docker-compose構成』をまとめてみた

先日、`docker-compose`を使ってFlaskアプリを構築しQiitaで紹介しました。

[■Flask+Docker+Vue.js+AWS...でゲームWebAppを作ってみた。](https://qiita.com/akinko/items/404a7312d0fd0dff6df6)

[■Githubにソースコード、ゲームルールを公開中](https://github.com/akiraseto/introdon)

今回は、Flaskアプリ開発での`docker-compose構成`を掘り下げてまとめてみました。
複数コンテナでのWebアプリ開発を検討している方の参考になればと思います:relaxed:


# はじめに
最初はとっつきにくいコンテナ化技術。しかし、一度慣れると手放すことができません。


## Dockerは実は楽
- コマンド1行で環境を再現
- 環境作るのが楽  
Docker-Hubから該当のimage取ってくるだけ
- どんな環境でも同じように開発  
コンテナに環境を閉じ込めているのでwinでも、macでも同じように動きすぐに開発できる
- システム構築過程をコード化できる
- いろいろと環境を試せて、簡単に消せて作れる。  
環境を色々と試したり、プラグインを手軽にインストールできる。  
試した後は、コンテナごと消せばいいのでノーストレス。


## docker-composeはさらに楽
- 複数のコンテナを連動して管理できる。
- コンテナ同士を簡単にnetworkできる。


### なんで複数コンテナ？
1つのコンテナに詰め込んだらだめなの？
別に悪い訳ではないですし、規模や内容によっては1コンテナで十分の場合もあります。

単純に、複数のコンテナに分けた方がメリットが多いんです。
基本**1コンテナ1タスク**にして役割を分けていきます。

- エラーの特定しやすい
- 負荷を分散できる
- 処理を並列化できる
- 個別にスケールできる
- エラーで連動して別の処理が、道連れに止まることを回避できる  

ざっと書き出しただけでもこんなにメリットがあります。
むしろDockerでのシステム構築は複数コンテナが連動する方が多いと思います。


# 主なコマンド
`docker-compose.yml`ファイルと同じディレクトリでコマンド実行します。
yamlファイルに基づき、Dockerコンテナを構築制御します。


## up 
複数コンテナの生成/起動
`docker-compose up -d`

`-d`デタッチモードでバックグラウンドで起動

コンテナ名を引数に取ればそのコンテナだけ起動します。

```sh

docker-compose up -d hoge
# hogeコンテナだけ起動
```


## kill
実行中コンテナを強制停止する
`docker-compose kill`


## down
コンテナを停止し、作成したコンテナ・ネットワークを削除します。
`docker-compose down`


## build
サービスをビルドします。
簡単に言うと、`image`を構築します。
`docker-compose build`


## 番外:よく使うコマンド
起動したコンテナにアタッチする
`docker exec -it ID_OR_NAME bash`

`ID_OR_NAME`箇所は、yamlで定義した`container_name`入れるだけでアタッチできます。
コンテナでDB初期化や、出力内容の確認、簡単なファイルの確認など何かと多用します。


# docker-compose.yml
ここからは、本題のFlaskアプリでの`docker-compose`設定に触れていきます。

```yaml:docker-compose.yml

version: "3"

services:
 nginx:
  build: nginx
  container_name: nginx
  ports:
   - "80:80"
  volumes:
   - ./nginx/nginx.conf:/etc/nginx/nginx.conf
   - ./docker_data/log/nginx:/var/log/nginx
  depends_on:
   - flask
  networks:
   - front

 flask:
  build: .
  container_name: flask
  environment:
   # PROD, DEV, TEST
   FLASK_ENV: PROD
  volumes:
   - .:/introdon
   - ./docker_data/log/flask/gunicorn_access.log:/var/log/gunicorn_access.log
   - ./docker_data/log/flask/gunicorn_error.log:/var/log/gunicorn_error.log
  depends_on:
   - db
   - db_test
  networks:
   - front
   - back
   - test
  expose:
   - 5000

 db:
  build: db
  container_name: mariadb
  ports:
   - "53306:3306"
  volumes:
   - ./db/my.conf:/etc/mysql/conf.d/my.cnf
   - ./docker_data/mysql:/var/lib/mysql
   - ./docker_data/log/mysql:/var/log/mysql
  networks:
   - back
  environment:
   MYSQL_DATABASE: introdon
  env_file: ./db/envfile #password記載のためignore

 db_test:
  build: db
  container_name: mariadb_test
  ports:
   - "63306:3306"
  volumes:
   - ./db/my.conf:/etc/mysql/conf.d/my.cnf
  networks:
   - test
  environment:
   MYSQL_DATABASE: introdon_test
  env_file: ./db/envfile #password記載のためignore

networks:
 front:
  driver: bridge
 back:
  driver: bridge
 test:
  driver: bridge
```
以下、簡単に基本のおさらいです。


## services:
立ち上げるコンテナを任意の名前で書いていきます。
今回は、nginx, flask, db, db_testの4つのコンテナとなります。


## build:
imageのbuild元を書きます。

```
build: .
```
該当ディレクトリ内の Dockerfileを元にbuildします。
この場合だと、`.`ymlファイルと同じディレクトリ内のDockerfileとなります。


## ports:
ローカルとのポートマッピングです。
左側がローカル、右側がコンテナ内のポートとなります。

```
ports:
- "63306:3306"
```
上記だと、ローカルの63306番に接続すると、コンテナ内の3306番に繋がります。

なお、ポートマッピングは文字列として入力します。
ymlの仕様がxx:yy形式を60進数の数値と認識してしまう、時刻として認識される可能性があるため文字列にします。

ローカルが使用していないポートを使う必要があり、すでに使用済みですと、`up`でエラーになります。
エラーが出たら、Listenしているポートを確認します。

```
sudo lsof -i -P | grep "LISTEN"
```


## volumes:
ローカルファイルと、コンテナ内ファイルをバインドします。

- データをローカルに保存して永続化できます。
- ローカルに保存した設定ファイルをコンテナに読み込ますのによく使います。


## depends_on:
依存コンテナを指定します。
簡単に言い換え得ると、
「指定したコンテナが立ち上がってから、このコンテナを立ち上げてね」って意味です。


## networks:
ネットワークを作ります。
任意の名前のネットワークを指定すれば、同じネットワークを定しているコンテナと接続できます。


## networks driver:
デフォルト設定は`bridge`です。
ドライバーは以下となります。

- `bridge`: 同一のホストでの通信に使用
- 'overlay': 異なるホスト間での通信に使用


続いて、各コンテナを個別に解説していきます。:arrow_down:


# flask contaner (Appサーバー)
アプリケーションサーバーの役割を担当するFlaskのコンテナ設定です。

```yml
#抜粋

 flask:
  build: .
  container_name: flask
  environment:
   # PROD, DEV, TEST
   FLASK_ENV: PROD
  volumes:
   - .:/introdon
   - ./docker_data/log/flask/gunicorn_access.log:/var/log/gunicorn_access.log
   - ./docker_data/log/flask/gunicorn_error.log:/var/log/gunicorn_error.log
  depends_on:
   - db
   - db_test
  networks:
   - front
   - back
   - test
  expose:
   - 5000
```

コンテナは同ディレクトリのDockerfileからbuildします。
コンテナ名はflaskです。

環境変数`FLASK_ENV`によりアプリのモードを変更しています。

- PROD: 本番環境モード、WSGIでGunicornを使用
- DEV: 開発モード。エラーやデバッグ内容が表示、オートリロードもされる
- TEST: pytestが実行さる。テスト用のDBコンテナを使用する

volumes:

- flaskアプリコードをコンテナにボリュームします。
- ホストにdocker_dataディレクトリを作成し、Gunicornのアクセスログと、エラーログを永続化します。

depends_on
db, db_testコンテナが起動してからAppコンテナを起動します。
あくまでコンテナ起動だけで、内部の起動は監視していません。
依存コンテナ内のDBが立ち上がる前に、テストを実行してエラーなどはよくある事です。
その場合は、数秒待ってから立ち上げるだけで正常にテストされます。

networks
- front: Webサーバーを担わせるNginxとの接続です
- back: 本番のDBコンテナとの接続となります。
- test: テスト用のDBコンテナとの接続となります。

expose
リンクされたサービスにのみポートを公開します。
この場合は、frontであるWebサーバー、backであるDBサーバー、testであるDBテストサーバーのみがPORT:5000でアクセスできます。
ホスト環境からはPORT:5000でアクセスできません。


## Dockerfile
AppサーバーをするflaskコンテナのDockerfileです。

```dockerfile

FROM python:3.8

WORKDIR /introdon

COPY Pipfile ./
COPY Pipfile.lock ./

RUN pip install pipenv && \
pipenv install --system

ENV PYTHONPATH /introdon

CMD ["./flask_env.sh"]
```
FROM python:3.8
公式pythonのversion3.8指定のimageを使ってビルドします。

Pipfileを使って、pipenvでpythonパッケージをインストールします。
`pipenv install --system`
`--system`でpythonの仮想環境を使わずに、直接systemにインストールします。
build時に、「仮想環境がありません」とWarningが出ますが直接インストールしているので問題ありません。
気になる場合は、requirements.txtを作成してpipでインストールしても良いと思います。
なお、pipenvを使うことで、依存パッケージとそのバージョンを厳密に管理、再現できます。

`CMD ["./flask_env.sh"]`
コンテナが立ち上がったら、`flask_env.sh`を実行させています。
このshellファイル内で環境変数に応じて、「本番、開発モード、テスト」のモードを切り分けてアプリを起動させています。


# nginx container (Webサーバー)
Webサーバーの役割を担当するコンテナです。

```yml
#抜粋

 nginx:
  build: nginx
  container_name: nginx
  ports:
   - "80:80"
  volumes:
   - ./nginx/nginx.conf:/etc/nginx/nginx.conf
   - ./docker_data/log/nginx:/var/log/nginx
  depends_on:
   - flask
  networks:
   - front
```
ports:
本番公開用に80:80としています。
開発段階でホスト環境が80番ぶつかる場合は、ホスト側の左側を変更して開発しても良いです。

volumes:
ホストに保存した設定ファイルをコンテナ側に読み込ませています。
Nginxのログデータをホストに保存し永続化しました。  

あとは、
flask コンテナが起動してからnginxコンテナを起動
front ネットワークに入って、flaskコンテナと接続しています。


## Dockerfile
短いですが、以下がDockerfileです。

```dockerfile

FROM nginx:1.17.9
CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]
```
ボリュームした`/etc/nginx/nginx.conf`を設定ファイルとして起動させています。


# mariadb container (DBサーバー)
DBサーバーの役割を担当するコンテナです。

```yml
#抜粋

 db:
  build: db
  container_name: mariadb
  ports:
   - "53306:3306"
  volumes:
   - ./db/my.conf:/etc/mysql/conf.d/my.cnf
   - ./docker_data/mysql:/var/lib/mysql
   - ./docker_data/log/mysql:/var/log/mysql
  networks:
   - back
  environment:
   MYSQL_DATABASE: introdon
  env_file: ./db/envfile #password記載のためignore
```
PORTを53306:3306でマッピングしています。

volumes:
ホストに保存した設定ファイルを読み込ませています。
また、mysqlのデータをホスト側に保存することで永続化しています。
ログもディレクトリを切り分けてホスト側に保存しています。

networks:
flaskコンテナと`back`ネットワークで接続しています。

environment:
データベース名など誰に見られても良いものはyamlにそのまま記述しました。
ユーザー名や、パスワードなど秘匿したいものは別ファイルにして読み込ませています。
`env_file: ./db/envfile`


## Dockerfile
以下が、Dockerfileです。

```dockerfile

FROM mariadb

RUN apt-get update && \
  apt-get install -y locales && \
  rm -rf /var/lib/apt/lists/* && \
  echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \
  locale-gen ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8

```
DBに日本語を使用したいのでコンテナOSに`locale`をインストールして、
locale設定を`ja_JP.UTF-8`に変更しています。


## my.conf
コンテナに読み込ませる設定ファイルです。

```conf

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
general_log_file=/var/log/mysql/mysql.log
general_log=1

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

[mysqld]
mysql deamon側での設定内容です。
UTF8に設定。
logの出力先も設定。
`general_log`を1または`ON`にして実行されたSQLのログ出力を有効にします。
指定しない場合や、0または`OFF`の場合はログ出力が無効になります。

[client]
接続元のクライアント側の設定です。
こちらも、文字コードをUTF8にします。


## envfile
ymlファイル内の`env_file`で指定した設定用の別ファイルです。

```
MYSQL_ROOT_PASSWORD=XXXXXXXXX
MYSQL_USER=XXXXXXXXX
MYSQL_PASSWORD=XXXXXXXXX
```
ファイルに切り分けることで、バージョン管理からignoreすることができます。

パスワードの参照先ファイルをひとつにまとめたかったので、
このenvfileはDBコンテナのbuildに使うだけでなく、flaskコンテナでのDB接続設定にも使用しています。

flask側からの読み込みは`python-dotenv`パッケージを利用しました。

```py:introdon/config_flask.py

import os
from dotenv import load_dotenv

load_dotenv('db/envfile')
MYSQL_USER = os.environ['MYSQL_USER']
MYSQL_PASSWORD = os.environ['MYSQL_PASSWORD']
```
①dotenvのメソッドload_dotenvを使ってenvfileの内容を環境変数に取り込む
②os.environから、keyで指定して該当する環境変数を取得
③取得したuser,passwordを使ってSQLAlchemyでDBコンテナに接続します


## DBコンテナに接続
データの取得や確認など、起動したDBコンテナに接続する機会は意外とあります。
DBコンテナの接続方法はいくつかあります。


### ローカル環境のコンテナの場合
#### CLI
コマンドで接続は2種類です。

①コンテナにアタッチして、DBに接続

```sh

# コンテナにアタッチする
docker exec -it mariadb bash

#アタッチ後に、DBにログインする
mysql -uXXXXX -pXXXXX
```

②直接DBにログインする

```
mysql -h 127.0.0.1 -uXXXXX -pXXXXX -P 53306
```
127.0.0.1はローカル・ループバック・アドレス。いわゆるローカルホストです。
`-P 53306`でPORT:53306を使用します。
53306はymlファイルでDBコンテナとマッピングするのに指定したホスト側のポートです。

①②の後は、mysql内の操作で、データ閲覧・取得をします。


#### Intellij
CLIで毎回入力だとめんどくさいですね、エクセルのように表で閲覧したいし。。

Intellij(JetbrainsのIDE)なら簡単に設定できて、一度設定すれば閲覧も編集もエクセル触るぐらい簡単になります。
(おそらくVisual Studioでも同じような機能があると思います)

過去の記事で取り上げているので、参考にしてください。
[IntelliJ(Jetbrains)からAWSを操作する設定方法まとめ。#DBとの簡単接続](https://qiita.com/akinko/items/d7001a8fe3ac87e1790c#db%E3%81%A8%E3%81%AE%E7%B0%A1%E5%8D%98%E6%8E%A5%E7%B6%9A)
記事では、
ローカルではなく、AWS
MariaDBではなく、MongoDB
と違いがありますが、赤マーカーしてある箇所を変更するだけで、DBコンテナでも接続できます。
入力の内容は②ローカルループや、ポートを使います。


### AWS本番接続
AWS上のDBコンテナもローカルと同じ接続方法になります。
ただ、AWSもIntellij(IDE)で接続するほうがかなり楽になります。

CLIだと、AWSにssh接続後した後に、先ほどの①②さらに、mysqlでのSQL操作をしてやっとDBデータにアクセスできます。
それもご存知の通りmysqlの見づらい表記での取得です。

こちらも[過去記事](https://qiita.com/akinko/items/d7001a8fe3ac87e1790c#db%E3%81%A8%E3%81%AE%E7%B0%A1%E5%8D%98%E6%8E%A5%E7%B6%9A)を参考して設定すれば、エクセルを開くようにクリックで手軽に扱えるようになります。


# mariadb_test container (TEST用DBサーバー)
ついでですが、テスト用のDBコンテナも軽く触れておきます。DBサーバーとほぼ同じです。

```yml

#抜粋
 db_test:
  build: db
  container_name: mariadb_test
  ports:
   - "63306:3306"
  volumes:
   - ./db/my.conf:/etc/mysql/conf.d/my.cnf
  networks:
   - test
  environment:
   MYSQL_DATABASE: introdon_test
  env_file: ./db/envfile #password記載のためignore
```
データを永続化する必要がないので、設定ファイルを読み込むのみ。
事故防止の念の為に、DATABASE名を本番と異なるintorodon_testに変更しています。


# IntelliJでDocker操作
DBコンテナの接続だけでなく、Dockerの操作・管理もIntellij(IDE利用)がおすすめです。


## サイドバー(Dockerサービス)
IntelliJのサイドバーからマウスで操作できるようになります。
コンテナの状態もリアルタイムで細かく確認することができます
![スクリーンショット 2020-07-16 22.59.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/f219a9ea-cbb7-1d2b-e199-9fa0665decd2.png)

- ログ: コンテナの標準出力
- プロパティ: コンテナのIDやimageIDなど閲覧
- 環境変数: 設定した環境変数がリスト表示
- Port Binding: ホストとのポートバインドを一覧表示
- ファイル: ディレクトリ構造を一覧表示。しかもファイルをダブルクリックすると開くこともできる

便利すぎて、正直コマンドで操作する気になりません。。:dancer_tone1:


## プロジェクトのSDKにコンテナ内言語に設定
Dockerドリブンでアプリ開発することができます。

- 開発使用言語を、Dockerコンテナ内の言語に設定できる。
- デバッグもDockerコンテナを使って開発できる
- テストもDockerコンテナを使って実行できる。

つまり、ローカルを一切汚さず、それどころか
**ローカルにインストールされているプログラム言語を一切使用せずに開発**することができます。

設定方法を次で順を追って説明します。:arrow_down:


### IntelliJの設定
Dockerコンテナ内のpythonで開発するための設定です。
プロジェクトで使うSDK(この場合、プログラム言語やバージョン)を設定します。
![スクリーンショット 2020-07-17 11.18.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/fc414923-ba88-6d2e-e6a5-35b489c315d3.png)
`プロジェクト構造 > プロジェクトSDK: > 編集`

通常だと、ローカルのデフォルトpythonだったり、pyenvなどの仮想環境pythonを選択していると思いますが、いつもと違い以下のようにします。
![スクリーンショット 2020-07-17 11.18.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/48a98dd6-4668-c72d-b610-db1bfc8fe891.png)
`Docker Compose `を選択して、

- サーバー: Docker
- Configuration file: docker-compose.ymlファイル
- サービス: pythonが入っているコンテナ名
- 環境変数: (使うときに環境変数を持たせたいなら入力)
- Python インタープリターパス: コンテナ内のpythonのパス。通常はpathを通していると思うので`python`のみでOK

上記設定で保存すれば、デバッグもテストもDockerコンテナ依存で開発すすめることができます。:raised_hands_tone2:


### テスト
上記の設定ですでに実行できますが、さらに楽をしてみます。

今回のFlaskアプリでは、flaskコンテナの環境変数の値によって、本番、開発、テストモードと切り替わる設計になっています。
テストをするたびに、
docker-compose.ymlの環境変数を書き換えてupして、テストが終わったら戻して。。めんどくさいです。
なるべく楽して生きていきたいです。

以下の用に、実行/デバッグ設定で、環境変数を入力してしまいます。
![スクリーンショット 2020-07-17 11.20.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/876776b2-6286-bbc2-3906-92fec0fc8bde.png)
これで設定ファイルからテストをすれば、環境変数を上書きしてから実行してくれます。テストが終わって書き戻すことも不要です。

余談ですが、`Docker Compose > Command and options`にあるように、コレを手動でCLIから実行しようとすると入力内容も多くあり毎回めんどくさいです。
Intllijで設定してしまえば、2回め以降は、クリックひとつです。


# おわりに
docker-composeは最初取っ付きにくかったです。
ただ、一度使い慣れるとWebアプリ開発がかなりスピードアップします:airplane:かなり使いまわしができます:recycle:
みなさまが、サクッと開発始めて、サクッとデプロイできる世の中になりますように:wave: