HubsCloudをDocker環境で動かしてみる

はじめに

こんにちは!サーバサイドエンジニアのクロ(@kro96_xr)です。
この記事はSynamon Advent Calendar 2021の10日目です。

弊社はUnityエンジニアが多く私自身も最近はUnityを触っていたりするのですが、せっかくのアドベントカレンダーということでサーバサイドの記事を書こうと思い筆を取りました。

テーマは「HubsCloudをDocker環境で動かしてみる」です!Elixir初体験だけどなんとかなるやろ!対戦よろしくお願いします。

※正直行き当たりばったりな部分も多く、自分の理解の浅さから改善できるところは多々あると思います。ご指摘などございましたらTwitterなどにいただけると嬉しいです。

Mozilla Hubs/HubsCloudとは?

Mozilla HubsとはFirefoxで有名なMozilla社が提供しているオープンソースのVRシステムです。ブラウザ上で動作するため、PCやスマートフォンを使ってVR空間に入ることが可能であり、起動自体も公式サイトからルームを作成するだけで可能です。

hubs.mozilla.com

HubsCloudとは上記のMozilla HubsをAWS上で動作させることができるものです。AWSのマーケットプレイスで公開されているので導入自体は比較的簡単に出来るかと思います。とはいえ常時起動だとそれなりにコストもかかるんですよね。

それならローカルで起動してみればよくない?

というわけで調べてみるといくつかローカルで起動している記事は見つかりましたが、Dockerで動かしている記事は見つけられませんでした。それなら自分で手を動かしてDockerの環境構築を試してみましょう!

なお、公式discordではDockerに関してプライベートチャンネルでやりとりされているようでした。後述しますがローカル環境構築のサポートはしていない旨の記載がありましたので開発者に絞っているのでしょうか。一応依頼すれば招待してもらえそうでしたが…。

注)以下全て記事公開時点での情報になります。

構成および名称について

まず初めにHubsのシステム構成と記事内で出てくる名称についてざっくり説明します。
詳細についてはこちらをご覧いただくと良いかと思います。

  • クライアント

    • Hubs クライアントはReact、Three.js、A-Frameを組み合わせて作られています。
      今回こちらもローカルで立ち上げますが、Dockerにはのせないこととします。
      リポジトリはこちら
  • サーバーサイド

    • Reticulum
      ビデオ・音声以外の部分はReticulumが使われています。
      Reticulum自体はElixir/Phoenixで作られています。
      指定バージョンはElixir v1.8 + Erlang v22となります。
      リポジトリはこちら

    • Dialog
      ビデオと音声についてはDialogというWebRTCサーバが担っています。
      Dialogは"mediasoup"というオープンソースのプロジェクトをベースとしています。
      リポジトリはこちら

  • データベース
    DBについてはPostgres DBが使われており、推奨バージョンは11.xです。

Docker環境構築

それでは早速環境構築に移りましょう。

Reticulumのリポジトリを見ると、「チーム規模が小さいからローカル環境構築のサポートはしてないよ。でも自分で設定することは可能だよ。やる場合はHubsとDialogもローカル実行が必要だよ。」(意訳)と書いてあります。

Due to our small team size, we don't support setting up Reticulum locally due to restrictions on developer credentials. Although relatively difficult and new territory, you're welcome to set up this up yourself. In addition to running Reticulum locally, you'll need to also run Hubs and Dialog locally because the developer Dialog server is locked down and your local Reticulum will not connect properly

というわけでdocker-composeを使ってReticulum, Dialog, DBを立ち上げていくことにします。
余談ですがreticulumリポジトリにあるdocker-compose.ymlの更新日が3年前でした。つらい。

ディレクトリ構成

ディレクトリ構成はざっくりこのような形にしました。dialogディレクトリとreticulumディレクトリの構成自体はクローンしたものとほぼ同じなので省略しています。 また、クライアント(hubs/)は任意の場所で構いません。

ReticulumTest/  
   ┠dialog/  
   ┃ ┠リポジトリからcloneされたファイル  
   ┃ ┗certs/ (後述)  
   ┃   ┠server.key  
   ┃   ┠server.pem  
   ┃   ┗pub.key  
   ┠reticulum/  
   ┃ ┠リポジトリからcloneされたファイル  
   ┃ ┗storage/  
   ┃   ┗dev/  
   ┠tmp/  
   ┃ ┗db/  
   ┠docker-compose.yml  
   ┗Dockerfile

Dockerの設定

Dialog用のDockerfileはリポジトリのものをそのまま使っているので割愛します。

  • reticulum/Dockerfile
# 指定バージョンのelixir/Erlangが入ったベースイメージを使用
# https://hub.docker.com/layers/hexpm/elixir/1.8.2-erlang-22.3.4.23-ubuntu-focal-20210325/images/sha256-825e3361145e2394690e2ef94d6cc4587a7e91519ad002526d98156466d63643?context=explore
FROM hexpm/elixir:1.8.2-erlang-22.3.4.23-ubuntu-focal-20210325

# ディレクトリの設定
ARG ROOT_DIR=/ret
RUN mkdir ${ROOT_DIR}
WORKDIR ${ROOT_DIR}

# ディレクトリのコピー
COPY ./reticulum ${ROOT_DIR}

# 依存するライブラリのインストール
RUN apt-get update && apt-get install -y git inotify-tools
RUN mix local.hex --force && mix local.rebar --force && mix deps.get

# キャッシュファイル用ディレクトリ作成
RUN mkdir -p /storage/dev && chmod 777 /storage/dev

EXPOSE 4000
  • reticulm/docker-compoes.yml
version: '3'
services:
  db:
    image: postgres:11
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
        - ./tmp/db:/var/lib/postgresql/data
    ports:
      - "5432:5432"
  ret:
    build:
      context: ./
      dockerfile: Dockerfile
    environment:
      - "MIX_ENV=dev"
      - "DB_HOST=db"
    volumes:
      - ./reticulum:/ret
      - ./storage/dev:/storage/dev
    tty: true
    ports:
      - "4000:4000"
    depends_on:
      - db
  dialog:
    build:
      context: ./dialog
      dockerfile: Dockerfile
    environment:
      - "HTTPS_CERT_FULLCHAIN=/app/certs/server.pem"
      - "HTTPS_CERT_PRIVKEY=/app/certs/server.key"
      - "AUTH_KEY=/app/certs/pub.key"
    tty: true
    volumes:
      - ./certs:/app/certs
    ports:
      - "4443:4443"
  • 各種環境変数について補足

    • MIX_ENV
      コンパイル時に使用される設定ファイルを指定しています。この場合reticulum/config/dev.exsが読み込まれます。

    • DB_HOST, POSTGRES_USER, POSTGRES_PASSWORD
      dev.exsにDB設定があるのですが、接続に使用するユーザー名とパスワードがdev環境ではpostgresで指定されています。本来であれば当然変えるべきなのですが、今回は環境構築がメインということでそのまま指定しています。また、DB_HOSTを指定しないとlocahostで接続しにいくのですが、コンテナ間通信では使えないのでコンテナ名であるdbを指定する必要があります。

    • HTTPS_CERT_FULLCHAIN, HTTPS_CERT_PRIVKEY, AUTH_KEY
      後述しますが、Dialogサーバの証明書ファイル秘密鍵Reticulumサーバの公開鍵を設定します。

env_db_host = "#{System.get_env("DB_HOST")}"

# Configure your database
config :ret, Ret.Repo,
  username: "postgres",
  password: "postgres",
  database: "ret_dev",
  hostname: if(env_db_host == "", do: "localhost", else: env_db_host),
  template: "template0",
  pool_size: 10

初回起動

  • ビルドとコンテナの起動 特に言うことはありませんね。

    docker-compose build
    docker-compoes up -d

  • DB生成
    reticulumのコンテナに入ります。

    docker-compose exec ret bash

    DB生成を行います。そこそこ時間がかかるので気長に待ちましょう。

    root@~:/ret# mix ecto.create

  • サーバ起動

    iex -S mix phx.server

ここで終わったと思ってhttps://localhost:4000/としても残念ながら動きません。

f:id:krocks96:20211209031048p:plain

ソースコードを追うとわかるのですが、クライアント(Hubs)側でwebpackでサーバを立ち上げており、そのhtmlを取得しています。 そのためクライアント側の設定を進めていく必要があります。
なお、接続先自体はdev.exsに設定があります。こちらも後で修正します。

config :ret, Ret.PageOriginWarmer,
  hubs_page_origin: "https://#{host}:8080",
  admin_page_origin: "https://#{host}:8989",
  spoke_page_origin: "https://#{host}:9090",
  insecure_ssl: true

また、それ以外にも多数躓いたポイントがあったのでひとつひとつ進めていきます!

動かすまでにやること

HOSTSファイルの修正

hubs.localhubs-proxy.local127.0.0.1を紐づけてください。(方法は割愛)

クライアント(Hubs)の起動

hubsリポジトリのREADMEに記載されている手順に沿ってクライアントの立ち上げを行いましょう。
まず、hubsのhubs-cloudリポジトリから任意の場所にクローンして依存関係のインストールを行います。

cd hubs
npm ci

インストールが完了したらWebpack Dev Serverを立ち上げます。  

npm run local

クライアント-管理画面(Hubs/admin)の起動

こちらもREADMEに従って依存関係のインストールとWebpack Dev Serverの起動を行います。

cd hubs/admin
npm install
npm run local

Dialogの設定

Dialogコンテナではdocker-compose.ymlで指定した通り証明書ファイル秘密鍵Reticulumサーバの公開鍵が必要になります。
また、Reticulumリポジトリにあったサーバ証明書は有効期限が切れているので新たにオレオレ証明書を生成して設定してください。(方法は各自でお願いしますmm)
ポイントとしては、SAN(Subject Alternative Name)のDNS Nameの値がhubs.localになっていないとChromeでエラーになります

Chromeがコモンネームの設定を非推奨化、そのエラー対策としての自己署名証明書のCSRの作り方

ちなみに各サーバの証明書ファイルの配置は下記の通りです。リネームして置き換えるか、設定ファイルをいじって参照先を変えるかご自由にどうぞ。

  • Reticulumではreticulum/priv/dev
  • Hubsではhubs/certs
  • Dialogではsialog/certs

置き換えた後、ブラウザにルートCAの証明書をインポートする必要がありますが、こちらも各自お願いします。

Google Chromeへ証明書ファイルをインポートするには | GMOグローバルサイン サポート

最後に、Reticulumサーバの環境変数に秘密鍵の内容を設定しておきます。

docker-compose exec ret bash
export PERMS_KEY={生成した秘密鍵の内容}

これがないとReticulumとDialog間の通信ができずルーム入室できません。
永続化できてないけど一旦このままで…

各ソースコードの修正

これで動くようになった…かと思いきやできません。

Dockerを使っていなければReticulum⇔Webpack Dev Server間の通信が出来るはずなのですが、 今回はコンテナ⇔ホスト間の通信のため、一工夫必要です。

誰だDocker使おうなんて言い出したの。

Reticulumの修正

dev.exsを開き、host_front = "host.docker.internal"を定義してhubs_page_originらを修正します。

host = "hubs.local"
host_front = "host.docker.internal"

略

config :ret, Ret.PageOriginWarmer,
  # hubs_page_origin: "https://#{host}:8080",
  # admin_page_origin: "https://#{host}:8989",
  # spoke_page_origin: "https://#{host}:9090",
  hubs_page_origin: "https://#{host_front}:8080",
  admin_page_origin: "https://#{host_front}:8989",
  spoke_page_origin: "https://#{host_front}:9090",
  insecure_ssl: true

これでやっと通信できる!…と思いきやもう少し修正が必要です。

こちらはクライアント側のhubs/webpack.config.jsですが、allowedHostshubs.localが指定されています。

    devServer: {
      https: createHTTPSConfig(),
      host: "0.0.0.0",
      public: `${host}:8080`,
      useLocalIp: true,
      allowedHosts: [host, "hubs.local"],
      headers: {
        "Access-Control-Allow-Origin": "*"
      },

ですので、reticulum/lib/ret/http_util.exを修正してリクエストヘッダでHOSTを指定してあげましょう。

  defp retry_until_success(verb, url, body, options) do
    default_options = [
      # headers: [],
      headers: [{"Host", "hubs.local"}],
      cap_ms: 5_000,
      expiry_ms: 10_000,
      append_browser_user_agent: false
    ]
略

続いてローカル環境のDialogとの通信のための修正です。
dev.exsを以下のように修正します。

# dev_janus_host = "dev-janus.reticulum.io"
dev_janus_host = "hubs.local"

# config :ret, Ret.JanusLoadStatus, default_janus_host: dev_janus_host, janus_port: 443
config :ret, Ret.JanusLoadStatus, default_janus_host: dev_janus_host, janus_port: 4443

続いてadd_csp.exを修正します。

    # default_janus_csp_rule =
    #   if default_janus_host != nil && String.length(String.trim(default_janus_host)) > 0,
    #     do: "wss://#{default_janus_host}:#{janus_port} https://#{default_janus_host}:#{janus_port}",
    #     else: ""
    default_janus_csp_rule =
      if default_janus_host,
          do: "wss://#{default_janus_host}:#{janus_port} https://#{default_janus_host}:#{janus_port} https://#{default_janus_host}:#{janus_port}/meta",
          else: ""

最後にiex -S mix phx.serverでサーバを立ち上げなおしましょう。

長い道のりでしたがこれでhttps://hubs.local:4000?skipadminにアクセスすればトップページが表示されるはずです。

f:id:krocks96:20211209034638p:plain

※開発者モードでコンソールを見るとわかるのですが、ロケールの問題で翻訳できない部分([@formatjs/intl Error MISSING_TRANSLATION])が出てきています。
これは本家mozilla hubsを見ても一部だけ日本語訳されているので仕方ないと思ってそのままにします。

ユーザー登録を試してみる

右上のサインインをクリックするとサインイン画面に遷移します。ここでメールアドレスを入力してNextを押すと…

f:id:krocks96:20211209040441p:plain

Reticulumを立ち上げているコンソールにリンクが表示されるのでURLをクリックして認証を完了します。
f:id:krocks96:20211209194819p:plain

f:id:krocks96:20211209040740p:plain

DBの確認

DBコンテナに入ってユーザーを確認します。

docker-compose exec db bash
root@~:/# psql -U postgres -d ret_dev
ret_dev=# ret_dev=# SELECT * FROM accounts;

ちゃんと登録されていますね。
f:id:krocks96:20211209041213p:plain

管理ポータルを試してみる

DBにアクセスしてis_admintrueに変更します。 f:id:krocks96:20211209041508p:plain

その後https://hubs.local:4000/adminにアクセスすると管理画面が表示されるはずです。 f:id:krocks96:20211209042108p:plain

ルームの作成を試してみる

Dialogコンテナに入りWebRTCサーバを起動します。この際、コンテナ内のIPアドレスが必要になります。

Start dialog with MEDIASOUP_LISTEN_IP=XXX.XXX.XXX.XXX MEDIASOUP_ANNOUNCED_IP=XXX.XXX.XXX.XXX npm start where XXX.XXX.XXX.XXX is the local IP address of the machine running the server. (In the case of a VM, this should be the internal IP address of the VM).

というわけでコンテナに入って下記のように立ち上げます。
本当はdocker-network使ってやればいいんでしょうが…それだとアドベントカレンダーに間に合わない

docker-compose exec dialog bash
root@~:/app# hostname -i
{ipアドレス}
root@~:/app# MEDIASOUP_LISTEN_IP={ipアドレス} MEDIASOUP_ANNOUNCED_IP={ipアドレス} npm start

トップページの部屋を作成するボタンから入室します。

f:id:krocks96:20211209192920p:plain

真っ暗な空間ではありますが、無事に入室することができました!

f:id:krocks96:20211209193005p:plain

リアクションなどもこの通り!不気味!

まだまだ実用には程遠いですがひとまず動くところまでいったので今回はこれでおしまいです。

終わりに

Dockerで環境構築出来たら遊び倒せるし、環境の共有も楽だなーと軽い気持ちで始めたら思った以上に大変で泣きそうでした。

しかし、苦しんだ分HubsCloudへの理解を少し深めることができたのではないかなと思います。

普段であればおそらくここまで記録に残すこともないでしょうし、あらためてアドベントカレンダーに参加して良かったです。

参考リンク

以下参考リンクです。ありがとうございました。

Home · gree/hubs-docs-jp Wiki · GitHub

Mozilla Hubsメモ - フレームシンセシス

Mozilla HubsのバックエンドサーバーReticulumを改造する方法

Reticulumをローカルで動かしてデプロイする - Qiita

告知

本テックブログやnote記事のお知らせは、Synamon公式Twitterで発信しています。

弊社の取り組みに興味を持っていただけましたらぜひフォローお願いします!

twitter.com

カジュアル面談も実施中ですので「詳しく話を聞いてみたい!」という方はチェックいただけると嬉しいです。

▼カジュアル面談はMeetyから   meety.net  

▼エントリーはこちら   herp.careers

Synamonアドベントカレンダーはまだまだ続きますので、今後もご覧いただけると嬉しいです!

qiita.com