CloudWatchアラームからLambdaなしでAWS DevOps Agentの調査を起動し、コードの改善案まで出せるか試してみた

CloudWatchアラームからLambdaなしでAWS DevOps Agentの調査を起動し、コードの改善案まで出せるか試してみた

CloudWatchアラームから、Lambdaを使わずにAWS DevOps Agentの調査を起動してみました。EventBridgeのAPI送信先からAPIキー方式のWebhookを直接呼ぶ構成です。ECSのCPUアラームを起点に、ログとGitHubのコードから重い関数を行番号まで特定し、修正案も出ました。
2026.10.07

こんにちは、製造ビジネステクノロジー部のはすとです。

CPU負荷が上がった際などに、AWS DevOps Agentにコードまで見たうえで調査報告をさせたいなと思い調べてみたところ、CloudWatchアラームとDevOps AgentのWebhookのあいだにLambdaを挟む構成が見つかりました。
ただ、Lambdaを使わずにやる方法はないかと思い、検証してみることにしました。

今回は、わざと重い処理を入れたワーカーをECS Fargateで動かし、CPUアラームからLambdaなしで調査を起動して、DevOps Agentがコードを見たうえでの調査報告を出してくれるのかを確かめてみました。

検証環境

  • aws-cdk-lib 2.272.0
  • AWS CDK CLI 2.1144.0
  • リージョン: ap-northeast-1

構成

以下のような流れになります。

構成図。ECS Fargate のワーカーの CPU が上がると CloudWatch アラームが ALARM になり、EventBridge ルールと API 送信先が DevOps Agent の Webhook を呼ぶ。DevOps Agent は CloudWatch と GitHub を読んで調査する

Agent Space、GitHubとの関連付け、ECS、アラーム、EventBridgeは、すべて1つのCDKスタックで作りました。
ただし、CDKで作れない部分が3つあり、そこはコンソールで操作しています。

操作 理由
GitHubの登録 GitHub Appのインストールにブラウザでの承認が必要
汎用Webhookの作成 CloudFormationにWebhookのリソースがない
週次評価の停止 Agent Spaceのリソースに設定項目がない

いずれも2026年10月時点のCloudFormationのスキーマで確認した内容です。

LambdaなしでWebhookを呼ぶ方法

DevOps Agentの汎用Webhookは、作成時に、認証方式をHMACとAPIキーから選べます。

Choose an authentication method: HMAC or API key (bearer token).
— Invoking DevOps Agent through Webhook - AWS DevOps Agent

HMAC方式だと、リクエストごとにタイムスタンプと本文から署名を計算して付ける必要があります。EventBridgeだけでは署名を計算できないので、この方式ではLambdaが必要です。

一方、APIキー方式なら、固定のトークンを Authorization: Bearer <トークン> ヘッダに付けるだけです。
EventBridgeのAPI送信先(API Destination)は、接続(Connection)にAPIキーを持たせてヘッダを付けられます。
そのため、EventBridgeルールのターゲットにAPI送信先を直接指定すれば、Lambdaを挟まずにWebhookを呼べます。

デモ用のワーカー

ワーカーは、注文データをまとめて処理する想定にしました。
1秒ごとに注文を作り、重複を取り除いてから種別を振り分けます。
1回に処理する件数は、環境変数で毎分少しずつ増えるようにしています。

重複除去は、配列を毎回先頭から探す書き方にしています。
件数をnとすると、計算量はO(n²)です。

app/worker.js
// 重複した注文を取り除く
function dedupeOrders(orders) {
  const unique = [];
  for (const order of orders) {
    if (!unique.some((o) => o.orderId === order.orderId)) {
      unique.push(order);
    }
  }
  return unique;
}

// 注文 ID から種別を取り出す
function classifyOrders(orders) {
  return orders.map((order) => {
    const pattern = new RegExp('^(\\d{10})_(\\d+)_([A-Za-z]+)$');
    const match = pattern.exec(order.orderId);
    return { ...order, category: match ? match[3] : 'unknown' };
  });
}

ログには、1回の処理にかかった時間を処理ごとに出しています。

app/worker.js
log({
  msg: 'batch processed',
  batchSize,
  uniqueCount: classified.length,
  durationMs: t3 - started,
  stepMs: { generateOrders: t1 - started, dedupeOrders: t2 - t1, classifyOrders: t3 - t2 },
});

手元のMacで動かすと、件数が3,000件で20ミリ秒、15,000件で240ミリ秒でした。
件数が5倍になると時間は約12倍になり、件数に比例するよりも速く増えています。

CDKで定義したもの

Agent Spaceと関連付け

Agent Spaceには、DevOps Agentが引き受けるIAMロールが2つ必要です。
1つは監視対象のアカウントを読むためのロールで、もう1つはWeb App(Operator App)用のロールです。
Agent SpaceのARNは作成後に決まるので、信頼ポリシーの条件はワイルドカードにしています。

lib/cpu-demo-stack.ts
const agentSpaceArnPattern = `arn:aws:aidevops:${this.region}:${this.account}:agentspace/*`;

const agentRole = new iam.Role(this, 'AgentSpaceRole', {
  assumedBy: new iam.ServicePrincipal('aidevops.amazonaws.com', {
    conditions: {
      StringEquals: { 'aws:SourceAccount': this.account },
      ArnLike: { 'aws:SourceArn': agentSpaceArnPattern },
    },
  }),
  managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AIDevOpsAgentAccessPolicy')],
});

const operatorRole = new iam.Role(this, 'OperatorAppRole', {
  assumedBy: new iam.ServicePrincipal('aidevops.amazonaws.com', {
    conditions: {
      StringEquals: { 'aws:SourceAccount': this.account },
      ArnLike: { 'aws:SourceArn': agentSpaceArnPattern },
    },
  }).withSessionTags(),
  managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AIDevOpsOperatorAppAccessPolicy')],
});

const agentSpace = new devopsagent.CfnAgentSpace(this, 'AgentSpace', {
  name: 'cpu-demo-space',
  locale: 'ja',
  operatorApp: { iam: { operatorAppRoleArn: operatorRole.roleArn } },
});

監視対象のアカウントとGitHubのリポジトリは、どちらも CfnAssociation で関連付けます。
AWSアカウントの serviceId は 'aws' 固定です。
GitHubの serviceId は、コンソールでGitHubを登録したときに発行されるIDを使います。

lib/cpu-demo-stack.ts
new devopsagent.CfnAssociation(this, 'AwsMonitorAssociation', {
  agentSpaceId: agentSpace.attrAgentSpaceId,
  serviceId: 'aws',
  configuration: {
    aws: { accountId: this.account, accountType: 'monitor', assumableRoleArn: agentRole.roleArn },
  },
});

new devopsagent.CfnAssociation(this, 'GitHubAssociation', {
  agentSpaceId: agentSpace.attrAgentSpaceId,
  serviceId: '<GitHub 登録時に発行された ID>',
  configuration: {
    gitHub: {
      owner: '<GitHub のユーザー名>',
      ownerType: 'user',
      repoName: '<リポジトリ名>',
      repoId: '<リポジトリの数値 ID>',
    },
  },
});

リポジトリの数値IDは gh コマンドで取れます。

gh api repos/<ユーザー名>/<リポジトリ名> --jq '{id, private}'
{"id":<リポジトリの数値 ID>,"private":true}

アラームからWebhookまで

CPUアラームは、ECSサービスのCPU使用率が70%を3分連続で超えたらALARMになるようにしました。

lib/cpu-demo-stack.ts
const cpuAlarm = new cloudwatch.Alarm(this, 'WorkerCpuHighAlarm', {
  alarmName: 'devops-agent-cpu-demo-worker-cpu-high',
  metric: service.metricCpuUtilization({ period: cdk.Duration.minutes(1) }),
  threshold: 70,
  evaluationPeriods: 3,
  datapointsToAlarm: 3,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

Webhookのトークンは、コードには書かずSecrets Managerに保存しました。
Bearer を付けた形で保存しておくと、接続がその値をそのままAuthorizationヘッダに入れて送れます。

EventBridgeルールは、対象のアラームがALARMになったイベントだけを拾います。
アラームのイベントはそのままではWebhookの形式と合わないので、入力トランスフォーマーで組み替えています。
入力トランスフォーマーは、元のイベントからJSONPathで値を取り出し、テンプレートに埋め込んでターゲットに渡すEventBridgeの機能です。
CDKでは、RuleTargetInput.fromObject() の中で EventField.fromPath() を使うと設定されます。

lib/cpu-demo-stack.ts
// 接続: Webhook を呼ぶときの認証情報を持つ
// Secrets Manager に保存した「Bearer <トークン>」を、Authorization ヘッダの値として付ける
const connection = new events.Connection(this, 'DevOpsAgentWebhookConnection', {
  authorization: events.Authorization.apiKey(
    'Authorization', // ヘッダ名
    cdk.SecretValue.secretsManager('devops-agent-cpu-demo/webhook-token'), // ヘッダの値
  ),
});

// API 送信先: 呼び出す先の URL と HTTP メソッド
const destination = new events.ApiDestination(this, 'DevOpsAgentWebhookDestination', {
  connection,
  endpoint: '<Webhook の URL>',
  httpMethod: events.HttpMethod.POST,
  rateLimitPerSecond: 1, // 1 秒あたりの呼び出し回数の上限
});

new events.Rule(this, 'CpuAlarmToDevOpsAgentRule', {
  // 拾うイベント: 対象のアラームが ALARM に変わったときだけ
  eventPattern: {
    source: ['aws.cloudwatch'],
    detailType: ['CloudWatch Alarm State Change'],
    detail: { alarmName: [cpuAlarm.alarmName], state: { value: ['ALARM'] } },
  },
  targets: [
    new targets.ApiDestination(destination, {
      // ここが入力トランスフォーマーになる
      // EventField の部分は、送るときにアラームのイベントの値に置き換わる
      event: events.RuleTargetInput.fromObject({
        eventType: 'incident', // Webhook の決まりで固定値
        incidentId: events.EventField.eventId, // イベントの id。アラームごとに一意になる
        action: 'created',
        priority: 'MEDIUM',
        title: `CPU 使用率の上昇: ${events.EventField.fromPath('$.detail.alarmName')}`, // アラーム名
        // エージェントへの指示はここに書く(届くのは title、description、priority とインシデントの参照だけ)
        description:
          `ECS サービス ${service.serviceName} の CPU 使用率がしきい値を超えました。` + // サービス名はデプロイ時に決まる
          `アラーム理由: ${events.EventField.fromPath('$.detail.state.reason')}。` + // ALARM になった理由の文
          'CloudWatch Logs のワーカーログと GitHub リポジトリのソースコードを突き合わせ、' +
          'CPU を消費している関数とコード箇所を特定してください。そのうえで、具体的なリファクタリング案(修正後のコード例を含む)を提示してください。',
        timestamp: events.EventField.time, // イベントの発生時刻
        service: 'devops-agent-cpu-demo',
      }),
    }),
  ],
});

コードを見て改善案を出すよう指示する文は、description に入れています。
Webhookのペイロードには data という自由欄もありますが、ここに書いてもエージェントには届きません。

The webhook accepts data, but doesn't include its contents in the investigation context. Only title, description, priority, and the incident reference reach the agent.
— Invoking DevOps Agent through Webhook - AWS DevOps Agent

和訳すると「Webhookは data を受け付けますが、その中身は調査の文脈に含めません。エージェントに届くのは、title、description、priorityと、インシデントの参照だけです」となります。

Webhookの疎通確認

念の為、アラームとつなぐ前に、Webhookを curl で1回叩きました。
APIキー方式のドキュメントの例には x-amzn-event-timestamp ヘッダが付いていますが、EventBridgeのAPI送信先はこのヘッダを付けないため、外します。

# Secrets Manager から「Bearer <トークン>」を読む
AUTH=$(aws secretsmanager get-secret-value \
  --region ap-northeast-1 \
  --secret-id devops-agent-cpu-demo/webhook-token \
  --query SecretString --output text)

# incidentId は送るたびに変える(同じ値は重複として捨てられる)
curl -X POST "<Webhook の URL>" \
  -H "Content-Type: application/json" \
  -H "Authorization: $AUTH" \
  -w "\nHTTP %{http_code}\n" \
  -d '{
    "eventType": "incident",
    "incidentId": "curl-test-001",
    "action": "created",
    "priority": "LOW",
    "title": "Webhook 疎通テスト",
    "description": "Webhook の疎通確認です。",
    "service": "devops-agent-cpu-demo"
  }'
{"message": "Webhook received"}
HTTP 200

Web Appの「インシデントレスポンス」に、「Webhook 疎通テスト」という調査が作られました。

Web App のインシデントレスポンスの一覧。左メニューの「インシデントレスポンス」と、「Webhook 疎通テスト」の行を囲んでいる

実際に動かしてみた

ワーカーは21:33に起動しました。
CPU使用率は起動直後から30%ほどあり、件数が増えるにつれて毎分少しずつ上がりました。

時刻 CPU使用率(1分平均)
21:34 31%
21:38 46%
21:42 66%
21:44 71%
21:46 81%
21:48 93%
21:50 100%

起動から11分ほどで70%を超え、その後100%に達しました。

CloudWatchのアラームの画面では、CPU使用率のグラフの下に、ALARMだった期間が赤い帯で表示されます。
帯の終わりは、調査のあとにワーカーを止めてOKに戻った時刻です。

CloudWatch のアラーム画面。CPU 使用率が 21:33 ごろから少しずつ上がって 100% に達し、下の帯で 21:50 から 22:20 ごろまでが ALARM になっている

そこからの流れは、次のとおりです。

時刻 出来事
21:49:53 アラームがALARMになる
21:49:54 EventBridgeルールが1回動き、Webhookを呼ぶ(失敗0件)
21:49:54 調査「CPU 使用率の上昇: devops-agent-cpu-demo-worker-cpu-high」が始まる
22:06:04 調査が終わる

アラームがALARMになってから1秒で調査が始まり、約16分で終わりました。

調査の様子は、Agent SpaceのWeb App(https://<Agent Space ID>.aidevops.global.app.aws/)で確認できます。
左メニューの「インシデントレスポンス」を開くと、調査の一覧が表示されます。
Webhookから起動した調査は、「以下によってトリガーされます」の列が「Event Channel」になっていました。

Web App のインシデントレスポンスの一覧。左メニューの「インシデントレスポンス」と、アラーム起点の調査の行を囲んでいる

一覧の行を選ぶと、調査の詳細が開きます。
詳細には「概要」「調査タイムライン」「根本原因」「緩和計画」の4つのタブがあります。
「調査タイムライン」では、エージェントが何を考えてどのツールを呼んだかが、開始からの経過時間つきで並んでいます。
先頭には、Webhookで送ったtitleとdescriptionがそのまま表示されていました。

調査の詳細画面。4 つのタブを囲み、調査タイムラインを開いている。アカウント ID やリソース名は伏せている

調査レポートの中身

エージェントは、ワーカーのログ、ソースコード、デプロイ履歴をそれぞれ読む3つのサブエージェントを並行して動かしていました。
3つの結果から、根本原因は dedupeOrders だと特定しています。

「概要」タブには、インシデントの概要、根本原因、緩和策がまとまっています。

調査レポートの概要。根本原因として重複除去の関数と行番号が挙がっている

レポートに挙がったCPU消費の原因は、次の4つです。

箇所 内容
dedupeOrders(36〜44行目) O(n²)。ログ上、処理時間の99%を占める
currentBatchSize(14〜17行目) 環境変数 RAMP_PER_MIN=300 により、毎分300件ずつ件数が増える
classifyOrders(47〜53行目) 要素ごとに new RegExp() を作り直している
タスク定義のCPU 256(0.25vCPU) 割り当てが小さく、上限に早く達する

行番号は、リポジトリのコードと一致していました。
ログの裏付けとして、件数が6,005件のときは dedupeOrders が290ミリ秒、11,524件のときは1,195ミリ秒だったことが挙げられています。

改善案は、すぐできる緩和策と恒久対策の2段に分かれていました。
緩和策は、環境変数を RAMP_PER_MIN=0、BATCH_SIZE=3000 にしたタスク定義を新しく登録し、サービスを更新する案です。
準備、事前確認、適用、事後確認、切り戻しのECSのコマンドまで付いています。
CPUやメモリを増やす案は「飽和を先送りするだけ」として、件数の増加を止めるほうを優先していました。
「緩和計画」タブを開くと、手順がステップごとに並び、コマンドはそのままコピーできるようになっていました。

緩和計画タブ。即時緩和のステップ 1 として、現行のタスク定義をバックアップする ECS のコマンドが並んでいる

恒久対策は、修正の要件と受け入れ条件の形でまとまっていました。
修正後のコードは、調査タイムラインにある、緩和策を考えるサブエージェントの出力に含まれていました。

function dedupeOrders(orders) {
  const seen = new Set();
  const unique = [];
  for (const order of orders) {
    if (!seen.has(order.orderId)) {
      seen.add(order.orderId);
      unique.push(order);
    }
  }
  return unique;
}

受け入れ条件として、「重複排除の結果が従来の実装と同じであること(最初に出てきたものを残す)」も添えられていました。
また、戻せる正常なリビジョンがないとして、ロールバックではなく、環境変数で件数の増加を止める方針にしていました。

実際のサービスにそのまま当てはまるとは言えない

今回の環境には、答えを見つけやすい条件がありました。

1つめは、コードの書き方です。
注文を2割重複させて作る処理や、件数を毎分増やす環境変数から、エージェントは「意図的な重複設計」「意図的な負荷試験」と読み取っていました。
リポジトリ名やスタック名にも cpu-demo という語が入っています。

2つめは、ログの粒度です。
今回のワーカーは処理ごとの所要時間をログに出しているので、どの関数が重いかがログだけでほぼ分かります。
実際のサービスで、ここまで細かいログを出していることは多くないと思います。

気をつけること

  • CloudFormationで作れない部分がある: GitHubの登録、汎用Webhookの作成、週次評価の停止はコンソールで操作します(2026年10月時点)
  • 週次評価は既定で有効: Agent Spaceを作ると、週1回の評価が自動で登録されます。検証用のスペースなら、Web Appの「改善事項」ページで「スケジュールを一時停止」にしておくと、意図しない課金を防げます
  • GitHub Appのリポジトリ範囲: GitHub Appのインストール時に「All repositories」を選ぶと、すべてのリポジトリへの読み書きを許可することになるので、リポジトリは絞っておくのが安全です。後から変更したい場合は、GitHubのSettings > Applications > Installed GitHub Appsで絞れます

まとめ

結果、CloudWatchアラームから、LambdaなしでDevOps Agentの調査を起動できました。
APIキー方式のWebhookを選べば、EventBridgeのAPI送信先から直接呼べます。
エージェントはGitHubのコードとログを突き合わせ、重い関数を行番号まで特定し、修正後のコードまで出してくれました。

一方で、今回はログが細かく、コードの書き方からも答えを推測しやすかったので、実際のプロダクト環境ではskill等で工夫が必要かもしれません。

アラームの調査をコードまでつなげたい方の参考になれば幸いです。

参考

この記事をシェアする

関連記事