Running the OpenTelemetry Collector as a Lambda

OpenTelemetry Collectorは通常、長時間実行されるプロセスとしてデプロイされます:サイドカー、DaemonSet、EC2インスタンス、または自分のコンピュータ上のdockerコンテナなどです。テレメトリーを待ち受けるためにそこに留まります。終日テレメトリーを送信したい場合には問題ありませんが、テレメトリーが稀な場合には不向きです。例えば、AgentCore上に定義されたエージェントがあり、それが週に数回しか実行されない場合や、ほとんどトラフィックのないウェブサイトの場合などです。

OpenTelemetry CollectorをLambda関数として実行することはできるのでしょうか?難しそうですが、コーディングアシスタントがいるので大丈夫です!実際に動作するようにした方法をご紹介します。

(ここから先はエージェントが作成した内容です。この投稿をコーディングエージェントに向け、「自分の環境ではどのように動作するのか?」と尋ねてください。)

OTel CollectorをLambdaコンテナイメージとしてパッケージ化し、Function URLで前面に配置できます。プロデューサーはOTLP/HTTPをURLに送信し、コレクターは設定したプロセッサーを実行し、結果はバックエンドに送信されます。

低ボリュームの場合、これは事実上コストがかかりません。無料枠以下のLambdaはゼロに丸められます。コールドスタートは約4秒で、ウォームな呼び出しは2〜4ミリ秒です。

完全なファイルセット(Dockerfile、config.yaml、およびbootstrap/build/deployスクリプト)は、付属のgistとして利用可能です。残りの投稿では各部分の役割を説明します。

この方式が適している場合

Lambdaの形状は以下の場合に有効です:

  • トラフィックが断続的である — ピーク時には1秒あたり1桁のリクエストで、しばしばゼロになる。
  • プロデューサーが非同期でエクスポートする(BatchSpanProcessor、AgentCoreランタイム、別のLambda)ため、コールドスタートがユーザーから見えない。
  • 必要な処理がリクエストごとにステートレスである — OTTLのtransformfilterattributes、redaction、routing。

以下の場合には適しません:

  • 持続的なスループットが約1リクエスト/秒を超える場合。Lambdaの呼び出しごとのオーバーヘッドと料金が無料ではなくなる。
  • キューイングやリトライが必要な場合。バックエンドが一時的に利用できない場合、Lambda内のリトライ状態はプロセスとともに消滅する。プロデューサー側のリトライのみが安全網となる。
  • メトリクスやログをスクレイピングする場合。これはプッシュされたログとトレースのみを対象とする。

アーキテクチャ

Producer (any OTLP/HTTP client)
   │  OTLP/HTTP/protobuf, bearer token in Authorization header
   ▼
Lambda Function URL  (auth_type=NONE; bearer enforced inside the collector)
   │
Lambda container image
   ├─ AWS Lambda Web Adapter (extension)
   │    polls the Lambda Runtime API, forwards each invocation as an HTTP
   │    request to localhost:4318
   └─ otelcol-contrib  (CMD, not ENTRYPOINT)
        ├─ otlp receiver (bearertokenauth)
        ├─ your processors
        └─ otlphttp exporter (sending_queue disabled)
             → backend (e.g. api.honeycomb.io)

AWS Lambda Web Adapterは重要な要素です。Lambda拡張機能として登録し、Lambda Runtime APIを任意のポートでHTTPリスナーとして公開し、各呼び出しをコンテナのHTTPサーバーに転送します。Collector's OTLP/HTTPレシーバーはHTTPサーバーです。LWAはこれらを橋渡しします。

認証に関する注意。図のbearerトークンは任意ですが推奨されます。Collector's OTLPレシーバーはトークンなしで動作します。しかし、auth_type=NONEのFunction URLは公開アクセス可能であるため、コレクター内部でチェックを行わないと、URLを知っている人なら誰でもバックエンドにデータを送信でき、取り込み料金を増やし、テレメトリーを汚染する可能性があります。ホスト名はランダムですが、URLは漏洩します(コミット履歴、スクリーンショット、パケットキャプチャ)。認可されたプロデューサーとこのLambdaの間で共有される静的なbearerトークンは、アクセス可能な人を「URLを知っている人」から「URLとトークンの両方を知っている人」に引き上げます。詳細は下記のAuthenticationセクションを参照してください。

コンテナイメージ

マルチステージビルドを使用します。公式のotel/opentelemetry-collector-contribイメージはdistrolessで、USER 10001として実行され、/etc/passwdを持たないため、Lambdaコンテナランタイムで問題が発生します。バイナリをAlpineにコピーしてそこから実行します。

FROM otel/opentelemetry-collector-contrib:0.151.0 AS collector

FROM alpine:3.20
RUN apk add --no-cache ca-certificates

COPY --from=collector /otelcol-contrib /app/otelcol-contrib
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.0 /lambda-adapter /opt/extensions/lambda-adapter
COPY config.yaml /etc/otel/config.yaml

ENV AWS_LWA_PORT=4318
ENV AWS_LWA_INVOKE_MODE=buffered
ENV AWS_LWA_READINESS_CHECK_PATH=/

CMD ["/app/otelcol-contrib", "--config=/etc/otel/config.yaml"]

各部分の役割:

  • ca-certificates — Alpineには信頼できるCAルートが含まれていません。otlphttpエクスポーターはバックエンドへHTTPSで送信するため、これがないとTLSで失敗します。
  • /opt/extensions/のLWA — Lambdaはコンテナ起動時にこのディレクトリの拡張機能を自動的に検出します。LWAは独自のRuntime Interface Clientを同梱しているため、別途RICを必要としません。
  • AWS_LWA_PORT=4318 — OTLP/HTTPレシーバーのデフォルトポート。LWAはFunction URLの呼び出しをここに転送します。
  • AWS_LWA_INVOKE_MODE=buffered — リクエストごとに1つのレスポンス。他のオプションであるresponse_streamはServer-Sent Events用です。
  • AWS_LWA_READINESS_CHECK_PATH=/ — LWAはトラフィックを転送する前に、このパスに何かが応答するまでポーリングします。OTLPレシーバーは/で404を返しますが、任意のHTTPレスポンスで十分なのでreadyとみなされます。
  • CMDENTRYPOINTではない) — LambdaはCMDのみのイメージとENTRYPOINT+CMDのイメージを起動時に異なる方法で扱います。ENTRYPOINTが設定されていると、メインプロセスは起動しません。トラブルシューティングを参照してください。

設定

設定を形作る3つの要素:レシーバーにはbearer-token認証が必要、処理はリクエストごとにステートレスでなければならない、エクスポーターはキューを持ってはならない。

extensions:
  bearertokenauth/ingest:
    scheme: Bearer
    token: ${env:INGEST_BEARER_TOKEN}

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
        auth:
          authenticator: bearertokenauth/ingest

processors:
  # ここにプロセッサーを記述。例: transform/, filter/, attributes/。
  # batchプロセッサーは含めない。

exporters:
  otlphttp/honeycomb:
    endpoint: https://api.honeycomb.io
    headers:
      x-honeycomb-team: ${env:HONEYCOMB_API_KEY}
    sending_queue:
      enabled: false

service:
  extensions: [bearertokenauth/ingest]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [...]
      exporters: [otlphttp/honeycomb]

これはHoneycombに送信します。他の場所に送信するには、エクスポーターを置き換えます — 任意のOTLP/HTTPバックエンドが同じ形状で動作します(endpointと認証ヘッダーの名前・値を変更)。

2つの非自明な設定:

  • batchプロセッサーを使用しない。 Lambdaはハンドラーが返った後にコンテナをフリーズします。batchプロセッサーに留まるスパンはフラッシュされず、次のコールドスタート(破棄される)または次の呼び出し(来ないかもしれない)までメモリに残ります。症状:エクスポーターはミリ秒単位で200を報告するが、バックエンドはトレースを受け取らない。
  • すべてのエクスポーターでsending_queue.enabled: false。同じ理由。デフォルトの非同期キューはスパンをメモリに保持し、フリーズすると取り残されます。

これらの公式な解決策はopentelemetry-lambdaコレクター配布のdecoupleプロセッサーであり、ハンドラーが返る前にフラッシュすることを認識しています。これはotel/opentelemetry-collector-contribには含まれていません。contribイメージを使用する場合は、同期エクスポートが正しい形状です。

認証

Bearer認証は任意ですが推奨されます。理由はArchitectureの注記を参照してください。このセクションでは実装方法を説明します。

Function URLはauth_type=AWS_IAMauth_type=NONEをサポートします。IAMが厳密な答えですが、プロデューサー側でOTLPリクエストにSigv4署名する必要があり、OpenTelemetry SDKはSigv4で署名しません。Sigv4署名OTLPエクスポーターを書く作業は、このパターン全体の価値を上回ります。

現実的な答えは、Function URLでauth_type=NONEとし、コレクター内部でbearertokenauth拡張機能を使用することです。bearerトークンはプロデューサーの環境とLambdaの環境の間で共有される秘密です。漏洩した場合はローテーションしてください。

プロデューサーは以下を設定します:

OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer <token>

ヘッダー名はレシーバーで大文字小文字を区別しません。トークン値は区別します。

bearer認証をスキップする場合は、extensionsブロック、レシーバーのauth:スタンザ、serviceの下のextensions: [bearertokenauth/ingest]行を削除します。設定の他の部分は同じです。

ビルド、プッシュ、デプロイ

付属のgistは以下のコマンドをbootstrap.shbuild.shdeploy.shとしてパッケージ化しています。ブログ記事からシェルを実行するよりスクリプトを実行したい場合に利用してください。

ビルド

docker buildx build \
  --platform linux/arm64 \
  --provenance=false \
  --sbom=false \
  --load \
  -t collector:local .

--provenance=false --sbom=falseが必要です。デフォルトのbuildx出力は証明書を含むOCIイメージマニフェストであり、Lambdaはこれを拒否します:InvalidParameterValueException: The image manifest, config or layer media type for the source image ... is not supported.

ECRへのプッシュ

イメージをECRにプッシュします。ECRリポジトリは事前に作成する必要があります。1回限りのコマンドについてはAppendix: ECR setupを参照してください。

Lambdaの作成

LambdaにはAWSLambdaBasicExecutionRoleを持つ実行ロールと、lambda.amazonaws.comがロールを引き受けることを許可する信頼ポリシーが必要です。1回限りのセットアップについてはAppendix: Lambda execution roleを参照してください。

aws lambda create-function \
  --function-name collector \
  --package-type Image \
  --code "ImageUri=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/$REPO:latest" \
  --role "arn:aws:iam::$ACCOUNT:role/CollectorLambda" \
  --architectures arm64 \
  --memory-size 512 \
  --timeout 30 \
  --environment "Variables={INGEST_BEARER_TOKEN=...,HONEYCOMB_API_KEY=...}"

aws lambda wait function-active-v2 --function-name collector

512 MBと30秒は快適なデフォルト値です。コレクター自体はこれよりはるかに少ないリソースを使用します。メモリを増やすとLambdaに比例してCPUが増え、コールドスタートが短縮されます。

Function URLの作成

aws lambda create-function-url-config \
  --function-name collector \
  --auth-type NONE

権限 — 両方のステートメントが必要です

これは2025年10月時点の一般的なLambda Function URLの動作であり、コレクター特有のものではありません。しかし、初めてデプロイする際の最も一般的なサイレント障害の原因であるため、付録ではなくメインの流れに記載しています。lambda:InvokeFunctionUrlだけでは公開Function URLの呼び出しを許可するには不十分で、--invoked-via-function-url付きのlambda:InvokeFunctionも必要です:

aws lambda add-permission \
  --function-name collector \
  --statement-id FunctionURLAllowInvokeUrl \
  --action lambda:InvokeFunctionUrl \
  --principal '*' \
  --function-url-auth-type NONE

aws lambda add-permission \
  --function-name collector \
  --statement-id FunctionURLAllowInvoke \
  --action lambda:InvokeFunction \
  --principal '*' \
  --invoked-via-function-url

2番目のステートメントがない場合、すべてのリクエストはURLゲートで403 AccessDeniedExceptionを受け取り、Lambdaはコンテナを呼び出しません — デバッグ用のCloudWatchログ行はありません。AWSは二重権限ルールをhttps://docs.aws.amazon.com/lambda/latest/dg/urls-auth.htmlで文書化しています。

検証

デプロイ後、合成OTLPリクエストを送信し、3つのことを確認します。

1. URLゲートが認証を受け入れる。正しいbearerで無効なボディを送信します:

curl -i -X POST "${URL}v1/traces" \
  -H 'Content-Type: application/x-protobuf' \
  -H "Authorization: Bearer ${TOKEN}" \
  --data-binary 'x'

期待値:400。コレクターは無効なOTLPボディを解析して拒否します。ここで400が返ることは、Function URLがリクエストを受け入れ、LWAが転送し、コレクターが実行されたことを意味します。403が表示される場合はトラブルシューティングを参照してください。401が表示される場合は、プロデューサーのbearerトークンがLambda環境のものと一致していません。

2. 実際のOTLPリクエストが成功する。任意のOTel SDKでOTLPSpanExporterSimpleSpanProcessorBatchSpanProcessorではない — 検証ではエクスポートの戻り値が実際のエクスポートステータスを反映するようにしたい)を使用します。span_exporter.export(...)の結果をキャプチャします。SUCCESSである必要があります。curlのみの代替案として、Testing an OpenTelemetry Collector deployed as a Daemonset in Kubernetessample-span.json + curlパターンがFunction URLに対しても同様に動作します — curlを${URL}v1/tracesに向け、bearerヘッダーを追加してください。

3. スパンがバックエンドに到達する。トレースIDまたはサービス名でクエリします。Honeycombに送信している場合、Honeycomb MCP serverにより、エディタやCLIから直接トレースをクエリできます — get_traceにトレースIDを指定すると、UIを開かずにスパンの形状を取得できます。プロデューサーがSUCCESSを報告したのにバックエンドに何も表示されない場合、最も可能性の高い原因は有効なsending_queueまたはbatchプロセッサーです — トラブルシューティングを参照してください。

CloudWatchのボリューム

呼び出しごとのログ出力の目安:

  • コールドスタート: 約12行(コレクター起動バナーとLambdaランタイムのSTART/END/REPORT)。
  • ウォーム呼び出し: 3行(Lambdaランタイムのみ;コレクターは定常状態の処理でinfoレベルでは何も出力しない)。
  • コンテナの廃棄(Lambdaはアイドル後にコンテナをリサイクル):4行(正常終了)。

1日100回の呼び出しで1回のコールドスタートの場合、約315行、1日あたり10 KB — 年間約4 MB。CloudWatchの$0.50/GB取り込み料金で、年間約$0.002です。

トラブルシューティング

障害が表面化する場所ごとに症状をグループ化しました。これらのほとんどはサイレントであり、有用なエラーメッセージを生成しないため、通常よりも症状から原因への表が重要になります。

Function URLで403 AccessDeniedException、CloudWatchログ行なし

二重権限の問題。auth_type=NONEのFunction URLではlambda:InvokeFunctionUrlだけではもはや十分ではありません。--invoked-via-function-url付きの2番目のlambda:InvokeFunctionステートメントを追加してください。上記のPermissionsセクションを参照してください。

特に混乱するのは、Lambdaがコンテナを全く呼び出さないことです — どこにもログがなく、コレクターは正常で、SCP、アカウントレベルのブロック、またはリソースポリシー自体を調査する時間を費やした後に、ゲートがLambdaに到達する前にリクエストを拒否したことに気づくことになります。

コレクターから401

リクエストのbearerトークンがLambda環境のINGEST_BEARER_TOKENと一致しません。比較してください。ヘッダー名は大文字小文字を区別しませんが、トークン値は区別します。

プロデューサーがSUCCESSを報告するが、バックエンドがトレースを受け取らない

batchプロセッサーまたは有効なsending_queueが、Lambdaのフリーズを跨いでメモリにスパンを保持しています。batchプロセッサーを削除し、すべてのエクスポーターでsending_queue.enabled: falseを設定してください。

原因を確認するには、コレクターのログレベルを一時的にdebugに設定します — エクスポーターにスパンが到着するが、呼び出しが終了する前にエクスポート試行がないことがわかります。config.yamlで設定します:

service:
  telemetry:
    logs:
      level: debug

再ビルドして再デプロイします。問題を診断したらinfoに戻してください。debugはCloudWatchのボリュームに影響するほどノイズが多いです。

関数作成時にInvalidParameterValueException: image manifest ... is not supported

デフォルトのdocker buildx出力はビルド証明書を含むOCIイメージマニフェストであり、Lambdaのイメージプルパスはこれを拒否します。--provenance=false --sbom=falseを付けて再ビルドしてください。

LWAがapp is not ready after 2000msを繰り返しログ出力し、10秒でinitがタイムアウト

コレクタープロセスが起動しませんでした。原因の1つはDockerfileにENTRYPOINTCMDの両方があることです — LambdaのコンテナinitはCMDのみのイメージとENTRYPOINT+CMDのイメージを異なる方法で扱い、ENTRYPOINTが設定されているとメインプロセスが起動しません。CMDのみを使用してください。これはdistroless、provided:al2023、Alpineベースで再現します。Lambdaランタイムの動作であり、ベースイメージの問題ではありません。

あまり一般的でない原因:コレクターは起動したが、AWS_LWA_PORTとは異なるポートにバインドした。OTLPレシーバーのHTTPエンドポイントが一致することを確認してください。

コンテナが起動しない。ログに権限や/etc/passwdに関する言及がある

公式のotel/opentelemetry-collector-contribイメージを直接実行している可能性があります。これはdistrolessで、USER 10001として実行され、/etc/passwdを持ちません。代わりにマルチステージビルドでバイナリをAlpineにステージしてください。

エクスポーターからx509: certificate signed by unknown authority

ca-certificatesのないAlpineベース。DockerfileにRUN apk add --no-cache ca-certificatesを追加してください。

コールドスタートが4秒より大幅に長い

--memory-sizeを増やしてください。Lambdaはメモリに比例してCPUを割り当てるため、128 MBではコールドスタートが2桁秒になることがあります。512 MBはcontribバイナリにとって快適なデフォルトです。

Function URLは到達可能だが、すべてのリクエストが詳細なしで500を返す

CloudWatchのコレクターのログを確認してください。最も一般的な原因は、必要な環境変数が欠落している(設定が${env:FOO}を参照しているがFOOが設定されていない)、またはデプロイ時に生き残った無効な設定ファイル(誰もotelcol validateを実行していない)です。CIの一部として、ビルドしたイメージ内でotelcol-contrib validate --config=/etc/otel/config.yamlを実行してください。

バックエンドが一時的にダウンし、トレースが失われる

想定内です。Lambdaには呼び出しを跨ぐバッファがありません。バックエンドが5xxを返す場合、その呼び出し内のインプロセスリトライが実行され、その後コンテナがフリーズします。プロデューサー側のBatchSpanProcessorのリトライがほとんどの短い停止をカバーします — これがこの形状で持っている唯一のバッファです。永続的なバッファリングが必要な場合は、このパターンは適切ではなく、永続的なコレクターを使用してください。

付録:1回限りのAWSセットアップ

上記のメインの流れは、ECRリポジトリとLambda実行ロールがすでに存在することを前提としています。これらはそれらを作成するための1回限りのコマンドです。

ECRセットアップ

ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
REGION=us-west-2
REPO=collector

aws ecr create-repository --repository-name "$REPO" --region "$REGION"

create-repositoryはリポジトリがすでに存在する場合にエラーになります。無視して問題ありません。

プッシュするには(メインの流れから):

aws ecr get-login-password --region "$REGION" \
  | docker login --username AWS --password-stdin "$ACCOUNT.dkr.ecr.$REGION.amazonaws.com"

docker tag collector:local "$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/$REPO:latest"
docker push "$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/$REPO:latest"

Lambda実行ロール

ロールには2つの要素が必要です:Lambdaがロールを引き受けることを許可する信頼ポリシーと、CloudWatchログへの書き込みを許可する権限ポリシー。AWSLambdaBasicExecutionRoleはログをカバーする管理ポリシーです。

cat > trust.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
EOF

aws iam create-role \
  --role-name CollectorLambda \
  --assume-role-policy-document file://trust.json

aws iam attach-role-policy \
  --role-name CollectorLambda \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

ロールのARNはメインの流れのaws lambda create-function--roleに入ります。