LoginSignup
1
0

Azure Container Apps で Dapr と Application Insights を使って分散トレーシング

Last updated at Posted at 2024-04-04

本記事について

Azure Container Apps では、サイドカーコンテナーとして展開される Dapr を使って、サービス間呼び出し、Pub/Sub, 状態管理、シークレット管理などを使用することができます。

この Dapr の機能のひとつとして、Observability (可観測性) があり、トレース情報を取得することができます。本記事では、その機能を使って Container Apps 上のトレース情報を Application Insights に送信し、分散トレーシングをどう行っていけるかを見ていきます。

本記事で用いるサンプルアプリケーションについて

本記事では、下記のサンプルアプリケーションに少し変更を加えて利用します。

下の絵の形で、コンテナーアプリを3つ用意します。checkout アプリはサンプルアプリケーションをほぼそのまま利用し order を発行する役割のコンソールアプリ、order-processor は checkout から order を受け取るアプリです。さらに、その order-processor アプリをベースに receipt アプリを追加し、order-processor から order 情報を受け取り、その情報を Blob Storage にファイルをアップロードします。

image.png

Azure Container Apps の構成

Container Apps で Application Insights を利用して、Dapr からのトレース情報を受け取るには、Container Apps 環境の作成時に Application Insights 利用を有効化する必要があります。これは、Azure Portal での作成では設定できないため、CLI コマンドか、ARM Template, Bicep を利用します。

CLI コマンドは下記になります。詳細は公式のリファレンスをご参照ください。

Azure CLI コマンド
az containerapp env create --name <Container Apps 環境名> --resource-group <リソースグループ名> --location <リージョン名> --dapr-instrumentation-key <利用する Application Insights のインストルメンテーションキー>

各アプリの app.py のコード

各コンテナーアプリの app.py コードを下記のように準備します。Docker ファイルはサンプルのものをそのまま利用します。(receipt アプリは、order-processor アプリのコードをベースにし、少し変更しています。)

checkout

app.py (checkout)
import json
import time
import logging
import requests
import os

base_url = os.getenv('BASE_URL', 'http://localhost') + ':' + os.getenv(
                    'DAPR_HTTP_PORT', '3500')
dapr_app_id = "<order-processor のコンテナーアプリの Dapr アプリ ID>"
content_type = "application/json"

while(True):
  for i in range(1, 20):
    order = {'orderId': i}
    headers = {
        'dapr-app-id': dapr_app_id, 
        'content-type': content_type
    }

    # Invoking a service
    result = requests.post(
        url='%s/orders' % (base_url),
        data=json.dumps(order),
        headers=headers
    )

    print(str(result.json()))
    print('Order passed: ' + json.dumps(order), flush=True)

    time.sleep(10)
  time.sleep(20)

order-processor

app.py (order-processor)

from flask import Flask, request
import json
import os
import requests

app = Flask(__name__)

base_url = os.getenv('BASE_URL', 'http://localhost') + ':' + os.getenv(
                    'DAPR_HTTP_PORT', '3500') 
dapr_app_id = "<receipt のコンテナーアプリの Dapr アプリ ID"
content_type = "application/json"

@app.route('/orders', methods=['POST'])
def getOrder():
    order = request.json
    print('Order received : ' + json.dumps(order), flush=True)
    
    traceparent = request.headers.get('traceparent')
    headers = {
        'dapr-app-id': dapr_app_id, 
        'content-type': content_type,
        'traceparent': traceparent
    }

    # Invoking a service
    result = requests.post(
        url='%s/orders' % (base_url),
        data=json.dumps(order),
        headers=headers
    )

    print(f"Request was sent to receipt: result = {result}", flush=True)

    return json.dumps({'success': True}), 200, {
        'ContentType': 'application/json'}
   
app.run(port=8001, host="0.0.0.0")

receipt

app.py (receipt)

from flask import Flask, request
import json
import requests
from azure.storage.blob import BlobServiceClient
import time

app = Flask(__name__)

account_url = "<ファイルの送信先のストレージアカウントの URL>"
shared_access_key = "<ストレージアカウントのキー>"
credential = shared_access_key
container_name = "<ストレージアカウントのBlob コンテナー名>"

@app.route('/orders', methods=['POST'])
def getOrder():
    order = request.json
    print('Order received : ' + json.dumps(order), flush=True)

    ut = time.time()
    filename = f"order-{ut}.json"

    blob_service_client = BlobServiceClient(account_url, credential=credential)
    container_client = blob_service_client.get_container_client(container=container_name)

    blob_client = container_client.get_blob_client(filename)
    blob_client.upload_blob(data=json.dumps(order), overwrite=True)

    return json.dumps({'success': True}), 200, {
        'ContentType': 'application/json'}
   
app.run(port=8001, host="0.0.0.0")

上記コードの Application Insights 利用時のポイント

Dapr を介して HTTP によるサービス間通信を行うアーキテクチャで分散トレーシングを行うのに重要なのが、HTTP ヘッダーの traceparent を介してトレース情報をやりとりすることです。traceparent の値は、W3C の標準に基づいており、<Version>-<Trace ID (16進数表記)>-<Parent ID (16進数表記)>-<Trace Flags> となっています。

Container Apps の Dapr では、Application Insights を利用する際に、Container Apps の Dapr が自動的に traceparent の値を付与します。ただ、前のサービスが利用した traceparent の値を明示的に次の HTTP リクエストのヘッダーに与えることで、同一のオペレーションとして、Application Insights が認識できるようになります。

When a request arrives without a trace ID, Dapr creates a new one. Otherwise, it passes the trace ID along the call chain.

そのため、今回は order-processor アプリは、checkout アプリからの Dapr 経由で渡された traceparent の値をそのまま次の receipt アプリへの HTTP リクエストのヘッダーに追記しています。絵で表現すると下記のようなイメージです。

image.png

こうしたときに、Application Insights でどうトレース情報を確認できるか、確認していきます。

アプリの Container Apps での実行

サンプルアプリケーションのフォルダーに含まれている Dockerfile を使って、それぞれコンテナーイメージを作成し、Azure Container Registry にプッシュします。そして、Application Insights 連携を有効化した、ひとつの Container Apps 環境に3つのコンテナーアプリをそれぞれデプロイします。

コンテナーアプリのデプロイ後は、各 コンテナーアプリで Dapr を有効化し Dapr アプリ ID を設定します。この値は各アプリ内で利用されるものになります。

Application Insights でのトレース情報の確認

Application Insights では、下記のように情報を見ることができます。

アプリケーションマップ

アプリケーションマップでは、3つのコンテナーアプリがそれぞれ通信を行っていることがビジュアルに確認できます

image.png

エンドツーエンドトランザクション

今回、checkout → order-processor と order-processor → receipt をひとつのオペレーションとして捉えたいというのが課題でした。そして、traceparent の値を伝搬させた結果、下記のようにトランザクション内で両通信を確認することができます。

image.png

ログとして確認しても、同じ operation_Id (OpenTelemetry における trace ID) の中に、それぞれの記録が入っていることが分かります。

image.png

Application Insights でトレースのデータをどう扱えるかについては、下記 Qiita 記事にまとめていますので併せてご参照ください。

Appendix: Dapr 以外の情報を Application Insights のトレースに含めたい場合の実装について

実際にトレースをする際に、Dapr 以外の情報もトレースに含めたいケースがあるかと思います。本記事の Appendix では、以下の2ケースについて実装の方法を見ていきます。

① receipt アプリの Blob Storage へのファイルアップロードの情報をトレースしたい

Application Insights の SDK を Python コードに埋め込むことで、Blob Storage へのアップロードをトレース情報として取得することができます。この際に、Trace ID を Dapr で利用していたものを使うことで同一のトランザクションとして扱えるようになります。全体像は以下のイメージです。

image.png

実装は下記になります。まず、以下のモジュールを receipt アプリの app.py でインポートします。

app.py (receipt-blob)
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry import trace
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

次に、receipt アプリの app = Flask(__name__) の下あたりに configure_azure_monitor()tracer = trace.get_tracer(__name__) を追加し、Application Insights へ接続と trace の起動を行います。

app.py (receipt-blob)
configure_azure_monitor(
    connection_string="<Application Insights のインストルメンテーションキー>",
)
tracer = trace.get_tracer(__name__)

Application Insights と tracer については、下記もご覧ください。

そして、def getOrder() 直下に下記コードを配置し、Dapr の使っていた Trace ID を使ったスパンをひとつ作成します。

app.py (receipt-blob)
    traceparent = request.headers.get('traceparent')
    carrier = {
        'traceparent': traceparent
    }
    ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
    with tracer.start_as_current_span('receipt-blob', context=ctx) as span:

Trace ID の取得や、指定した Trace ID による span の作成についての詳細は下記をご参照ください。

また、Dockerfile に下記の行を追加し、OpenTelemetry のサービス名の環境変数を設定し、Dapr と SDK が同一の名前を使うようにしておきます。

ENV OTEL_SERVICE_NAME="<receiptアプリのアプリ名>"

このようにコードを変更し、再度 Container Apps にアプリをデプロイします。そして実行結果を Application Insights のトランザクションで確認すると、下記のようになります。

image.png

またログで見ると下記になり、こちらでもすべてが同一の operation_Id (Trace ID) に紐づいていることが分かります。

image.png

副次的ですが嬉しい内容として、アプリケーションマップにも、Blob Storage がノードとして表示されるようにもなります。

image.png

② checkout アプリ側でトレースを起動したい

一方で、今回のアプリケーションでは、checkout アプリから処理が開始しています。そのため、checkpoint アプリでの order 作成前に trace の span を作成したくなるかもしれません。その場合、checkpoint アプリ側で tracer を起動し、その Trace ID や Span ID を traceparent ヘッダーを介して Dapr に渡すことで、実行することができます。

以下のようにコードを修正します。

モジュールの追加と tracer の起動

app.py (checkpoint)
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry import trace

configure_azure_monitor(
    connection_string="<Application Insights のインストルメンテーションキー>",
)
tracer = trace.get_tracer(__name__)

span の作成とヘッダーの変更

app.py (checkpoint)
dapr_app_id = "<order-processor アプリの Dapr アプリ ID>"
content_type = "application/json"
traceparent_version = "00"
traceparent_trace_flags = "01"

while(True):
  for i in range(1, 20):
    order = {'orderId': i}
    
    span_name = f"order-{i}"
    with tracer.start_as_current_span(name=span_name) as span:
      span_context = span.get_span_context()
      trace_id = int(span_context.trace_id)
      span_id = int(span_context.span_id)
      trace_id_hex = hex(trace_id)
      span_id_hex = hex(span_id)
      trace_id_hex_value = trace_id_hex[2:]
      span_id_hex_value = span_id_hex[2:]
      traceparent = f"{traceparent_version}-{trace_id_hex_value}-{span_id_hex_value}-{traceparent_trace_flags}"
            
      headers = {
        'dapr-app-id': dapr_app_id, 
        'content-type': content_type,
        'traceparent': traceparent
      }

      # --- result = 以下は同じ

結果、Application Insights でトランザクション画面で、最初に自分で作成したスパンから捜査が始まっていることが確認できます。

image.png

最後に

*本稿は、個人の見解に基づいた内容であり、所属する会社の公式見解ではありません。また、いかなる保証を与えるものでもありません。正式な情報は、各製品の販売元にご確認ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0