はじめに
ヘンリーで SRE をしている galoitsu です。
ヘンリーが提供するレセコン一体型電子カルテ Henry はオブザーバビリティバックエンドに Honeycomb を採用しています。 AWS 側*1のアプリケーションは ECS で動き、トレースは OpenTelemetry Collector を経由して Honeycomb に送られます。 collector はアプリと同じタスクのサイドカーではなく、独立した ECS サービスとして動かしています。
[アプリの ECS タスク] --OTLP--> [collector の ECS タスク] --> Honeycomb
トレースのスパンには、それがどこで動いていたかを表す属性を付与するようにしています。
ECS なら aws.ecs.task.arn や aws.ecs.task.family などがそれに当たり、タスク単位の調査・分析に活用でき...るはずでしたが、その aws.ecs.* にアプリの ECS タスクではなく collector 自身の ECS タスクの情報が付与されていました。
原因や修正方法やその判断などを残していきます。
何が起きた?どう困った?
たとえば aws.ecs.task.family 属性を Honeycomb で確認するとスパンのを見ると、すべてのアプリのスパンで aws.ecs.task.family の値が collector のタスクファミリーになっていました。
Henry のアプリ側では service.name という属性を付与しているいるため、これを活用することでアプリの区別はつきますが、タスクの区別まではできません。
これにより、「特定のタスクだけレイテンシーが伸びていないか」「直前に入れ替わったタスクにエラーが偏っていないか」などといったタスクに強く関連する問いに対して、トレースを基に答えられない状態でした。
なぜ起きた?
collector の traces パイプラインには、resourcedetection プロセッサーの ECS detector を設定していました。
processors: resourcedetection/ecs: detectors: [ecs] timeout: 2s override: false
この detector は、自分のプロセスから見える環境変数 ECS_CONTAINER_METADATA_URI_V4 からタスク情報を取得します。
つまり、collector がアプリと同じタスクのサイドカーなら読み取った値はアプリのタスクと一致しますが、独立した ECS サービスなら collector 自身のタスクになります。
そして collector を通過するすべてのスパンにその値を付与しようとします。
また、override: false も設定していました。
この設定は、スパンがすでに同名の属性を持っているならそれを尊重し、持っていないならフォールバックとして detector が取得した値を付与します。
これらの設定により、 アプリ側の SDK で aws.ecs.* を付与していれば、それがそのまま Honeycomb に送信されるはずでした。
しかし Honeycomb でスパンの aws.ecs.* を見ると、すべてのアプリで collector 自身の ECS タスクの情報になっていました。そう...
誰も... aws.ecs.* を付与していないのである!!!
つまるところ原因は以下です。
- Henry のすべてのアプリが、アプリ側の SDK で
aws.ecs.*を付与していなかった - アプリ側の SDK で
aws.ecs.*が付与されていなかったため、 collector 側のフォールバックが常に効いてしまっていた- detector は collector から見て正しい ECS タスクの情報を代わりに埋めてくれていた(が、それは適切な値ではない)
修正方法は?
原因がわかれば修正方法はシンプルで、アプリ側の SDK で適切な aws.ecs.* を付与してあげればよいだけです。アプリ側の SDK で ECS resource detector を有効にすれば、適切な aws.ecs.* を付与することができ、override: false による collector のフォールバックは発火しなくなります。
環境変数で有効化する
Henry ではバックエンドの言語として Kotlin と TypeScript の2種類を使用しているので、それぞれの設定を変更して修正していきました。 コードの変更で修正することもできそうでしたが、 The Twelve-Factor App (日本語訳) に則り設定は環境変数で制御できるほうが望ましいと判断しました。
Kotlin(JVM)
AWS resource provider がバンドルされていますが、デフォルトで無効です。環境変数 OTEL_RESOURCE_PROVIDERS_AWS_ENABLED を足すだけで有効になるのでこれを有効にしました。
OTEL_RESOURCE_PROVIDERS_AWS_ENABLED=true
TypeScript(Node.js)
こちらにも似た役目の環境変数 OTEL_NODE_RESOURCE_DETECTORS があります。
OTEL_NODE_RESOURCE_DETECTORS=aws,env
ところが、これを足しても属性は増えませんでした。
この環境変数を解釈するのは SDK 本体ではなく、@opentelemetry/auto-instrumentations-node の関数 getResourceDetectors() です。
Henry の TypeScript アプリケーションでは detector を自前で組み立てており、環境変数を読む関数 getResourceDetectors() を通らない形になっていました。
// 変更前: envDetector だけを直接渡している .merge(detectResources({ detectors: [envDetector] })) // 変更後: 有効にする detector を(コードではなく)環境変数 `OTEL_NODE_RESOURCE_DETECTORS` で制御できる .merge(detectResources({ detectors: getResourceDetectors() }))
なお、この状態で環境変数を未設定のままにすると
OTEL_NODE_RESOURCE_DETECTORS=all
と同等になり、GCP、Azure、Alibaba Cloud など他の detector まで動くようになりますが、明示的に指定しておくのが無難だと判断して aws,env を指定しています。(ゼロコード計装の構成 | OpenTelemetry)
仲間はずれの aws.ecs.task.id も修正してあげる
さて、これまでの治療により aws.ecs.* の大半は適切に送信されるようになりましたが、aws.ecs.task.id だけは collector の値が残りました。
semconv はこの属性を次のように定めています。( AWS ECS | OpenTelemetry )
The ID of a running ECS task. The ID MUST be extracted from
task.arn.
ECS のメタデータエンドポイントに ECS タスク ID は存在せず、抽出は実装側の仕事になっています。
そして opentelemetry-js の AwsEcsDetector も opentelemetry-java-contrib の EcsResource も、この抽出を実装していません。
一方で collector 側の ECS detector はこの抽出を実装しています。
(この差分は upstream でも認識されているようですが、実装はされていないようです → たとえば Go での issue は開かれたまま )
これにより、アプリ側の SDK では aws.ecs.task.id は付与されず、collector 側はフォールバックとして aws.ecs.task.id を付与するので collector 側の値のままになってしまっていました。
そこで collector 側でタスク ARN から導出するようにしました。
transform/ecs-task-id: error_mode: ignore trace_statements: - context: resource conditions: - resource.attributes["aws.ecs.task.arn"] != nil statements: - set(resource.attributes["aws.ecs.task.id"], resource.attributes["aws.ecs.task.arn"]) - replace_pattern(resource.attributes["aws.ecs.task.id"], "^.*/", "")
処理としては、ARN の最後のスラッシュ以降を抽出するだけです。 クラスター名を含む新形式のタスク ARN でも、含まない旧形式でも同じ結果になるようになっています。(Access Amazon ECS features with account settings - Amazon Elastic Container Service)
最後にフォールバックをなくす
すべてのアプリが aws.ecs.* 属性を付与するようになれば、collector 側の ECS detector によるフォールバックは不要になります。
フォールバックそのものが誤情報を生みうるので、削除しておくのが望ましいと判断しました。
Honeycomb を見てすべてのアプリで aws.ecs.* 属性を付与できていることを確認し、フォールバックを削除しました。
まとめ
今回は、すべてのアプリの aws.ecs.* 属性が OpenTelemetry Collector の ECS タスク情報になってしまっているのを修正しました。
一連の修正を通して、以下の知見を得られました。
- collector を独立したサービスとして運用している構成では、collector の
resourcedetectionはアプリの ECS タスク情報の供給源にならないoverride: falseを付けていても、そもそもアプリが属性を送っていなければ collector 自身の値が入ってしまう- アプリの実行環境に関する属性はアプリ側で埋めるのが素直でわかりやすい構成になる
- TypeScript で
OTEL_NODE_RESOURCE_DETECTORSを設定しても効果がないときは、getResourceDetectors()を呼んでいるかを確認してみるのがよい- 環境変数を読む主体が SDK 本体ではないため、detector を自前で組み立てている場合には環境変数は無視されてしまう
- 誤った値を付与するようなフォールバックは避ける
- 安全面に倒そうとして設定したフォールバックによって誤った値が付与されてしまうこともある
- 本当に設定すべきフォールバックなのか、慎重に検討してから設定するべき
ヘンリーは顧客の体験を改善していきたい仲間を絶賛採用しております。オブザーバビリティに投資して調査可能なシステムを育てていきたい方、マイクロサービスの計装を横断的に整えることに関心のある方は気軽にご連絡ください。お待ちしています!
*1:Henry の実行基盤を Google Cloud から AWS に移設しようとしており、現在絶賛移設中です。詳しくは ヘンリーではクラウド基盤の移行プロジェクトを開始しています や なぜ AWS 移設をするのか を参照ください。








