はじめに
こんにちは。Product Unit SINIS for Instagram開発チームの諸石です。 今回、AWSのECSのタスクが異常終了した時Sentryに通知する方法を調べたのでお話しします。
経緯
先日ECSで動いている定期バッチでメモリリークが起こり、処理が最後まで実行されず途中で終了してしまうエラーがありました。その際ECSのタスクが異常終了していることに気が付くのが遅れたので、今後の対策としてECSのタスクが異常終了したら通知する仕組みを作るための調査と検証を行いました。
構成
全体の構成は以下のようなイメージです。 タスクが終了したらEventBridgeにイベントデータが送られます。送られたイベントデータがEventBridgeで設定した条件に一致したらLambdaを実行します。条件はタスクが異常終了したときにLambdaを実行するというものです。Lambda側ではSentryに終了理由などのメッセージを通知します。今回はTerraformで構築して検証を行いました。
EventBridgeの設定
EventBridgeに設定する内容は以下のイメージです。
// EventBridge Rule resource "aws_cloudwatch_event_rule" "lambda_eventbridge_rule" { name = "eventbridge_rule" event_bus_name = "default" event_pattern = jsonencode({ "source" : [ "aws.ecs" ], "detail-type" : [ "ECS Task State Change" ], "detail" : { "clusterArn" : [aws_ecs_cluster.main.arn], "containers" : { "exitCode" : [{ "anything-but" : [0] }], "reason" : [{ "exists" : true }] } } } ) } // EventBridge Target resource "aws_cloudwatch_event_target" "lambda_eventbridge_target" { rule = aws_cloudwatch_event_rule.lambda_eventbridge_rule.name target_id = "lambda_eventbridge_target" arn = aws_lambda_function.eventbridge.arn } // CloudWatch Lambda Permission resource "aws_lambda_permission" "eventbridge" { statement_id = "AllowExecutionFromCloudWatch" action = "lambda:InvokeFunction" function_name = aws_lambda_function.eventbridge.function_name principal = "events.amazonaws.com" source_arn = aws_cloudwatch_event_rule.lambda_eventbridge_rule.arn }
aws_cloudwatch_event_rule.lambda_eventbridge_rule
のevent_patternに条件となるイベントデータのパターンを書きます。イベントパターンの書き方はAWSの公式ドキュメントにあります。いきなり0からイベントパターンを書くのは難しいので、最初はECSからのすべてのパターンを受け入れてCloudWatchLogsに出力するEventBridgeを設定して、イベントデータの形式を確認しながら進めました。
"exitCode" : [{
"anything-but" : [0]
}],
のところで条件を指定しており、タスクの終了コードが0以外の時にLamdbaが実行されます。
aws_lambda_permission.eventbridge
はTerraformからLambdaの設定を行う際に必要になります。AWSコンソールからEventBridgeを設定する際は自動的に付与されている権限なので意識せずにLambdaを実行できているのですが、Terraformではこの記述をしないとLambdaが実行されません。
Lambdaの実装
Lambdaの実装内容は以下のイメージです。
const Sentry = require("@sentry/aws-serverless"); require('dotenv').config(); Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, }); exports.handler = async (event) => { const reason = event.detail.containers.map(container => container.reason).join(', '); const exitCode = event.detail.containers.map(container => container.exitCode).join(', '); const detail = JSON.stringify(event); Sentry.withScope((scope) => { scope.setExtra("reason", reason); scope.setExtra("exitCode", exitCode); scope.setExtra("detail", detail); Sentry.captureMessage("ECSタスクが異常終了しました"); }); await Sentry.flush(); };
ここでのポイントは await Sentry.flush();
の部分で、この処理が無いとSentryの通知処理が終わる前にLambdaが終了してしまい、Sentryに通知されませんでした。
最後に
実際には検証のためにどうやって異常終了するコンテナを作ってECSで動かすかや、LambdaをServerlessFrameworkで実装するかコンテナで実装するかなど、他にも考えることはありました。最終的には期待通りの通知処理が動かせたので良かったです。今後はこの内容を元に実装をしていきます。
テテマーチでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! herp.careers
エンジニアチームガイドはこちら! tetemarche01.notion.site