AWS DevOps Agent で CloudWatch アラームをトリガーに自動インシデント調査を行えるか試してみた #AWSreInvent

AWS DevOps Agent で CloudWatch アラームをトリガーに自動インシデント調査を行えるか試してみた #AWSreInvent

2025.12.10

こんにちは。オペレーション部のしいなです。

はじめに

注目されているAWS DevOps Agentは、各種オブザーバビリティツールと連携し、根本原因の分析だけでなく、予防的な改善提案まで行ってくれる自律型の自動化エージェントです。
先日 Datadog と連携した自動インシデント調査をやってみました。
https://dev.classmethod.jp/articles/aws-devops-agent-datadog-automated-incident-investigation/

SaaS 連携のみならず、CloudWatch アラームをトリガーに自動インシデント調査もできたら便利ですよね。
CloudWatch には CloudWatch investigations といった強力な機能もありますが、AWS DevOps Agent を組み合わせることで、原因特定にとどまらず、再発防止や緩和策の提案まで含めたインシデント対応が可能になります。
今回、Webhook を利用した連携フローを構築し、CloudWatch をトリガーに自動インシデント調査が行えるか検証してみました。

最初に結論

Lambda を挟んで Webhook 連携を行うひと手間は必要ですが、 CloudWatch アラームをトリガーに AWS DevOps Agent で自動インシデント調査を実行することは可能です。

概要

CloudWatch アラームのアクションとして Lambda 関数を呼び出し、その Lambda から AWS DevOps Agent の Webhook へアラーム情報を連携します。
これにより、アラーム発生をトリガーに自動でインシデント調査を開始させます。

セットアップ

前提

  • AWS DevOps Agent の AgentSpace 作成済み

1. Webhook 設定

  1. マネジメントコンソールより AWS DevOps Agent を選択し、[Agent Spaces]をクリックします。

  2. 対象の Agent Space の[View details]をクリックし、Capabilities タブを選択します。

  3. Webhook より [Add] をクリックします。
    1-1

  4. データスキーマと HMAC の説明がガイダンスが表示されますので、[次へ]で進みます。
    1-2

  5. [Generate URL and secret key]をクリックします。
    1-3

  6. Webhook URL と Secret Key を控えます。

  7. "I’ve saved and stored my URL and secret key"にチェックを入れて[Add]をクリックします。
    1-4

2. Lambda 関数の作成

  1. マネジメントコンソールより Lambda を選択し、[関数の作成]をクリックします。
  2. 以下を指定して関数の作成を行います。
  • 関数名:任意の値(例:CloudWatchWebhook
  • ランタイム:Node.js 24.x
  • アクセス許可:デフォルトのまま
  • アーキテクチャ:x86_64
  • その他の設定:デフォルトのまま
    2-1
  1. コードソースに以下サンプルコードをコピー&ペーストし、デプロイを行います。
index.mjs
// index.mjs
import { createHmac } from "node:crypto";
import https from "node:https";
import { URL } from "node:url";

const EVENT_AI_WEBHOOK_URL = process.env.EVENT_AI_WEBHOOK_URL;
const EVENT_AI_SECRET = process.env.EVENT_AI_SECRET;

export const handler = async (event) => {
  try {
    console.log("Raw CloudWatch Alarm event:", JSON.stringify(event, null, 2));

    if (event.source !== "aws.cloudwatch" || !event.alarmData) {
      console.log("Unsupported event, skipping:", event.source);
      return { statusCode: 200, body: "Ignored" };
    }

    const alarmData = event.alarmData;
    const alarmName = alarmData.alarmName || "UnknownAlarm";
    const state = alarmData.state || {};
    const stateValue = state.value || "UNKNOWN"; // ALARM | OK | INSUFFICIENT_DATA
    const reason = state.reason || "";
    const reasonData = state.reasonData || "";
    const alarmArn =
      event.alarmArn ||
      `arn:aws:cloudwatch:${event.region}:${event.accountId}:alarm:${alarmName}`;

    // InstanceId 抽出(取得できなければスキップ)
    let instanceId = "";
    try {
      const metrics = alarmData.configuration?.metrics || [];
      const metric = metrics[0];
      const dims = metric?.metricStat?.metric?.dimensions;
      if (dims && typeof dims.InstanceId === "string") {
        instanceId = dims.InstanceId;
      }
    } catch {
       // noop
    }

    // description を組み立て:
    // 既存の reason に加え region, InstanceId, reasonData を連結
    const descriptionLines = [];

    if (reason) {
      descriptionLines.push(`Reason: ${reason}`);
    }

    if (event.region) {
      descriptionLines.push(`Region: ${event.region}`);
    }

    if (instanceId) {
      descriptionLines.push(`InstanceId: ${instanceId}`);
    }

    if (reasonData) {
      descriptionLines.push(`ReasonData: ${reasonData}`);
    }

    const description = descriptionLines.join("\n");

    // action マッピング
    let action = "updated";
    if (stateValue === "ALARM") action = "created";
    else if (stateValue === "OK") action = "resolved";

    const priority = "HIGH";

    let timestamp;
    try {
      timestamp = new Date(event.time).toISOString();
    } catch {
      timestamp = new Date().toISOString();
    }

    const payload = {
      eventType: "incident",
      incidentId: alarmArn,
      action,
      priority,
      title: `CloudWatch Alarm: ${alarmName} is ${stateValue}`,
      description,
      timestamp,
      service: "cloudwatch",
      data: alarmData
    };

    const payloadString = JSON.stringify(payload);
    const tsHeader = new Date().toISOString();

    // HMAC-SHA256(`${timestamp}:${payload}`) → base64
    const hmac = createHmac("sha256", EVENT_AI_SECRET);
    hmac.update(`${tsHeader}:${payloadString}`, "utf8");
    const signature = hmac.digest("base64");

    const url = new URL(EVENT_AI_WEBHOOK_URL);

    const options = {
      hostname: url.hostname,
      path: url.pathname + url.search,
      method: "POST",
      port: url.port || 443,
      headers: {
        "Content-Type": "application/json",
        "x-amzn-event-timestamp": tsHeader,
        "x-amzn-event-signature": signature,
        "Content-Length": Buffer.byteLength(payloadString)
      }
    };

    console.log("Sending payload to event-ai:", payload);

    const response = await new Promise((resolve, reject) => {
      const req = https.request(options, (res) => {
        let data = "";
        res.on("data", (chunk) => {
          data += chunk;
        });
        res.on("end", () => {
          resolve({
            statusCode: res.statusCode || 200,
            body: data
          });
        });
      });

      req.on("error", (err) => reject(err));
      req.write(payloadString);
      req.end();
    });

    console.log("event-ai response:", response.statusCode, response.body);
    return response;
  } catch (err) {
    console.error("Error in CloudWatch Alarm → event-ai handler:", err);
    return {
      statusCode: 500,
      body: "Internal server error"
    };
  }
};
  1. 設定の一般設定タブにて、必要に応じてタイムアウト値を調整します。今回は30秒にしました。
    2-2

  2. 設定のアクセス権限タブにてリソースベースのポリシーステートメントの[アクセス権限を追加]をクリックします。
    2-3

  3. 以下を指定してアクセス権限の追加を行います。

  • ステートメントID:任意の値(例:cloudwatchinvoke
  • プリンシパル:lambda.alarms.cloudwatch.amazonaws.com
  • アクション:lambda:InvokeFunction
    2-4
  1. 設定の環境変数タブより以下の環境変数を追加します。
  • EVENT_AI_WEBHOOK_URL:Webhook 設定時に生成した Webhook URL
  • EVENT_AI_SECRET:Webhook 設定時に生成した secret key
    2-5

3. CloudWatch アラーム設定

  1. マネジメントコンソールより CloudWatch を選択し、すべてのアラームを選択します。

  2. [アラームの作成]をクリックします。

  3. アラームの設定を行うメトリクスを選択します。今回は CPUUtilization としました。

  4. 条件を指定の上、次へをクリックします。
    3-1

  5. Lambda アクションの追加を行い、以下を指定します。

  • アラーム状態トリガー:アラーム状態
  • 関数タイプ:サインインしたアカウントから Lambda 関数を選択
  • 関数を選択:作成した関数名を指定(例:CloudWatchWebhook
    3-2
  1. 任意のアラーム名を指定の上、アラームを作成します。

試してみた

実際に CloudWatch アラームをアラーム状態に変化させ、Lambda アクションを実行し、Webhook 経由で自動インシデント調査が動くかを見てみます。

CPU 負荷を与える

セッションマネージャーから EC2 インスタンスへログインし、stress コマンドで CPU 負荷をかけます。

stress -c 4

アラーム状態へ変化

CloudWatch アラームの状態を確認すると、アラーム状態となったことが確認できます。
履歴タブより Lambda アクションが実行されたことが確認できます。
4-0-ex

インシデント調査の確認

DevOps Agent コンソールでインシデント調査一覧を確認します。
「CloudWatch Alarm:〜」で始まる調査が 「In progress」 になっており、自動調査が進行中であることが確認できます。
4-1

調査結果の確認

どのような調査が行われたのか、詳細を見ていきます。
※スクリーンショットはブラウザの翻訳機能を使用しています。

  • Webhook で連携された CloudWatch アラームの情報をユーザリクエストとして受け取っています。
    5-1

  • CPU使用率が高い症状から調査計画が作成されました。
    5-2

  • インフラ変更を調査が進んでいます。
    5-3

  • セッションマネージャーでプロセスを開始したことが検出されました。
    5-4

調査が完了すると、結論が提示されます。

「インスタンスの起動直後にSSMセッションを介してCPUを集中的に使用するプロセスが開始されました」

的中です。
CloudWatch アラームを起点にしたインシデント調査でも、障害の状況と原因をかなり正確に言い当ててくれました。

根本原因も見てみましょう。
5-5-ex

セッションマネージャのセッション確立後、ネットワークトラフィックやディスク I/O は留まっている観察が得られています。
一方で CPU は持続的に使用されている洞察よりストレステストや無限ループの可能性を見抜いた形で示されていますね。

調査のギャップも示されています。
セッションマネージャのログを CloudWatch ログに記録する設定を行っておくと、原因となったコマンドレベルまで明らかになってくるものと想定されます。

まとめ

CloudWatch アラームに Lambda を組み合わせることで、AWS DevOps Agent の Webhook を介した自動インシデント調査が実現できました。
CPU 高騰のような障害シナリオでも、しっかりと原因の特定が行えていました。
既存の CloudWatch ベース監視に少し手を加えるだけで導入できるため、試してみる価値は高いと思います。
深掘り調査とインサイトに AWS DevOps Agent を利用するのもありだと思います。

本記事が参考になれば幸いです。

#AWSreInvent

参考

https://docs.aws.amazon.com/devopsagent/latest/userguide/configuring-capabilities-webhook-configuration.html

この記事をシェアする

FacebookHatena blogX

関連記事