ASMを用いた双方向通信アプリケーション（WebSocketで構築）のエンドツーエンド・カナリアリリース

> 本記事は[こちらのブログ](https://www.alibabacloud.com/blog/end-to-end-canary-release-based-on-asm-for-bidirectional-communication-applications-built-with-websocket_601933?spm=a2c65.11461433.0.0.4ee35355YuBnwJ)を参考にしています。
> 翻訳にはアリババクラウドの[ModelStudio](https://www.alibabacloud.com/en/product/modelstudio)([Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen))を使用しております。

<div class="wrap-main-left-article markdown-body">
# トラフィックレーンの概要
著者: Hang Yin

トラフィックレーンは、クラウドネイティブアプリケーション内の複数のサービスをバージョンや他の特徴に基づいて複数の独立した実行環境に分離する機能です。この分離は、<a href="https://www.alibabacloud.com/product/servicemesh?spm=a3c0i.14089169.6791778070.434.1e6a78acKp6o4C" target="_blank">Alibaba Cloud Service Mesh</a> (ASM) によってサービス間の東西トラフィックの細粒度な制御を通じて達成されます。トラフィックレーンは、エンドツーエンドのカナリーリリースやアプリケーション開発とテスト用の隔離された環境の構築などのシナリオでよく使用されます。現在、パーミッシブモードのトラフィックレーンは主にHTTPプロトコルで通信するサービスをサポートしています。アプリケーションのコンテキスト通過が適切に設定されている場合、パーミッシブモードのトラフィックレーンはWebSocketで構築された双方向通信アプリケーションにも適用できます。この記事では、WebSocketで構築された双方向通信アプリケーションに対してパーミッシブモードのトラフィックレーンを実装し、エンドツーエンドのカナリーリリース効果を達成する方法について説明します。

## 概要
<a href="https://www.alibabacloud.com/blog/manage-end-to-end-traffic-based-on-alibaba-cloud-service-mesh-1-traffic-lanes-in-strict-mode_600777" target="_blank">Manage End-to-end Traffic Based on Alibaba Cloud Service Mesh (ASM): Traffic Lanes in Strict Mode</a> および <a href="https://www.alibabacloud.com/blog/manage-end-to-end-traffic-based-on-alibaba-cloud-service-mesh-asm-traffic-lanes-in-loose-mode_600788" target="_blank">Manage End-to-end Traffic Based on Alibaba Cloud Service Mesh (ASM): Traffic Lanes in Permissive Mode</a> で、レーンの概念、エンドツーエンドのカナリーリリース管理にレーンを使用するソリューション、およびASMの厳格モードとパーミッシブモードのトラフィックレーンについて紹介しました。トラフィックレーンは、クラウドネイティブアプリケーション内の複数のサービスをサービスバージョンや他の特徴に基づいて複数の独立した実行環境に分離します。
- 厳格モードでは、各トラフィックレーンには関連する呼び出しチェーン内のすべてのサービスが含まれ、アプリケーション自体には要求はありません。
- パーミッシブモードでは、関連する呼び出しチェーン内のすべてのサービスを含むベースラインレーンを作成するだけで十分です。他のレーンは関連する呼び出しチェーン内のすべてのサービスを含む必要はありません。あるレーン内のサービスが互いに呼び出す場合、呼び出されるサービスがそのレーンに存在しない場合、要求はベースラインレーンの同じサービスに転送されます。呼び出されるサービスがレーン内で利用可能になると、要求は再度そのレーンに転送されます。

パーミッシブモードのトラフィックレーンは柔軟なエンドツーエンドのカナリーリリースを達成できますが、アプリケーションが全呼び出しチェーン（E2E通過要求ヘッダー）を通過できる要求ヘッダーを含むことを要求します。WebSocketはHTTPプロトコルを介して通信するため、接続確立時にHTTP要求ヘッダーなどのメタデータを追加することも可能です。Socket.ioは高性能なリアルタイム双方向通信フレームワークであり、クライアントとサーバー間での効率的かつ低遅延のデータ交換を可能にします。この記事では、Socket.ioで開発されたサンプルサービスを例に、このようなアプリケーションに呼び出しチェーンがある場合に、パーミッシブモードのトラフィックレーンを通じてWebSocketアプリケーションにエンドツーエンドのカナリーリリース機能を導入する方法を示します。

## シナリオの説明
この記事では、シンプルな呼び出しチェーンを持つSocket.ioで構築されたWebSocketアプリケーションを示し、そのアプリケーションに対してパーミッシブモードのトラフィックレーンを作成して基本的なエンドツーエンドのカナリーリリース効果を達成する方法を示します。アプリケーションのトポロジーは以下の通りです：
! [1](https://yqintl.alicdn.com/34135c8e240d4b985b45c31ddc63daeafa95c7bc.jpeg)

アプリケーションの主要なロジックは以下の通りです：`client_socket` はクラスター外のクライアントで、ASMゲートウェイを介してクラスター内の `wsk` サービスとSocket.ioで通信します。`wsk` サービスが接続されている `client_socket` クライアントにメッセージを送信するとき、クラスター内の `helloworld` HTTPサービスに対してHTTP GET要求を行い、`helloworld` からの応答をメッセージとして `client_socket` に送信します。図に示すように、`helloworld` には現在、バックエンドに2つのバージョンのワークロード（`helloworld-v1` と `helloworld-v2`）がデプロイされています。この例では、期待される効果は、`wsk` サービスが接続を確立するときに `client_socket` から提供される要求ヘッダー情報（バージョン要求ヘッダー）に基づいて、後続の `wsk` 要求の具体的なワークロードバージョンを決定することです。これは典型的でシンプルなエンドツーエンドのカナリーリリースシナリオです：アプリケーションのv2がリリースされるとき、呼び出しチェーンの末端にある `helloworld` アプリケーションのみがリリースされます。この場合、`wsk` サービスは単一のバージョン1のままであり、クライアントが接続を開始するときに提供する要求ヘッダーメタデータに基づいて、呼び出す `helloworld` のターゲットバージョンを決定します。

## 必要条件
- エンタープライズエディションまたはアルティメットエディションのASMインスタンスが作成され、インスタンスのバージョンが1.18.2.111以降である。詳細については、<a href="https://www.alibabacloud.com/help/en/asm/getting-started/create-an-asm-instance#task-2370657" target="_blank">ASMインスタンスの作成</a>を参照してください。
- ACKクラスターがASMインスタンスに追加されている。詳細については、<a href="https://www.alibabacloud.com/help/en/asm/getting-started/add-a-cluster-to-an-asm-instance-1#task-2372122" target="_blank">ASMインスタンスへのクラスターの追加</a>を参照してください。
- 名前が `ingressgateway` のASMゲートウェイが作成され、ポート30080が作成されている。詳細については、<a href="https://www.alibabacloud.com/help/en/asm/user-guide/create-an-ingress-gateway#task-2372970" target="_blank">イングレスゲートウェイの作成</a>を参照してください。
- `istio-system` 名前空間に名前が `ws-gateway` のIstioゲートウェイが作成されている。詳細については、<a href="https://www.alibabacloud.com/help/en/asm/user-guide/manage-istio-gateways" target="_blank">Istioゲートウェイの管理</a>を参照してください。
- デフォルト名前空間での自動サイドカー・プロキシ注入が有効になっている。詳細については、<a href="https://www.alibabacloud.com/help/en/asm/user-guide/configuring-a-sidecar-injection-policy#task-1962690" target="_blank">自動サイドカー・プロキシ注入の有効化</a>を参照してください。
</div>
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: ws-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
    - hosts:
        - *
      port:
        name: http
        number: 30080
        protocol: HTTP

## ステップ1: サンプルアプリケーションのデプロイ
kubectl を使用して ACK クラスターに接続し、次のコマンドを実行してサンプルアプリケーションをデプロイします。
kuebctl apply -f- <<EOF
apiVersion: v1
kind: Service
metadata:
  name: wsk-svc
spec:
  selector:
    app: wsk
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10
  ports:
    - protocol: TCP
      port: 5000
      targetPort: 5000
      name: http-ws
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wsk-deploy
  labels:
    app: wsk
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wsk
      version: v1
  template:
    metadata:
      labels:
        app: wsk
        track: stable
        version: v1
      annotations:
        instrumentation.opentelemetry.io/inject-nodejs: true
        instrumentation.opentelemetry.io/container-names: websocket-base
    spec:
      containers:
        - name: websocket-base
          image: registry-cn-hangzhou.ack.aliyuncs.com/dev/asm-socketio-sample:669297ea
          imagePullPolicy: Always
          ports:
          - name: websocket
            containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
  name: helloworld
  labels:
    app: helloworld
spec:
  ports:
  - port: 5000
    name: http
  selector:
    app: helloworld
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: helloworld
  labels:
    account: helloworld
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-v1
  labels: 
    apps: helloworld
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v1
  template:
    metadata:
      labels:
        app: helloworld
        version: v1
    spec:
      serviceAccount: helloworld
      serviceAccountName: helloworld
      containers:
      - name: helloworld
        image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/examples-helloworld-v1:1.0
        imagePullPolicy: IfNotPresent 
        ports:
        - containerPort: 5000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld-v2
  labels: 
    apps: helloworld
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: helloworld
      version: v2
  template:
    metadata:
      labels:
        app: helloworld
        version: v2
    spec:
      serviceAccount: helloworld
      serviceAccountName: helloworld
      containers:
      - name: helloworld
        image: registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/examples-helloworld-v2:1.0
        imagePullPolicy: IfNotPresent 
        ports:
        - containerPort: 5000
EOF

helloworld サービスの v1 と v2 がクラスターにデプロイされます。このサービスは /hello エンドポイントへの HTTP GET リクエストに応答し、そのバージョン情報を返します。wsk サービスの v1 もデプロイされます。これはサンプルの Socket.io サービスで、クライアントからのメッセージに応答し、helloworld サービスを呼び出してその出力をクライアントに返します。コードは以下の通りです。javascript
import os from os;
import fetch from node-fetch;
import http from http;
import socketio from socket.io;

const ifaces = os.networkInterfaces();
const makeRequest = async (url, baggage) => {
  try {
    let headers = {}
    if (! !baggage) {
      headers[baggage] = baggage
    }
    const response = await fetch(url, {
      headers
    });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.text(); // or response.json() if you need JSON-formatted data
    return data;
  } catch (error) {
    console.error('Error fetching the data:', error);
    return error;
  }
}

const privateIp = (() => {
  return Object.values(ifaces).flat().find(val => {
    return (val.family == 'IPv4' && val.internal == false);
  }).address;
})();

const randomOffset = Math.floor(Math.random() * 10);
const intervalOffset = (30 + randomOffset) * Math.pow(10, 3);

// WebSocket Server
const socketPort = 5000;
const socketServer = http.createServer();
const io = socketio(socketServer, {
  path: '/'
});

// Handlers
io.on('connection', client => {
  console.log('New incoming Connection from', client.id);
  client.on('test000', async (message) => {
    console.log('Message from the client:', client.id, '->', message);
    const response = await makeRequest('http://helloworld:5000/hello', client.handshake.headers['baggage']);
    client.emit('hello', response, resp => {
      console.log('Response from the client:', client.id, '->', resp);
    })
  })
});

const emitOk = async () => {
  const response = await makeRequest('http://helloworld:5000/hello');
  let log0 = `I am the host: ${privateIp}. I am healty. Hello message: ${response}`;
  console.log(log0);
  io.emit('okok', log0);
}

setInterval(() => {
  emitOk();
}, intervalOffset);

// Web Socket listen
socketServer.listen(socketPort);

このコードは、クライアントからメッセージが送信されたときに helloworld サービスを呼び出すようにアプリケーションが構成されていることを示しています。この場合、接続確立時にクライアントによって提供された baggage リクエストヘッダーを読み取り、それを helloworld サービスを呼び出す HTTP リクエストに渡します。これにより、Socket.io アプリケーションは呼び出しチェーンでのコンテキストパススルーを実装します。このコンテキストを使用して、許容モードでトラフィックレーンを構成できます。

### Baggageとは？
Baggageは、OpenTelemetryによって開発された標準化されたメカニズムであり、分散システムの呼び出しチェーンのプロセス間でコンテキスト情報を伝達するためのものです。HTTPヘッダーに「Baggage」という名前のHTTPヘッダーを追加できます。Baggageヘッダーの値はキーと値のペア形式です。Baggageヘッダーを使用して、テナントID、トレースID、セキュリティ資格情報などのコンテキストデータを転送できます。例：
baggage: userId=alice,serverNode=DF%2028,isProduction=false

Baggageは、OpenTelemetryコミュニティによって提案された呼び出しチェーンでのコンテキストパススルーの標準化されたメカニズムです。したがって、このメカニズムに基づいて許容モードでトラフィックレーンを構成することも推奨します。次に、ゲートウェイ上に仮想サービスをデプロイして、トラフィックを wsk サービスにリダイレクトし、次のコマンドを実行します。
kubectl apply -f- <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: ws
  namespace: default
spec:
  gateways:
    - default/ws-gateway
  hosts:
    - *
  http:
    - route:
        - destination:
            host: wsk-svc
            port:
              number: 5000
EOF

この仮想サービスは、ゲートウェイ上でルーティングルールを宣言します。この仮想サービスを通じて、ASM ゲートウェイはクライアントからの Socket.io リク
# Swimlane Services

Kubernetes Clustersドロップダウンリストから目的のACKクラスターを選択し、Namespaceドロップダウンリストからdefaultを選択します。次に、下のリストからwsk-svcとhelloworldサービスを選択し、![icon](https://yqintl.alicdn.com/3d9614efbce3e3d0d310b7b8dfc424e88665ce89.png)をクリックして、選択したセクションにサービスを追加します。 ![2](https://yqintl.alicdn.com/0b2872272b1df86519fcae4e2b69dd59689e5f0c.png)

次に、レーングループ内のv1とv2のレーンを作成して、サービスのv1とv2に対応させます。
- トラフィックレーンページのトラフィックルール定義セクションで、「スウィムレーンを作成」をクリックします。
- 「スウィムレーンを作成」ダイアログボックスで、必要なパラメータを設定し、「OK」をクリックします。 ![3](https://yqintl.alicdn.com/50b099b324cd8387b6ae30b777cb96cde014f278.png)

v1レーンにはwsk-svcとhelloworldサービスが含まれており、v2レーンにはhelloworldサービスのみが含まれています（許容モードのトラフィックレーンでは、レーンがすべてのサービスを含まないことが許可され、wskサービスのv1がサービスのベースラインバージョンとして機能します）。

## ステップ3: レーンのパフォーマンスをテストする

次の内容を持つclient-socket.jsファイルを作成します（ASMゲートウェイのIPアドレスを実際のIPアドレスに置き換えてください）：javascript
import uuid from 'uuid';
import io from 'socket.io-client';

const client = io('http://{ASMゲートウェイのIPアドレス}:30080', {
  reconnection: true,
  reconnectionDelay: 500,
  transports: ['websocket'],
  extraHeaders: {
    version: 'v2'
  }
});

const clientId = uuid.v4();
let disconnectTimer;

client.on('connect', function() {
  console.log('Connected! ', clientId);
  setTimeout(function() {
    console.log('Sending first message');
    client.emit('test000', clientId);
  }, 500);
  // disconnection timeout clear
  clearTimeout(disconnectTimer);
});

client.on('okok', function(message) {
  console.log('The server has a message for you:', message);
});

client.on('hello', (arg, callback) => {
  console.log('the server respond to your test000: ' + arg);
  callback('got it');
});

client.on('disconnect', function() {
  console.log('Disconnected! ');
  disconnectTimer = setTimeout(function() {
    console.log('Not reconnecting in 30s. Exiting...');
    process.exit(0);
  }, 10000);
});

client.on('error', function(err) {
  console.error(err);
  process.exit(1);
});

setInterval(function() {
  console.log('Sending repeated message');
  client.emit('test000', clientId);
}, 5000);

client_socket.jsを実行して、双方向通信の結果を確認します。期待される結果は以下の通りです：plaintext
❯ node client_socket.js
Connected! ffcff9b2-2e41-4334-b64c-60512bdb7c7a
Sending first message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk
The server has a message for you: I am the host: 192.168.1.60. I am healty. Hello message: Hello version: v1, instance: helloworld-v1-978dbcf9-vsvw8
Sending repeated message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk
Sending repeated message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk
Sending repeated message
the server respond to your test000: Hello version: v2, instance: helloworld-v2-7b94944d69-wcckk

期待される結果は、サーバーからの各レスポンスがクライアントに対してv2のhelloworldサービスを呼び出していることを示しています。これは、クライアントが接続を開始する際にversion: v2という追加情報を使用しているためです。wskを通じてhelloworldへのリクエストを送信するときにリクエストコンテキストをコールチェーン上に復元することで、ASMはリクエストを正しいバージョンのhelloworldサービスにルーティングし、エンドツーエンドのカナリアリリースを達成します。特定のクライアントへの応答だけでなく、この例ではサーバーからすべてのクライアントへのメッセージブロードキャストも実装されています。クライアントのログで定期的に以下のような内容を見ることができます：plaintext
The server has a message for you: I am the host: 192.168.1.60. I am healty. Hello message: Hello version: v1, instance: helloworld-v1-978dbcf9-vsvw8

このメッセージは、サーバーからクライアントへのブロードキャストメッセージで、サーバーに接続されているすべてのクライアントが定期的に受信します。ブロードキャストメッセージは実際にはhelloworldサービスのv1を呼び出します。これは、ブロードキャストメッセージが特定のクライアントコンテキストを持たないため、helloworldのベースラインバージョン（つまりv1）が呼ばれるからです。

### FAQ: OpenTelemetry Auto-instrumentationを使用しない理由

自動インストルメンテーションについて学ぶには、<a href="https://opentelemetry.io/docs/kubernetes/operator/automatic/" target="_blank">Injecting Auto-instrumentation</a>を参照してください。この機能は、OpenTelemetry Operatorを使用してアプリケーションに自動インストルメンテーション機能を注入し、アプリケーションコードを変更せずにトレースIDリクエストヘッダーのパススルーを実現します。自動インストルメンテーションを実装するには、前述のコミュニティドキュメントに従ってOpenTelemetry Operatorをインストールし、自動インストルメンテーションを構成し、アプリケーションポッドに注釈を追加する必要があります。OpenTelemetry自動インストルメンテーションは、W3C BaggageやB3など、複数の一般的な分散コールチェーンコンテキストパススルースタンダードをサポートしています。

Socket.ioの場合、自動インストルメンテーションの意味合いが少し異なります。Socket.ioはHTTP（WebSocket）に基づいていますが、主にサービス間の双方向通信を実装しており、クライアントとサーバーはリアルタイムでメッセージを送受信できます。そのため、コミュニティによって実装されたSocket.ioの自動インストルメンテーションは、実際にはメッセージレベルで動作します。HTTPレベルでは、クライアントとサーバー間の接続は大きなHTTPリクエストであり、主に接続時にクライアントによって提供される追加リクエストヘッダーに基づいてコールチェーンコンテキストを決定する必要があります。このシナリオでは、コミュニティの自動インストルメンテーションによって提供されるコンテキストパススルーキャパビリティは限られた意味しかなく、コードの若干の修正が必要となります。詳細については、コミュニティの記事「<a href="https://dev.to/aspecto/instrument-your-node-js-socket-io-apps-with-opentelemetry-like-a-pro-d5p" target="_blank">Instrument Your Node.js Socket.io Apps with OpenTelemetry Like a PRO</a>」を参照してください。</div>