サーバーレス環境ではオブザーバビリティの実現が難しい場合がありますが、AWS Distro for OpenTelemetry(ADOT)を使用すると、標準化されたベンダーニュートラルな方法でテレメトリーの収集やエクスポートが可能になり、運用を簡素化できます。ADOTなら、業界標準のOpenTelemetry APIを活用して、特定のオブザーバビリティベンダーに縛られることなくアプリケーションを計装できます。
コンテナ化したLambda関数には、標準のLambdaレイヤーに対応していないという課題があります。ADOTは通常、Lambda関数のLambdaレイヤーとしてデプロイされるため、代替手段によって実行環境にテレメトリーエージェントを導入する必要があります。この記事では、Dockerのマルチステージビルドを用いてADOTをコンテナイメージに直接埋め込む実践的な回避策を紹介し、収集したテレメトリーデータをNew Relicにエクスポートする手順を説明します。
コンテナイメージを使う理由
AWS Lambdaは、ZIPパッケージ(Lambdaレイヤーを含む)とコンテナイメージという2種類のデプロイ方式に対応しています。コンテナイメージによるデプロイには、次のような大きな利点があります。
- 展開パッケージのサイズは最大10GB(ZIPは250MBまで)まで対応可能
- 既存のCI/CDパイプラインと一貫性のあるツール
- 実行環境の依存関係やバージョンを完全に管理できる
- 構築済みの依存関係により、複雑なアプリケーションのコールドスタート時間を短縮
導入に伴う課題
コンテナイメージはLambdaレイヤーを直接使用できません。LambdaレイヤーはZIPデプロイ用の機能です。ZIP形式でデプロイすると、AWSは実行時にレイヤーの内容を/optに展開します。コンテナイメージを使うと、この仕組みがスキップされます。コンテナイメージはユーザーがすべてを制御できる自己完結したファイルシステムである一方、AWSにはレイヤーの内容を組み込むためのフックが用意されていません。そのため、ADOTのインテグレーションには代替のアプローチが必要になります。
解決策:Dockerのマルチステージビルド
重要な点は、Lambdaレイヤーは単純なZIPアーカイブであり、実行時に/optに展開されるということです。Dockerのビルド時にADOTレイヤーの内容をダウンロードして展開することで、この仕組みを再現できます。
プロジェクト構造
├── Dockerfile
├── template.yaml
└── src/
├── index.js
└── package.json
ステップ1:Dockerfileの作成
マルチステージビルドでは、軽量のAlpineコンテナにADOTレイヤーをダウンロードし、その内容をLambdaイメージにコピーします。
# ステージ1:ビルダー - ADOT Lambdaレイヤーをダウンロードして展開する
# イメージサイズの軽量化とダウンロードの高速化のためにAlpineを使用する
FROM alpine as builder
# レイヤーの取得と展開に必要なツールをインストールする
RUN apk add --no-cache curl unzip
# ADOTレイヤーのURL - 別の言語ランタイムを使用する場合は変更してください
ARG ADOT_LAYER_URL="https://github.com/aws-observability/aws-otel-js-instrumentation/releases/latest/download/layer.zip"
# /optにダウンロードして展開する(AWSがレイヤーに使用する場所と同じ)
RUN curl -Lo /tmp/layer.zip "${ADOT_LAYER_URL}" && \
unzip /tmp/layer.zip -d /opt && \
rm /tmp/layer.zip
# ステージ2:最終的なLambdaイメージ
FROM public.ecr.aws/lambda/nodejs:22
# ビルダーからADOTレイヤーの内容をコピーする
COPY --from=builder /opt /opt
# ADOTラッパースクリプトに実行権限を付与する
RUN chmod +x /opt/otel-instrument
# 依存関係をインストールし、関数コードをコピーする
COPY src/package.json ${LAMBDA_TASK_ROOT}
RUN npm install
COPY src/index.js ${LAMBDA_TASK_ROOT}
CMD [ "index.handler" ]
注:chmod +xコマンドは必須です。ADOTが関数を計装するには、ラッパースクリプトに実行権限を付与する必要があります。
ステップ2:SAMテンプレートの設定
SAMテンプレートでは、Lambda関数を設定し、ADOTに必要な環境変数を設定します。デフォルトでは、ADOTはAWS X-Rayにエクスポートしますが、ここではエクスポート先をNew Relicに設定します。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Container Lambda with ADOT and New Relic
Parameters:
NewRelicLicenseKey:
Type: String
Description: "New Relic Ingest License Key"
NoEcho: true
Globals:
Function:
Timeout: 30
MemorySize: 256
LoggingConfig:
LogFormat: JSON
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument
# 干渉を防ぐためにAWS Application Signalsを無効にする
OTEL_AWS_APPLICATION_SIGNALS_ENABLED: 'false'
# 特定の計装を有効にする(任意)
OTEL_NODE_ENABLED_INSTRUMENTATIONS: 'aws-sdk,aws-lambda,http,pino'
OTEL_SERVICE_NAME: container-lambda-hello
OTEL_PROPAGATORS: 'tracecontext,baggage'
# エクスポーターズ
OTEL_TRACES_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_EXPORTER: otlp
# OTLPエンドポイント(シグナル別)
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: https://otlp.nr-data.net:4318/v1/traces
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: https://otlp.nr-data.net:4318/v1/metrics
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: https://otlp.nr-data.net:4318/v1/logs
OTEL_EXPORTER_OTLP_HEADERS: !Sub "api-key=${NewRelicLicenseKey}"
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
Architectures:
- x86_64
Events:
Api:
Type: HttpApi
Metadata:
DockerTag: latest
DockerContext: .
Dockerfile: Dockerfile
Outputs:
ApiEndpoint:
Description: "API Gateway endpoint URL"
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
New Relic用の主な環境変数:
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:トレース用のエンドポイント
- OTEL_EXPORTER_OTLP_HEADERS:New Relic Ingestライセンスキーを指定するヘッダー(CloudFormationパラメーター経由で渡されます)
- OTEL_SERVICE_NAME:New Relic上でサービスを識別する名前
- OTEL_NODE_ENABLED_INSTRUMENTATIONS:(任意) 構造化ログ用のpinoなど、特定の計装を有効にする
既知の問題:本記事の執筆時点では、ADOT Node.jsレイヤーはOTEL_EXPORTER_OTLP_ENDPOINTを無視し、OTEL_EXPORTER_OTLP_TRACES_ENDPOINTなどのシグナル別エンドポイントを優先します。そのため、ここではトレース、メトリクス、ログごとに別々のエンドポイントを使用します。
詳細は、GitHub Issue #297を参照してください。
ステップ3:カスタムインストゥルメンテーションおよびメトリクスの追加(任意)
より詳細な可観測性を実現するには、pinoのOpenTelemetry API を使用して、カスタムスパン、メトリクス、構造化ログを追加します。以下の依存関係を追加してください。
{
"type": "module",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"pino": "^9.0.0"
}
}
メトリクスヘルパーを作成します(metrics.js)。
import { metrics } from '@opentelemetry/api';
const meter = metrics.getMeter('my-lambda-metrics');
const workDurationHistogram = meter.createHistogram('work_item_duration', {
description: 'Duration of work items in milliseconds',
unit: 'ms',
});
const workItemCounter = meter.createCounter('work_item_count', {
description: 'Count of work items processed',
});
export const recordWorkMetrics = (id, duration, success = true) => {
workDurationHistogram.record(duration, { 'work.item.id': id, 'work.success': success });
workItemCounter.add(1, { 'work.item.id': id, 'work.success': success });
};
ハンドラーにPinoロギングを組み込みます。PinoはOpenTelemetryライブラリでデフォルトでサポートされており、実行時にパッチが適用されます。
import { trace, SpanStatusCode } from '@opentelemetry/api';
import pino from 'pino';
import { recordWorkMetrics } from './metrics.js';
// Pino logger - ADOT auto-instruments this
const logger = pino({ level: 'info' });
const doWork = async (id) => {
const tracer = trace.getTracer('my-lambda-tracer');
const span = tracer.startSpan(`doWork-${id}`);
span.setAttribute('work-item-id', id);
try {
logger.info({ itemId: id }, `Starting work item ${id}`);
const delay = Math.floor(Math.random() * 200) + 100;
await new Promise(resolve => setTimeout(resolve, delay));
logger.info({ itemId: id, duration: delay }, `Completed work item ${id}`);
recordWorkMetrics(id, delay, true);
span.setStatus({ code: SpanStatusCode.OK });
return { id, duration: delay };
} catch (error) {
logger.error({ itemId: id, error: error.message }, `Work item ${id} failed`);
recordWorkMetrics(id, 0, false);
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
return { id, error: error.message };
} finally {
span.end();
}
};
export const handler = async (event) => {
const tracer = trace.getTracer('my-lambda-tracer');
return tracer.startActiveSpan('handler-span', async (span) => {
logger.info({ event }, 'Event received');
try {
logger.info('Initializing processing...');
const results = await Promise.all([1, 2, 3].map(doWork));
span.setStatus({ code: SpanStatusCode.OK });
span.end();
return { statusCode: 200, body: JSON.stringify({ results }) };
} catch (error) {
logger.error({ error: error.message }, 'Handler failed');
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.end();
return { statusCode: 502, body: JSON.stringify({ error: 'Bad Gateway' }) };
}
});
};
ステップ4:ビルドとデプロイ
# コンテナイメージをビルドする
sam build
# AWSにデプロイする
sam deploy --guided
ガイド付きデプロイでは、SAMが次の処理を行います。
- コンテナイメージ用のECRリポジトリの作成
- イメージのビルドとプッシュ
- API Gateway経由でのLambda関数のデプロイ
ステップ5:New Relicでの検証
関数を数回呼び出したら、New Relicでテレメトリデータを確認します。
1. APM & Services:OTEL_SERVICE_NAMEに設定した名前でサービスを検索します

2. Distributed Tracing:トレースのウォーターフォール全体(カスタムスパンを含む)を表示します

3. Errors & Custom Instrumentation:span.recordException()で記録された例外はすべて、Errors inboxに表示されます。OpenTelemetry APIを用いて作成したカスタムスパン(doWork-{id}スパンなど)は、その属性とともにトレースに表示されます。

考慮事項
ADOTを使用する場合、Lambda関数に幾つかの運用オーバーヘッドが発生するため、注意する必要があります。
- コールドスタート:ADOTはOpenTelemetry Lambdaレイヤーを再パッケージしたものであるため、コールドスタート時に若干のオーバーヘッドが発生します。
- メモリ制限:まず256MBから開始し、ワークロードに応じて調整することをおすすめします。New RelicのLambda監視機能でメモリ使用量を監視し、割り当てを最適化してください。
注:メモリに関する上記の推奨事項はNode.js ADOTレイヤーに基づくものです。メモリ要件は対応言語によって異なります。PythonランタイムとJavaランタイムではオーバーヘッドの傾向が異なる場合があります。必ず、使用している言語とワークロードでベンチマークテストを実施してください。
主なポイント
- コンテナベースのLambdaは、Dockerのマルチステージビルドを使ってADOTを利用できます
- ネイティブなLambdaレイヤーと同様に、ADOTレイヤーは/optに展開されます
- New RelicのOTLPエンドポイントを使うと、ADOTとのシームレスな統合が可能になります
- カスタムインストゥルメンテーションにより、自動インストゥルメンテーション以上に詳細なトレースを実現できます
まとめ
コンテナイメージとOpenTelemetryは、サーバーレスのオブザーバビリティを実現する強力な組み合わせです。ADOTをコンテナビルドに直接組み込み、New Relicにエクスポートすることで、柔軟性の高いコンテナデプロイと、詳細かつ実用的なテレメトリーの両方の利点を得られます。
始める準備は良いですか?以下のリソースをご覧ください。
本ブログに掲載されている見解は著者に所属するものであり、必ずしも New Relic 株式会社の公式見解であるわけではありません。また、本ブログには、外部サイトにアクセスするリンクが含まれる場合があります。それらリンク先の内容について、New Relic がいかなる保証も提供することはありません。