1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS CDK】CloudWatch Alarm → Bedrock → Slack のインシデント自動通知ボットを構築した話

1
Posted at

はじめに

CloudWatch Alarm の発火をトリガーに、エラーログを Bedrock で要約して Slack に通知するインシデントボットを AWS CDK (TypeScript) + Python Lambda で構築しました。

「アラームが鳴ったら Slack に通知する」だけなら SNS で済みますが、このボットはさらに Bedrock でエラーログを要約して原因と対応手順を生成するところまでやります。
オンコール対応のファーストレスポンスを自動化するイメージです。

この記事では なぜこの連携フローを選んだか と、実際にハマった Bedrock Nova Lite のリクエスト形式の落とし穴 をまとめます。

構成図:

v1.0

ソースコード: GitHub

全体フロー

Script(エラーログ投入)
  ↓ ① エラーログ投入
CloudWatch Logs
  ↓ ② Metric Filter(ERROR パターン検知)
CloudWatch Alarm
  ↓ ③ アラーム検知
EventBridge
  ↓ ④ Lambda 実行
Lambda
  │ ⑤ Logs Insights でログ取得(ポーリング)→ CloudWatch Logs
  ↓ ⑥ ログを投入
Bedrock(Nova Lite)
  ↓ ⑦ 要約・対応提案テキストを投稿
Slack(Incoming Webhook)

CDK スタック構成

責務の独立性のため、マルチスタック分割を採用しました。

スタック 変更頻度 独立させる理由
IamStack 低い 権限だけ変更したいときにこのスタックのみ再デプロイできる
MonitoringStack 中程度 Alarm の閾値や Metric Filter のパターンを変えても Lambda を再デプロイしなくて済む
ComputeStack 高い Lambda コードやロジックの変更が最も多い。他スタックに影響を与えずにデプロイできる

単一スタックでも動作しますが、IamStack を分離しておくと最小権限の見直しがしやすく、MonitoringStack を分離しておくと監視設定のチューニングが Lambda の変更と独立します。

設計決定①: エラー発生方法はスクリプトでダミーログ投入

実際のアプリケーションを用意してエラーを発生させる方法もありますが、学習の本題(CloudWatch → Bedrock → Slack の連携)以外の実装が増えるのでスクリプトを作成してエラーを投入する形にしました。

判定 理由
スクリプトで CloudWatch Logs にダミーログ投入 ◎ 採用 本題の連携フローを最短で体験できる
意図的にエラーを吐く Lambda を用意 × Lambda の実装が増え、学習テーマの本題から外れる

スクリプトで put-log-events を叩くだけでも、Metric Filter → Alarm → EventBridge → Lambda の連携フローは同様に体験できます。

スクリプト自体はGitHubに格納してます。

設計決定②: 検知は Metric Filter → CloudWatch Alarm → EventBridge

判定 理由
Metric Filter → CloudWatch Alarm → EventBridge ◎ 採用 実リソース不要でコストを抑えられる。実務フローに近い
CloudWatch Alarm(EC2/ECS のメトリクス閾値超過) × EC2/ECS 等の実リソースを別途用意してメトリクスを発生させる必要がある
EventBridge スケジュール(手動トリガー) × 実務フローと乖離する

CDK での実装:

const metricFilter = new logs.MetricFilter(this, 'ErrorMetricFilter', {
  logGroup,
  metricNamespace: 'IncidentBot',
  metricName: 'ErrorCount',
  filterPattern: logs.FilterPattern.literal('ERROR'),
  metricValue: '1',
  defaultValue: 0,
});

const alarm = new cloudwatch.Alarm(this, 'ErrorAlarm', {
  alarmName: 'incident-bot-error-alarm',
  metric: metricFilter.metric({ statistic: 'Sum', period: cdk.Duration.minutes(1) }),
  threshold: 1,
  evaluationPeriods: 1,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

設計決定③: ログ取得は Logs Insights(Lambda 内ポーリング)

EventBridge のイベントにはログ本文が含まれません。Lambda 内で Logs Insights クエリを発行してログを取得します。

判定 理由
Logs Insights クエリ(Lambda 内ポーリング) ◎ 採用 アラーム発火時刻を基準に直近 N 分のエラーログをまとめて取得できる
Subscription Filter で直接 Lambda を起動 × ログ1件ずつ Lambda が起動するため、前後 N 分のエラーログをまとめて取得できず Bedrock への入力が不足する

Lambda の実装:

def _fetch_error_logs() -> str:
    end_time = int(time.time())
    start_time = end_time - LOOKBACK_MINUTES * 60

    response = logs_client.start_query(
        logGroupName=LOG_GROUP_NAME,
        startTime=start_time,
        endTime=end_time,
        queryString="fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc | limit 20",
    )
    query_id = response["queryId"]

    # クエリ完了までポーリング(最大 30 秒)
    for _ in range(30):
        time.sleep(1)
        result = logs_client.get_query_results(queryId=query_id)
        if result["status"] in ("Complete", "Failed", "Cancelled"):
            break

将来の改造候補として、Lambda 内ポーリング(sleep + ループ)を Step Functions に置き換えることで、待機コストとタイムアウトリスクを解消できます。

設計決定④: Slack 通知は Incoming Webhook

判定 理由
Incoming Webhook ◎ 採用 固定チャンネルへの通知のみが要件。シンプルに実装できる
Slack API(Bot Token) × 複数チャンネルへの動的投稿やメッセージ更新が必要な場合に使う。このボットには過剰

IAM 最小権限設計

Lambda 実行ロールに付与する権限:

new iam.PolicyStatement({
  actions: ['logs:StartQuery', 'logs:GetQueryResults'],
  resources: [
    `arn:aws:logs:${parameter.region}:${parameter.accountId}:log-group:${parameter.logGroup.name}:*`,
  ],
}),
new iam.PolicyStatement({
  actions: ['bedrock:InvokeModel'],
  resources: [
    `arn:aws:bedrock:${parameter.region}::foundation-model/${parameter.bedrock.modelId}`,
  ],
}),

logs:StartQuery / GetQueryResults は対象ロググループの ARN に絞り、bedrock:InvokeModel は Nova Lite のモデル ARN に絞っています。

なお GetQueryResults は IAM の Service Authorization Reference 上ではリソースタイプが log-group です。クエリ ID はリソース ARN として表現できないため、ロググループ ARN でスコープを絞る形になります。

ハマりポイント: Nova Lite の invoke_model リクエスト形式

Bedrock の invoke_model API は Converse API とリクエスト形式が異なります。Nova Lite では以下の形式が正しいです。

body = {
    "schemaVersion": "messages-v1",  # invoke_model では必須
    "messages": [
        {"role": "user", "content": [{"text": prompt}]}  # {"text": ...} のみ
    ],
    "inferenceConfig": {"maxTokens": 512},
}

よくある間違い:

# NG①: content に文字列を渡す
{"role": "user", "content": "テキスト"}
# → expected type: JSONArray, found: String

# NG②: Converse API 形式の {"type": "text", "text": ...} を渡す
{"role": "user", "content": [{"type": "text", "text": "テキスト"}]}
# → extraneous key [type] is not permitted

{"type": "text", "text": ...} 形式は Converse API 専用です。invoke_model では {"text": ...} のみ有効です。また schemaVersion: "messages-v1"invoke_model では必須ですが、Converse API では不要です。

ハマりポイント: LOOKBACK_MINUTES の範囲外にログがある

アラームが ALARM 状態になるまでに数分かかります。ログ投入からアラーム発火まで時間が経過すると、LOOKBACK_MINUTES の範囲外にログが出てしまい「エラーログなし」でスキップされます。

if not logs:
    print("[INFO] エラーログなし。通知をスキップします")
    return {"statusCode": 200, "body": "no logs"}

学習用途では LOOKBACK_MINUTES: 30 程度に余裕を持たせるのが適切です。

Bedrock プロンプト設計

要約・原因・対応手順の3点セットを出力させるプロンプトです:

prompt = (
    "以下は AWS アプリケーションのエラーログです。\n"
    "1. エラーの概要を 2〜3 文で要約してください。\n"
    "2. 考えられる原因を箇条書きで挙げてください。\n"
    "3. 推奨する対応手順を箇条書きで挙げてください。\n\n"
    f"```\n{logs}\n```"
)

Nova Lite はコストが低く(入力 $0.00006 / 出力 $0.00024 / 1,000トークン)、このユースケース(ログの要約・対応提案)に必要な性能を満たしています。

コスト

リソース 概算月額
CloudWatch Logs(保存) $0.033/GB(学習用途はほぼ $0)
Logs Insights クエリ $0.0076/GB スキャン(学習用途はほぼ $0)
Lambda ほぼ $0(無料枠内)
Bedrock Nova Lite 1回あたり数十円未満
EventBridge ほぼ $0($1/100万件)
合計 ほぼ $0〜数十円

動作確認

実際に Slack に届いた通知です。

🚨 アラーム検知: incident-bot-error-alarm

1. エラーの概要
このエラーログは、アプリケーションが S3 バケットへのアップロードとデータベース接続に失敗し、
NullPointerException を投げていることを示しています。また、リクエストの再試行も行われますが、
最大試行回数を超えたため、操作は失敗しています。

2. 考えられる原因
- S3 バケットへのアクセス権限が不足している
- OrderService.processOrder メソッドで NullPointerException が発生している
- データベース接続のタイムアウト
- リクエストの再試行回数が不足している
- ネットワークの問題が原因でリクエストがタイムアウトしている

3. 推奨する対応手順
- S3 バケットのアクセス権限を確認し、必要な権限を付与する
- OrderService.processOrder メソッド内の NullPointerException の原因を調査し修正する
- データベース接続の設定を確認し、タイムアウト値や接続プールのサイズを調整する
- リクエストの再試行回数を増やすか、再試行の間隔を調整する
- ネットワークの安定性を確認し、不安定な場合はリクエストの再試行設定を調整する

アラーム名・エラー概要・原因・対応手順がセットで届くので、ログを掘りに行かなくてもファーストレスポンスに必要な情報が揃っています。

まとめ

CloudWatch → EventBridge → Lambda → Bedrock → Slack の連携パターンは、実務でも頻出の運用自動化パターンです。

設計のポイント

  • Subscription Filter ではなく Logs Insights を使うことで、アラーム発火時点の前後 N 分のエラーログをまとめて Bedrock に渡せる
  • invoke_model と Converse API でリクエスト形式が異なる。Nova Lite では schemaVersion: "messages-v1" が必須
  • LOOKBACK_MINUTES はアラーム発火までの遅延を考慮して余裕を持たせる

将来の改造として、Lambda 内ポーリングを Step Functions に置き換えることで、タイムアウトリスクの解消と処理の可視化が期待できます。

ソースコードはGitHubで公開してるので良ければ参考にしてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?