見出し画像

StepFunctions × Lambdaでリーズナブルに高頻度外形監視を実装した

SREチームの大竹です。今回は、StepFunctionsとLambdaを使って実装した、コスパの良い外形監視の話をご紹介します!


はじめに:なぜ新しい監視が必要だったのか?

以前から弊社のサービスに対して、CloudWatch Syntheticsを使った外形監視を30分間隔で実施していました。これは30分毎に実際のブラウザ操作をエミュレートして、ログイン処理や各種機能の稼働状況をテストし、証跡としてスクリーンショットの保存まで行えたりする本格的な監視でした。
一方で以下のような課題がありました:

  • 最大30分間のダウンタイムを見逃す可能性がある

  • かといって頻度を上げると月額$500以上のコストが...

💡 そこでStepFunctions × Lambdaの登場!

これらの課題を解決し、従来の外形監視を補完する形で、新しい監視の仕組みを実装しました。

実装のポイント

1.シンプルな構成

  • StepFunctionsで監視、リトライ、通知等のワークフローを管理

  • ログイン処理等は行わずに、トップページの死活監視に絞る

  • 以下のように視覚的にワークフローの設計や、実行状況を確認できる

2.コストを大幅カット

  • サーバレスな仕組みのため高頻度で実施しても非常に安価

  • 従来の仕組みだと月額$500かかるところを月額$2で実現

実装方法

StepFunctionsのワークフロー定義

今回はCDK(TypeScript)で実装しました。 他にも定義の仕方として、

  • マネコンでドラッグ&ドロップで作成

  • JSONで定義

があるのですが、マネコン上だと一部設定できない項目がある点と、 独特なJSONを書くよりよりCDK(TypeScript)で書いた方が型補完が効いて開発しやすいよという点を鑑みて、CDKで実装しました。 (以前JSONで書いたときはドキュメントと睨めっこしながら書いて大分苦労しました…)

  • CDK

   	// 以下ステートマシーンの定義
    const initialize = new sfn.Pass(this, "Initialize", {
      result: sfn.Result.fromObject({
        retry_count: 0,
        urls: props.targetUrls,
      }),
    });

    // ヘルスチェックLambda関数を実行
    const healthCheck = new tasks.LambdaInvoke(this, "HealthCheck", {
      lambdaFunction: healthCheckLambda,
      outputPath: "$.Payload", // Lambda関数の出力からPayload部分を取り出す
    });
    healthCheck.addCatch(
      new sfn.Fail(this, "LambdaError", {
        error: "Lambda Error",
        cause: "Lambda上でエラーが発生しました",
      }),
      {
        resultPath: "$.errorInfo",
      },
    );

    // ヘルスチェックの結果を判定
    const checkResult = new sfn.Choice(this, "CheckResult")
      .when(
        // 成功した場合は通知せずSucceedに遷移
        sfn.Condition.stringEquals("$.result", "OK"),
        new sfn.Succeed(this, "Success"),
      )
      .when(
        // retry_countが3以上の場合はSNS通知
        sfn.Condition.numberGreaterThanEquals("$.retry_count", props.maxRetry),
        new tasks.LambdaInvoke(this, "NotifyFailure", {
          lambdaFunction: notifyFailurePagerDutyLambda,
          payload: sfn.TaskInput.fromObject({
            accountId: this.account,
            executionArn: sfn.JsonPath.stringAt("$$.Execution.Id"),
            lambdaArn: healthCheckLambda.functionArn,
            lambdaLogGroup: healthCheckLambda.logGroup.logGroupName,
            region: this.region,
            result: sfn.JsonPath.stringAt("$.result"),
            failedUrls: sfn.JsonPath.stringAt("$.failed_urls"),
            retryCount: sfn.JsonPath.stringAt("$.retry_count"),
          }),
        }).next(
          // 通知後Failに遷移
          new sfn.Fail(this, "HealthCheckFailed", {
            error: "HealthCheckFailed",
            cause: sfn.JsonPath.stringAt(
              "States.Format('死活監視に失敗しました')",
            ),
          }),
        ),
      )
      .otherwise(
        // 失敗かつ、retry_countが3未満の場合は時間を置いて再度実行
        new sfn.Wait(this, "WaitTime", {
          time: sfn.WaitTime.duration(props.stepFunction.retryInterval),
        }).next(
          new sfn.Pass(this, "RetryHealthCheck", {
            parameters: {
              "urls.$": "$.failed_urls",
              "retry_count.$": "$.retry_count",
            },
          }).next(healthCheck), // 再度healthCheckを実行
        ),
      );
  • 監視&通知用Lambda
    こちらも同じくTypeScriptで実装しました。 本筋とは少し逸れますが、 LambdaをTypeScriptで実装する場合、CDKのNodejsFunctionコンストラクトを使ってリソース作成し、 Lambda関数のコード内でsource-map-supportをimportして実装することで以下の利点を享受できます。

    • デプロイ時にJS→TSのトランスパイルを自動で行ってくれる

    • CloudWatch Logsで、trace logがTSの行番号にマッピングされる

  • CDK

// 死活監視Lambda関数
const healthCheckLambda = new aws_lambda_nodejs.NodejsFunction(
  this,
  "HealthCheckLambda",
  {
    ...
  },
);
  • Lambda関数

import type { Handler } from "aws-lambda";
import "source-map-support/register";

...

export const handler: Handler<Event> = async (
  event: Event,
): Promise<Output | undefined> => {
  ...
};

まとめ

今回は、StepFunctionsとLambdaを組み合わせることで、高頻度な外形監視を低コストで実現することができました。
また単純にコスト削減だけでなく、以下のような価値も得られました

  • 従来より迅速な障害検知の実現

  • StepFunctionsによる視覚的で分かりやすいワークフロー管理

  • サーバーレスならではのメンテナンス性の高さ

この記事が、みなさんのインフラ改善のヒントになれば幸いです。もし似たような課題をお持ちの方がいらっしゃれば、ぜひ参考にしてみてください

いいなと思ったら応援しよう!