AWS Inspector v2、本番導入6ヶ月で見えてきた運用の現実

「初日4,200件のアラート」で絶望しかけたInspector v2、今は週200件に落ち着いています。マルチアカウント環境での誤検知対応・優先度設定・コスト管理まで、実務で得た正直な知見を共有します。

Inspector v2を導入した経緯と最初の絶望

先日、SOC2の継続審査を前にセキュリティチームから「EC2とLambdaの脆弱性管理ポリシー、文書化されてます?」と言われて焦った。正直に言うと、それまでうちのチームはECRのBasicスキャンと手動のTrivy実行でなんとかごまかしていた。でも審査官が求めるのは「継続的な自動検出と修正追跡のサイクル」だったので、Inspector v2を本格導入することになったのが約6ヶ月前の話だ。

最初に有効化したときの印象は「とにかくアラートの量が多すぎる」だった。Organization配下のアカウント全体で有効化したら、初日だけで約4,200件のfindingsが出た。EC2のOSパッチ漏れ、Lambdaのランタイム脆弱性、ECRイメージのCVE……全部一気に降ってくる。GuardDuty導入時に経験したFalse Positiveの地獄と似た感覚で、最初の2週間は「これ本当に運用できるのか」と思っていた。

ただ、そこから徐々に設定を調整して、今は週次のfinding数が200件前後に落ち着いている。その過程で見えてきたことを共有したい。

Inspector v2の実際のアーキテクチャと設定

まず、うちが組んだ構成図から見てほしい。

graph TB
    subgraph Management["Management Account"]
        OrgAdmin["AWS Organizations\n管理アカウント"]
        InspectorDelegated["Inspector v2\n委任管理者"]
        SecurityHub["Security Hub\n集約"]
    end

    subgraph ProdAccount["Production Account"]
        subgraph VPC_Prod["VPC (10.0.0.0/16)"]
            subgraph AZ_A["AZ: ap-northeast-1a"]
                EC2_A["EC2 Instance\n(SSM Agent付き)"]
                ECS_A["ECS Task"]
            end
            subgraph AZ_B["AZ: ap-northeast-1c"]
                EC2_B["EC2 Instance\n(SSM Agent付き)"]
                RDS["RDS PostgreSQL"]
            end
        end
        ECR_Prod["ECR Repository"]
        Lambda_Prod["Lambda Functions"]
        InspectorAgent_Prod["Inspector v2 Agent\n(自動インストール)"]
    end

    subgraph StagingAccount["Staging Account"]
        subgraph VPC_Stg["VPC (10.1.0.0/16)"]
            EC2_Stg["EC2 Instance"]
        end
        Lambda_Stg["Lambda Functions"]
        ECR_Stg["ECR Repository"]
        InspectorAgent_Stg["Inspector v2 Agent"]
    end

    subgraph Notification["通知・チケット化"]
        EventBridge["EventBridge Rules"]
        SNS["SNS Topic"]
        Lambda_Notify["Lambda\n(Jira連携)"]
        Slack["Slack\n#security-alerts"]
    end

    OrgAdmin -->|委任| InspectorDelegated
    InspectorAgent_Prod -->|findings送信| InspectorDelegated
    InspectorAgent_Stg -->|findings送信| InspectorDelegated
    InspectorDelegated -->|集約| SecurityHub
    SecurityHub -->|CRITICAL/HIGH| EventBridge
    EventBridge --> SNS
    SNS --> Lambda_Notify
    Lambda_Notify --> Slack
    Lambda_Notify -->|自動チケット作成| Jira["Jira"]
    EC2_A & EC2_B -->|スキャン対象| InspectorAgent_Prod
    Lambda_Prod -->|コードスキャン| InspectorAgent_Prod
    ECR_Prod -->|イメージスキャン| InspectorAgent_Prod

ポイントは委任管理者アカウントを専用のSecurityアカウントに分離していること。管理アカウントで直接Inspector v2を動かすのは避けた方がいい——権限が爆発的に広がるので、後から絞るのが大変になる。

有効化のコード例。うちはCDKで管理している:

import * as inspector2 from 'aws-cdk-lib/aws-inspector';
import * as organizations from 'aws-cdk-lib/aws-organizations';

// Organizations全体でInspector v2を有効化
// 実際にはCustom Resourceでauto-enable設定を入れる
const inspectorAutoEnable = new cr.AwsCustomResource(this, 'InspectorAutoEnable', {
  onCreate: {
    service: 'Inspector2',
    action: 'updateOrganizationConfiguration',
    parameters: {
      autoEnable: {
        ec2: true,
        ecr: true,
        lambda: true,
        lambdaCode: true, // 2025年後半から正式GAのコードスキャン
      },
    },
    physicalResourceId: cr.PhysicalResourceId.of('InspectorOrgConfig'),
  },
  policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
    resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
  }),
});

lambdaCode: true はLambda関数のコード自体に含まれるOSSライブラリの脆弱性をスキャンしてくれる機能で、2025年後半にGAになった。これが思ったより便利で、npm/pip packageのCVEをCIの外で継続的に拾ってくれる。地味だけど、CIが通ったあとにサプライチェーン側で新しいCVEが出たときに助かる。

6ヶ月で学んだFinding優先度の付け方

正直、最初の1ヶ月は「CRITICAL全部対応しよう」と意気込んでいたが、それは無理だった。CRITICALだけで毎週50〜80件出てくる環境では、全件対応はリソース的に不可能だ。チームと話し合って以下の優先度ポリシーを作った。

Inspector Scoreインターネット露出アクティブエクスプロイト対応SLA
9.0以上 (CRITICAL)ありあり24時間以内
9.0以上 (CRITICAL)なしあり72時間以内
9.0以上 (CRITICAL)なしなし2週間以内
7.0〜8.9 (HIGH)あり-1週間以内
7.0〜8.9 (HIGH)なし-1ヶ月以内
4.0〜6.9 (MEDIUM以下)--次回定期メンテ

ここでキーになるのが「アクティブエクスプロイト」フィルタだ。Inspector v2のfindingsには exploitAvailable: YES というフィールドがある。これがYESのものは実際に悪用コードが出回っているCVEなので、スコアが同じでも優先度を上げている。

このフィルタをEventBridgeのルールに組み込んだ:

{
  "source": ["aws.inspector2"],
  "detail-type": ["Inspector2 Finding"],
  "detail": {
    "severity": ["CRITICAL", "HIGH"],
    "status": ["ACTIVE"],
    "exploitAvailable": ["YES"],
    "fixAvailable": ["YES"]
  }
}

fixAvailable: YES も重要なフィルタだ。修正パッチが存在しないCVEをアラートに含めても、開発者がどうしようもないので混乱するだけだった——これは実際にチームから苦情が来て気づいた話だ。修正可能なものだけを即時通知の対象にした。

その結果のfinding件数の推移:

xychart-beta
  title "Inspector v2 対応必要Finding数の推移(週次)"
  x-axis ["Week1", "Week4", "Week8", "Week12", "Week16", "Week20", "Week24"]
  y-axis "件数" 0 --> 500
  bar [420, 310, 240, 190, 160, 140, 120]
  line [420, 310, 240, 190, 160, 140, 120]

最初の4週間でルール整備をしたことで急激に減り、12週目以降は安定してきた。ただし「減った」のは「対応が進んだ」だけじゃなく「無視するルールが整備された」という側面もある。ここは正直好みが分かれると思うし、うちも最初は若干後ろめたかった。でも対応不能な件数のアラートを流し続けても誰もアラートを信じなくなるので、現実的な落としどころとして受け入れた。

ECRイメージスキャンとCI/CDとの連携

うちでもっとも効果があったのはECRイメージスキャンをCI/CDに組み込んだことだ。Inspector v2はECRに対してプッシュ時とその後も継続的にスキャンしてくれる。「継続的」というのがポイントで、今日は問題なくても3ヶ月後に新しいCVEが発見されたら自動でfindingが上がってくる。静的なスナップショットじゃなく生きたスキャンだと思うと、かなり頼もしい。

ECR脆弱性スキャンで1000個のCVE検出した話でも書いたような状況に発展しないよう、CI段階でのゲートを設けている:

import boto3
import json
import sys
import time

def check_inspector_findings(repository_name: str, image_digest: str, 
                              max_critical: int = 0, max_high: int = 5) -> bool:
    """
    Inspector v2のfindingsを確認してCI/CDをブロックするかどうか判断する
    """
    client = boto3.client('inspector2', region_name='ap-northeast-1')
    
    # findingsが反映されるまで少し待つ(Inspector v2は非同期スキャン)
    print("Waiting for Inspector v2 scan to complete...")
    time.sleep(30)
    
    response = client.list_findings(
        filterCriteria={
            'ecrImageRepositoryName': [{
                'comparison': 'EQUALS',
                'value': repository_name
            }],
            'ecrImageHash': [{
                'comparison': 'EQUALS', 
                'value': image_digest
            }],
            'findingStatus': [{
                'comparison': 'EQUALS',
                'value': 'ACTIVE'
            }],
            'fixAvailable': [{
                'comparison': 'EQUALS',
                'value': 'YES'  # 修正可能なものだけチェック
            }]
        }
    )
    
    severity_counts = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0}
    exploit_available_critical = []
    
    for finding in response.get('findings', []):
        severity = finding['severity']
        severity_counts[severity] = severity_counts.get(severity, 0) + 1
        
        if severity == 'CRITICAL' and finding.get('exploitAvailable') == 'YES':
            exploit_available_critical.append({
                'title': finding['title'],
                'cvss_score': finding.get('inspectorScore', 'N/A'),
                'cve_id': finding.get('packageVulnerabilityDetails', {}).get('vulnerabilityId', 'N/A')
            })
    
    print(f"Findings summary: {json.dumps(severity_counts, indent=2)}")
    
    if exploit_available_critical:
        print("\n[BLOCKING] Active exploit available for CRITICAL findings:")
        for f in exploit_available_critical:
            print(f"  - {f['cve_id']}: {f['title']} (Score: {f['cvss_score']})")
        return False
    
    if severity_counts['CRITICAL'] > max_critical:
        print(f"[BLOCKING] CRITICAL findings ({severity_counts['CRITICAL']}) exceed threshold ({max_critical})")
        return False
    
    if severity_counts['HIGH'] > max_high:
        print(f"[BLOCKING] HIGH findings ({severity_counts['HIGH']}) exceed threshold ({max_high})")
        return False
    
    print("[PASS] Inspector v2 findings within acceptable range")
    return True

if __name__ == '__main__':
    repo_name = sys.argv[1]
    image_digest = sys.argv[2]
    
    if not check_inspector_findings(repo_name, image_digest):
        print("\nDeploy blocked due to security findings. Fix vulnerabilities and retry.")
        sys.exit(1)
    
    sys.exit(0)

CIでこれを実行するのはCodePipelineのステージとして組み込んでいる。max_high: 5 にしているのは「0にしたら誰もデプロイできなくなった」という悲惨な経験からだ(本番環境で学んだ、は本当にやめてほしい)。アクティブエクスプロイトのあるCRITICALだけは例外なくブロックする。

正直まだ検証中なのだが、Inspector v2の2026年アップデートで追加された「コンテナランタイム脆弱性スキャン」機能が面白そうだ。EKS上で動いているコンテナをランタイムレベルでスキャンして、実際に実行中のプロセスに紐づくライブラリの脆弱性を検出してくれる。コンテナセキュリティ完全ガイド2026でも触れられているSBOMとの連携がここで活きてくる。まだステージング環境での検証段階だが、かなり良さそうだ。

コストとの戦い

Inspector v2のコストは正直バカにならない。うちの環境(EC2: 約120台、Lambda: 約80関数、ECRイメージ: 月500プッシュ前後)で月に大体$800〜$1,200かかっている。

コストの内訳はこんな感じだ:

pie title Inspector v2 月次コスト内訳(概算)
  "EC2 インスタンス評価" : 45
  "ECR イメージスキャン" : 30
  "Lambda 関数スキャン" : 15
  "Lambda コードスキャン" : 10

EC2が一番高い。課金モデルが「インスタンス時間 × スキャン対象の数」なので、大量のEC2を抱えているとじわじわ効いてくる。うちがとった対策は3つある。

1. 開発環境は除外する

// Inspector v2の除外設定(タグベース)
{
  "filterCriteria": {
    "resourceTags": [
      {
        "comparison": "NOT_EQUALS",
        "key": "Environment",
        "value": "dev"
      }
    ]
  }
}

dev環境のEC2はInspectorから除外した。開発者が気軽に立てたインスタンスまでスキャンすると費用が爆発する。

2. スケジュールドスキャンの活用

Lambda関数は「常時スキャン」ではなく「デプロイ時トリガー」に変更した。Lambda Aliasの更新をEventBridgeで検知してスキャンを起動する構成にしている。これで約20%コスト削減できた。

3. ECRのイメージライフサイクルポリシー整備

これはECR脆弱性スキャンでも触れたが、古いイメージが山積みになっているとスキャン対象が増える一方なので、ライフサイクルポリシーで90日以上前のuntaggedイメージを削除するようにした。

それでも「高いな」とは正直思っている。個人的には月$1,000を超えてくると稟議が通りにくくなるラインだと感じていて、タグ戦略を先に整備してから有効化すればよかったと少し後悔している。ただSOC2審査でこの仕組みを見せたときに審査官の反応が良かったので、コンプライアンス的な価値は確かにある。SOC2対応の自動化についてはこちらの記事でも詳しく書いているので参考にしてほしい。

Jira自動連携とチームへの展開

最後に、findingsをチームが実際に対応できる仕組みにするために、Jira自動連携を実装した。SecurityHubからEventBridgeを経由してLambdaを起動し、JiraにCRITICAL/HIGHのfindingで自動チケットを作成する。

import boto3
import json
import requests
import os
from datetime import datetime

JIRA_URL = os.environ['JIRA_URL']
JIRA_API_TOKEN = os.environ['JIRA_API_TOKEN']
JIRA_PROJECT_KEY = os.environ['JIRA_PROJECT_KEY']

def create_jira_ticket(finding: dict) -> str:
    """Inspector v2のfindingからJiraチケットを作成する"""
    
    severity = finding['detail']['severity']
    resource_type = finding['detail']['resources'][0]['type']
    resource_id = finding['detail']['resources'][0]['id'].split('/')[-1]
    title = finding['detail']['title']
    cve_id = finding['detail'].get('packageVulnerabilityDetails', {}).get('vulnerabilityId', 'N/A')
    inspector_score = finding['detail'].get('inspectorScore', 0)
    exploit_available = finding['detail'].get('exploitAvailable', 'NO')
    fix_available = finding['detail'].get('fixAvailable', 'NO')
    
    # アクティブエクスプロイトありはラベルで目立たせる
    labels = ['security', 'inspector-v2', severity.lower()]
    if exploit_available == 'YES':
        labels.append('active-exploit')
    
    # 優先度マッピング
    priority_map = {'CRITICAL': 'Highest', 'HIGH': 'High', 'MEDIUM': 'Medium'}
    
    description = f"""
## 脆弱性情報

**CVE ID**: {cve_id}
**Inspector Score**: {inspector_score}
**リソース**: {resource_type} - `{resource_id}`
**アクティブエクスプロイト**: {exploit_available}
**修正パッチ**: {fix_available}

## 対応方法

AWS Inspector v2コンソールで詳細を確認し、推奨されるパッケージバージョンに更新してください。

**Finding ARN**: `{finding['detail']['findingArn']}`

## 参考リンク
- [Inspector v2 Console](https://console.aws.amazon.com/inspector/v2/home#/findings)
- [NVD: {cve_id}](https://nvd.nist.gov/vuln/detail/{cve_id})
    """
    
    payload = {
        "fields": {
            "project": {"key": JIRA_PROJECT_KEY},
            "summary": f"[{severity}][Inspector v2] {cve_id}: {title[:80]}",
            "description": {
                "type": "doc",
                "version": 1,
                "content": [{"type": "paragraph", "content": [{"type": "text", "text": description}]}]
            },
            "issuetype": {"name": "Bug"},
            "priority": {"name": priority_map.get(severity, 'Medium')},
            "labels": labels
        }
    }
    
    response = requests.post(
        f"{JIRA_URL}/rest/api/3/issue",
        json=payload,
        auth=(os.environ['JIRA_EMAIL'], JIRA_API_TOKEN),
        headers={"Content-Type": "application/json"}
    )
    response.raise_for_status()
    
    return response.json()['key']

def lambda_handler(event, context):
    finding = event
    ticket_key = create_jira_ticket(finding)
    print(f"Created Jira ticket: {ticket_key} for finding: {finding['detail']['findingArn']}")
    return {'ticketKey': ticket_key}

これで開発チームはJiraのバックログにセキュリティチケットが自動で積まれるようになり、スプリントの計画にも組み込みやすくなった。最初は「セキュリティチームが突然Jiraにチケット入れてくる」という感じで開発者に嫌がられたが、「自分たちのサービスのリスクを見える化しているだけ」と説明したら徐々に受け入れられてきた。正直、最初の1ヶ月は社内調整の方が技術的な設定より大変だったかもしれない。

皆さんのチームはセキュリティfindingをどうやって開発チームに伝えてますか?Slack通知だけだと絶対に埋もれるので、チケット化は必須だと思っている。

まとめ

6ヶ月のInspector v2運用で得た知見を整理するとこうなる。

  1. 初期のfinding爆発は正常exploitAvailable=YESfixAvailable=YES のフィルタを組み合わせてノイズを削ることが最優先。最初の1ヶ月はルール整備に集中すべきだった(対応に集中しようとして燃え尽きた)。

  2. ECRとLambdaのスキャンが地味に強力。特にLambdaコードスキャンは2025年GA後に急速に精度が上がっており、CIのTrivy検査と相互補完できている。

  3. コストはEC2台数に比例する。dev環境を除外するだけで30〜40%削減できる。先にタグ戦略を整備してから有効化すべきだった——これは本当に後悔している。

  4. チケット化しないと誰も直さない。EventBridge → Lambda → Jiraの自動連携はほぼ必須。Slackアラートだけでは半年後に見返したら誰も対応していなかった、という未来が容易に想像できる。

  5. SOC2との相性は良い。継続的スキャンのエビデンスが自動で溜まるので、審査の準備コストが大幅に下がった。特に「脆弱性の検出から修正までのサイクルが文書化されている」という要件を自然に満たせる。

次のアクション: まず委任管理者アカウントにInspector v2を有効化して、exploitAvailable=YES のCRITICALだけをSlack通知する最小構成から始めるのをおすすめしたい。いきなり全finding対応しようとすると確実に燃え尽きる——これは体験談だ。

U

Untanbaby

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

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

関連記事