Kong + ECS Fargate + OpenTelemetry で分散トレーシングを試してみる

Kong + ECS Fargate + OpenTelemetry で分散トレーシングを試してみる

2025.12.19

ゲームソリューション部の えがわ です。

本ブログはKong Advent Calendar 2025の19日目のブログとなります。

https://qiita.com/advent-calendar/2025/kong

今回は、Kong GatewayのOpenTelemetryプラグインを使用した分散トレーシングを試してみます。
KongはTerraformでECS Fargateにデプロイできるようにし、New Relicでトレースを確認できるところまで実装します。

やりたいこと

マイクロサービスアーキテクチャでは、1つのリクエストが複数のサービスを経由します。
問題が発生した際、「どのサービスで」「どのくらい時間がかかったか」を把握することは困難です。

分散トレーシングを導入することで、リクエストの流れを一意のトレースIDで追跡し、サービス間の関係性とレイテンシを可視化できます。

今回は以下の構成で分散トレーシングを構築します。

  • Kong Gateway: API Gatewayでトレースを開始
  • AWS Fargate: Kong + バックエンドAPIをコンテナで実行
  • OpenTelemetry Collector: 各サービスからトレースを収集
  • New Relic: トレースを可視化・分析

アーキテクチャ

今回の構成図はこちらです。

分散トレーシングを実現するポイント

  • Kong OpenTelemetryプラグイン: API Gatewayでトレースを開始し、W3C Trace Context(traceparent)を下流に伝播
  • サイドカーパターン: 各ECSタスクにOTEL Collectorを配置、アプリはlocalhostにトレースを送信するだけ
  • アプリケーション計装: Node.jsアプリもOpenTelemetry SDKで計装し、トレースを継続
  • Kong Konnect(SaaS): Control Plane(CP)はKong Konnectで管理、Data Plane(DP)のみAWSにデプロイ

環境

  • Kong Konnect(Control Plane)
  • Kong Gateway 3.12(Data Plane)
  • AWS ECS Fargate
  • OpenTelemetry Collector
  • New Relic(トレース収集先)
  • Node.js
  • Terraform

本記事で使用するコードは以下リポジトリに保存しています。
https://github.com/egawa-takeki/kong-fargate-otel-newrelic

ディレクトリ構成

kong-fargate-otel-newrelic/
├── terraform/           # インフラ構成(Terraform)
├── app/
│   ├── dummy-api/       # フロントエンドAPI(Kong経由でアクセス)
│   └── downstream-api/  # バックエンドAPI(dummy-apiから呼び出し)
├── otel-collector/      # OTEL Collector設定
└── docs/                # ドキュメント

トレース連携の流れ

分散トレーシングがどのように動作するか、流れを確認しましょう。

1. Client → ALB → Kong
   Kong が traceparent を生成: 00-{trace_id}-{span_id}-01

2. Kong → dummy-api
   traceparent ヘッダーを付与してリクエスト転送

3. dummy-api → downstream-api
   同じ trace_id を引き継いで下流に伝播

4. 各サービス → OTEL Collector → New Relic
   同一 trace_id のスパンが New Relic で結合・可視化

W3C Trace Contextのtraceparentヘッダーが各サービス間で伝播され、New Relicで一連のリクエストとして可視化されます。

W3C Trace Context の伝播

Kongがtraceparentヘッダーを生成・付与し、下流サービスがこのヘッダーを受け取ることで、同一トレースとして認識されます。

traceparent: 00-{trace_id}-{span_id}-01
             │   │          │        │
             │   │          │        └── トレースフラグ
             │   │          └── 親スパンID(16文字)
             │   └── トレースID(32文字)
             └── バージョン

Node.js アプリケーションの計装

OpenTelemetry SDK の初期化

アプリケーションの起動前にトレーシングを初期化する必要があります。

app/dummy-api/src/tracing.js
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

// Configure OTLP exporter
const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
    ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`
    : 'http://localhost:4318/v1/traces',
});

// Create the SDK with automatic instrumentation
const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'dummy-api',
    [ATTR_SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter,
  instrumentations: [
    new HttpInstrumentation({
      requestHook: (span, request) => {
        span.setAttribute('http.request.header.traceparent',
          request.headers?.traceparent || 'none');
      },
    }),
    new ExpressInstrumentation(),
    new UndiciInstrumentation(), // fetch() API用(Node.js 18+)
  ],
});

// Start the SDK
sdk.start();

// Graceful shutdown
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) => console.error('Error terminating tracing', error))
    .finally(() => process.exit(0));
});

console.log('OpenTelemetry tracing initialized');

traceparent ヘッダーの自動伝播

instrumentation-undiciがfetch()呼び出し時に自動でtraceparent`ヘッダーを伝播します。
これはOpenTelemetry SDKの自動計装機能により、アプリケーションコードで明示的にヘッダーを設定することなく、トレースコンテキストが下流に引き継がれます。

app/dummy-api/src/index.js
// Downstream API URL (Service Discovery or environment variable)
const DOWNSTREAM_API_URL = process.env.DOWNSTREAM_API_URL
  || 'http://downstream-api.kong-otel-dev.local:3001';

// Chain endpoint - calls downstream-api to demonstrate distributed tracing
app.get('/api/chain', async (req, res) => {
  const tracer = trace.getTracer('dummy-api');

  tracer.startActiveSpan('chain-request', async (span) => {
    try {
      span.setAttribute('downstream.url', DOWNSTREAM_API_URL);

      // traceparentは instrumentation-undici により自動で付与される
      const downstreamResponse = await fetch(`${DOWNSTREAM_API_URL}/api/data`);
      const downstreamData = await downstreamResponse.json();

      span.setAttribute('downstream.status', 200);
      span.setAttribute('downstream.success', true);
      span.end();

      res.json({
        message: 'Chain request completed successfully',
        localData: { service: 'dummy-api', userCount: users.length },
        downstreamData: downstreamData,
      });
    } catch (error) {
      span.recordException(error);
      span.end();
      res.status(500).json({ error: 'Failed to call downstream service' });
    }
  });
});

OTEL Collector の設定

New Relic 向け設定

otel-collector/collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 50

exporters:
  otlphttp:
    endpoint: https://otlp.nr-data.net
    headers:
      api-key: ${NEW_RELIC_LICENSE_KEY}
  debug:
    verbosity: detailed

extensions:
  health_check:
    endpoint: 0.0.0.0:13133

service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp, debug]
  telemetry:
    logs:
      level: debug
項目 説明
receivers.otlp アプリケーションからトレースを受信(ポート4318)
processors.batch バッチ処理でネットワーク効率化
exporters.otlphttp New RelicへOTLP形式で送信
exporters.debug デバッグ用にログ出力
extensions.health_check ヘルスチェック用エンドポイント(ポート13133)

※New Relic UIのAPI KeysセクションでIngest - Licenseを選択し、表示されたキーをコピーしてください。

ECS タスク定義(Terraform)

Kong Data Plane タスク定義

terraform/ecs.tf
resource "aws_ecs_task_definition" "kong" {
  family                   = "${local.name_prefix}-kong"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.kong_task_cpu
  memory                   = var.kong_task_memory
  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name      = "kong"
      image     = "kong/kong-gateway:3.12"
      essential = true

      portMappings = [
        { containerPort = 8000, hostPort = 8000, protocol = "tcp" },
        { containerPort = 8100, hostPort = 8100, protocol = "tcp" }
      ]

      environment = [
        { name = "KONG_ROLE", value = "data_plane" },
        { name = "KONG_DATABASE", value = "off" },
        { name = "KONG_CLUSTER_MTLS", value = "pki" },
        { name = "KONG_CLUSTER_CONTROL_PLANE", value = var.kong_cluster_endpoint },
        { name = "KONG_TRACING_INSTRUMENTATIONS", value = "all" },
        { name = "KONG_TRACING_SAMPLING_RATE", value = "1.0" }
        # ... Kong Konnect 認証設定
      ]

      healthCheck = {
        command     = ["CMD-SHELL", "kong health"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
    },
    {
      name      = "otel-collector"
      image     = "${aws_ecr_repository.otel_collector.repository_url}:latest"
      essential = true

      portMappings = [
        { containerPort = 4318, hostPort = 4318, protocol = "tcp" }
      ]

      environment = [
        { name = "NEW_RELIC_LICENSE_KEY", value = var.new_relic_license_key }
      ]
    }
  ])
}

dummy-api タスク定義

terraform/ecs.tf
resource "aws_ecs_task_definition" "dummy_api" {
  family                   = "${local.name_prefix}-dummy-api"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.api_task_cpu
  memory                   = var.api_task_memory
  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name      = "dummy-api"
      image     = "${aws_ecr_repository.dummy_api.repository_url}:latest"
      essential = true

      portMappings = [
        { containerPort = 3000, hostPort = 3000, protocol = "tcp" }
      ]

      environment = [
        { name = "PORT", value = "3000" },
        { name = "OTEL_EXPORTER_OTLP_ENDPOINT", value = "http://localhost:4318" },
        { name = "OTEL_SERVICE_NAME", value = "dummy-api" },
        { name = "DOWNSTREAM_API_URL", value = "http://downstream-api.${local.name_prefix}.local:3001" }
      ]

      healthCheck = {
        command     = ["CMD-SHELL", "wget -q --spider http://localhost:3000/health || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 30
      }
    },
    {
      name      = "otel-collector"
      image     = "${aws_ecr_repository.otel_collector.repository_url}:latest"
      essential = true

      portMappings = [
        { containerPort = 4318, hostPort = 4318, protocol = "tcp" }
      ]

      environment = [
        { name = "NEW_RELIC_LICENSE_KEY", value = var.new_relic_license_key }
      ]
    }
  ])
}

ポイント

  • OTEL_EXPORTER_OTLP_ENDPOINT: http://localhost:4318 でサイドカーに送信
  • DOWNSTREAM_API_URL: Cloud MapのDNS名でサービス間通信
  • KONG_TRACING_INSTRUMENTATIONS: all で全てのリクエストをトレース

AWS Cloud Map(サービスディスカバリ)

設定

terraform/ecs.tf
resource "aws_service_discovery_private_dns_namespace" "main" {
  name        = "${local.name_prefix}.local"
  description = "Private DNS namespace for ${local.name_prefix}"
  vpc         = aws_vpc.main.id
}

resource "aws_service_discovery_service" "dummy_api" {
  name = "dummy-api"

  dns_config {
    namespace_id = aws_service_discovery_private_dns_namespace.main.id

    dns_records {
      ttl  = 10
      type = "A"
    }

    routing_policy = "MULTIVALUE"
  }

  health_check_custom_config {
    failure_threshold = 1
  }
}

ECSサービスとの連携

terraform/ecs.tf
resource "aws_ecs_service" "dummy_api" {
  name            = "${local.name_prefix}-dummy-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.dummy_api.arn
  # ...

  service_registries {
    registry_arn = aws_service_discovery_service.dummy_api.arn
  }
}

サービス間通信

サービス DNS名
kong kong.kong-otel-dev.local:8000
dummy-api dummy-api.kong-otel-dev.local:3000
downstream-api downstream-api.kong-otel-dev.local:3001

ECSサービスをservice_registriesに登録することで、タスクのIPアドレスが自動的にDNSに登録されます。

構築手順

Terraform apply前にKong Konnectの設定が必要なので、順番に進めていきます。

Kong Konnect で Control Plane を作成

Terraform applyの前に、Kong KonnectでData Plane用の認証情報を取得します。

Kong Konnect にログインし、API GatewayNew API Gateway をクリックします。
Self-managedを選択してGateway nameを入力して作成します。

kong-fargate-otel_06.png

PlatformでLinux(Docker)を選択してGenerate certificateをクリックします。
エンドポイントや証明書が表示されるので、控えておきます。

kong-fargate-otel_01.png

New Relic の準備

New Relic にログインし、API KeysIngest - License Key を取得します。

terraform.tfvars を設定

# リポジトリをクローン
git clone https://github.com/egawa-takeki/kong-fargate-otel-newrelic.git
cd kong-fargate-otel-newrelic/terraform

# 変数ファイルをコピー
cp terraform.tfvars.example terraform.tfvars

terraform.tfvars を編集し、Kong Konnectで取得した情報を次のように設定します。

terraform/terraform.tfvars
# Kong Konnect の設定
kong_cluster_endpoint   = "xxxxxxxx.us.cp0.konghq.com:443"
kong_telemetry_endpoint = "xxxxxxxx.us.tp0.konghq.com:443"

# 証明書(改行は \n でエスケープ)
kong_cluster_cert = "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----"
kong_cluster_cert_key = "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----"

# New Relic License Key
new_relic_license_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

ECR リポジトリを作成

terraform init
terraform apply -target=aws_ecr_repository.dummy_api \
                -target=aws_ecr_repository.downstream_api \
                -target=aws_ecr_repository.otel_collector

Docker イメージのビルドとプッシュ

# AWS にログイン
aws ecr get-login-password --region ap-northeast-1 | \
  docker login --username AWS --password-stdin \
  $(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-northeast-1.amazonaws.com

# ECR URL を取得
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-1

# 各イメージをビルド・プッシュ
cd ../app/dummy-api
docker build -t ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/kong-otel-dev-dummy-api:latest .
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/kong-otel-dev-dummy-api:latest

cd ../downstream-api
docker build -t ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/kong-otel-dev-downstream-api:latest .
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/kong-otel-dev-downstream-api:latest

cd ../../otel-collector
docker build -t ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/kong-otel-dev-otel-collector:latest .
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/kong-otel-dev-otel-collector:latest

インフラのデプロイ

cd ../terraform
terraform plan
terraform apply

デプロイ完了後、Kong Konnect接続できれば成功です。

kong-fargate-otel_02.png

Kong Konnect でルーティング設定

Data Planeが接続されたら、Kong Konnectコンソールでルーティングを設定します。

Gateway ServicesNew Gateway Service を選択します。

項目
Name dummy-api-service
Host dummy-api.kong-otel-dev.local
Port 3000
Protocol http

作成したServiceの RoutesAdd Route を選択します。

項目
Name dummy-api-route
Paths /api
Strip Path OFF

OpenTelemetry プラグインを有効化

PluginsAdd PluginOpenTelemetry を選択します。

項目
Traces Endpoint http://localhost:4318/v1/traces
Propagation Default Format w3c

kong-fargate-otel_04.png

これで分散トレーシングの設定が完了です。

動作確認

# ALB DNS名を取得
ALB_DNS=$(terraform output -raw alb_dns_name)

# APIにアクセス
curl -i http://${ALB_DNS}/api/users

# 分散トレーシング確認(downstream-apiを呼び出し)
curl http://${ALB_DNS}/api/chain

# 特定IDでの分散トレーシング確認
curl http://${ALB_DNS}/api/chain/123

New Relic での確認

設定が完了すると、New RelicのTraces画面でトレースを確認できます。

kong-fargate-otel_05

さいごに

Kong GatewayのOpenTelemetryプラグインとOTEL Collectorを組み合わせた分散トレーシングを試してみました。
インフラ構築はTerraformで再現できるようにしているので、ぜひ試してみてください。
この記事がどなたかの参考になれば幸いです。

この記事をシェアする

FacebookHatena blogX

関連記事