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(保存)$35Parquet + SNAPPY圧縮
Kinesis Firehose$40データ処理量
AWS Glue$15Crawler実行
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年半で実際に検知できたインシデント

ツールの話ばかりになってしまったけど、実際に何が見えたかも書いておきたい。

検知できたケース:

  1. 開発者の手作業S3アクセス: ある開発環境のEC2インスタンスから、本番S3バケットへの異常なトラフィックを検知。調べたら開発者がデバッグ目的でAWS CLIを直叩きしてた。設定ミスではなく意図的な行動だったけど、それが検知できたこと自体が重要だった。

  2. NATゲートウェイ経由の外部スキャン: 自社IPから外部の特定ポートレンジへのスキャンが検知。後から判明したのは、テスト用に建てたEC2に侵害されたDockerイメージが入っていたケース。GuardDutyでも検知できてたけど、フローログの生データで経路が追えたのがすごく助かった。

  3. 内部ラテラルムーブメントの兆候: あるサブネットのインスタンスから、他のサブネットへの通常と異なるポートへのアクセスが増加。結果的に誤検知だったけど、正常なトラフィックパターンを把握する機会になったという意味では無駄じゃなかった。

正直、見えなかったケース:

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週間後に眺めるだけでも、ネットワークの動きが全然違って見えてくるはずだから。皆さんのチームではフローログどう使ってますか?特に分析基盤の選択で悩んでる人は気軽にコメントください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事