黄昏酒場スコアボード作成②〜ホスト作成とDB定義

## はじめに

これは[①](https://qiita.com/wefmaika/items/3c5c3267a7364735aaa3)の続きです。
次回はバックエンド～と前回言いましたが、余白がなくなったので次回に回します…

## 今回の構成

今回は以下の構成で実装していく
- 物理コンピュータ
    - MS-01
- 物理コンピュータ上OS
    - proxmox
- ホスト上OS
    - Ubuntu
- エディタ
    - VSCode
- 各ホストのライブラリ
    - バックエンド
        - fastapi
    - データベース
        - postgresql
    - フロントエンド
        - vue.js

現在自分のホストはMS-01というサーバPCの上にproxmoxを建てて動いている。
proxmoxとはハイパーバイザー型の仮想化ソフトウェアで近年話題になっているVMwareからの移行先としても有力視されている。仮想的に立てたホストをブラウザ上で簡単に切り替えられるのでかなり便利である。
proxmox上に建てるホストは友人と環境を合わせたいということもありUbuntuホストを建てて開発することにした。

さらにバックエンドはfastapi、データベースはpostgresql、フロントエンドはvue.jsで作成することにした。
選定理由は、fastapiは知人にPythonマスターがいるから、データベースは知人が全員postgresqlを使っているから、vue.jsはなんとなくである。

今回のエディタはVSCodeである。フリーでかなり便利なのでこれ以外の択はない。

## docker

フロント、バックエンド、DBそれぞれでホスト建ててansibleで構成を定義すればよいのだろうかと考えたが、dockerによってホスト1台ですべて賄うことにした。
dockerはコンテナという単位でホスト内を分割してさも個別のOSを持った別々のホストが立っているように見せる技術である。
これによりプログラム群の移植性が高くなる。

この手の開発には付き物の技術だが自分が曖昧な理解だった技術なので一応説明する。
コンテナはdockerイメージというものを元に作成される。
dockerイメージはdocker公式ページが配っているのでそこを参照しつつ、よしなに選ぶ。
例えばvueを使用したフロントエンドを司るホストを作成したければ[vue公式ページ](https://v2.ja.vuejs.org/v2/cookbook/dockerize-vuejs-app)を見てnode:lts-alpineというイメージを持ってくる、という具合である。
しかし公式が提供するイメージは最低限のものしか入っておらずそのままでは使い物にならない。
そこでDockerfileというファイルを定義し、そこでどのDockerイメージを取ってくるか、母艦(今回はUbuntu)とどのファイルを共有するか、何をプリインストールしておくかを定義する。
以下に主要なコマンドを示す。

```dockerfile
# dockerイメージを取得してくる
FROM {イメージ名}

# コンテナ上に/appというディレクトリを作成したうえでコンテナにログインした直後のディレクトリを/appとする
WORKDIR /app

# Dockerfileと同じ階層にあるrequirements.txtをコンテナ内の/appにコピー
COPY ./requirements.txt /app

# dockerイメージをビルドするときに `pip install -r requirements.txt` というコマンドを実行する
RUN pip install -r requirements.txt

# コンテナ起動時にapp.pyを実行
CMD ["python", "app.py"]
```


このDockerfileを元にdockerイメージをビルドし、そのイメージを動かすことによってホストを立たせる。
ここで名前付きボリュームという物があることに気をつける。
開発中のプロダクトを母艦から編集するために、一般に母艦の特定ディレクトリとコンテナ上のディレクトリを共有(バインド)させる。
しかしユーザデータ(ここではDBに保存されるメタデータや、ダウンロードできるように保存してあるリプレイファイル)はわざわざ母艦から編集できるようにしなくてよいし、簡単に編集出来るのはマズい。
そこで名前付きボリュームが登場する。これは開発者からはさも母艦とバインドされておらずコンテナの中に入らないと見えない保存領域である。
母艦とバインドされてないと言ったが、これはそのように見えるだけでdocker側でよしなに管理されているため、コンテナを落とした後再度立ち上げても消えていない。
この母艦とコンテナのバインドや名前付きボリュームの定義は、通常dockerコンテナを立ち上げる時のコマンドで行うが、あまりにも煩雑になってしまうため後述のdocker composeによって行う。

## docker compose

dockerの作成時には母艦とコンテナのディレクトリのバインドの定義を行ったり、そもそも今回のサーバはフロントエンドサーバ、バックエンドサーバ及びデータベースサーバの3つのホストを一気に建てなければならない。これを全てdockerを立ち上げる時にコマンドで定義していく必要がある。
ここの処理がだるいので `docker-compose.yml` というファイルでコンテナをどう立ち上げるかを定義する。
これにより `docker-compose.yml` と同じ階層で `docker compose up --build` とコマンドを打つだけで煩雑なコマンドを打たずにイメージをビルドしてコンテナを立ち上げるところまで行うことができる。
特に、今回使用するDBであるpostgresqlはそのまま公式dockerイメージがあるのでDockerfileに定義するまでもなくそのままデプロイできるため、 `docker-compose.yml` にイメージを直書きするだけで良い。

## postgresqlのdockerイメージ

[postgresqlの公式イメージ](https://hub.docker.com/_/postgres)はいくつか仕様がある。
DBのUSERとPASSWORDはそれぞれ環境変数で決まるので `docker-compose.yml` にて定義する。
さらにDBの実体の場所も公式ページで指定されているのでそこに名前付きボリュームを設定する。
また、postgresqlのイメージは `/docker-entrypoint-initdb.d/` というディレクトリ下にあるSQLを初回起動時に行ってDBを初期化できる。よってSQLのCREATE文をここに書いておけばコンテナ立ち上げ時に勝手にデータベースを定義してくれる。
ただしこれはDBが存在しないときのみ行ってくれるので、新しくDBの定義を変えた時は名前付きボリュームを吹き飛ばす必要がある。
これはコンテナを一旦停止した後 `docker container prune` でコンテナ自体を削除し、 `docker volume rm {DBの実体の名前付きボリューム}` で削除する。

## 今回のDB実装

旧ロイヤルフレアによるとユーザはユーザ名、投稿コメント、削除パスワード、及びリプレイファイルをアップロードし、サーバ内部でリプレイファイルをバイナリ解析してメタデータを取得する。
そして内部では黄昏酒場のユーザ名と投稿コメント、削除パスワード、リプレイファイルのメタデータを保管しておきたい。
今回の実装では旧ロイヤルフレアに倣って今回は `/docker-entrypoint-initdb.d/init.sql` というファイルに以下のようなSQL文を定義した。

```sql
CREATE TABLE replays(
    replay_id           SERIAL          NOT NULL,
    user_name           TEXT            NOT NULL, -- ユーザ名。今回作成するアプリ側で入力する名前
    replay_name         TEXT            NOT NULL, -- リプレイ名。黄昏酒場側で入力する名前
    created_at          TIMESTAMP       NOT NULL, -- リプレイ作成日時
    stage               TEXT            CHECK (stage IN('1', '1 〜 2', '1 ～ 3', 'All Clear')),
    score               INT             NOT NULL, -- スコア
    uploaded_at         TIMESTAMP       NOT NULL, -- 投稿日時
    game_version        TEXT            NOT NULL, -- DB作成時点では [1.00a] のみが入る
    slow_rate           FLOAT           NOT NULL, -- 処理落ち率
    upload_comment      TEXT            , --投稿時コメント
    delete_password     TEXT           NOT NULL, -- 削除パスワード。SHA256で変換した文字列を入れる

    PRIMARY KEY(replay_id)
);
```

ここで、 `replay_name` , `created_at` , `stage` , `score` , `game_version` , `slow_rate` がリプレイファイルをバイナリ解析することによって得られるデータである。
特に `stage` に入る値が `1` , `1 ～ 2` , `1 ～ 3` , `All Clear` の4択という気持ち悪い書き方をしているが、これは黄昏酒場のバイナリデータにはその4択で書かれるからである。キモい。
`replay_id` は `replays` テーブルの主キーである。特に最終的にリプレイファイルをダウンロードさせるにあたり、旧ロイヤルフレアでは `alco_ud{4桁の英数字}.rpy` という名前でダウンロードさせていた。ダウンロードさせるときにIDを含ませてダウンロードさせるという不親切設計[^1]だが、東方projectの特殊な事情[^2]によりこのリプレイファイルフォーマットを継承することにし、作成には `replay_id` をフォーマットさせた文字列を使用することにした。

## 次回

今度こそバックエンドの開発を行っていく。

[^1]:本来であればユーザ名に紐づくような名前にし、主キーなどの内部事情を隠してユーザが必要な情報を入れるのが一般的
[^2]:東方projectシリーズは{ゲームのID}_ud{4桁の英数字}.rpyというリプレイファイルしか認識しない。変にフォーマットを崩すよりこの形式でダウンロードさせた方が、名前を変更せずに東方が認識できるのでユーザフレンドリーである。むしろ東方界隈では『ud{4桁の英数字}のリプレイを上げたから参考にして～』という会話が日常茶飯事であるため、ここを変えると東方民から総スカンを喰らう。