「CloudWatch Log Analytics」でCloudTrailからSecurity Hubまでのログを横断分析してみよう
第28回の今回は、「CloudWatch Log Analytics」でCloudTrail・Container Insights・GuardDuty・Security Hubのログを横断的に検索・集計する実践クエリについて解説します。
6:30
目次
- はじめに
- CloudWatch Log Analyticsとは
- 料金と課金抑制の実践
- クエリ言語の基本
- CloudTrailログの分析
- Container Insightsログの分析
- エラーログの検索
- Pod別ログ量ランキング
- HTTP status別のリクエスト集計
以下の2つのクエリは、アプリケーションが
http_statusとresponse_time_msをJSONフィールドで出力している想定です。テキストログの場合はparse @message "..." as ...でこれらのフィールドを抽出してからフィルタします。 ステータスコード別のリクエスト件数を集計します。fields @timestamp, http_status | stats count(*) as request_count by http_status | sort http_status asc5xxだけを抽出したい場合はfilter http_status >= 500を追加します。急激な増加はデプロイ直後の異常検知に使えます。レスポンスタイムの分位数集計 平均値だけでなく、分位数(パーセンタイル)を見ることで外れ値の影響を分離します。fields @timestamp, response_time_ms | stats avg(response_time_ms) as avg_ms, pct(response_time_ms, 50) as p50, pct(response_time_ms, 95) as p95, pct(response_time_ms, 99) as p99pct(<field>, <n>)はn分位数を返す関数で、p95/p99はユーザー体感に近い指標です。p95やp99がavgより大きく離れている場合は、少数の遅いリクエストが分布の裾を伸ばしている状態を示します。 GuardDutyとSecurity Hub CSPM検出結果の分析 GuardDutyとSecurity Hub CSPMは、標準ではCloudWatch Logsに検出結果を配信しません。EventBridgeルールにCloudWatch Logsをターゲット追加することで、Log Analyticsから検索可能になります。 検出結果をCloudWatch Logsへ配信する 以下をmain.tfに追加します。# terraform/environments/dev/main.tf resource "aws_cloudwatch_log_group" "security_findings" { name = "/aws/events/security-findings" retention_in_days = 30 } resource "aws_cloudwatch_log_resource_policy" "events_to_logs" { policy_name = "AllowEventBridgeToLogs" policy_document = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = ["events.amazonaws.com", "delivery.logs.amazonaws.com"] } Action = ["logs:CreateLogStream", "logs:PutLogEvents"] Resource = "${aws_cloudwatch_log_group.security_findings.arn}:*" }] }) } resource "aws_cloudwatch_event_rule" "security_findings_to_logs" { name = "security-findings-to-logs" description = "Forward GuardDuty and Security Hub findings to CloudWatch Logs for analysis" event_pattern = jsonencode({ source = ["aws.guardduty", "aws.securityhub"] detail-type = [ "GuardDuty Finding", "Security Hub Findings - Imported" ] }) } resource "aws_cloudwatch_event_target" "security_findings_to_logs" { rule = aws_cloudwatch_event_rule.security_findings_to_logs.name target_id = "SendToCloudWatchLogs" arn = aws_cloudwatch_log_group.security_findings.arn }配信の許可は、aws_cloudwatch_log_resource_policyでevents.amazonaws.comとdelivery.logs.amazonaws.comに付与します。第26回と第27回で作成したEventBridge → SNS通知ルールとは別ルールとして共存する構成で、Slackへの通知はそのまま維持されます。 Log Analyticsでの集計クエリ 配信されたイベントに対して重要度別の件数を集計します。fields @timestamp, source, detail.findings.0.Severity.Label as severity | filter source = "aws.securityhub" | stats count(*) as finding_count by severity | sort finding_count descSecurity Hub CSPMのイベントはdetail.findingsが配列で、通常は1件が格納されます。配列要素はドット記法detail.findings.0とブラケット記法detail.findings[0]のどちらでも参照でき、本記事ではドット記法を使います。 上記のクエリでは配列先頭のSeverity.Labelをseverityとして抽出し、CRITICAL/HIGH/MEDIUM/LOWGuardDutyのイベント構造はSecurity Hubと異なり、/INFORMATIONALの別で件数集計します。detail直下に検出結果が単一オブジェクトとして格納されます。重要度はdetail.severityに0.0〜10.0の数値で入ります。GuardDutyを対象にする場合は次のクエリを使います。fields @timestamp, source, detail.severity, detail.type | filter source = "aws.guardduty" | stats count(*) as finding_count by detail.severity | sort finding_count desc保存クエリと共有 よく使うクエリはLog Analytics画面の「保存」機能でアカウント内に保存でき、右上の「保存されたクエリ」ボタンから開くサイドパネルでチームメンバー全員が呼び出せます。命名規則を「用途_対象ロググループ」の形(例:cloudtrail_root_activity、eks_error_by_pod)で揃えると、増えても探しやすくなります。 Terraformで管理する場合はaws_cloudwatch_query_definitionリソースが使え、環境間で同じクエリセットを再現できます。resource "aws_cloudwatch_query_definition" "cloudtrail_root_activity" { name = "cloudtrail/root_activity" log_group_names = [ "/aws/cloudtrail/organization" ] query_string = -QUERY fields @timestamp, eventName, sourceIPAddress, userAgent | filter userIdentity.type = "Root" | sort @timestamp desc QUERY }nameにスラッシュを含めるとコンソール上で階層フォルダとして表示されるため、対象や用途で分類できます。おわりに CloudTrailの操作履歴、Container Insightsのアプリケーションログ、GuardDutyとSecurity Hubの検出結果。これらを統一されたクエリ言語で横断的に検索、集計できるようになりました。 本記事のクエリは基本パターンです。運用中に独自の集計軸や複雑な絞り込みが必要になっても、Logs Insightsクエリ言語のfields/filter/stats/sortの組み合わせで多くの要件は満たせます。料金面では時刻範囲を狭めること、対象ロググループを絞ることを徹底し、意図せぬ大規模スキャンを避けてください。 DevOpsサイクルの一巡 本記事でDevOpsサイクルが一巡しました。- Develop(第01〜09回): Git、GitHub、VS Code、コードレビュー
- Delivery(第10〜18回): Docker、GitHub Actions、EKS、Terraform
- Monitoring(第19〜22回): CloudWatch、Container Insights
- Feedback(第23〜27回): Budgets、CloudTrail、GuardDuty、Security Hub、Slack通知
- Feedbackを受けた調査(第28回(今回)): CloudWatch Logs Insights
- レスポンスタイムの分位数集計
- GuardDutyとSecurity Hub CSPM検出結果の分析
- 保存クエリと共有
- おわりに
はじめに
前回では、「AWS Security Hub CSPM」で複数のセキュリティサービスの検出結果を集約する仕組みを構築しました。併せて、CIS AWS Foundations BenchmarkやFSBPに沿った基準評価も自動化しました。これで記録(第25回:CloudTrail)、検知(第26回:GuardDuty)、集約と基準評価(第27回:Security Hub CSPM)が揃いました。
ただし、集約画面と通知だけでは、個別のログを掘り下げた調査には追いつきません。特定のIAMユーザーが過去1週間に呼び出したAPIの一覧、リージョン別のAPI呼び出し件数、EKS上のアプリケーションで頻発するエラーメッセージ。これらは、蓄積された生ログに直接クエリを実行しなければ得られません。
「CloudWatch Log Analytics」は、CloudWatch Logsに保存されたログに対して、SQLに似た独自のクエリ言語で対話的に検索、集計、可視化するUIです。2026年6月に導入された統合画面で、従来のLogs Insights、Live Tail、Contributor Insightsを1つに束ねています。
本記事で使うクエリ言語は、旧UI名がそのまま残っているLogs Insightsクエリ言語です。コンソール左ペインから「Log Analytics」を選び、その画面上でLogs Insightsクエリを書いていきます。画面上部の通知バーからopt-outすれば、従来のLogs Insights、Live Tail、Contributor Insightsを個別画面として使う運用も継続できます。
第25回で有効化したCloudTrailや第21〜22回で有効化したContainer Insightsが出力するログを対象に、実運用で使うクエリを示します。
CloudWatch Log Analyticsとは
Log Analyticsは、CloudWatch Logsに蓄積されたログに対して対話的にクエリを実行するためのUIです。AWSマネジメントコンソールのクエリエディタから直接クエリを入力し、結果を数秒から数十秒で得られます。分析基盤の構築やログのエクスポートは不要で、CloudWatch Logsにログが送信された時点で分析対象となります。
クエリ言語自体は、統合前のUI名を引き継いだLogs Insightsクエリ言語です。料金体系やAPI、Terraformリソースもこの名称のまま残っており、コンソールUIだけが「Log Analytics」に変わりました。
特徴
Log Analyticsの主な特徴は次の3点です。
- サーバレス:クラスタや検索インデックスの管理は不要。CloudWatch Logsに送信されるログを自動的に検索対象とする
- 自動フィールド抽出:JSON形式のログは自動的にフィールド展開される。文字列形式のログは
parseコマンドで切り出せる - 可視化:
statsコマンドの集計結果は棒グラフや折れ線グラフとしてそのまま表示できる
クエリ言語はSQLとは別物ですが、filter(絞り込み)やstats(集計)などSQLの発想に近い演算子を備えており、SQL経験者であれば数十分で基本を掴めます。
対応するログの種類
Log Analyticsの対象はCloudWatch Logsに保存されたログです。本連載で構築してきたリソースのうち、既にCloudWatch Logsへログを配信しているものは以下です。
- CloudTrail管理イベント(第25回):
aws_cloudtrailリソースでcloud_watch_logs_group_arnを指定してCloudWatch Logsへ配信済み - Container Insights(第21〜22回):Fluent Bitがコンテナ標準出力を
/aws/containerinsights/<cluster>/applicationへ配信済み
GuardDutyとSecurity Hub CSPMの検出結果はデフォルトではCloudWatch Logsに配信されません。EventBridge経由でCloudWatch Logsをターゲットに指定する追加設定が必要で、本記事の後半「GuardDutyとSecurity Hub CSPM検出結果の分析」節で扱います。
料金と課金抑制の実践
Log Analyticsは、Logs Insightsクエリ1回ごとの走査データ量に応じた従量課金です。S3やAthenaと同種の課金モデルで、ログの本数ではなく走査されたバイト数で決まります。
スキャンデータ量に対する従量課金
東京リージョンでは1GBあたり0.0076USDです(2026年7月時点)。無料枠は存在しないため、クエリを実行した瞬間から課金が発生します。検証環境で1〜2週間分のCloudTrailログを検索する程度であれば、数円から数十円の範囲に収まります。一方、本番環境の数TBに及ぶログを何気なく全期間検索すると、数十USDから数百USDの請求につながる場合があります。
最新の料金と地域別価格は、AWS公式のCloudWatch料金ページにある「Logs Insightsクエリ」の項で確認してください。
課金を抑える基本パターン
クエリ実行時のスキャン量を抑える手段は3つあります。
- 時刻範囲を狭める:クエリエディタ右上の期間セレクタで1時間から数時間の範囲から始める。全期間検索は避ける
- 対象ロググループを絞る:複数ロググループを一度に指定できるが、明らかに関係ないロググループは含めない
filterを先に置く:filterで絞り込んでからstatsやsortを実行する。スキャン量そのものは変わらないが、後段の処理時間を短縮できる
クエリが対象とするデータ量はクエリ実行前に画面上で概算表示されません。初回は狭い範囲から始めて段階的に広げる運用が安全です。
クエリ言語の基本
Logs Insightsクエリ言語のクエリは複数のコマンドをパイプ(|)で連結する構文です。上から順に処理が流れる点はUnixパイプに似ています。
主なコマンド
代表的なコマンドは以下です。
fields:出力するフィールドを選択するfilter:条件に合致するログのみ抽出するstats:件数、平均、合計などを集計するsort:指定フィールドで並び替えるlimit:出力件数を制限するparse:文字列ログを正規表現やワイルドカードで分解し、抽出結果を新しいフィールドとして扱う
単純なクエリの例は次のようになります。指定した期間内の最新20件を新しい順に表示するクエリです。
fields @timestamp, @message
| sort @timestamp desc
| limit 20@timestampと@messageはCloudWatch Logsが自動的に付与する組み込みフィールドで、それぞれログの発生時刻とログ本文を指します。
時刻範囲と対象ロググループの指定
時刻範囲と対象ロググループはクエリ本文には書きません。Log Analytics画面右上の期間セレクタ(5m/1h/1dなど)で時刻範囲を選び、画面上部のロググループ選択欄で対象ロググループをチェックボックスで選択します。複数ロググループを同時に選択するとそれぞれのロググループを対象に同一クエリが実行され、結果はマージされて表示されます。
CloudTrailログの分析
第25回で有効化したCloudTrailは、/aws/cloudtrail/organizationロググループにAPI呼び出しの履歴を配信しています。このロググループを対象に、実運用でよく使うクエリを2つ紹介します。
rootアカウントの利用状況
rootアカウントは通常のオペレーションでは使わない前提とし、実行があった場合は監査対象とします。過去24時間のroot利用を一覧表示するには次のクエリを使います。
fields @timestamp, eventName, sourceIPAddress, userAgent
| filter userIdentity.type = "Root"
| sort @timestamp descuserIdentity.typeはCloudTrailログのJSONフィールドで、Root操作は"Root"、IAMユーザーは"IAMUser"、IAMロールは"AssumedRole"が入ります。同じフィールドで対象を切り替えられます。
失敗イベントの抽出
権限不足やAPIエラーで失敗したイベントはerrorCodeフィールドに値が入ります。
fields @timestamp, eventName, userIdentity.arn, errorCode, errorMessage
| filter ispresent(errorCode)
| sort @timestamp desc
| limit 50ispresent(<field>)は指定フィールドが存在する場合に真を返す関数です。権限設計の不備や、意図しないAPIコールの発生源の特定に使えます。
Container Insightsログの分析
第21〜22回で有効化したContainer Insightsは、EKSクラスタのアプリケーションログを配信しています。配信先のロググループ名は/aws/containerinsights/<cluster>/applicationです。このロググループを対象に、EKS運用でよく使うクエリを2つ紹介します。
エラーログの検索
アプリケーションが出力するERRORレベルのログを時系列で取り出します。
fields @timestamp, kubernetes.pod_name, log
| filter log like /(?i)error/
| sort @timestamp desc
| limit 100like /(?i)error/は正規表現による部分一致で、(?i)は大文字小文字を区別しない指定です。ログレベルがJSONフィールドとして格納されている場合はfilter level = "ERROR"のように書くことができます。
Pod別ログ量ランキング
想定外に大量のログを出力しているPodを特定します。
fields @timestamp, kubernetes.pod_name
| stats count(*) as log_count by kubernetes.pod_name
| sort log_count desc
| limit 20stats count(*) by <field>は指定フィールドの値ごとに件数を集計する構文で、SQLのGROUP BYに相当します。kubernetes.pod_nameはContainer InsightsがPodメタデータとして自動付与するフィールドで、特定Podが突出している場合はロギング設定の見直し候補となります。
HTTP status別のリクエスト集計
以下の2つのクエリは、アプリケーションがhttp_statusとresponse_time_msをJSONフィールドで出力している想定です。テキストログの場合はparse @message "..." as ...でこれらのフィールドを抽出してからフィルタします。
ステータスコード別のリクエスト件数を集計します。
fields @timestamp, http_status
| stats count(*) as request_count by http_status
| sort http_status asc5xxだけを抽出したい場合はfilter http_status >= 500を追加します。急激な増加はデプロイ直後の異常検知に使えます。
レスポンスタイムの分位数集計
平均値だけでなく、分位数(パーセンタイル)を見ることで外れ値の影響を分離します。
fields @timestamp, response_time_ms
| stats avg(response_time_ms) as avg_ms,
pct(response_time_ms, 50) as p50,
pct(response_time_ms, 95) as p95,
pct(response_time_ms, 99) as p99pct(<field>, <n>)はn分位数を返す関数で、p95/p99はユーザー体感に近い指標です。p95やp99がavgより大きく離れている場合は、少数の遅いリクエストが分布の裾を伸ばしている状態を示します。
GuardDutyとSecurity Hub CSPM検出結果の分析
GuardDutyとSecurity Hub CSPMは、標準ではCloudWatch Logsに検出結果を配信しません。EventBridgeルールにCloudWatch Logsをターゲット追加することで、Log Analyticsから検索可能になります。
検出結果をCloudWatch Logsへ配信する
以下をmain.tfに追加します。
# terraform/environments/dev/main.tf
resource "aws_cloudwatch_log_group" "security_findings" {
name = "/aws/events/security-findings"
retention_in_days = 30
}
resource "aws_cloudwatch_log_resource_policy" "events_to_logs" {
policy_name = "AllowEventBridgeToLogs"
policy_document = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = ["events.amazonaws.com", "delivery.logs.amazonaws.com"] }
Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "${aws_cloudwatch_log_group.security_findings.arn}:*"
}]
})
}
resource "aws_cloudwatch_event_rule" "security_findings_to_logs" {
name = "security-findings-to-logs"
description = "Forward GuardDuty and Security Hub findings to CloudWatch Logs for analysis"
event_pattern = jsonencode({
source = ["aws.guardduty", "aws.securityhub"]
detail-type = [
"GuardDuty Finding",
"Security Hub Findings - Imported"
]
})
}
resource "aws_cloudwatch_event_target" "security_findings_to_logs" {
rule = aws_cloudwatch_event_rule.security_findings_to_logs.name
target_id = "SendToCloudWatchLogs"
arn = aws_cloudwatch_log_group.security_findings.arn
}配信の許可は、aws_cloudwatch_log_resource_policyでevents.amazonaws.comとdelivery.logs.amazonaws.comに付与します。
第26回と第27回で作成したEventBridge → SNS通知ルールとは別ルールとして共存する構成で、Slackへの通知はそのまま維持されます。
Log Analyticsでの集計クエリ
配信されたイベントに対して重要度別の件数を集計します。
fields @timestamp, source, detail.findings.0.Severity.Label as severity
| filter source = "aws.securityhub"
| stats count(*) as finding_count by severity
| sort finding_count descSecurity Hub CSPMのイベントはdetail.findingsが配列で、通常は1件が格納されます。配列要素はドット記法detail.findings.0とブラケット記法detail.findings[0]のどちらでも参照でき、本記事ではドット記法を使います。
上記のクエリでは配列先頭のSeverity.Labelをseverityとして抽出し、CRITICAL/HIGH/MEDIUM/LOW/INFORMATIONALの別で件数集計します。
GuardDutyのイベント構造はSecurity Hubと異なり、detail直下に検出結果が単一オブジェクトとして格納されます。重要度はdetail.severityに0.0〜10.0の数値で入ります。GuardDutyを対象にする場合は次のクエリを使います。
fields @timestamp, source, detail.severity, detail.type
| filter source = "aws.guardduty"
| stats count(*) as finding_count by detail.severity
| sort finding_count desc
保存クエリと共有
よく使うクエリはLog Analytics画面の「保存」機能でアカウント内に保存でき、右上の「保存されたクエリ」ボタンから開くサイドパネルでチームメンバー全員が呼び出せます。命名規則を「用途_対象ロググループ」の形(例:cloudtrail_root_activity、eks_error_by_pod)で揃えると、増えても探しやすくなります。
Terraformで管理する場合はaws_cloudwatch_query_definitionリソースが使え、環境間で同じクエリセットを再現できます。
resource "aws_cloudwatch_query_definition" "cloudtrail_root_activity" {
name = "cloudtrail/root_activity"
log_group_names = [
"/aws/cloudtrail/organization"
]
query_string = <<-QUERY
fields @timestamp, eventName, sourceIPAddress, userAgent
| filter userIdentity.type = "Root"
| sort @timestamp desc
QUERY
}
nameにスラッシュを含めるとコンソール上で階層フォルダとして表示されるため、対象や用途で分類できます。
おわりに
CloudTrailの操作履歴、Container Insightsのアプリケーションログ、GuardDutyとSecurity Hubの検出結果。これらを統一されたクエリ言語で横断的に検索、集計できるようになりました。
本記事のクエリは基本パターンです。運用中に独自の集計軸や複雑な絞り込みが必要になっても、Logs Insightsクエリ言語のfields/filter/stats/sortの組み合わせで多くの要件は満たせます。料金面では時刻範囲を狭めること、対象ロググループを絞ることを徹底し、意図せぬ大規模スキャンを避けてください。
DevOpsサイクルの一巡
本記事でDevOpsサイクルが一巡しました。
- Develop(第01〜09回): Git、GitHub、VS Code、コードレビュー
- Delivery(第10〜18回): Docker、GitHub Actions、EKS、Terraform
- Monitoring(第19〜22回): CloudWatch、Container Insights
- Feedback(第23〜27回): Budgets、CloudTrail、GuardDuty、Security Hub、Slack通知
- Feedbackを受けた調査(第28回(今回)): CloudWatch Logs Insights
Log Analyticsで得た調査結果を開発チームに戻せば、次のイテレーションの起点となります。
次回からは少し趣向を変えて、このサイクルにAIをどう織り交ぜるかについて取り上げていきます。第29回ではDevOpsとAIの全体像を、第30回ではDev側での活用について、第31回ではOps側での活用について扱います。
この記事をシェアしてください
