VPCフローログ、有効化して放置してませんか?Athena×Grafana構成を1年半運用してわかったこと
「フローログって結局見てるんですか?」と聞かれてドキッとした話。CloudWatch Logs Insightsの限界から始まり、Athena+Grafana構成に移行するまでの失敗と現実を書きました。
先日、チームのセキュリティレビューで「VPCフローログって結局見てるんですか?」って聞かれて、正直ちょっと刺さった。有効化してS3に流してる、でもそれっきりになりがちなやつ。うちも一時期そうだった。
2024年末から本格的にVPCフローログの分析基盤を整えて、1年半ほど運用してきた。最初は「CloudWatch Logs Insightsでいけるでしょ」って甘く見てたんだけど、ログ量が増えるにつれてコストと使い勝手の問題が出てきて、結果的にAthena+Grafanaの構成に落ち着いた。その過程で踏んだ失敗と、今の構成でどこまで見えるようになったかを書いておきたい。
なお、GuardDutyとの連携については GuardDuty導入3ヶ月の地獄と脱出戦略 に詳しく書いてあるので、そっちも合わせて読んでもらえると文脈がつながる。Network FirewallとVPCフローログを組み合わせた話は AWS Network Firewall設計2026 にある。
構成の全体像と設計思想
まず今の構成を図で示しておく。
graph TB
subgraph VPC["VPC (10.0.0.0/16)"]
subgraph AZ_A["AZ-a"]
EC2_A["EC2 / ECS Tasks"]
ALB["ALB"]
end
subgraph AZ_B["AZ-b"]
EC2_B["EC2 / ECS Tasks"]
RDS["RDS Aurora"]
end
subgraph Private["Private Subnets"]
NAT["NAT Gateway"]
end
end
subgraph Logging["ログ収集・分析"]
S3_RAW["S3: vpc-flowlogs-raw"]
Firehose["Kinesis Firehose"]
S3_PARQUET["S3: vpc-flowlogs-parquet\n(Partitioned by date)"]
Glue["AWS Glue\nCrawler + ETL"]
Athena["Amazon Athena"]
end
subgraph Visualization["可視化・アラート"]
Grafana["Amazon Managed Grafana"]
SNS["SNS"]
Slack["Slack Alert"]
end
subgraph Security["セキュリティ連携"]
GuardDuty["GuardDuty"]
SecurityHub["Security Hub"]
Lambda_Alert["Lambda: 異常検知"]
end
EC2_A -->|「フローログ」| Firehose
EC2_B -->|「フローログ」| Firehose
ALB -->|「フローログ」| Firehose
NAT -->|「フローログ」| Firehose
Firehose -->|「Parquet変換」| S3_PARQUET
Firehose -->|「Raw保存」| S3_RAW
S3_PARQUET --> Glue
Glue --> Athena
Athena --> Grafana
Athena --> Lambda_Alert
Lambda_Alert --> SNS
SNS --> Slack
GuardDuty --> SecurityHub
SecurityHub --> Lambda_Alert
構成のキモは FirehoseでParquet変換をインラインでやる こと。最初はS3にJSON形式で直接流してたんだけど、Athenaでのクエリコストが跳ね上がった。Parquet化するだけでスキャン量が10分の1以下になる。体感じゃなく、Athenaのコンソールで実際に確認できた数字だ。
Athenaテーブル設計とクエリの実際
VPCフローログのv5フォーマットを使ってる。2025年以降、VPC内のサービス間通信もトレースできるフィールドが追加されて、これが地味に便利だった。
CREATE EXTERNAL TABLE vpc_flow_logs (
version int,
account_id string,
interface_id string,
srcaddr string,
dstaddr string,
srcport int,
dstport int,
protocol bigint,
packets bigint,
bytes bigint,
start bigint,
`end` bigint,
action string,
log_status string,
vpc_id string,
subnet_id string,
instance_id string,
tcp_flags int,
type string,
pkt_srcaddr string,
pkt_dstaddr string,
region string,
az_id string,
sublocation_type string,
sublocation_id string,
pkt_src_aws_service string,
pkt_dst_aws_service string,
flow_direction string,
traffic_path int
)
PARTITIONED BY (dt string)
STORED AS PARQUET
LOCATION 's3://vpc-flowlogs-parquet/AWSLogs/ACCOUNT_ID/vpcflowlogs/ap-northeast-1/'
TBLPROPERTIES (
'parquet.compression' = 'SNAPPY'
);
dtでパーティションを切っておくのは必須。日付範囲を指定しないクエリを誰かが書いてしまうと全スキャンになってコストが爆発する。うちではAthenaのワークグループにスキャン上限を設定して防いでる(後述)。
実際に運用で使ってるクエリをいくつか紹介しよう。
拒否トラフィックの集計(日次)
SELECT
srcaddr,
dstaddr,
dstport,
COUNT(*) AS reject_count,
SUM(bytes) AS total_bytes
FROM vpc_flow_logs
WHERE dt = '2026/05/05'
AND action = 'REJECT'
AND log_status = 'OK'
GROUP BY srcaddr, dstaddr, dstport
HAVING COUNT(*) > 100
ORDER BY reject_count DESC
LIMIT 50;
これを毎朝Grafanaのダッシュボードで眺めてる。100件以上のREJECTが出てるIPはだいたいスキャンか設定ミスのどちらかで、どっちも早めに気づけると対応が格段に楽になる。
外部への異常な通信量検出
SELECT
srcaddr,
instance_id,
pkt_dst_aws_service,
flow_direction,
SUM(bytes) / 1024 / 1024 AS total_mb,
COUNT(*) AS flow_count
FROM vpc_flow_logs
WHERE dt BETWEEN '2026/05/01' AND '2026/05/05'
AND flow_direction = 'egress'
AND action = 'ACCEPT'
AND pkt_dst_aws_service IS NULL -- AWS内部サービスへの通信を除外
GROUP BY srcaddr, instance_id, pkt_dst_aws_service, flow_direction
HAVING SUM(bytes) > 1073741824 -- 1GB以上
ORDER BY total_mb DESC;
pkt_dst_aws_serviceフィールドがv5で追加されてから、S3やDynamoDBへの通信とインターネットへの通信を分けて見れるようになった。データ漏洩の検出に地味に効いてる、これが。
Grafanaダッシュボードと異常検知の組み方
Amazon Managed GrafanaでAthenaをデータソースに設定してる。2026年現在、Grafana 11.xベースになっていて、Athenaプラグインも安定してきた感じ。ただ、Athenaのクエリは正直遅いので、Grafana側のリフレッシュ間隔は最短でも5分にしないとコストが溶ける。まだ試行錯誤中ではあるけど、5分サイクルで異常検知には今のところ十分な感触だ。
リアルタイム性が必要なアラートはLambdaで別に実装してる。
import boto3
import json
from datetime import datetime, timedelta
athena = boto3.client('athena', region_name='ap-northeast-1')
sns = boto3.client('sns', region_name='ap-northeast-1')
WORKGROUP = 'security-analysis'
DATABASE = 'vpc_logs_db'
OUTPUT_LOCATION = 's3://athena-query-results/security/'
SNS_TOPIC_ARN = 'arn:aws:sns:ap-northeast-1:ACCOUNT:security-alerts'
def detect_port_scan(event, context):
"""ポートスキャン検知: 同一送信元から10分以内に20ポート以上にアクセス"""
now = datetime.utcnow()
ten_min_ago = now - timedelta(minutes=10)
dt_str = now.strftime('%Y/%m/%d')
query = f"""
SELECT
srcaddr,
COUNT(DISTINCT dstport) AS unique_ports,
COUNT(*) AS total_flows
FROM vpc_flow_logs
WHERE dt = '{dt_str}'
AND start >= {int(ten_min_ago.timestamp())}
AND action = 'REJECT'
GROUP BY srcaddr
HAVING COUNT(DISTINCT dstport) >= 20
ORDER BY unique_ports DESC
"""
response = athena.start_query_execution(
QueryString=query,
QueryExecutionContext={'Database': DATABASE},
WorkGroup=WORKGROUP,
ResultConfiguration={'OutputLocation': OUTPUT_LOCATION}
)
execution_id = response['QueryExecutionId']
# ... ポーリングと結果取得の処理
results = wait_for_results(execution_id)
if results:
message = format_alert(results, 'PORT_SCAN_DETECTED')
sns.publish(
TopicArn=SNS_TOPIC_ARN,
Subject='[SECURITY] ポートスキャン検知',
Message=message
)
return {'statusCode': 200, 'detected': len(results)}
def wait_for_results(execution_id: str) -> list:
"""Athenaクエリ結果の取得(タイムアウト60秒)"""
import time
for _ in range(12): # 5秒 × 12回 = 60秒
status = athena.get_query_execution(
QueryExecutionId=execution_id
)['QueryExecution']['Status']['State']
if status == 'SUCCEEDED':
results = athena.get_query_results(
QueryExecutionId=execution_id
)
rows = results['ResultSet']['Rows'][1:] # ヘッダーをスキップ
return [
{
'srcaddr': row['Data'][0]['VarCharValue'],
'unique_ports': int(row['Data'][1]['VarCharValue']),
'total_flows': int(row['Data'][2]['VarCharValue'])
}
for row in rows
]
elif status in ('FAILED', 'CANCELLED'):
break
time.sleep(5)
return []
このLambdaをEventBridgeで10分おきに動かしてる。最初はCloudWatch Logs InsightsのMetric Filterで似たようなことをやろうとしたけど、複数フィールドをまたいだ条件が書けなくてAthenaに移行した経緯がある。地味なところだけど、この制約に気づくのが遅くて割と時間を無駄にした。
コストと実運用の数字
1年半運用してきたコストの変遷を正直に書いておく。
xychart-beta
title "VPCフローログ分析 月額コスト推移(USD)"
x-axis ["2024/12", "2025/01", "2025/02", "2025/03", "2025/06", "2025/09", "2025/12", "2026/03"]
y-axis "コスト(USD)" 0 --> 500
line [380, 420, 410, 320, 280, 250, 210, 195]
最初の数ヶ月は月400ドル超えてて正直焦った。Parquet変換とパーティショニングを最適化したら一気に下がって、今は200ドル前後で安定してる。構成別の内訳はこんな感じ。
| サービス | 月額コスト(概算) | 主なコスト要因 |
|---|---|---|
| S3(保存) | $35 | Parquet + SNAPPY圧縮 |
| Kinesis Firehose | $40 | データ処理量 |
| AWS Glue | $15 | Crawler実行 |
| Amazon Athena | $65 | クエリスキャン量 |
| Amazon Managed Grafana | $20 | エディタ席数 |
| Lambda(異常検知) | $5 | 実行回数 |
| SNS | $1 | アラート通知 |
| 合計 | $181 |
Athenaのコストを下げるために実際にやったことが2つある。
ひとつは ワークグループのスキャン制限設定。誰かがコンソールから全件クエリを走らせるのを防ぐために、スキャン上限を5GBに設定してる。
{
"Name": "security-analysis",
"Configuration": {
"ResultConfiguration": {
"OutputLocation": "s3://athena-query-results/security/"
},
"BytesScannedCutoffPerQuery": 5368709120,
"PublishCloudWatchMetricsEnabled": true,
"EnforceWorkGroupConfiguration": true
}
}
もうひとつは S3のライフサイクル設定。フローログはホットデータとしての寿命が短いので、90日でGlacier Instant Retrievalに移して365日で削除してる。コンプライアンス要件があるチームはここが変わるかもしれないけど、うちはこれで問題なかった。
1年半で実際に検知できたインシデント
ツールの話ばかりになってしまったけど、実際に何が見えたかも書いておきたい。
検知できたケース:
-
開発者の手作業S3アクセス: ある開発環境のEC2インスタンスから、本番S3バケットへの異常なトラフィックを検知。調べたら開発者がデバッグ目的でAWS CLIを直叩きしてた。設定ミスではなく意図的な行動だったけど、それが検知できたこと自体が重要だった。
-
NATゲートウェイ経由の外部スキャン: 自社IPから外部の特定ポートレンジへのスキャンが検知。後から判明したのは、テスト用に建てたEC2に侵害されたDockerイメージが入っていたケース。GuardDutyでも検知できてたけど、フローログの生データで経路が追えたのがすごく助かった。
-
内部ラテラルムーブメントの兆候: あるサブネットのインスタンスから、他のサブネットへの通常と異なるポートへのアクセスが増加。結果的に誤検知だったけど、正常なトラフィックパターンを把握する機会になったという意味では無駄じゃなかった。
正直、見えなかったケース:
HTTPS(443)の通信の中身が見えないのは当然として、VPCエンドポイント経由のS3アクセスはフローログに乗らないケースがある。VPCエンドポイントのアクセスログを別途有効化する必要があるんだけど、これは導入して3ヶ月後に気づいた盲点だった。S3のサーバーアクセスログとの組み合わせが実質必須だと今は思ってる。
セキュリティ対応の全体的なフレームワークについては インシデント対応の最新ベストプラクティス2026 が参考になる。VPCフローログはあくまで検知の一要素で、対応フローとセットで設計しないと宝の持ち腐れになる。
コンプライアンスの文脈では SOC2対応2026年版 でフローログをエビデンスとして使う話も触れられてるので参照してほしい。
まとめ
1年半やってきて、VPCフローログ分析で本当に重要だと感じたことを整理しておく。
1. 保存形式はParquet一択 JSONのまま分析しようとすると必ずコストで詰まる。Firehoseでインライン変換するのが最小コスト構成だった。
2. パーティションとスキャン制限は導入初日から設定する 後から入れようとすると既存データの再配置が面倒になる。最初からやっておくべきだった、と今でも思う。
3. CloudWatch Logs Insightsは入門向け、本格運用はAthena ログ量が月100GB超えてくるとCloudWatchのコストが急増する。うちは50GBあたりで移行した。
4. フローログだけでは中身は見えない S3アクセスログ、ALBアクセスログ、CloudTrailと組み合わせて初めて「何が起きたか」が分かる。フローログが教えてくれるのは「どこからどこに何バイト」だけだ。
5. 正常なトラフィックパターンを先に把握する 異常検知のしきい値設定のために、まず2〜4週間は観察期間に充てるべき。うちは最初から検知を始めてアラート地獄になった。これはかなり反省してる。
まだVPCフローログを有効化してない人は今すぐやってほしい。設定自体は5分でできる。今日有効化して1週間後に眺めるだけでも、ネットワークの動きが全然違って見えてくるはずだから。皆さんのチームではフローログどう使ってますか?特に分析基盤の選択で悩んでる人は気軽にコメントください。