Flask+Docker+Vue.js+AWS...でゲームWebAppを作ってみた。

**イントロで曲当てクイズ『イントロドン！』**
コロナ自粛中のリモート呑みで遊べるゲームアプリが欲しくて作ってみました！
お酒呑みながら、みんなでガヤガヤ、時にはひとりでじっくりと楽しんでもらえたら嬉しいです。

●こちらのリンクから遊べます♪
http://introdon.akinko.work/

●Githubにソースコード、ゲームルールを公開しました
https://github.com/akiraseto/introdon

![スクリーンショット 2020-07-15 14.33.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/8de54a46-bb11-e55d-610b-3fb3b4971694.png)![スクリーンショット 2020-07-15 14.30.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/5dd56eba-ea5e-e8a4-067a-d13f5becd547.png)![スクリーンショット 2020-07-15 14.30.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/df1e1278-ebf9-1bd6-916b-7ab4078b585e.png)
![スクリーンショット 2020-07-15 14.30.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/c3958af9-2785-61cf-ead6-3ed55d5af9e7.png)![introdon.akinko.work_user_entrance(iPhone 6_7_8) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/3fca3907-1e6d-e466-313f-06a111a7dcf5.png)


# ゲーム内容
楽曲のイントロ部分を聴いて曲名を当てるゲーム。4択問題で全10問出題。

## 遊べるモードは2つ
### ひとりでじっくりモード  
時間制限無しでじっくり解答

### みんなで早押しモード  
最大5人同時参加の早押し形式
正解順に高得点をGet
勝敗は合計点によるランキング発表！


## PCブラウザ・スマホについて
PCブラウザ、スマートフォンともに対応していますが、
スマホ版は、出題ごとに「音楽を再生する」ボタンをタップしないとイントロが流れない仕様です。
(PCブラウザ版は自動で曲が流れる)

*モバイル版Chrome、Safariのブラウザポリシーによりメディア要素の自動再生は禁止。
ユーザーの意図的な操作によってメディアを再生する。*

残念ながらスマホ版は、みんなモードだとかなり不利になってしまいます。


# 技術内容
楽曲情報を「itunes api」から取得して問題を作成。
https://itunes.apple.com/search

楽曲情報をDBから取得して問題作成。
該当する楽曲がDB内で少ない場合、itunes APIから楽曲情報を取得し、被った情報を削除した上でDBに記録。同時に問題も作成します。

## 全体の流れ

1. ゲームを開始する
2. DBから該当する楽曲情報を取得
3. 楽曲がある場合は、選択肢4×10問の問題を作成。
4. 問題をsessionに渡す
5. sessionから引き出して出題
6. 解答内容をDBに記録
7. 全10問解答後にDBからログを取得し、ランキング、正誤内容を表示

楽曲情報が足りない場合、
3. itunes APIから該当する楽曲情報を取得
4. duplicateする情報を削除
5. API取得した楽曲情報をDBに記録
6. 問題を作成(以下、同じ)

## 技術構成
主な技術構成は以下となります。

- Flask
- Docker
- Nginx
- Gunicorn
- Vue.js + Jinja2
- MariaDB
- AWS
- テスト(pytest, CircleCI)


# Flask
## MTVフレームワーク
Model, Template, ViewのMTVフレームワークに沿って開発。

### ディレクトリ構成
  
```sh
  .
  ├── introdon
  │  ├── __init__.py
  │  ├── config_flask.py
  │  ├── models
  │  │  ├── games.py
  │  │  ├── logs.py
  │  │  ├── songs.py
  │  │  └── users.py
  │  ├── scripts
  │  ├── static
  │  ├── templates
  │  │  ├── _render_field.html
  │  │  ├── admin
  │  │  ├── games
  │  │  ├── layout.html
  │  │  └── users
  │  └── views
  │    ├── __init__.py
  │    ├── config_introdon.py
  │    ├── form.py
  │    ├── games.py
  │    ├── songs.py
  │    ├── users.py
  │    └── views.py
  ├── manage.py
  └── server.py

```


### models
一言で、`データベース連携の機能`。
単純なCRUDだけでなく、ビジネスロジックもModelにコーディングしてデータの取り回しを担当させる。

#### classmethod
インスタンスを作成して使い回したり、アトリビュートを使用することが無い場合は、その場で使えるクラスメソッドが便利です。

```py:introdon/models/users.py
 # Classの中でデコレータをつけて宣言

@classmethod
def fetch_user_records(cls, users_id_list: list) -> list:
```
```py:introdon/views/games.py
# インスタンス作らずにその場で使える

users_record_list = User.fetch_user_records(users_id_list)
```


#### traceback.print_exc
try exceptで例外表示に使用。例外が発生した際に、エラー詳細を表示させて原因が追いやすくなります。

```py:introdon/models/games.py
#DB書き込み時

this_game = Game(**records)
db.session.add(this_game)
try:
  db.session.commit()
except:
#例外が発生したら

  db.session.rollback()
#DBロールバックし

  traceback.print_exc()
#エラーをスタックトレースして出力

```
エラーが発生した際、
①スタックトレースを抽出して、
②書式も整えた上で出力してくれます。
エラー解決にはprint(),loggerよりもおすすめです。


### templates
TemplateはMTVの中で一番分かり易い描画(render)機能です。
その反面、フロントエンドフレームワークと連携しだすと一番複雑化しやすい箇所でもあります。

Jinja2を通して共通パーツをまとめた方が全体のコードがスッキリします。

```html:introdon/templates/users/index.html

{% from "_render_field.html" import render_field %}
{% extends "layout.html" %}
{% block body %}

# 内容

{% endblock %}
```
ナビバーや、ヘッダーなど共通のパーツは`layout.html`から読み込み
フォームなど使い回すパーツはmacroにして`_render_field.html`から読み込んでます


### views
viewにビジネスロジックまで書いてしまうと、あっという間に肥大化&複雑化してスパゲティコードになってしまいます。(T_T)
APIのI/Oインターフェイスぐらいの認識にして、なるべくスリム化するように常に意識します。
viewはあくまで

- 値の入出力
- 処理全体の制御

この2つの役割に徹した方が良いです。
Flaskは比較的自由が効くフレームワークなのでいくらでもviewに書けてしまいますので。。


### static
画像などのstaticなファイル置き場となります。
今回は、正解不正解などのSE音源を格納しました。


## app.run設定:config_flask.py
flaskアプリ起動の`app.run()`の設定ファイルです。
ファイル場所:`introdon/config_flask.py`

設定変数をファイルにまとめて一括で読み込ませています。
今回は環境変数によって分岐させて、読み込む変数をモードごとに変えています。


### SECRET_KEY
データベースと、セッション情報を暗号化するためのキー。
分かり辛い方がいいので、`os.random(24)`で毎回値を変えてます。


### DEBUG
DEBUGモードをオンにすると

- ファイル変更したらappが自動リロードされる。
- ブラウザにエラー内容を出力する
- ツール:DebugToolBarが使用可能になる

開発では Trueにして本番では必ずFalseにします。


### SQLALCHEMY_DATABASE_URI
SQLAlchemyを通してDBを利用する際の接続先です。
SQLAlchemyとMySQL(MariaDB)には`pymysql`のスキーマが必要。


### SQLALCHEMY_RECORD_QUERIES
デバッグ出力用にSQL内容を記録します。
DebugToolBarで SQLを確認したいなら設定が必要。


## よく使う基本機能
Flaskで高頻度でお世話になる機能をまとめてみました。


### ルーター
#### route
関数とURLをバインドするデコレーター
POST,GETの許可を制御する


#### errorhandler
サーバーエラー発生時に処理を制御します

```py:introdon/views/views.py

@app.errorhandler(500)
def internal_server_error(error):
 flash('Internal server error')
 return redirect(url_for('index'))
```
サーバーエラー500が発生したら
①flashメッセージを作り、
②indexメソッドバインドしているURLにリダイレクトさせる。


### レスポンス
#### render_template
テンプレートhtmlを描画します。
jinja2を使ってhtml側に変数を渡すことができます。


#### flash
Flashメッセージは処理が終わった事や、エラーが発生した場合にユーザーに知らせるメッセージです。

1. render時にview側でflash()に入力されたメッセージがセットされます。
2. template側でget_flashed_messages()でメッセージを受け取り描画します。

```html:introdon/templates/layout.html
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
  #内容をhtmlで出力
{% endfor %}
{% endif %}
{% endwith %}
```
複数受け取りも可能。
layout.htmlですべてのtemplateページにメッセージを出力できるよう設定しています。


#### redirect
任意のページにredirectすることもできます。
特に`url_for`と組み合わせて使うことが多いです。


#### url_for
メソッド名を引数に取ると、バインドしているURLの文字列を返します。

```py
return redirect(url_for('start_multi'))
```
1. start_multiメソッドがバインドしているURLに文字列変換
2. 文字列変換されたURLにリダイレクト

また、templateのhtmlでよく使うのが、
`ディレクトリ名のstaticを第1引数、ファイル名の指定`で素材ファイルとリンクすることができます。

```html
url_for('static', filename='right.mp3')
```
staticディレクトリの'right.mp3'のリンク文字列を作成


#### jsonify
名前のとおりjson形式にシリアライズします。


### request
ユーザーのrequest内容が格納されています。

よく使うのが、ユーザーがformに入力した内容の取得です。

```py:introdon/views/songs.py

# song登録画面
@app.route('/admin/song', methods=['POST'])
def add_song():
 term = request.form['term']
```
POSTで送信されたformのname=termにユーザーが入力した値を取得します。

flask.requestはユーザーの様々なリクエスト内容を取得できるので使いみちが幅広いです。

```py
# POSTでリクエストされているなら
if request.method == 'POST':
pass

#GETでクエリが'game_id'の内容を取得
game_id = request.args.get('game_id')
```
などなどあります。


### session
ユーザーごとに、リクエスト(ページ)をまたいで情報を利用することができるSessionを手軽に利用できるようになります。
利用するには、app.run設定のSECRET_KEYを設定する必要があります。


## DBとの接続
ORMとしてSQLAlchemyを使用しました。
MariaDB(MySQL)との接続にはpymysqlをスキームにかませる必要があります。


### flask-script
DB環境の初期化をコマンド実行できるようにしました。
以下のコマンドで必要なTableが作成されDBを初期化します。

```sh
 python manage.py init_db
```
アプリ開始時にコマンド実行します。


### flask-marshmallow
オブジェクトをシリアライズ化してくれます。
クイズ作成時に、SQLAlchemyでオブジェクト化された楽曲情報をjsonフォーマットに格納するために使用しました。


## ログイン系の処理
flaskの拡張プラグインを利用してコーディング。


### flask-login
- ユーザーのログイン
- ログアウト
- そのユーザーはログイン状態か
- ログインしている場合のユーザー情報取得


### password_hash
- パスワードをハッシュ化:generate_password_hash
- ハッシュ化パスワードを確認:check_password_hash


## フォームの処理
### flask-WTF
フォームを作る際の便利な機能が揃っています。

- form内容のvalidate
- form項目の設定
- CSRF対策


## 管理画面をかんたん設置したい
Adminユーザーから簡単な管理画面を閲覧・操作したい。
でも、イチからコーディングするのはシンドい。。これもflaskのプラグイン使います。


### flask_admin
自分で作るのに比べると圧倒的に楽に管理画面が作れます。

DB内TableのCRUD操作がWebの管理画面から可能になります。
細かくカスタマイズすることも可能です。

![スクリーンショット 2020-07-15 14.35.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/9a8db59c-24d3-4f5d-454d-1cc06ba53a6f.png)![スクリーンショット 2020-07-15 14.35.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/c0027cf0-eebd-b071-2abd-746fb99e9318.png)![スクリーンショット 2020-07-15 14.35.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215645/04ae2f43-db9b-6152-8a91-492495cbed48.png)


## 開発に便利なツール
使うと開発作業が捗るツールで、利用をおすすめします。


### flask-debugtoolbar
DEBUGモードで利用できるようになる開発援助ツールです。
Webアプリ上にツールバーとして表示されます。変数や履歴、ヘッダー内容、SQL処理内容をブラウザから確認できるようになります。


# Dockerでシステム構築
Webサーバー、アプリサーバー、DBサーバーをそれぞれDockerコンテナでシステム構築しました。
正確には、テスト用のDBコンテナがひとつ追加で4つのコンテナ構成を`docker-compose`で構築しています。

**docker-composeでの詳しい設定方法は別記事**でまとめました。
[Flask Webアプリのサクッと作れる『docker-compose構成』をまとめてみた](https://qiita.com/akinko/items/103148aac79743160540)

システムのモードとして3種類あります。
Flaskコンテナの環境変数FLASK_ENVを変更することで以下の3つのモードでBuildします。

- PROD
    - 本番モード
    - Gunicorn使用
- DEV
    - 開発環境モード
    - DEBUGオン
    - debugtoolbarが表示
    - エラー時、内容がWEBに表示
    - コード変更でwebアプリが自動リロード
- TEST
    - Func,Unitテストが自動実行
    - テスト用DBコンテナを使用
    - テストCoverageを表示


# Nginx
Webサーバーに使用しました。

Nginxはメモリ使用量が小さく、処理も早い。
リバースプロキシの機能、ロードバランサ、HTTPキャッシュを持ち合わせています。

設定内容と解説は別記事でまとめています、ご参考くださいませ。
[Nginxの設定まとめ(Flask+Docker+Gunicorn編)](https://qiita.com/akinko/items/bbfdddb738c7d8a6b9c6)


# Gunicorn
WSGIに使用しました。

uWSGIも検討しましたが、
①Gunicornの方が扱いやすくネット上の使用事例が多く(海外も含めて)、
②公式サイトも解説が丁寧だったのでGunicornを採用しました。

docker-composeを通して、FALSK_ENV=PRODなら以下から起動するようにしています。

```sh:flask_env.sh
gunicorn server:app -c gunicorn_setting.py
```


# MariaDB
MySQLと完全互換があり、MySQLより処理が早い、MariaDBを使用しました。
完全互換なのでSQLAlchemyや、スキーマなどMySQLの設定がそのまま使えます。


# フロントエンド
Jinja2 + Vue.js + Bootstrapで作成しました。

MPA(Multi Page Application)としてJinja2を基本に、Vue.jsをCDNで読み込んで組み合わせて実装。
CSSはBootstrapを利用してレスポンシブデザインにしてPC、スマホの両ブラウザ対応にしています。

FontAwesomeを初めて使用したのですが、簡単なタグのみで手軽にアイコンを組み込めるのでおすすめです。
https://fontawesome.com/


# AWS
1つのEC2インスタンスに全てのDockerコンテナを構築しました。

順当ならECSを使って、コンテナ毎にEC2インスタンスに分配して制御管理するのがベストプラクティスだと思います。
1つのEC2インスタンスに収めた理由として
①AWS無料枠内に収めたい
②クラスタリングが必要なほどのアクセス数ない
③趣味なので強い可用性も堅牢性も求めない
上記理由で、今回は見送りました。

ただ、ECSは思ったよりも導入しやすく便利そうなので今後の別アプリで検討してみたいです。
さらに、Kubenatesを導入しても面白そうです。


# テスト
pytestで書いています。
python標準のunittestよりもシンプルにかけて読みやすく、プラグインも豊富なためpytestを採用しました。

pytestについては別記事で解説しています、ご参考くださいませ。
[pytestをFlask+docker開発で初めて使ってみた](https://qiita.com/akinko/items/1185d154f651de72a1cb)


CIには CircleCIを利用しました。
DockerのDBコンテナを使ったテストはせずに、mock利用のテストだけをCircleCI上で実行しています。


# 終わりに
楽しかった。。
Flaskは先人達の優れたプラグインが多くあり、組み合わせることでやれることが加速的に多くなっていく。。
その「やれることが増えていく感」が気持ちいいフレームワークです。皆様も機会があればぜひ触ってみてください！