Cloud Run 用 WebSocket チャット サービス構築のチュートリアル

このチュートリアルでは、永続的な接続により双方向通信を行う WebSocket を使用して、マルチルームのリアルタイム チャット サービスを作成する方法について説明します。WebSocket を使用すると、サーバーに更新をポーリングすることなく、クライアントとサーバーの両方がメッセージを相互に push できます。

Cloud Run は、すべてのトラフィックを処理するためにコンテナ インスタンスの数を自動的にスケーリングしますが、Cloud Run はセッション アフィニティを提供しません。これは、新しいリクエストを別のコンテナ インスタンスにルーティングできることを意味します。そのため、チャット サービスのユーザー メッセージは、1 つのコンテナ インスタンスに接続されたクライアント間だけでなく、すべてのコンテナ インスタンス間で同期する必要があります。

デザインの概要

このサンプル チャット サービスは、Memorystore for Redis インスタンスを使用して、ユーザー メッセージを保存し、すべてのコンテナ インスタンス間で同期します。Redis は Pub/Sub メカニズムを使用して(Cloud Pub/Sub プロダクトではありません)、任意のコンテナ インスタンスに接続された登録済みクライアントにデータを push し、HTTP で更新をポーリングしないようにします。

ただし、push 更新があっても、起動されたコンテナ インスタンスはコンテナに push された新しいメッセージのみを受信します。以前のメッセージを読み込むには、メッセージ履歴を永続ストレージ ソリューションに保存して取得する必要があります。このサンプルでは、Redis の従来のオブジェクト ストアの機能を使用して、メッセージ履歴をキャッシュに保存して取得します。

アーキテクチャ図
この図は、各 Cloud Run コンテナ インスタンスへの複数のクライアント接続を示しています。各コンテナ インスタンスは、サーバーレス VPC アクセス コネクタ経由で Memorystore for Redis インスタンスに接続します。

Redis インスタンスは、アクセス制御されたプライベート IP を使用してインターネットから保護され、Redis インスタンスと同じバーチャル プライベート ネットワークで実行されているサービスに限定されます。したがって、Cloud Run サービスが Redis に接続するには、サーバーレス VPC アクセス コネクタが必要です。サーバーレス VPC アクセスの詳細をご確認ください。

制限事項

  • このチュートリアルでは、エンドユーザー認証やセッション キャッシュについては説明しません。エンドユーザー認証の詳細については、Cloud Run チュートリアルのエンドユーザー認証をご覧ください。

  • このチュートリアルでは、チャット メッセージの履歴を制限なく保存し、取得するために、Firestore などのデータベースを実装することはありません。

  • このサンプル サービスを本番環境で使用できるようにするには、追加の要素が必要です。レプリケーションと自動フェイルオーバーを使用した高可用性を実現するには、スタンダード ティアの Redis インスタンスをおすすめします。

目標

  • WebSocket を使用する Cloud Run サービスを作成、ビルド、デプロイする。

  • Memorystore for Redis インスタンスに接続して、コンテナ インスタンス間で新しいメッセージを公開および登録する。

  • サーバーレス VPC アクセス コネクタを使用して、Cloud Run サービスを Memorystore に接続する。

料金

このチュートリアルでは、Google Cloud の課金対象となる以下のコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを出すことができます。

このプロジェクトは、無料トライアル クレジットの範囲内で完了できるはずです。

新しい Cloud Platform ユーザーは無料トライアルをご利用いただけます。

始める前に

  1. Google Cloud アカウントにログインします。Google Cloud を初めて使用する場合は、アカウントを作成して、実際のシナリオでの Google プロダクトのパフォーマンスを評価してください。新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  3. Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  5. Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  6. Cloud Run, Memorystore for Redis, Serverless VPC Access, Artifact Registry, and Cloud Build API を有効にします。

    API を有効にする

  7. Cloud SDK をインストールして初期化します。

gcloud のデフォルトを設定する

Cloud Run サービスを gcloud のデフォルトに構成するには:

  1. デフォルト プロジェクトを設定します。

    gcloud config set project PROJECT_ID

    PROJECT_ID は、このチュートリアルで作成したプロジェクトの名前に置き換えます。

  2. 選択したリージョン向けに gcloud を構成します。

    gcloud config set run/region REGION

    REGION は、任意のサポートされている Cloud Run のリージョンに置き換えます。

Cloud Run のロケーション

Cloud Run はリージョナルです。つまり、Cloud Run サービスを実行するインフラストラクチャは特定のリージョンに配置され、そのリージョン内のすべてのゾーンで冗長的に利用できるように Google によって管理されます。

レイテンシ、可用性、耐久性の要件を満たしていることが、Cloud Run サービスを実行するリージョンを選択する際の主な判断材料になります。一般的には、ユーザーに最も近いリージョンを選択できますが、Cloud Run サービスで使用されている他の Google Cloud サービスのロケーションも考慮する必要があります。使用する Google Cloud サービスが複数のロケーションにまたがっていると、サービスの料金だけでなくレイテンシにも影響します。

Cloud Run は、次のリージョンで利用できます。

ティア 1 料金を適用

  • asia-east1(台湾)
  • asia-northeast1(東京)
  • asia-northeast2(大阪)
  • europe-north1(フィンランド) リーフアイコン 低 CO2
  • europe-west1(ベルギー) リーフアイコン 低 CO2
  • europe-west4(オランダ)
  • us-central1(アイオワ) リーフアイコン 低 CO2
  • us-east1(サウスカロライナ)
  • us-east4(北バージニア)
  • us-west1(オレゴン) リーフアイコン 低 CO2

ティア 2 料金を適用

  • asia-east2(香港)
  • asia-northeast3(ソウル、韓国)
  • asia-southeast1(シンガポール)
  • asia-southeast2 (ジャカルタ)
  • asia-south1(ムンバイ、インド)
  • asia-south2(デリー、インド)
  • australia-southeast1(シドニー)
  • australia-southeast2(メルボルン)
  • europe-central2(ワルシャワ、ポーランド)
  • europe-west2(ロンドン、イギリス)
  • europe-west3(フランクフルト、ドイツ)
  • europe-west6(チューリッヒ、スイス) リーフアイコン 低 CO2
  • northamerica-northeast1(モントリオール) リーフアイコン 低 CO2
  • northamerica-northeast2(トロント)
  • southamerica-east1(サンパウロ、ブラジル) リーフアイコン 低 CO2
  • us-west2(ロサンゼルス)
  • us-west3(ソルトレイクシティ)
  • us-west4(ラスベガス)

Cloud Run サービスをすでに作成している場合は、Cloud Console の Cloud Run ダッシュボードにリージョンが表示されます。

サンプルコードを取得する

使用するコードサンプルを取得するには:

  1. ローカルマシンにサンプル リポジトリのクローンを作成します。

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. Cloud Run のサンプルコードが含まれているディレクトリに移動します。

    Node.js

    cd nodejs-docs-samples/run/websockets/

コードについて

Socket.io は、ブラウザとサーバー間でのリアルタイム双方向通信を可能にするライブラリです。Socket.io は WebSocket の実装ではありませんが、複数の通信プロトコルに対応するシンプルな API を提供する機能を、信頼性の向上、自動再接続、すべてのクライアントまたは一部のクライアントへのブロードキャストなどの追加機能とラップします。

クライアントサイドの統合

<script src="/socket.io/socket.io.js"></script>

クライアントは、接続ごとに新しい Socket インスタンスをインスタンス化します。このサンプルはサーバーサイドでレンダリングされるため、サーバー URL を定義する必要はありません。ソケット インスタンスはイベントを出力しリッスンできます。

// Initialize Socket.io
const socket = io('', {
  transports: ['websocket'],
});
// Emit "sendMessage" event with message
socket.emit('sendMessage', msg, error => {
  if (error) {
    console.error(error);
  } else {
    // Clear message
    $('#msg').val('');
  }
});
// Listen for new messages
socket.on('message', msg => {
  log(msg.user, msg.text);
});

// Listen for notifications
socket.on('notification', msg => {
  log(msg.title, msg.description);
});

// Listen connect event
socket.on('connect', () => {
  console.log('connected');
});

サーバーサイドの統合

サーバーサイドで、Socket.io サーバーが初期化され、HTTP サーバーに接続されます。クライアントサイドと同様に、Socket.io サーバーがクライアントと接続すると、接続ごとにソケット インスタンスが作成され、メッセージの出力やリッスンに使用されます。Socket.io では、「チャットルーム」用の簡易インターフェース、または任意のチャンネルも用意されていて、ソケットが自由に参加または離脱できます。

// Initialize Socket.io
const server = require('http').Server(app);
const io = require('socket.io')(server);

const redisAdapter = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
io.adapter(redisAdapter(redisClient, redisClient.duplicate()));

// Listen for new connection
io.on('connection', socket => {
  // Add listener for "signin" event
  socket.on('signin', async ({user, room}, callback) => {
    try {
      // Record socket ID to user's name and chat room
      addUser(socket.id, user, room);
      // Call join to subscribe the socket to a given channel
      socket.join(room);
      // Emit notification event
      socket.in(room).emit('notification', {
        title: "Someone's here",
        description: `${user} just entered the room`,
      });
      // Retrieve room's message history or return null
      const messages = await getRoomFromCache(room);
      // Use the callback to respond with the room's message history
      // Callbacks are more commonly used for event listeners than promises
      callback(null, messages);
    } catch (err) {
      callback(err, null);
    }
  });

  // Add listener for "updateSocketId" event
  socket.on('updateSocketId', async ({user, room}) => {
    try {
      addUser(socket.id, user, room);
      socket.join(room);
    } catch (err) {
      console.error(err);
    }
  });

  // Add listener for "sendMessage" event
  socket.on('sendMessage', (message, callback) => {
    // Retrieve user's name and chat room  from socket ID
    const {user, room} = getUser(socket.id);
    if (room) {
      const msg = {user, text: message};
      // Push message to clients in chat room
      io.in(room).emit('message', msg);
      addMessageToCache(room, msg);
      callback();
    } else {
      callback('User session not found.');
    }
  });

  // Add listener for disconnection
  socket.on('disconnect', () => {
    // Remove socket ID from list
    const {user, room} = deleteUser(socket.id);
    if (user) {
      io.in(room).emit('notification', {
        title: 'Someone just left',
        description: `${user} just left the room`,
      });
    }
  });
});

Socket.io には、ソケットを提供しているサーバーに関係なく、すべてのクライアントにイベントをブロードキャストする Redis アダプタも用意されています。Socket.io では、Redis の Pub/Sub メカニズムを使用するだけで、データは保存しません。

const redisAdapter = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
io.adapter(redisAdapter(redisClient, redisClient.duplicate()));

Socket.io の Redis アダプタは、チャットルームのメッセージ履歴の保存に使用された Redis クライアントを再利用できます。各コンテナは Redis インスタンスへの接続を作成します。Cloud Run には最大 1,000 個のインスタンスがあります。これは、Redis がサポートする 65,000 接続をかなり下回っています。この量のトラフィックをサポートする必要がある場合は、サーバーレス VPC アクセス コネクタのスループットも評価する必要があります。

再接続

Cloud Run のタイムアウトは最大 60 分です。そのため、タイムアウトが発生する可能性に備えて、再接続ロジックを追加する必要があります。場合によっては、Socket.io が切断または接続エラーイベントの後に自動的に再接続を試みます。クライアントが同じコンテナ インスタンスに再接続する保証はありません。

// Listen for reconnect event
socket.io.on('reconnect', () => {
  console.log('reconnected');
  // Emit "updateSocketId" event to update the recorded socket ID with user and room
  socket.emit('updateSocketId', {user, room}, error => {
    if (error) {
      console.error(error);
    }
  });
});
// Add listener for "updateSocketId" event
socket.on('updateSocketId', async ({user, room}) => {
  try {
    addUser(socket.id, user, room);
    socket.join(room);
  } catch (err) {
    console.error(err);
  }
});

すべてのリクエストが閉じるかタイムアウトするまで、アクティブな接続がある間はコンテナ インスタンスが保持されます。セッション アフィニティがないため、新しいリクエストはアクティブなコンテナに負荷分散され、コンテナをスケールインできます。トラフィックの急増後に多数のコンテナが保持されることが懸念される場合は、未使用のソケットが頻繁にクリーンアップされるように最大タイムアウト値を下げることができます。

サービスの配布

  1. Memorystore for Redis インスタンスを作成します。

    gcloud redis instances create INSTANCE_ID --size=1 --region=REGION

    INSTANCE_ID はインスタンスの名前(例: my-redis-instance)に置き換え、REGION_ID はすべてのリソースとサービスのリージョン(例: us-central1)に置き換えます。

    インスタンスに、デフォルトのサービス ネットワーク範囲から IP 範囲が自動的に割り振られます。このチュートリアルでは、Redis インスタンスのメッセージのローカル キャッシュに 1 GB のメモリを使用します。詳細については、ユースケース用の Memorystore インスタンスの初期サイズの決定をご覧ください。

  2. サーバーレス VPC アクセス コネクタを設定します。

    Redis インスタンスに接続するには、Cloud Run サービスが Redis インスタンスの承認済み VPC ネットワークにアクセスする必要があります。

    VPC コネクタごとに、コネクタ インスタンスを配置する独自の /28 サブネットが必要です。この IP 範囲は、VPC ネットワーク内の既存の IP アドレス予約と重複してはいけません。たとえば、10.8.0.0/28)はほとんどの新規プロジェクトで機能しますが、10.9.0.0/28)など、別の未使用のカスタム IP 範囲を指定することもできます。現在予約されている IP 範囲は、Cloud Console で確認できます。

    gcloud compute networks vpc-access connectors create CONNECTOR_NAME \
      --region REGION \
      --range "10.8.0.0/28"
    

    CONNECTOR_NAME は、アプリケーションの名前に置き換えます。

    このコマンドは、Redis インスタンスと同じデフォルト VPC ネットワークに、e2-micro マシンサイズのコネクタを作成します。コネクタのマシンサイズを大きくすると、コネクタのスループットを向上させることができますが、コストも増加します。また、コネクタは Redis インスタンスと同じリージョンに配置する必要があります。サーバーレス VPC アクセスの構成の詳細をご確認ください。

  3. Redis インスタンスの承認済みネットワークの IP アドレスを使用して、環境変数を定義します。

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  4. サービス ID として機能するサービス アカウントを作成します。このアカウントには、デフォルトでは、プロジェクト メンバーシップ以外の権限は付与されません。

    gcloud iam service-accounts create chat-identity

    このサービスは、Google Cloud の他のサービスとやり取りする必要がないため、このサービス アカウントに追加の権限を割り当てる必要はありません。

  5. コンテナ イメージをビルドして Cloud Run にデプロイします。

    gcloud beta run deploy chat-app --source . \
        --vpc-connector CONNECTOR_NAME \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --update-env-vars REDISHOST=$REDISHOST

    プロンプトが表示されたら、y と入力して必要な API をインストールするように応答します。これが必要なのはプロジェクトに対して 1 回だけです。設定ページに記載されているように、デフォルト値を設定しない場合は、別のプロンプトにプラットフォームとリージョンを指定して応答します。ソースコードからのデプロイで詳細をご確認ください。

試してみる

完成したサービスを試すには:

  1. ブラウザで、前述のデプロイの手順により提供された URL に移動します。

  2. 自分の名前とチャットルームを追加してログインします。

  3. チャットルームにメッセージを送信します。

これらのサービスを開発し続けることにした場合、Google Cloud の他のサービスへの Identity and Access Management(IAM)アクセスが制限されます。他の多くのサービスにアクセスするには、追加の IAM ロールをこれらのサービスに与える必要があることにご注意ください。

費用の説明

5 GB の Redis インスタンスとサーバーレス VPC アクセス コネクタを備えたアイオワ(us-central1)にホストされているチャット サービスの費用内訳の例。

プロダクト 月額
Redis 費用 = プロビジョニングされた容量(5 GB)× リージョン階層の料金(us-central1)

スタンダード ティア: 5 GB × $0.054 GB/時間 × 730 時間/月 = $197
ベーシック ティア: 5 GB × $0.027 GB/時間 × 730 時間/月 = $99
サーバーレス VPC アクセス 費用 = マシンサイズの料金 × インスタンス数(最小インスタンス数はデフォルトで 2)

f1-micro: $3.88 × 2 インスタンス = $7.76
e2- micro: $6.11 × 2 インスタンス = $12.22
e2-standard-4: $97.83 × 2 インスタンス = $195.66
Cloud Run 費用 = Cloud Run サービス リソース利用時間 × 料金 × インスタンス数

メモリ: 0.5 GiB × $0.00000250 / GiB-s × 60 秒/分 × 60 分/時間 × 8 時間 × 30.5 日 × 4 インスタンス = $4.39†
CPU: 1 vCPU × $0.00002400 / vCPU-s × 60 秒/分 × 60 分/時間 × 8 時間 × 30.5 日 × 4 インスタンス = $84.33†
リクエスト: $0.40 / 100 万 ~= $0
ネットワーキング: $0.085/GB/月 ~= $0
合計 $197 + $12.22 + $89 ~= $298/月

† シナリオの詳細については、以下の説明をご覧ください。

このチュートリアルでは、スタンドアロンのベーシック ティアの Redis インスタンスを使用します。サービス階層をスタンダードにアップグレードすると、高可用性を実現するためにクロスゾーン レプリケーションと自動フェイルオーバーが自動的に有効になります。リージョンと容量も Redis の料金に影響します。たとえば、アイオワ(us-central1)のスタンダード ティア 5 GB インスタンスは 1 時間あたり $0.054/GB です。時間あたりの費用は 5 × $0.054 なので、1 時間につき約 $0.27、つまり月額 $197 ということになります。Memorystore インスタンスの初期サイズを決定して、Redis の料金の詳細を確認してください。

サーバーレス VPC アクセス コネクタは、インスタンスのサイズと数、下り(外向き)ネットワークによって課金されます。サイズと数を大きくすると、スループットが向上し、メッセージのレイテンシが短縮されます。マシンには、f1-micro、e2-micro、e2-standard-4 の 3 つのサイズがあります。インスタンスの最小数は 2 であるため、最小コストはマシンサイズの 2 倍になります。

Cloud Run の料金は、メモリ、CPU、リクエスト数、ネットワーキングなどのリソースの使用量に応じて課金され、100 ミリ秒単位で切り上げて計算されます。このチュートリアルでは、Cloud Run のデフォルトの設定である 512 MiB と 1 vCPU を使用します。各費用は、それぞれ 0.5 GiB × $0.00000250 / GiB 秒、1 vCPU × $0.00002400 / vCPU 秒、またはインスタンスあたり合計 $0.091/時間です。制限事項や推奨事項は言語によって異なりますが、同時実行は合計費用に大きく影響します。同時実行数を増やすと、必要なコンテナ インスタンスの数が減ることがあります。Cloud Run の同時実行リクエストは最大 250 件です。最大 1,000 個のコンテナ インスタンス(同時リクエスト 250 個)で、最大 25 万個のクライアントに対応できます。たとえば、250 の同時実行で 1,000 人の従業員に対応するサービスの場合、少なくとも 4 つのインスタンスが必要になります。1 日 8 時間で 1 か月にわたり 4 つのインスタンスを使用する場合、$0.091 × 4 インスタンス × 8 時間 × 30.5 日 = $89 になります。リクエスト数とネットワーキングは追加コストが発生しますが、おそらく最小限になります。

このアイオワ(us-central1)でホストされているチャット サービスの場合、5 GB の標準 Redis インスタンスのコストが $197、デフォルトのサーバーレス VPC アクセス コネクタのコストが $12.22、Cloud Run サービスの見積もりコストが $89、月額合計が $298 となりました。Google Cloud 料金計算ツールで概算費用を確認します。

クリーンアップ

このチュートリアル用に新規プロジェクトを作成した場合は、そのプロジェクトを削除します。既存のプロジェクトを使用し、このチュートリアルで変更を加えずに残す場合は、チュートリアル用に作成したリソースを削除します。

プロジェクトを削除する

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

チュートリアル リソースを削除する

  1. このチュートリアルでデプロイした Cloud Run サービスを削除します。

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME は、選択したサービス名です。

    Cloud Run サービスは Google Cloud Console から削除することもできます。

  2. チュートリアルの設定時に追加した gcloud のデフォルト リージョン構成を削除します。

     gcloud config unset run/region
    
  3. プロジェクト構成を削除します。

     gcloud config unset project
    
  4. このチュートリアルで作成した他の Google Cloud リソースを削除します。

次のステップ