大富豪サーバーを移転した

heroku の無料プランが終わるっていうから

大富豪を稼働させ続けるためには、どこか別の場所にサーバーを立てる必要があります。

最初は、自分で構築するのがめんどくさかったので、 Google App Engine に乗せようと思ったのですが、無料ではできないことがわかりました。

  • サーバーのタイプを flexible にしないと Websocket 接続を張ることができない
  • flexible はどのインスタンスであっても有料である

まぁ、30 分アクセス無しで止めるとかにすればそこまで請求されることもなく、数百円でできると思うんですけど、ぶっちゃけフリーで公開していて自分は金を払い続けるというのはちょっと本質的じゃないと思っていました。Apple Developers の有料メンバーシップは、実質 Screaming Strike 2 を公開するためだけに払い続けている状態ですが、これは自分が iOS アプリとかを作りたくなったときに使えるのでとりあえずまぁいいかと思っています。

ということで、色々考えたり会社の人に聞いたりしたんですけど、結局自分で構築することにしました。

構築メモ

まずは大富豪サーバーを docker-compose up で立ち上がるようにする

べつにやらなくてもいいっちゃいいんですけど、 docker compose up -d が使えれば自動的にバックグラウンドプロセスにできて、ssh セッションの終了に引っ張られないから便利だなっていうだけです。 nohup npm start & という手もあるんですけど、これをあとから落とそうと思ったら、pid を調べて殺すみたいな処理が必要になってこれまた面倒くさい。docker-compose down で落ちれば楽だよねっていうだけです。 で、今回はとりあえず、イメージを docker hub に乗っけておいて pull するやり方ではなく、デプロイするときにインスタンス側で docker build して上げ直すという方法にしました。これだとたぶんビルド時間が余分にかかりますが、そんなクリティカルなサービスを運用してるわけじゃないので、手軽さを優先しました。あとあるとすれば、npm registory がぶっ壊れたり落ちたりすると、ビルドができなくなるかもね。まぁでもそれでこけたらとりあえず今ビルド済みのやつを主導で上げればいいから、それはそれでなんとかなるでしょう。

docker image

普通に node の公式イメージを適当にいじっただけです。必要なファイルをコピーして npm ci をやって終わりみたいなやつ。

docker-compose

カレントディレクトリからビルドするようにして、port を指定して、起動コマンドを npm start にしただけです。なんか gracefull shutdown ができてないっぽいですが、あまり気にしません。たぶんアプリ側の問題。なんか development mode で動いてるんだよね。でも、公式マニュアルを見ても、prod で動かす方法書いてないし、たしか npm run build でできるファイルを実行してもなんか動かなかったんだよね。

大富豪サーバーは、db とか kvs とか持ってないので、依存関係はなく、これだけで終わりです。楽だね!

GCP 無料枠のインスタンスを作る

Google Cloud Platform のダッシュボードに行って、まずは Compute Engine の API を有効にします。で、インスタンス作成に進みます。

インスタンスタイプは E2.Micro にして、region はそのまま us-central-1 にします。これが無料枠のインスタンスです。ほかの場所でもいくつか無料枠あるようですが、わざわざ買える利点はあまりないと思うので、まぁ別の場所に立てたかったら公式マニュアルでリストを見ればいいです。ちなみに日本の region は無料枠にありません。

ブートイメージは自由に選ぶことができます。私はずっと ubuntu を使っているので、ubuntu が入ってそうなイメージにしておきました。

インスタンス作成の途中で、http と https の接続を許可するチェックボックスがあるので、これを入れておきます。便利。

インスタンス作成が完了して稼働中になったら、ページの上にある「ナビゲーション」っていうメニューボタンを押して、下のほうにずらずら出てくるところから「VPC ネットワーク」を選びます。

VPC ネットワークのページが表示されます。ここで、「IP アドレス 2/9」とか読むところを選んで、IP アドレス画面を出します。テーブルがあって、さっき作ったインスタンスの外部エフェメラル・IP が書いてあるのを探して、これを「予約」ボタンで永続化します。これで外部 IP を固定できます。

次は ssh の設定です。手元で ssh-keygen とかやってキーペアを作ります。作ったら、ダッシュボードから行ける、さっき作ったインスタンスの書斎画面を開きます。「編集」というのを押します。「ssh」タブを押します。

ここにはテーブルがあって、登録済みの ssh 公開鍵が並んでいます。ここに行追加して、さっき作った public key のテキストを貼り付けて保存します。作業が完了したら、テーブルの左の列にユーザー名がでるので、そのユーザー名と private key、固定した IP アドレスで ssh 接続できるようになります。

インスタンスをいじっていく

手始めに

sudo apt update
sudo apt upgrade
sudo do-release-upgrade

あたりをやっておきました。

で、とりあえず docker を入れます。公式にあるコマンドをコピペでいいです。なんか docker-compose 1 系が入っちゃいましたが、まあいいでしょう。do-release-upgrade する前に入れちゃったからかも。知らんけど。

とりあえず大富豪サーバーを立ち上げてみる

大富豪サーバーを git clone してきて、sudo docker-compose up でもう立ち上がります!素晴らしい!!!

nginx の設定

https 化したいですが、自分のアプリで証明書の設定と化するのは面倒くさいので、nginx をフロントに立たせることにします。

普通に apt で nginx を入れます。これだけで http 疎通できて nginx の画面が表示されます。

ドメインを取っておく

私は、なんか知らんけど使ってないドメイン持ってたので、そのドメインのネームサーバーにレコード追加して、いい感じに大富豪サーバーのインスタンスに名前解決されるようにしておきました。

繋げたドメインで https 化

次に、let’s encrypt の certbot の公式説明に従って、certbot による https 化をやっていきます。とはいえ、やることは、snap install で certbot を入れて、certbot –nginx みたいなコマンドを打って、聞かれることにへーへー答えていけば 1 分で終わります。–nginx オプションを使うと、http を https にリダイレクトするルーティングとかも自動で書き込んでくれます。便利。

リバースプロキシの設定

443 番に来た通信を、大富豪サーバーに転送してくれる設定をします。デフォルトでは、大富豪サーバーは 2567 番のポートを使うので、 2567 に送ってあげます。

大富豪サーバーのドメインを certbot で https 化している場合、 /etc/nginx/conf.d の中ではなく、 /etc/nginx/sites-enabled/default というファイルに書かれるようです。grep で探すまで気づかなくてあれーっていってました。ちなみに nginx は今回初めて触るのでよく分かってません。

/etc/nginx/sites-enabled/default にずらずら書かれているのを見ます。443 で listen しているサーバーの設定を探して、以下の設定を追加しておきます。

    proxy_set_header    Host    $host;
    proxy_set_header    X-Real-IP    $remote_addr;
    proxy_set_header    X-Forwarded-Host       $host;
    proxy_set_header    X-Forwarded-Server    $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://localhost:2567;
    }

ファイルを上書きして、 sudo nginx -t で問題がないかどうか確認して、 sudo service restart nginx で nginx を再起動します。

全てがうまくいってれば、大富豪サーバーを docker-compose up した状態で、ドメイン名でブラウザからアクセスして疎通できるようになっているはず。

WebSocket 通信をプロキシで着るように追加設定

ここまでやると https でつながるので、wss でもつながるんじゃねと思うかもしれませんが、じつはまだだめです。っていうか、だめでした。

なんでだめかというと、nginx がデフォルトで http 1.0 を想定して動くことと、http ヘッダー upgrade と connection フィールドがそのままではリバースプロキシ先サーバーに転送されないからです。つまり、webSocket handshake のはずがなんかヘッダーが抜け落ちたリクエストになって届くせいでエラーになってつながりません。という説明が https://nginx.org/en/docs/http/websocket.html に書いてあります。

これを解消するには、2 箇所の設定をいじります。

まずは /etc/nginx/nginx.conf の http から始まってるフィールドのどこかに以下を追加します。

	map $http_upgrade $connection_upgrade {
		default upgrade;
		''      close;
    }

nginx は、変数を使うことができるらしいです。$で始まるのが変数名ですね。map というのは、ある変数の値を見て、どんな値が入ってるかに応じて、もう一つの変数の値をセットする構文らしいです。

上のやつを日本語にすると、こういうことらしい。

“http_upgrade っていう変数を見て、connection_upgrade っていう変数の値を決めるぞ。http_upgrade が空の文字列だったら、connection_upgrade に’close’を入れるぞ。それ以外だったら、’upgrade’を入れるぞ。”

これはあれですね。websocket handshake は、 Update: websocket と Connection: upgrade のセットで送られてくる性質を利用して、 upgrade ヘッダーがあったときは connection: upgrade をセットして、なかったら connection: close になるように変数を設定してるわけですね。$http_hogehoge という変数名で、http ヘッダーの key/value が参照できるように最初からセットされているのでしょう。知らんけど。とおもったら、 http://nginx.org/en/docs/varindex.html に変数の一覧がありますね。

で、もう 1 箇所変更するのは、さっき proxy の設定を書いたところです。location のセクション全体をこんな感じにします。いくつか追加があります。

    location / {
        proxy_pass    http://localhost:2567;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }

まず proxy_http_version 1.1; を追加しているので、ここで http 1.1 が使えるようになります。 upgrade フィールドは http 1.0 ではサポートされてないので、必要ですね。

proxy_set_header Upgrade $http_upgrade; が追加になっているので、明示的に upgrade ヘッダーの内容を中継します。

proxy_set_header Connection $connection_upgrade; が追加になっています。ここで、リバースプロキシ先に connection ヘッダーの内容を中継しますが、ここがさっき map でセットした変数で動的に変わるようにしています。なので、 Upgrade: websocket と Connection: upgrade のセットが来た場合に、ちゃんと中継されて接続が維持されるようになるってわけですね。賢いですねえ。

大富豪できることを確認

遊べました。やったね!

これで heroku がぶっ飛んでも大丈夫。なんか設定を間違ってて、来月 GCP から請求が来ないことを祈る。まぁ、ちょっとならべつにいいんだけどさ。

おまけ: デプロイしてるときに under maintenance と表示する

最初にリバースプロキシの先を見に行きますが、そこでエラーが起きたりつながらなかったりしたときに、アクセスもとにはなんかそれっぽいメッセージを返したいと思いました。普通は websocket 経由でアクセスされるので見えないんですが、サーバーの URL を特定されて直接たたかれると、大富豪サーバーが生きていればファンシーなメッセージを返してくれますが、落ちていると 502 bad gateway になってしまいます。

これをなんとかするのが、どうにも予想通り行かなくて、一番時間を使いました。

最初は、 error_page の指定から別の location に飛ばして、rewrite ドメイン名/under_maintenance.html break; みたいにして、あらかじめ用意しておいた html に書き換えればいいかと思ったのですが、うまくいきませんでした。rewrite のオプションに break;をちゃんと指定してたはずなんですが、なんかうまくいってなかったらしく、302 でリダイレクトされてしまい、そのままリダイレクトループに落ちて chrome 爆死、みたいになりました。なので、最終的にはこれでやりました。

root が設定されているのを確認。

サーバー設定の root で指定してあるディレクトリに under_maintenance.html を置いておく。

サーバー設定のほうに以下を追加。

error_page 500 502 @server_error; を追加して、500 と 502 のときに @server_error location に飛ばす。

@server_error location を追加して、ファイルを読ませる。

	location @server_error {
		try_files /under_maintenance.html =404;
	}

この try_files に最初は under_maintenance.html って書いていたんだけど、どうやら / を付けないとだめらしい。ファイルあるのに 404 になるからなんでやねーんってなってた。

ちなみに、これの意味は、 root 直下の under_maintenance.html を探して、あったらそれを返して、なかったら status=404 にするってことです。

これで、 curl -verbose でたたいて確認して、302 にならずに、俺の作ったファンシーなメンテ中メッセージが出るようになりました。

でけた

やる気になったら 2 日でできました。nginx は初めて設定したので、勉強になりました。nginx も含めてコンテナ化することもできるだろうけど、証明書の情報が入っちゃうので、まあ今回はここまででいいでしょう。

デプロイは git push 1 発だとできなくなりますが、まあ docker-compose down して git pull して docker build して docker-compose up するのをやればいいだけなので、いくらでもやりようがあるでしょう。