PaaSを使ってクライアント/サーバーサイドAppをデプロイしよう

クライアント/サーバーサイドを両方使うAppを、PaaSにデプロイする方法について、概要を紹介します。
実践した環境は下記の通りです。

- Next.js/Phaser/MaterialUI
- Express + Node.js
- GoogleAppEngine

他のサービスでも同様にデプロイが可能なように、仕組みの全体像や大まかな概念について解説します。

## 【結論】クライアントとサーバーサイドで別々のGAEインスタンスを構築する
こういった構成でアプリを作りたい場合は、それぞれについてGAEインスタンスを構築して、アップロードしなければなりません。
そのため通信を行う際には、CORSの設定が必要ですし、各インスタンスに環境変数を設定する必要があります。
ただこの方法、わかる人にとっては当然なようです。
しかし初学者にとっては、日本語ではっきりと名言している文献は皆無なので、気づくのは厳しいかもしれません。

### 【POINT1】サーバーサイド側にCORS設定を追加する
クライアント側からのアクセスを受け入れるように、CORS設定をしておきましょう。
``` sample.ts
const io = new Server(httpServer, {
  cors: {
    origin: [
      'http://localhost:3000',
      'https://frontend-bombattacker.an.r.appspot.com',
    ],
  },
});
```

### 【POINT2】クライアント側に環境変数を設定する
クライアント側の環境設定として、アクセス先のURLを記述します。
app.yamlに下記を記述することで解決できます。
```app.yaml
env_variables:
  NODE_URL: "https://~~~~~"
```

### 【POINT3】他のPaaSでも同様の仕組みで実装可能
上記の内容は、他のPaaSでも似たような仕組みによって成り立っています。
つまり、1つのサーバーにデプロイするのではなく、クライアントとサーバーサイドをそれぞれデプロイします。
しかしその組み合わせは、今回のように両方ともGAEにデプロイせずとも可能です。

例）
クライアント・・・Vercel
サーバーサイド・・・Heroku

環境変数の設定方法については異なるので、使いたい環境に応じて調べてみてください。

## GAEの機能と動作する基本の流れ
GAEは大まかに下記のような処理により動作しています。

### app.yamlの準備
このファイルがあるディレクトリ以下のファイルをアップロードします。
自分は下記のように作成しました。
``` app.yaml
runtime: nodejs16 // バックエンドの環境
automatic_scaling: // 無料枠を超えないための設定
  min_idle_instances: automatic
  max_idle_instances: 1
  min_pending_latency: 3000ms
  max_pending_latency: automatic
  max_instances: 2
env: standard // スタンダードかフレキシブル
instance_class: F1 // インスタンスの規模
```
なお、.gcloudignoreというファイルも自動的に生成されます（多分）。
node_modulesや.gitなどをアップロードしないように設定します。

### サーバーにファイルをアップロードする
GoogleSDKをインストールすると、gcloudコマンドが使用できるようになります。
これがアップロードの起点になります。
```
gcloud init // 初期化・インスタンスへの接続
gcloud app deploy // アップロード
```
Docsでは上記が紹介されていますが、他にもオプションがあるようです。
なお下記にもある通り、アップロードする前にはローカル上でbuildコマンドを使っておく必要があります。

### npm installを実行する
アップロードされた後、まず自動的にnode_modulesが展開されます。
ですので同ディレクトリのアップロードは不要です。

### npm run startまたはnode server.jsを実行する
起動コマンドを実行します。
ここが第一にハマる点かもしれません（自分はハマった）。
startコマンドの指定がなければルート上にあるserver.jsを探しに行くので、「何度やっても起動しない！」となります。
特にクライアント側を起動する場合、startコマンドが必須です。

### 過去のアップロードを上書きしてバージョン管理
アップロードしたファイルは常に全て上書きされます。
ダッシュボードでは、下記を行うことができます。
- バージョン管理
- バージョンの削除
- トラフィックの割り当て
- トラフィックの移行

## GAEやその他PaaSなどでできないこと
下記についてはできませんでした（IaaSなどサービスによっては可能かもしれません）。

- 1つのインスタンスでクライアントとサーバーサイドの両方を動かすことはできない
- npm installを複数のディレクトリ下で走らせることはできない（最初のみ）
- サーバー上でbuildコマンドを走らせることはできない
- サーバー上でtscコマンドを走らせることはできない

## 【番外編】デプロイやビルドを便利にするパッケージ
[npm-run-all](https://www.npmjs.com/package/npm-run-all)

複数のnpmコマンドを同一のターミナルから走らせることができます。
クライアントとサーバーサイドで別々のnode_modulesを使う構成だったので、こちらのパッケージが便利でした。
以下の例だと、npm run devコマンド1つで、クライアントとサーバーサイド両方の起動を行うことができます。
ターミナルも1つあればOK。

```package.json

  "scripts": {
    "dev": "run-p dev:*",
    "dev:serve": "cd server && npm run dev",
    "dev:client": "cd client && npm run dev",
    "build": "run-p build:*",
    "build:serve": "cd server && tsc",
    "build:client": "cd client && npm run build",
    "start": "run-p st:*",
    "st:serve": "cd server && node dist/index.js",
    "st:client": "cd client && npm run start"
  },
```
## まとめ・感想
インフラ周りを学習してみて感じたこと。
- PaaSはどれもほとんど同じ仕組みで動いているみたい
- 1つやり方を覚えればある程度は代用が効く
- 2つのインスタンスに別々にデプロイのは発想さえできなかった
- フロント側は静的ファイルだけで動くわけではない（勘違いしてた）
- package.jsonのscriptの便利さを実感

## 謝辞
今回の学習内容は、[Recursion](https://recursionist.io/)のチーム開発に参加して得られたものです。
貴重な場を提供してくださり、ありがとうございます。

またチームメンバーのKohさん・Hayatoさん。
Kohさんは上記アイディアを指摘してくれて本当に感謝です。
1人では間違いなく、期日内にデプロイまで到達できなかったです。
Hayatoさんには今思えば最後はやや無茶振りでしたが、ぜひ今後に活かしてもらいたいです。

その他、チームVとXの皆さま。
やり取りがとても参考になりました。
改めてお礼申し上げますm(_ _)m

ではまた、これからも良きプログラミングライフを。

※曖昧な点、間違いなどありましたら、ぜひご指摘ください。