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 エージェントを実装してみました.
本家の mackerelio/mackerel-agent や mackerelio/mackerel-container-agent は Go 言語で実装されている & Go 言語勉強したいので今回は Go 言語で実装することにしました.
この Mackerel エージェントは Lambda layer として提供され, Lambda 関数にこの Lambda layer を設定するだけで当該 Lambda 関数の実行環境をこんな感じで Mackerel でモニタリングできます.
工夫したこと
メトリック送信タイミング
Lambda Extensions は 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
と指定すると Makefile
の build-${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 エージェントを実装してみました.ぜひ遊び半分で試してみてください.
また,色々可能性のありそうな新機能なので何か作ってみるのも面白いかもしれません.