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:
- 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を使いたい場合はこのように設定すると実現できます。
ぜひあなたのアプリケーションにも導入して、オブザーバビリティを向上させてみてください!