はじめに
これは①の続きです。
次回はバックエンド~と前回言いましたが、余白がなくなったので次回に回します…
今回の構成
今回は以下の構成で実装していく
- 物理コンピュータ
- 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公式ページを見てnode:lts-alpineというイメージを持ってくる、という具合である。
しかし公式が提供するイメージは最低限のものしか入っておらずそのままでは使い物にならない。
そこでDockerfileというファイルを定義し、そこでどのDockerイメージを取ってくるか、母艦(今回はUbuntu)とどのファイルを共有するか、何をプリインストールしておくかを定義する。
以下に主要なコマンドを示す。
# 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の公式イメージはいくつか仕様がある。
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文を定義した。
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
をフォーマットさせた文字列を使用することにした。
次回
今度こそバックエンドの開発を行っていく。