人間に優しくないjson形式の通知をBedrockで可読化する
はじめに
AWS環境を運用していると、セキュリティや運用の観点からいろんな監視サービスを利用しますよね。InspectorやConfig Rules、Security Hub、GuardDutyなどなど…。
でも、これらのサービスを有効化しただけじゃ意味がなくて、当然検知したことに気づかなければなりません。もちろんproduction環境なら当然の運用設計ですが、私が開発で利用しているAWSアカウントはそこまでパワーをかける必要もないので、とりあえずEventBridge経由でSNSに通知を送り、最終的にメールを受信して気付けるようにはしています。
ところが、実際に届くメールって人間が読むには辛すぎるJSONの塊なんですよね…
結果として、通知は来てるけど内容を把握するのに時間がかかるし、場合によっては読み間違えたり、重要な通知を見落とすことにもなりかねません。
従来のアプローチとその課題
この課題の解決策のひとつは、SNSとメール送信の間にLambdaを挟んで、JSONを人間が読みやすい形式に成形してから送る、とかですよね。少し調べると関連記事もあるので、悪くない手なんだと思います。
ただ面倒なところもあって…
-
結局読みにくい
- 単純なテキストの調整だけだと情報の濃淡を表現しづらく、目が滑りやすい点にあまり変わりはない。
-
サービスごとに処理を書く必要がある
- かといって表現をリッチにしようとしてもサービスによってJSONスキーマが違うので、それぞれに対応したパース処理を書かないといけない。スキーマが変わる可能性もある
-
保守が面倒
- その結果、新しいサービスを追加するたびにコードをいじる必要が出てきて、メンテが大変に…
そんなこんなで「とりあえずインデントと改行だけでも調整するLambdaを書くか…?」と思いながらも、重い腰が上がらずにいたわけですが、昨日ふと「あ、生成AI使えばいいんじゃね?」という天啓を得たわけです。
生成AIを活用した新しいアプローチ
いまさらですけど、LLMは構造化されていないデータから意味を抽出して、人間が理解しやすい形で表現するのが得意ですよね。(まぁ今回はインプットもちゃんと構造化されてるんですけど。)
この特性を活かせば、簡単に解決できるんじゃないかと思い、こんなアーキテクチャを組んでみました。
詳細はこのあと説明しますが、SNSまでの流れは同じです。そのあとでBedrockを呼ぶ必要があるわけですが、あんまりコードを増やしたくなかったのでサービス間統合はStep Functionsに任せる構成にしました。
実装の詳細
Lambda関数
ただの橋渡し役です、土管です。
import json
import boto3
def lambda_handler(event, context):
stepfunctions = boto3.client('stepfunctions')
# SNSメッセージを取得
for record in event['Records']:
sns_message = record['Sns']['Message']
# Step Functions実行
response = stepfunctions.start_execution(
stateMachineArn='<StateMachineのARN>',
input=json.dumps(sns_message)
)
return {'statusCode': 200}
SNSから受け取ったペイロードをそのままStep Functionsに渡すだけです。ロジックをStep Functionsに集約することで、保守しやすくすることにしました。
Step Functions
普段そんなにsFnを触らないけど、コードを書かずにAWSサービスを統合できるし、パラメータは変数とプレイスホルダーでいい感じにできるし、ログなんかもオプション有効化するだけでいいから便利〜〜。
というわけでこんな感じのStateMachineになりました。
{
"Comment": "Dynamic email with Bedrock",
"QueryLanguage": "JSONata",
"StartAt": "GenerateContent",
"States": {
"GenerateContent": {
"Type": "Task",
"Resource": "arn:aws:states:::bedrock:invokeModel",
"Arguments": {
"ModelId": "<inference-profileのARN>",
"Body": {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "{% 'あなたは優秀な AWS 運用オペレータです。あなたには以下の Json フォーマットで提供された AWS の通知を上司に報告する義務があります。json フォーマットの情報を人間が理解しやすい HTML フォーマットの表形式に変換して提出してください。さらに、あなたの見解も追加してください。あなたの出力結果は HTML フォーマットのメールの本文になるため、マークダウン記法を使ってはいけません。必ず HTML フォーマットのみで構成してください。\\n\\n' & $string($states.input) & '\\n\\n回答は必ず次のJSON形式で返してください。それ以外のコメントは不要です。:\\n{\"title\": \"メールタイトル\", \"report\": \"HTMLレポート内容\"}' %}"
}
]
}
]
}
},
"Assign": {
"bedrockResponse": "{% $states.result.Body.content[0].text %}",
"originalInput": "{% $states.input %}"
},
"Next": "ParseResponse"
},
"ParseResponse": {
"Type": "Pass",
"Assign": {
"parsedResponse": "{% $parse($bedrockResponse) %}",
"emailTitle": "{% $parse($bedrockResponse).title %}",
"emailBody": "{% $parse($bedrockResponse).report %}"
},
"Next": "SendEmail"
},
"SendEmail": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:ses:sendEmail",
"Arguments": {
"Destination": {
"ToAddresses": [
"<通知先のアドレス>"
]
},
"Message": {
"Body": {
"Html": {
"Charset": "UTF-8",
"Data": "{% $emailBody %}"
}
},
"Subject": {
"Data": "{% $emailTitle %}"
}
},
"Source": "<送信元のアドレス>"
},
"End": true
}
}
}
Jsonだとわかりづらいと思いますが、マネコンで見るとこう。
Bedrockで推論して、レスポンスを変数に詰めて、SESでメールを送る。
たったの3 stateで終わるシンプルさです。
Tips的な備忘メモ
JSONata式の活用
place holderを利用した変数の置換はいいですね。
{% %}
記法でテンプレート内に動的な値を埋め込めたし、マネコンで編集する際は補完もしてくれるので編集しやすいです。
プロンプトエンジニアリング
大して試行錯誤してないですが、「運用オペレータとして上司に報告する」という設定が良かったのかな。メール本文でマークダウン書かれるとさげぽよだったので、マークダウン記法の禁止は後から明記しました。あとはアウトプットをjsonに指定した点ですかね。これで一つのStateでSendEmailに必要なパラメータを複数個渡せました、Pass state便利ですね。ちなみに今回はClaude 3.7 Sonnetを使いました。
実際の動作結果
実際に動かしてみた結果がこちらです。マスクもしていますが、一応数年前に来ていたふる〜い通知を掘り起こしてテストデータに利用しました。
マスキングや補足説明が面倒で、 一部分だけのスクショですが、かなり見やすい!助かる〜〜!
従来のJSONベタ書きメールと比べて、以下の点が改善されました。
- 読みやすさが段違い : 表形式で整理されて、ほんと見やすい。
- 重要度が分かりやすい : 色分けやアイコンで視覚的に重要度を表現できる。
- サマった報告文章もGood : 背景や推奨事項も推論してくれるので、考えるヒントになる。
いや〜よかったよかった。
この仕組みの良いところ
冒頭のほうで書いた課題も概ねクリアできる良い構成ができたんじゃないかと思います。
-
今後のサービス追加にも対応できる
- どんなJSONスキーマでも適切に解釈・変換してくれるはず。上記の例以外にConfig Ruleでも試しましたが、問題なく報告してくれました。
-
運用の負担を軽減できる
- ハルシネーションの可能性もあるとはいえ、事象と考察を文章で説明しくれるのはありがたい。
-
保守しやすいアーキテクチャ
- ザ・マイクロサービス繋げましたって感じなので、メンテはしやすいはず。今後ファンアウトもしやすいし。Lambdaに全部詰め込まず、Step Functionsにして正解でした。
-
コストも安い
- 通知の量とか要因はたくさんあると思いますが、少なくともフルサーバレスなのでアイドル中の課金はないです。
まとめ
「面倒だから後回し」にしがちな運用改善も、生成AIを使えば意外とサクッと解決できることが分かりましたね!特に今回のように「人間が読みやすい形に変換する」系のタスクはLLMの得意分野そのものですね。というかもっと早くやればよかった、灯台下暗しでした。
今後時間があれば、検知対象のリソースの現在の状態なんかも合わせてレポートできるようにしてみたいですね。
こうやってちょっとずつでも生成AIによる業務効率化を取り入れて、本来の業務にもっと時間が割けると良いですね。
Discussion