はじめに
CloudWatch Alarm の発火をトリガーに、エラーログを Bedrock で要約して Slack に通知するインシデントボットを AWS CDK (TypeScript) + Python Lambda で構築しました。
「アラームが鳴ったら Slack に通知する」だけなら SNS で済みますが、このボットはさらに Bedrock でエラーログを要約して原因と対応手順を生成するところまでやります。
オンコール対応のファーストレスポンスを自動化するイメージです。
この記事では なぜこの連携フローを選んだか と、実際にハマった Bedrock Nova Lite のリクエスト形式の落とし穴 をまとめます。
構成図:
ソースコード: 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で公開してるので良ければ参考にしてください。
