Google Cloud Run は WebRTCのシグナリングサーバーに使えるか？

# はじめに

この資料は WebRTC Meetup Tokyo #21 のLT発表資料です。
（が、WebRTC成分はゼロです）

# 自己紹介

- インフォコム株式会社　がねこまさし
- @massie_g 
- WebRTC Meetup Tokyo / Begginers Tokyo スタッフ

---

# Cloud Run とは

- Cloud Run は Google Cloud Platform (GCP)の新しいサービス
- 自分で作ったコンテナを、Googleが用意したサーバー環境上で動かせる
- http(s) の口を持ったWebアプリ/Web APIサーバーを手軽に作れる
- スケーリングも勝手に面倒見てくれる

---

# Cloud Run でできないこと

- コンテナを使っていても、できないことがある
- 単体ではステートを持てない
- データーの永続化はできない

→ 他のサービスを組み合わせて利用

※WebRTC的に気になること

- シグナリングサーバーに使えるのか？ （WebSocketは使えるか？）


---

# Cloud Run を使うには

- 準備
- コンテナのビルド
- コンテナのデプロイ

詳しくは別記事にて ... [Google Cloud Run を使うまで](https://qiita.com/massie_g/items/5a9ce514eaa7c460b5e3)

---

# サンプルの準備

- シンプルな、1対1のビデオチャットのサンプル
- サーバー側（コンテナ）は、Webサーバーと、WebSocketサーバー機能を持つ
    - Node.js + express + ws
- 同一ポートで、httpとwebsocketを使用
    - PORT環境変数で指定されたポート番号を使用

---

# サーバーのソースコードの抜粋

GitHubはこちら https://github.com/mganeko/webrtc_1to1

```js
// --- get PORT from env --
let port = process.env.PORT;

// --- prepare server ---
const http = require("http");
const WebSocketServer = require('ws').Server;
const express = require('express');

const app = express();
app.use(express.static('public'));
let webServer = null;
const hostName = 'localhost';

// --- http ---
webServer = http.Server(app).listen(port, function () {
  console.log('Web server start. http://' + hostName + ':' + webServer.address().port + '/');
});

// --- websocket signaling ---
const wsServer = new WebSocketServer({ server: webServer });
```

---

#  コンテナのビルド

Dockerfile

```
# Use the offical node.js image

FROM node:10.15.3-alpine

RUN mkdir /root/work 
WORKDIR /root/work/
RUN apk add git
RUN git clone https://github.com/mganeko/webrtc_1to1.git
WORKDIR /root/work/webrtc_1to1
RUN npm install


# Run the web service on container startup.
EXPOSE 8080
CMD [ "node", "server_1to1.js" ]
```

※本当は node を直接起動するのは良くないらしい
 「Docker node PID 1」で検索

---

# ローカルでコンテナを実行

```:コンテナのビルド
$ docker build -t mganeko/webrtc_1to1 .
```

```:コンテナの実行
$ docker run -d -e PORT=1323 -p 8002:1323 --name webrtc mganeko/webrtc_1to1
```
_mganeko/webrtc_1to1_ はイメージ名（適宜置き換えてください）

2つのブラウザで http://localhost:8002 に接続 → OK

---

# Cloud Run で実行

```:CloudBuildでビルド
$ gcloud builds submit --project cloud-run-webrtc1to1 --tag gcr.io/cloud-run-webrtc1to1/webrtc-1to1
```

```:CloudRunにデプロイ
$ gcloud beta run deploy --project cloud-run-webrtc1to1 --image gcr.io/cloud-run-webrtc1to1/webrtc-1to1
```

それぞれの値は適宜置き換えてください。

_cloud-run-webrtc1to1_ ... プロジェクトID
_webrtc-1to1_ ... サービス名
_gcr.io/cloud-run-webrtc1to1/webrtc-1to1_ ... イメージのURL

---

# 結果は NG

Chrome DevConsoleのNetworkタブ
<img width="698" alt="cloudrun_ws.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/dde7ba32-2acf-c954-c558-c8d4de0e1bef.png">

WebSocketが繋がらない (400 Bad Request)

---

# Cloud Run の制約

[Container runtime contract](https://cloud.google.com/run/docs/reference/container-contract?hl=ja) より

- The container must listen for requests on 0.0.0.0 on the port defined by the PORT environment variable.
- Your container instances must start an HTTP server within 4 minutes after receiving a request.

どうやらHTTPのみ使えて、WebSocketは使えない

※（2021.02.07追記）CloudRunでWebSocketやHTTP/2, gRPCが使えるようになりました。

- [Introducing WebSockets, HTTP/2 and gRPC bidirectional streams for Cloud Run](https://cloud.google.com/blog/products/serverless/cloud-run-gets-websockets-http-2-and-grpc-bidirectional-streams)

---

# 無理やりシグナリングするには

- HTTPで無理やり → Long Polling (COMET) しかない

自分で Log Polling 実装するにはどうすれば..？

あ、それって Socket.io がフォールバックでサポートしているはず！

---

# Socket.io版サンプル

- シンプルな、1対1のビデオチャットのサンプル
- サーバー側（コンテナ）は、Webサーバーと、Socket.ioサーバー機能を持つ
    - Node.js + express + socket.io
- 同一ポートで、httpとsocket.io(websocketとフォールバック)を使用
    - PORT環境変数で指定されたポート番号を使用

---

# サーバー側ソースコードの抜粋

https://github.com/mganeko/webrtc_1to1_socketio

```js

// --- get PORT from env --
let port = process.env.PORT;

// --- prepare server ---
const http = require("http");
const express = require('express');

const app = express();
app.use(express.static('public'));
let webServer = null;
const hostName = 'localhost';

// --- http ---
webServer = http.Server(app).listen(port, function () {
  console.log('Web server start. http://' + hostName + ':' + webServer.address().port + '/');
});

// --- socket.io server ---
const io = require('socket.io')(webServer);
console.log('socket.io server start. port=' + webServer.address().port);
```

---

# 結果

- websocket接続はエラー
- xhr poll でシグナリングはできる

時間があったらデモ

---

# 厳密には

- リクエストが増えると、自動的にスケールアウト
    - デフォルト設定では 80
- 実運用では、同じインスタンスに繋がるとは限らない → シグナリングできない可能性あり
    - 試しに同時リクエスト数を 1 に設定 → シグナリングできない
    - ※どころか、コンソールの動きが一部おかしくなる？？

---

# シグナリングには、別の手段が順当

- リアルタイム通信サービスを使う (Firebase, milkcocoaなど）
- Cloud Run on GKE を使う（多分、行けるはず）
- Google App Engine を使う
    - Standard Environment ... WebSocket NG
    - Flexible Environment ... WebSocket OK
        - [Creating Persistent Connections with WebSockets ](https://cloud.google.com/appengine/docs/flexible/nodejs/using-websockets-and-session-affinity)
- Google Compute Engine や、他のIaaSを使う
- heroku も WebSocket 使える様子... [Using WebSockets on Heroku with Node.js](https://devcenter.heroku.com/articles/node-websockets)

ちなみに Google App Engine FE は IaaSより高い

---

# まとめ

- Cloud Run 便利そう
- が、シグナリングザーバーには向いてない

どうもありがとうございました！