(COM307) OpenTelemetry Lambda Layersをローカル起動してみた

2023.12.28

はじめに

re:Inventでのセッション「COM307 Seamless observability with AWS Distro for OpenTelemetry」で紹介されていた OpenTelemetry Lambda Layersが気になったので少し触ってみました。

セッション資料

やりたいこと

OpenTelemetry Lambda Layersにはテレメトリを収集、処理するコレクタだけのレイヤー(collector)と各言語用のラッパーを含むレイヤーが含まれています。 今回はCollectorの方をビルド&ローカルで起動してみようと思います。

動かしてみる

aws-lambda-runtime-interface-emulator

Lambda Extensionをローカルで起動するにはLambdaのランタイムが必要です。今回はaws-lambda-runtime-interface-emulatorを使います。 オフィシャルではmacOS向けのバイナリは配布されていないのでarm64版をコンテナ上で動かします。リポジトリのREADMEを参考に以下のようなDockerfileをビルドします。

このエミュレータはREADMEに記載されている関数起動用のエンドポイント(http://localhost:9000/2015-03-31/functions/function/invocations)以外にRuntime APIを公開しますが、エンドポイントは--runtime-api-addressオプションで指定しないと外部からアクセスできないので明示的に指定します。 コードはこのあたり

Dockerfile

FROM node:18
RUN apt-get update && \
    apt-get install -y \
    g++ \
    make \
    cmake \
    unzip \
    libcurl4-openssl-dev && \
    npm install -g aws-lambda-ric

COPY ./aws-lambda-rie-arm64 /usr/local/bin/aws-lambda-rie
WORKDIR /app
COPY ./entrypoint.sh entrypoint.sh
COPY ./index.js index.js

ENTRYPOINT [ "/usr/local/bin/aws-lambda-rie", "--runtime-api-address", "0.0.0.0:9001"]
CMD  ["aws-lambda-ric", "index.handler"]
EXPOSE 8000
EXPOSE 9001

docker-compse.yml

version: '3'
services:
  runtime:
    build: ./rie
    ports:
      - 9000:8080
      - 9001:9001
    environment:
      - LOG_LEVEL=debug

ハンドラ

"use strict";

exports.handler = async (event, context) => {
    return 'Hello World!';
}

エミュレータだけで動かす

まずはエミュレータだけで動かしてみます。起動用のエンドポイントにPOSTするとハンドラが実行されてレスポンスが得られました。

> curl -XPOST http://localhost:9000/2015-03-31/functions/function/invocations -d '{}'
"Hello World!"

エミュレータ側の出力は下記の通り。

runtime-1  | 28 Dec 2023 04:15:50,775 [INFO] (rapid) exec 'aws-lambda-ric' (cwd=/app, handler=index.handler)
runtime-1  | 28 Dec 2023 04:15:50,776 [DEBUG] (rapid) Runtime API Server listening on 0.0.0.0:9001
runtime-1  | 28 Dec 2023 04:16:08,870 [DEBUG] (rapid) invoke: -> POST /2015-03-31/functions/function/invocations map[Accept:[*/*] Content-Length:[2] Content-Type:[application/x-www-form-urlencoded] User-Agent:[curl/8.4.0]]
runtime-1  | START RequestId: e9db1493-b2e1-4fbf-9393-2daef083d7d2 Version: $LATEST
runtime-1  | 28 Dec 2023 04:16:08,870 [INFO] (rapid) INIT START(type: on-demand, phase: init)
runtime-1  | 28 Dec 2023 04:16:08,871 [INFO] (rapid) The extension's directory "/opt/extensions" does not exist, assuming no extensions to be loaded.
runtime-1  | 28 Dec 2023 04:16:08,871 [DEBUG] (rapid) Preregister runtime
runtime-1  | 28 Dec 2023 04:16:08,871 [DEBUG] (rapid) Start runtime
runtime-1  | 28 Dec 2023 04:16:08,871 [INFO] (rapid) Starting runtime without AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN , Expected?: false
runtime-1  | Executing 'index.handler' in function directory '/app'
runtime-1  | 28 Dec 2023 04:16:08,941 [DEBUG] (rapid) API request - GET /2018-06-01/runtime/invocation/next, Headers:map[Accept:[*/*] User-Agent:[AWS_Lambda_Cpp/0.2.8]]
runtime-1  | 28 Dec 2023 04:16:08,941 [INFO] (rapid) INIT RTDONE(status: success)
runtime-1  | 28 Dec 2023 04:16:08,941 [INFO] (rapid) INIT REPORT(durationMs: 70.232000)
runtime-1  | 28 Dec 2023 04:16:08,941 [INFO] (rapid) INVOKE START(requestId: d1006b4d-2117-4939-850c-f192e7b71cbe)
runtime-1  | 28 Dec 2023 04:16:08,941 [DEBUG] (rapid) Initialize invoke flow barriers
runtime-1  | 28 Dec 2023 04:16:08,941 [DEBUG] (rapid) Set renderer for invoke
runtime-1  | 28 Dec 2023 04:16:08,941 [DEBUG] (rapid) Release agents conditions
runtime-1  | 28 Dec 2023 04:16:08,941 [DEBUG] (rapid) Release runtime condition
runtime-1  | 28 Dec 2023 04:16:08,941 [DEBUG] (rapid) Await runtime response
runtime-1  | 28 Dec 2023 04:16:08,942 [DEBUG] (rapid) API request - POST /2018-06-01/runtime/invocation/d1006b4d-2117-4939-850c-f192e7b71cbe/response, Headers:map[Accept:[*/*] Content-Length:[14] Content-Type:[application/json] User-Agent:[AWS_Lambda_Cpp/0.2.8]]
runtime-1  | 28 Dec 2023 04:16:08,942 [DEBUG] (rapid) Await runtime ready
runtime-1  | 28 Dec 2023 04:16:08,942 [DEBUG] (rapid) API request - GET /2018-06-01/runtime/invocation/next, Headers:map[Accept:[*/*] User-Agent:[AWS_Lambda_Cpp/0.2.8]]
runtime-1  | 28 Dec 2023 04:16:08,943 [INFO] (rapid) INVOKE RTDONE(status: success, produced bytes: 0, duration: 1.827000ms)
runtime-1  | 28 Dec 2023 04:16:08,943 [DEBUG] (rapid) Invoke() success
runtime-1  | 28 Dec 2023 04:16:08,943 [DEBUG] (rapid) execute finished, autoreset cancelled
runtime-1  | END RequestId: d1006b4d-2117-4939-850c-f192e7b71cbe
runtime-1  | REPORT RequestId: d1006b4d-2117-4939-850c-f192e7b71cbe Init Duration: 0.51 ms  Duration: 72.27 ms  Billed Duration: 73 ms  Memory Size: 3008 MB    Max Memory Used: 3008 MB

コレクタを起動する

APIが起動したのでいよいよコレクタを起動してみます。

設定ファイル

コレクタの設定ファイルを作成します。

receivers:
  otlp:
    protocols:
      grpc:
      http:
exporters:
  debug:
    verbosity: detailed
service:
  pipelines:
    metrics:
      receivers: [otlp]
      exporters: [debug]

環境変数

設定ファイルやランタイムAPIのエンドポイントを環境変数で指定します。

# エミュレータのエンドポイント
export AWS_LAMBDA_RUNTIME_API=localhost:9001
# 設定ファイルのパス
export OPENTELEMETRY_COLLECTOR_CONFIG_FILE=/app/config/config.yaml
export OPENTELEMETRY_EXTENSION_LOG_LEVEL=DEBUG
export AWS_SAM_LOCAL=true

起動する

例によってmacOS向けのビルドスクリプトは無いのでgo runで起動します。

go run main.go

うまくいくと以下のようにコレクタが起動し待機状態になります。

{"level":"info","ts":1703738142.770088,"msg":"Launching OpenTelemetry Lambda extension","version":"latest"}
(略)
{"level":"info","ts":1703738142.786889,"caller":"service@v0.91.0/service.go:171","msg":"Everything is ready. Begin running and processing data."}

はまった点

コレクタの起動時にはまった点がいくつかあります。

エミュレータ起動後は関数実行前にコレクタを登録する必要がある

関数実行後にはextensionの登録ができないので必ず最初にコレクタを起動します。

エミュレータ起動後コレクタは一度しか登録できない

コレクタの設定不備やエラーでコレクタを再起動しても同じextensionを重複して登録できない仕様のためにエラーになります。エミュレータとコレクタはセットで再起動する必要があります。

上記の対策のため以下のようなラッパーを準備して使いました。

#!/bin/bash

docker compose down
docker compose up -d
# ランタイムの起動をちょっと待つ
sleep 1
go run main.go

Extension API クライアントをパッチする

Extensionの登録中にエラー「Failed to register main: event SHUTDOWN: ShutdownEventNotSupportedForInternalExtension」が発生したので、SHUTDOWNイベントフックを登録しないようにAPIクライアントのここを下記のようにパッチしました。

	reqBody, err := json.Marshal(map[string]interface{}{
		"events": []EventType{Invoke},
	})

まとめ

なんやかんややってOpenTelemetry Lambda Layersに含まれるコレクタをローカルで起動してみました。

おまけ: コレクタの実装

このExtensionのコレクタはOpenTelemetryの標準実装を使っています。またディストリビューションにはいくつかの独自コンポーネントが含まれています。コンポーネントの初期化とコレクタの起動処理はそれぞれ以下で確認できます。 - コンポーネントの初期化 - コレクタの起動