- 投稿日:2019-05-22T23:59:24+09:00
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 を使うまで
サンプルの準備
- シンプルな、1対1のビデオチャットのサンプル
- サーバー側(コンテナ)は、Webサーバーと、WebSocketサーバー機能を持つ
- Node.js + express + ws
- 同一ポートで、httpとwebsocketを使用
- PORT環境変数で指定されたポート番号を使用
サーバーのソースコードの抜粋
GitHubはこちら https://github.com/mganeko/webrtc_1to1
// --- 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_1to1mganeko/webrtc_1to1 はイメージ名(適宜置き換えてください)
2つのブラウザで http://localhost:8002 に接続 → OK
Cloud Run で実行
CloudBuildでビルド$ gcloud builds submit --project cloud-run-webrtc1to1 --tag gcr.io/cloud-run-webrtc1to1/webrtc-1to1CloudRunにデプロイ$ 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
WebSocketが繋がらない (400 Bad Request)
Cloud Run の制約
- 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は使えない
無理やりシグナリングするには
- 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
// --- 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
- Google Compute Engine や、他のIaaSを使う
- heroku も WebSocket 使える様子... Using WebSockets on Heroku with Node.js
ちなみに Google App Engine FE は IaaSより高い
まとめ
- Cloud Run 便利そう
- が、シグナリングザーバーには向いてない
どうもありがとうございました!
- 投稿日:2019-05-22T16:17:29+09:00
Sequelize + Jest のユニットテスト環境構築手順
はじめに
Sequelize な環境で開発を進めていくに当たり、
環境構築に手間取らず async/await なテストしたかったときに、
Jest を採用したらスグにテストを書き始められて良かったので記事にしました。プロジェクトファイルは Github にアップしておきました↓
https://github.com/nuhs/sequelize-jestSequelize のセットアップ
まずは npm で Sequelize のインストールを行います。
今回は Sequelize の裏側に postgresql を使用するので、
ついでに pg ライブラリもインストールします。⊨ npm install --save sequelize pg ⊨ npm install --save-dev sequelize-cliインストール完了後、Sequelize の初期化コマンドを実行します。
⊨ npx sequelize init Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Created "config/config.json" Successfully created models folder at "/Users/nika/Desktop/sequelize-jest/models". Successfully created migrations folder at "/Users/nika/Desktop/sequelize-jest/migrations". Successfully created seeders folder at "/Users/nika/Desktop/sequelize-jest/seeders".初期化コマンドの実行に成功すると、Sequelize を使用するのに必要なファイル群がプロジェクトフォルダ内に生成されます。
プロジェクトルートに config/config.json が生成されているはずなので PostgreSQL と接続出来るように書き換えます。config/config.json{ "development": { "username": "sequelize_jest", "password": "password", "database": "sequelize_jest_development", "host": "127.0.0.1", "dialect": "postgres" }, "test": { "username": "sequelize_jest", "password": "password", "database": "sequelize_jest_test", "host": "127.0.0.1", "dialect": "postgres" }, "production": { "username": "sequelize_jest", "password": "password", "database": "sequelize_jest_production", "host": "127.0.0.1", "dialect": "postgres" } }上記で設定した config/config.json そのままで動作確認するために、
テスト用に PostgreSQL ユーザを作成しました。
本記事のサンプルを利用するためだけの用途でご利用ください。⊨ psql postgres psql (11.2) Type "help" for help. postgres=# CREATE ROLE sequelize_jest with login password 'password'; CREATE ROLE postgres=# alter role sequelize_jest CREATEDB; ALTER ROLE postgres=# \q各種データベース周りの設定が完了次第、データベースの作成を行います。
⊨ npx sequelize db:create Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "development". Database sequelize_jest_development created.データベースの作成に成功できれば、Sequelize のセットアップは完了です。
Sequelize でモデルを追加する
今回は Food というモデルを追加します。
⊨ npx sequelize model:create --name Food --attributes 'name:string, type:string' Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] New model was created at /Users/nika/Desktop/sequelize-jest/models/food.js . New migration was created at /Users/nika/Desktop/sequelize-jest/migrations/20190522034233-Food.js .実行に成功すると migrations というフォルダに PostgreSQL に Food モデル(テーブル)を追加するのに必要なファイルが生成されます。
早速 Sequelize 経由で PostgresSQL に Food モデル(テーブル)を追加します。
⊨ npx sequelize db:migrate Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "development". == 20190522034233-create-food: migrating ======= == 20190522034233-create-food: migrated (0.016s)これで Sequelize を動作させるための最低限のセットアップが完了しました。
試しに Sequelize の動作確認してみます。プロジェクトのルートに index.js を追加します。
index.js の中身は Food のデータを全て取得して標準出力するだけの処理です。index.jsconst { Food } = require('./models'); const main = async () => { const foods = await Food.findAll({raw: true}); console.log(foods); } main();index.js を追加したら早速実行してみます。
⊨ node index.js Executing (default): SELECT "id", "name", "type", "createdAt", "updatedAt" FROM "Food" AS "Food"; []現在は Food テーブルに何もデータが入っていないため、空の配列が出力されていますが、
正しく SQL が実行されていることが標準出力から確認出来ます。Sequelize で初期データを用意する
空の配列だと正しくスクリプトが実行出来ているか分かりづらいため、
Food モデルのシードデータをいくつか用意します。シードデータを追加するには seeders フォルダの中に追加する必要があります。
試しに seeders フォルダに food.js を追加して Food のデータをいくつか追加してみます。seeders/food.jsmodule.exports = { up: async (queryInterface, Sequelize) => { const data = [{ name: 'focaccia', type: 'bread', updatedAt: new Date(), createdAt: new Date() }, { name: 'french bread', type: 'bread', updatedAt: new Date(), createdAt: new Date() }, { name: 'muffin', type: 'bread', updatedAt: new Date(), createdAt: new Date() }, { name: 'candy', type: 'sweets', updatedAt: new Date(), createdAt: new Date() }, { name: 'chocolate', type: 'sweets', updatedAt: new Date(), createdAt: new Date() }]; return await queryInterface.bulkInsert('Food', data); }, down: async (queryInterface, Sequelize) => { await queryInterface.bulkDelete('Food', null, {}); }, };上記を追加した状態でコマンドを実行します。
⊨ npx sequelize db:seed:all Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "development". == food: migrating ======= == food: migrated (0.008s)コマンドの実行に成功したら、index.js を実行してみて、出力について確認してみます。
⊨ node index.js Executing (default): SELECT "id", "name", "type", "createdAt", "updatedAt" FROM "Food" AS "Food"; [ { id: 2, name: 'focaccia', type: 'bread', createdAt: 2019-05-22T04:16:02.498Z, updatedAt: 2019-05-22T04:16:02.498Z }, { id: 3, name: 'french bread', type: 'bread', createdAt: 2019-05-22T04:16:02.498Z, updatedAt: 2019-05-22T04:16:02.498Z }, { id: 4, name: 'muffin', type: 'bread', createdAt: 2019-05-22T04:16:02.498Z, updatedAt: 2019-05-22T04:16:02.498Z }, { id: 5, name: 'candy', type: 'sweets', createdAt: 2019-05-22T04:16:02.498Z, updatedAt: 2019-05-22T04:16:02.498Z }, { id: 6, name: 'chocolate', type: 'sweets', createdAt: 2019-05-22T04:16:02.498Z, updatedAt: 2019-05-22T04:16:02.498Z } ]無事にシードデータの追加に成功しました。
Jest のセットアップ
基本的には公式ページの手順に沿ってインストールしていきます。
https://github.com/facebook/jestまずは jest をインストールします。
npm install --save-dev jestjest を npm 経由で実行出来るよう package.json の scripts の test を設定します。
package.json{ "scripts": { "test": "jest" } }jest は実行できるようになったので、実際にテストファイルを追加します。
jest はテスト実行時に自動で__tests__フォルダを見に行くため、
__tests__フォルダの中にテストファイルを追加します。まずは
__tests__フォルダをプロジェクトルートに追加します。
その後 sample.js を__tests__フォルダに追加します。
sample.js は 1 + 2 は 3 かどうかをテストで確認しています。__tests__/sample.jstest('adds 1 + 2 to equal 3', () => { const sum = 1 + 2; expect(sum).toBe(3); });実際に jest でテストを実行してみます。
⊨ npm run test > sequelize-jest@1.0.0 test /Users/nika/Desktop/sequelize-jest > jest PASS __tests__/sample.js ✓ adds 1 + 2 to equal 3 (3ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.878s Ran all test suites.テストの実行に成功しました。
Sequelize のユニットテストを Jest で行うための準備
現状は開発環境下でテストを行っているため、
Sequelize でモデルを追加/更新を行うテストを行うたびに、
開発環境に影響が出てしまう状態となっています。そのため、テストの実行が開発環境に影響を及ぼさないよう、
テスト環境下でテストをが実行できるように package.json を少し変更します。package.json{ "scripts": { "test": "export NODE_ENV=test ; npm run db:setup ; jest --forceExit ; npm run db:drop", "db:setup": "sequelize db:create ; sequelize db:migrate ; sequelize db:seed:all", "db:drop": "sequelize db:drop" } }
__tests__フォルダに Food モデルのテストを書くためのファイル food.js を追加します。
food.js は test 実行時に毎回シードデータが入るようになったため、その数が 5 であるかをテストしています。__tests__/food.jsconst { Food } = require('../models'); describe('Food Model', () => { test('Total of Food is 5', async () => { const total = await Food.count(); expect(total).toBe(5); }); });テストを実行します。
⊨ npm run test > sequelize-jest@1.0.0 test /Users/nika/Desktop/sequelize-jest > export NODE_ENV=test ; npm run db:setup ; jest --forceExit ; npm run db:drop > sequelize-jest@1.0.0 db:setup /Users/nika/Desktop/sequelize-jest > sequelize db:create ; sequelize db:migrate ; sequelize db:seed:all Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "test". Database sequelize_jest_test created. Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "test". == 20190522034233-create-food: migrating ======= == 20190522034233-create-food: migrated (0.016s) Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "test". == food: migrating ======= == food: migrated (0.009s) PASS __tests__/food.js ● Console console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): SELECT count(*) AS "count" FROM "Food" AS "Food"; PASS __tests__/sample.js Test Suites: 2 passed, 2 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 0.994s, estimated 1s Ran all test suites. Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished? > sequelize-jest@1.0.0 db:drop /Users/nika/Desktop/sequelize-jest > sequelize db:drop Sequelize CLI [Node: 11.14.0, CLI: 5.4.0, ORM: 5.8.6] Loaded configuration file "config/config.json". Using environment "test". Database sequelize_jest_test dropped.出力が少し長くて見づらいですが、
テスト環境下でデータベースのセットアップからシードデータの追加、jest でのテスト実行、テスト実行後のデータベース削除が実行されているのが分かります。テスト環境を用意する前の開発環境下でテストを実行してしまうと、
都度シードデータが開発環境の Food テーブルに追加されてしまい、
それらはテスト実行ごとにクリーンアップされてくれないため、
__tests__/food.jsのテストは 2回目以降失敗するようになります。Sequelize のユニットテストを Jest で書く
ここまで来ればあとは気軽にテストコードを追加していきます。
まずは models/food.js を少し改修します。
Food モデルの追加の際は type に bread か sweets しか設定出来ないようにしました。models/food.js'use strict'; module.exports = (sequelize, DataTypes) => { const Food = sequelize.define('Food', { name: DataTypes.STRING, type: { type : DataTypes.STRING, validate: { isValidType: function(value, next) { if(['bread', 'sweets'].includes(value)) { return next() } next('Unacceptable type of food.') } } } }, {}); Food.associate = function(models) { // associations can be defined here }; return Food; };Food モデルの type に bread か sweets を設定出来ないよう改修したので、その検証のためのテストコードを
__tests__/food.jsに追加してみます。__tests__/food.jsconst { Food } = require('../models'); describe('Food Model', () => { //... describe('create', () => { describe('succeeded', () => { test('type of bread', async () => { const food = await Food.create({ name: 'jam bread', type: 'bread', updatedAt: new Date(), createdAt: new Date() }, { validate: true }); expect(food.id).not.toBeUndefined(); expect(food.name).toBe('jam bread'); expect(food.type).toBe('bread'); }); test('type of sweets', async () => { const food = await Food.create({ name: 'strawberry cake', type: 'sweets', updatedAt: new Date(), createdAt: new Date() }, { validate: true }); expect(food.id).not.toBeUndefined(); expect(food.name).toBe('strawberry cake'); expect(food.type).toBe('sweets'); }); }); describe('failed', () => { test('type of chair', async () => { await expect(Food.create({ name: 'reclining chair', type: 'chair', updatedAt: new Date(), createdAt: new Date() }, { validate: true })). rejects.toThrow(); }); }); }); });テストを実行します。
⊨ npm run test # jest のテスト結果の部分のみ出力 Test Suites: 2 passed, 2 total Tests: 5 passed, 5 total Snapshots: 0 total Time: 1.123s Ran all test suites.全てのテストが無事に実行されました。試しにテストをわざと失敗させてみます。
__tests__/food.jsに失敗するテストを追加します。__tests__/food.jsconst { Food } = require('../models'); describe('Food Model', () => { //... describe('failed', () => { //... test('type of chair(failed)', async () => { const food = await Food.create({ name: 'reclining chair', type: 'chair', updatedAt: new Date(), createdAt: new Date() }, { validate: true }); expect(food.id).not.toBeUndefined(); expect(food.name).toBe('reclining chair'); expect(food.type).toBe('chair'); }); }); //...テストを再度実行します。
⊨ npm run test # jest のテスト結果の部分のみ出力 FAIL __tests__/food.js ● Food Model › create › failed › type of chair(failed) SequelizeValidationError: Validation error: Unacceptable type of food. at Promise.all.then (node_modules/sequelize/lib/instance-validator.js:74:15) PASS __tests__/sample.js Test Suites: 1 failed, 1 passed, 2 total Tests: 1 failed, 5 passed, 6 total Snapshots: 0 total Time: 1.148s Ran all test suites.テストが失敗しました。
おわりに
Jest を使用することで楽にテスト環境をセットアップすることが出来ました。
豊富にマッチャーも用意されているので様々なパターンのテストが Jest のみで完結できそうな印象でした。参考リンク
https://qiita.com/ckoshien/items/9afc60546ba1c9ce04f4
https://blog.honjala.net/entry/2018/08/08/022027
https://jestjs.io/ja/
