こんにちは・こんばんは・おはようございます、エンジニアのid:aerealです。
以前、 実践OpenTelemetry - Classi開発者ブログ で紹介したようにOpenTelemetryを監視フレームワークとして導入しています。
前回の記事ではECSサービスで動くGoで書かれたWebアプリケーションへ導入しましたが、今回はAWS Lambdaの関数からOpenTelemetryを用いてDatadogにトレースを送るための試行錯誤について紹介します。
LambdaからDatadogにトレースを送りたい
AWS Lambdaは小〜中程度のタスクを大量に実行することに向いたFaaSです。
汎用性・柔軟性という点ではECSなどのコンテナワークロードと比べると実行環境などに設けられた制約がやや多いですが、その代わりに最適化された実行環境で実行されるのでうまくワークロードと噛み合えばまさに安い・早い・うまいの三拍子が揃うとても魅力的なサービスです。
ClassiでもLambdaを活用しており、本番環境で使うからには当然監視も充実させたいです。
冒頭の記事でも触れたようにフレームワークとしてはOpenTelemetryを、SaaSとしてはDatadogを活用しているのでLambda関数の分散トレースも当然OpenTelemetryを用いてDatadogへ送りたいです。
ECSサービスでホストされるアプリケーションの場合はOpenTelemetry Collectorをサイドカーコンテナとして実行し、アプリケーションからはサイドカーを通じてDatadogなりへシグナルを送ればよいのですが、既に述べたようにLambdaの実行環境は特殊で、Lambdaのエコシステムに沿った準備をしないといけません。
幸いながら現在Lambda ExtensionというLambdaのライフサイクルに沿って実行されるサイドカーのような仕組みが整えられており、一般的なコンテナワークロードとまったく同じ体験とはいきませんが、アプリケーション本体と補助的なソフトウェアをうまく分離することはできます。
まさにDatadog agentがLambda Extensionとして提供されていたり、OpenTelemetry CollectorがLambda Extensionとして提供されていたりと、これらがそのまま使えるとよさそうです。
しかし、それで済めば筆者は筆をとってこの記事を書いていないでしょう。つまり我々がやりたいことを実現するには一筋縄ではいかないのが現状なのです。
具体的には Lambda関数からOpenTelemetry SDKを用いてDatadogへトレースを送る という3つの要素要求すべてを満たすにはありものでは賄えないのです。
言い換えると (「Lambda関数から」という点はともかく) SDKかDatadogのどちらかを諦めれば実現できます。
しかしOpenTelemetry SDKさえ使えば複数のライブラリを使わなくて良いという魅力に惹かれて導入・移行したので今更後戻りはしたくありません。Lambdaの監視だけDatadogではない別のSaaSを使うというのもいざ障害などに遭遇した時に不安が残ります。
なので:
- Lambda関数から
- OpenTelemetry SDKのみを用いて
- Datadogへトレースを送る
……という3点すべてを満たすべく格闘していきます。
Lambda Extensionについて
改めてLambda Extensionについて述べます。
Lambda Extensionは関数本体とは別にLambdaのライフサイクルイベントごとに実行される補助的なソフトウェアです。
たとえば関数の起動時に初期化処理を走らせ、関数の呼び出しごとにデータを集めるなどし、関数がコールド状態に移行する時に集積したデータを送信する……といったことができます。
リリースと同時にいくつかのSaaSがLambda Extensionを発表しており、その中にはDatadogのextensionもあります。
関数の中の所定のディレクトリに実行ファイルとして配置され、それがLambdaによって都度呼び出されるというかたちになっているため、デーモンのようには振る舞えないのでデーモンとして実行することを想定しているソフトウェアは実装を変える必要があります。
AWS Distro for OpenTelemetry (ADOT) とopentelemetry-lambdaについて
OpenTelemetry Collector (以下、OTel Collector) をLambda Extensionとして実行する方法も既にあり、それがAWS Distro for OopenTelemetry (ADOT)です。
ADOTはAWSがサポートするOTel Collectorのディストリビューションです。LambdaやECSなど各環境で公式にサポート・推奨するexporterなどをあらかじめ有効にしたビルド済みのOTel Collectorです。 また、ビルド済みのLambda ExtensionもADOTプロジェクトの一部として提供されています。
ADOTが提供するLambda ExtensionはExtensionのプロトコルを喋りOTel Collectorを動かすラッパとして振る舞うopentelemetry-lambdaという別リポジトリにパッチを当ててビルドしたものです。
opentelemetry-lambdaはopentelemetry org.がオーナーであり、Lambda Extensionプロトコルを喋る実装はopentelemetryプロジェクトが責任を持ち、配布するソフトウェアとしてはAWSが別にオーナーシップを持っているというモデルのようです。
至れり尽くせりなプロジェクトがあるのでその恩恵に与れたらよいのですが、なんとこのADOTにはdatadogexporterが含まれていません。 つまり、ADOTを使ってDatadogへトレースやメトリクスを送れません。
X-Rayなど競合するサービスを提供しているという事情も想像できますが、それ以外にdatadogexporterの初期化処理に時間がかかりLambdaのInitフェイズでタイムアウトするというissueが過去に報告されており、それも関係しているのかもしれません。
いずれにせよADOTを用いてDatadogへトレースを送ることはできないことがわかりました。
Lambda ExtensionおよびOpenTelemetry Collectorの自前ビルド
ADOTはopentelemetry-lambdaにパッチを当ててビルドしているだけと説明しました。ということはdatadogexporterを使えるようパッチを当ててビルドすればLambdaからDatadogへトレースを送れそうです。
OTel Collectorはビルド済みのディストリビューションを使うほか、自分でディストリビューションを作ることもでき、その方法やツールも公式にまとめられています。
最終的に作りたいのはLambda Extensionなので「『datadogexporterを含むOTel Collector』を含むLambda Extensionをビルドする」ことが目標です。
言葉にするとややこしいですが、要はADOTであてているパッチを変えてビルドし、その結果をホストできればよいです。
そしてできあがったopentelemetry-lambdaの差分が次のURLです: https://github.com/open-telemetry/opentelemetry-lambda/compare/main...aereal:opentelemetry-lambda:support-datadog-exporter
続いて変更内容について詳しく説明します。
datadogexporterを追加
今回の本命となる変更です。
datadogexporterがLambda Extension向けにそもそもコンパイルできないといった事情はないので、単にimportしてファクトリメソッドに渡すだけです。
Parameters Store confmap providerを追加
実用上、必要になって追加した変更です。
LambdaはECSなどと異なり環境変数のデータソースとして直接Parameters StoreやSecrets Managerなど秘匿情報を安全に伝達できるサービスがサポートされておらず、関数の設定に直接値を記述するほかありません。
datadogexporterを使うにはOTel Collectorの設定にDatadogのAPI keyを渡す必要があり、これは当然ながら秘匿情報です。
Lambda関数で秘匿情報を扱うにはParameters StoreやSecrets Managerなどへ関数内から直接アクセスするほかありませんが、今回サービスを通じて秘匿情報を得たいのは関数本体ではなくLambda Extension内で動くOTel Collectorです。
設定ファイルにParameters Storeへアクセスするコードを書けるわけもないのでconfmap providerを介して取得できるようカスタマイズしました。
confmapとは環境変数やHTTPエンドポイントなど、外部データソースから値を設定に埋め込む仕組みです。 各データソースごとの実装がconfmap providerと呼ばれます。
OTel Collectorは標準で環境変数のconfmap providerが組み込まれているので、 ${env:DD_API_KEY}
のように書くとOTel Collectorの起動時に環境変数を参照して設定が補完されます。
Parameters Storeからパラメータを取得し、値を展開するconfmap providerを実装しそれを使うことで設定ファイルに ${ssm:/path/to/datadog-api-key}
のような記述をして、適切なIAM許可を与えておけばOTel CollectorにAPIキーを渡せます。
使ったconfmap providerの実装はこちらです: https://github.com/aereal/otel-confmap-provider-awsssm
Lambda Extensionをビルドし、GitHub Packagesでホストする
https://github.com/aereal/otel-collector-dist
最後にopentelemetry-lambdaのforkをビルドします。ADOTを参考にExtensionをGitHub Actionsでビルドするリポジトリを別に用意しました。
opentelemetry-lambdaのfork自体は随時上流と同期しないといけないので、差分を減らすために別リポジトリにしています。
ビルドしたExtensionを含むコンテナイメージもビルドし、GitHub Packagesでホストするようにしました。 GitHub Actionsと連携する上で便利ですし、更に社内用ビルドを社内のinternal/privateなリポジトリから参照する際のアクセス許可も簡単に設定できます。
むすび
Datadogへトレースを送れるようカスタマイズしたOpenTelemetry CollectorをLambda Extensionとしてビルドする方法とOpenTelemetry Collectorにまつわるエコシステムについて紹介しました。
筆者もそうだったのですが、初見では様々な概念が登場するため混乱しがちですが落ち着いて整理するときれいに依存が分離されていますし、それぞれのエコシステムも十分な情報がまとまっているため、自家ビルドも簡単にできました。
自家版ビルドを作り運用することはもちろん運用の負荷が増すことになるので、いずれ上流で解決できたらいいなと思いますしそのように働きかけたいと思いますが、それはそれとしてOpenTelemetry Collectorのエコシステムの理解を深めるいいきっかけになりました。
みなさまもぜひ自家版ビルドをしてみてはいかがでしょうか。