mackerel-lambda-agent を作ってみた話

先日, AWS Lambda の新機能 Lambda Extensions がリリースされました.

モニタリング,オブザーバビリティ,セキュリティ,ガバナンス用のツールと Lambda との統合を簡単にすることを可能にします.

従来 Lambda でモニタリングするときなどは,どうしてもアプリケーションコードにモニタリングのコードを埋め込む必要があったので,ビジネスロジックが汚されてしまうという難点がありましたが, Lambda Extensions を使うことでこれを軽減することができます.

さらに,Lambda Extensions には Internal extensions と External extensions があるのですが, External extensions は Lambda 関数とは独立したプロセスで動くため Lambda 関数とは別の言語で書かれていても動作したりします.

sidecar pattern みたいな面白新機能ですね.

Lambda Extensions の仕組みについては公式ドキュメントに詳しく載っていますのでそちらをご覧ください.


ということでここからが本題ですが,今回この Lambda Extensions 用の Mackerel エージェントを実装してみました.

github.com

本家の mackerelio/mackerel-agentmackerelio/mackerel-container-agent は Go 言語で実装されている & Go 言語勉強したいので今回は Go 言語で実装することにしました.

この Mackerel エージェントは Lambda layer として提供され, Lambda 関数にこの Lambda layer を設定するだけで当該 Lambda 関数の実行環境をこんな感じで Mackerel でモニタリングできます.

f:id:pyto86:20201031225922p:plain f:id:pyto86:20201031225944p:plain

工夫したこと

メトリック送信タイミング

Lambda Extensions は Lambda 関数が起動されていない間は同じく起動されていないので,メトリックの送信のタイミングが難しかった.

f:id:pyto86:20201031221959p:plain
Lambda のライフサイクル

  • Lambda 関数が起動されていない間は起動されていないので,1分に1回など定期的に送信することはできない.
  • Lambda 関数の処理の開始イベントは受け取れるが, 終了イベントを受け取ることができないので,処理の開始から終了までをまとめて終了時に送信する,ということもできない.

Lambda 関数の処理されているならなるべくメトリック送信したい & 長時間処理されているなら定期的に送信したい...ということで,1分に1回送信する処理を非同期で走らせつつ,Lambda 関数の処理の開始のタイミングでもメトリックを送信することにしました. ただし,そうするとライフサイクルの短い Lambda 関数では過剰にメトリックが送信される恐れがあるので,前の送信時間から1分が経っていなければメトリック送信を skip するようにして凌いだりしています.

実行環境ID取得

Lambda 関数の実行環境=1ホストとしたため,ホストを登録するにあたり,何らかの実行環境ごとのIDが必要になりました. まぁ自前で生成してもいいですが,実行環境のどこかにしまってあるだろう...ということで Lambda の実行環境をリバースエンジニアリングしてみたところ...

$ lambdash ls /proc/sys/kernel/random
boot_id
entropy_avail
poolsize
read_wakeup_threshold
urandom_min_reseed_secs
uuid
write_wakeup_threshold
$ lambdash cat /proc/sys/kernel/random/boot_id
cc30aa62-bc25-46bb-96b2-bc700f761613

明らかに実行環境ごとのIDっぽい!!!ということでこれをホストの名前にしたりして遊んでました.(もちろん公式ではないです...)

ちなみにリバースエンジニアリングするときには lambdash というツールを使いました.これは便利.

AWS SAM を活用したビルド・デプロイ

SAM テンプレートで Metadata > BuildMethod を指定することで Lambda layer を sam build コマンドでビルドでき, さらに,そこに makefile と指定すると Makefilebuild-${layer の論理ID}ターゲットに定義されたコマンドでビルドすることができる!!

のでこれを活用しました. 知らない間にめちゃくちゃ便利になっていた.

Layer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: mackerel-lambda-agent
      Description: Mackerel agent for AWS Lambda
      LicenseInfo: MIT
      ContentUri: ./
      CompatibleRuntimes:
        - go1.x
        - python3.8
    Metadata:
      BuildMethod: makefile
.PHONY: build-Layer
build-Layer:
    GOOS=linux go build -ldflags=$(BUILD_LDFLAGS) -o $(ARTIFACTS_DIR)/extensions/$(BIN) ./cmd/

ということでビルド・デプロイは sam build && sam deploy でことが済むようにしました.

難しかったこと

Go 言語,少し触ってはわかった気になって,時が立ち忘れて,また触って...というのを繰り返していたのですが,やっぱりよくわかっていなかった.

最近 Typescript を触る機会が多いので, Typescript でいう union 型どうやって実現するんだ!?とか継承どうするんだ!?とか悩んでました.

Go 言語めっちゃシンプルだしわかりやすいのだけれど,一般的なプログラミング言語によくある機能がなかったり?して混乱する...(多分まだあまり理解できていないだけな気もする

その他

とりあえず,本家エージェントを参考にして同じようなメトリックを取れるようにしたけれども,CPU 使用率とかは,恐らく1実行環境を100として取れていなくて,CPU バウンドな Lambda 関数でも最大値が4%とかにしかならなかったりした.こんな感じであまり実用的でないメトリックもあるかもしれない.

ただ, Network I/O とか Filesystem とかはある程度役に立つのでは?とは思っています.

今後やってみたいこと

今回はやらなかったですが, Lambda 関数とプロセス間通信できるので,うまく活用できたら面白そう!と思っています.例えば,ログをプロセス間通信で Lambda Extensions に送信して, Lambda Extensions はそのログを非同期で送信する...とか.

あとは, Lambda の実行環境のライフサイクル短いと退役されてメトリック残らず,ファントムメトリックになってしまうのは何とかしたいと思っています.

まとめ

Lambda Extensions 用の Mackerel エージェントを実装してみました.ぜひ遊び半分で試してみてください.

また,色々可能性のありそうな新機能なので何か作ってみるのも面白いかもしれません.