unless’s blog

日々のちょっとした技術的なことの羅列

ECS上のGoアプリケーションでAWS X-RayとOpenTelemetryを使って分散トレーシングを実現する

マイクロサービスアーキテクチャを採用すると、リクエストが複数のサービスを横断することが一般的になり、パフォーマンスのボトルネック特定やエラー追跡が複雑になります。
この課題を解決するのが「分散トレーシング」です。

この記事では、Amazon ECS (Elastic Container Service) 上で動作するGoアプリケーションに、OpenTelemetry (Otel) を導入し、AWS X-Ray でトレースを可視化する具体的な方法を解説します。
OpenTelemetryを利用することで、特定のベンダーにロックインされることなく、標準化された方法でトレーシングを実現できるのが大きなメリットです。

アーキテクチャ概要

今回構築するシステムの構成は以下の通りです。

  • Go Application Container: OpenTelemetry SDK for Goを組み込み、リクエストのトレース情報を生成します。
  • AWS Otel Collector Sidecar Container: アプリケーションコンテナと同じタスク内で動作するサイドカーです。アプリケーションからOtel Protocol (OTLP) 経由でトレース情報を受け取ります。
  • AWS X-Ray: Otel Collectorがトレース情報をX-Rayフォーマットに変換し、AWS X-Rayサービスに送信します。これにより、トレースデータをコンソールで可視化・分析できます。

アプリケーションは localhost に対してトレースを送信するだけでよく、CollectorがAWSとの通信をすべて担ってくれるため、アプリケーションコードをシンプルに保てます。

ステップ1: AWS Otel Collectorサイドカーの設定

まず、ECSタスク定義にAWS Otel Collectorをサイドカーとして追加します。アプリケーションコンテナの定義に加えて、以下のコンテナ定義を追加してください。

ポイントは、環境変数 AOT_CONFIG_CONTENT を使ってCollectorの設定をインラインで記述している点です。これにより、設定ファイルを別途管理する必要がなくなり、タスク定義だけで完結します。

{
  "name": "aws-otel-collector",
  "image": "public.ecr.aws/aws-observability/aws-otel-collector:latest",
  "cpu": 32,
  "memory": 256,
  "essential": true,
  "portMappings": [
    {
      "containerPort": 4317,
      "protocol": "tcp"
    }
  ],
  "environment": [
    {
      "name": "AWS_REGION",
      "value": "ap-northeast-1"
    },
    {
      "name": "AOT_CONFIG_CONTENT",
      "value": "receivers:\n  otlp:\n    protocols:\n      grpc:\n        endpoint: 0.0.0.0:4317\n\nexporters:\n  awsxray:\n    region: ap-northeast-1\n\nservice:\n  pipelines:\n    traces:\n      receivers: [otlp]\n      exporters: [awsxray]"
    }
  ],
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": {
      "awslogs-group": "/ecs/my-app-log-group",
      "awslogs-region": "ap-northeast-1",
      "awslogs-stream-prefix": "otel-collector"
    }
  }
}

Collector設定の解説

  • receivers:
    • データの受信設定です。otlp レシーバーを使い、gRPCプロトコルで 0.0.0.0:4317 で待ち受けます。アプリケーションは localhost:4317 にデータを送信します。
  • exporters:
    • データの送信設定です。awsxray エクスポーターを使い、指定されたリージョン (ap-northeast-1) のX-Rayにデータを送信します。
  • service.pipelines:
    • receivers と exporters を繋ぐパイプラインを定義します。traces パイプラインで、otlp で受け取ったデータを awsxray に流します。

✅ 重要: このコンテナがX-Rayにデータを送信できるよう、ECSタスクロールに AWSXRayDaemonWriteAccess のIAMポリシーをアタッチするのを忘れないでください。

ステップ2: Goアプリケーション側のOpenTelemetry設定

次に、Goアプリケーション側でOpenTelemetry SDKをセットアップします。ここでは、Connectフレームワーク (connect-go) を利用している例で説明します。

トレーサープロバイダーの初期化

まず、トレース情報のエクスポーターやサービス名などのリソース情報を設定するトレーサープロバイダーを作成します。この処理を独立した trace パッケージにまとめておくと便利です。

package trace

import (
    "context"
    "time"

    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

const (
    // ここで設定したサービス名がX-Rayのサービスマップに表示される
    serviceName = "user-service" 
)

// NewTracerProvider は新しいTracerProviderを生成します
func NewTracerProvider(ctx context.Context, opts ...otlptracegrpc.Option) (*trace.TracerProvider, error) {
    // オプションが渡されない場合、トレーシングは無効 (No-op)
    if len(opts) == 0 {
       return trace.NewTracerProvider(), nil
    }

    // OTLP/gRPCエクスポーターを作成
    traceExporter, err := otlptracegrpc.New(ctx, opts...)
    if err != nil {
       return nil, err
    }

    // サービス名などのリソース情報を定義
    rsc, err := resource.New(ctx,
       resource.WithAttributes(
          semconv.ServiceNameKey.String(serviceName),
       ),
    )
    if err != nil {
       return nil, err
    }
    rsc, err = resource.Merge(resource.Default(), rsc)
    if err != nil {
       return nil, err
    }

    // トレーサープロバイダーを構成
    traceProvider := trace.NewTracerProvider(
       trace.WithBatcher(traceExporter,
          // デフォルトは5秒。デモのため1秒に設定
          trace.WithBatchTimeout(time.Second)),
       trace.WithResource(rsc),
    )
    return traceProvider, nil
}

アプリケーション起動時の設定

アプリケーションの起動時に、環境変数などから設定を読み込み、トレーサープロバイダーをセットアップしてグローバルに登録します。

環境変数 TRACE_ENDPOINT を使うことで、ローカル開発時などトレーシングが不要な場合に簡単に無効化できる設計になっています。
ECSで実行する際は、この環境変数localhost:4317 を設定します。

package main

import (
    "context"
    "net/http"

    "github.com/bufbuild/connect-go"
    "go.opentelemetry.io/contrib/connect-go/otelconnect"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"

    "your/app/internal/trace" // 先ほど作成したパッケージ
)

func main() {
    // ... (設定の読み込みなど)
    
    var traceOpts []otlptracegrpc.Option

    // 環境変数 `TRACE_ENDPOINT` が設定されている場合のみトレーシングを有効化
    // ECSではこの値に "localhost:4317" を設定する
    if cfg.Trace.Endpoint != "" {
        traceOpts = append(traceOpts, otlptracegrpc.WithEndpoint(cfg.Trace.Endpoint))
        // サイドカーへの通信はコンテナ内に閉じるため、暗号化は不要
        if cfg.Trace.Insecure {
           traceOpts = append(traceOpts, otlptracegrpc.WithInsecure())
        }
    }

    // トレーサープロバイダーを作成
    tp, err := trace.NewTracerProvider(context.Background(), traceOpts...)
    if err != nil {
        // ... (エラーハンドリング)
        panic(err)
    }

    // グローバルなトレーサープロバイダーとして設定
    otel.SetTracerProvider(tp)

    // Connect用のOtelインターセプターを作成
    otelInterceptor, err := otelconnect.NewInterceptor(
        otelconnect.WithTracerProvider(tp),
    )
    if err != nil {
        panic(err)
    }

    // ... (サーバー起動処理)
}

otelconnect.NewInterceptor を使うことで、手動でSpanを開始・終了するコードを書かなくても、ConnectのRPC呼び出しが自動的にトレースされるようになります。非常に便利ですね!

X-Rayでのトレース確認

アプリケーションをデプロイしてリクエストを送信すると、AWSマネジメントコンソールの X-Ray 画面でトレースが確認できるようになります。

  • サービスマップ: リクエストの経路やレイテンシ、エラー率などが視覚的にわかります。
  • トレース: 個々のリクエストの詳細なトレースを確認できます。リクエストがサービス内のどの処理にどれくらいの時間を要したかが、タイムライン(セグメント)で表示されます。

これで、パフォーマンスのボトルネックやエラーが発生した箇所を迅速に特定できます。

まとめ

OpenTelemetryとAWS Otel Collectorサイドカーを利用することで、Goアプリケーションに簡単かつ標準的な方法で分散トレーシングを導入できました。

要点:

  • Otel Collectorサイドカー: アプリケーションからトレース収集のロジックを分離します。
  • AOT_CONFIG_CONTENT: Collectorの設定を環境変数で完結させ、管理を簡素化します。
  • Otel SDK for Go: アプリケーション内でトレースを生成します。
  • Connect Interceptor: RPC呼び出しを自動で計装し、コードをクリーンに保ちます。

X-Rayを使いたいけど標準化されたOtelを使いたい場合はこのように設定すると実現できます。
ぜひあなたのアプリケーションにも導入して、オブザーバビリティを向上させてみてください!