every Tech Blog

株式会社エブリーのTech Blogです。

ECR イメージスキャンでコンテナの脆弱性を検知する

目次

はじめに

こんにちは、開発本部開発1部トモニテグループのエンジニアの パンダム/rymiyamoto です。

2025年末に Next.js の React Server Components に DoS(サービス拒否)とソースコード露出の脆弱性が公開され、App Router を使用するサービスでのアップグレード対応が求められました。

このように、利用しているフレームワークやライブラリに深刻な脆弱性が見つかることは珍しくありません。 こうした脆弱性が公開中のサービスに影響していないかを素早く把握できる体制を整えるべく、弊社でも ECR のイメージスキャンを導入しました。

本記事では、その取り組みの一つとして ECR のイメージスキャンを導入した際の設計・構築・運用について紹介します。 同じように ECR のイメージスキャンをこれから導入しようとしている方の参考になれば幸いです。

ECR イメージスキャンとは

Amazon ECR のイメージスキャンは、コンテナイメージに含まれるソフトウェアの脆弱性(CVE)を検出する機能です。

スキャンには Basic Scanning と Enhanced Scanning の2種類があります。

項目 Basic Scanning Enhanced Scanning
スキャンエンジン Clair(オープンソース) Amazon Inspector2
検出対象 OS パッケージの脆弱性 OS パッケージ + プログラミング言語パッケージ(npm, pip, Maven 等)
スキャンタイミング プッシュ時 / 手動 プッシュ時 / 継続スキャン
料金 無料 有料(スキャンしたイメージ数に応じた従量課金)

構成の全体像

導入した構成は以下の通りです。

ECR Enhanced Scanning (Inspector2)
    ↓ 脆弱性検知
EventBridge Rule (CRITICAL のみフィルタ)
    ↓
SNS Topic
    ↓
AWS Chatbot → Slack チャンネルに通知

設計にあたって意識したのは以下です。

検知の網羅性

OS パッケージだけでなく言語パッケージもカバーしたかったため、Enhanced Scanning を採用しました。対応言語の詳細は公式ドキュメントを参照してください。

docs.aws.amazon.com

一方で、OS パッケージの脆弱性検知だけで十分なケースや、まずは無料で始めたいケースでは Basic Scanning も有力な選択肢です。自社の要件に合わせて検討してみてください。

通知のノイズ低減

すべての severity を通知すると対応が追いつかなくなるため、まずは CRITICAL に絞って運用を開始しました。実際に HIGH まで含めて試してみたところ、本当に対応すべき通知が埋もれかねないと感じたので、まずは CRITICAL で運用を開始し、必要に応じてフィルタを広げる方針としています。

認知のスピード

脆弱性の存在に気づかないことが一番のリスクなので、Slack への即時通知を組み込みました。Slack への通知方法としては EventBridge → Lambda で通知内容をカスタマイズする方法もありますが、今回はまず検知できる状態を素早く作ることを優先し、コードを書かずに構築できる AWS Chatbot を採用しました。

コスト

Enhanced Scanning は Amazon Inspector2 の料金体系に基づきます。料金は以下の2つで構成されます(2026年4月時点)。

最新の料金は公式ドキュメントをご確認ください。

aws.amazon.com

  • 初回スキャン: イメージがプッシュされた時のスキャン、$0.09 / イメージ
  • 再スキャン: 継続スキャンにより新しい CVE が公開された際の自動再スキャン、$0.01 / イメージ

試算の考え方

スキャン頻度によってコストの構造が異なります。

スキャン頻度 発生するコスト 計算式
プッシュ時 初回スキャンのみ 月間プッシュ数 × $0.09
継続スキャン 初回スキャン + 再スキャン 上記 + 保持イメージ数 × 再スキャン回数/月 × $0.01

弊社では本番環境は継続スキャン、開発環境はプッシュ時スキャンで運用しています。本番環境では新しい CVE が公開されたタイミングでも即座に検知したいため継続スキャン、開発環境では脆弱性を含む実装が入った時点で素早く検知しつつコストも抑えたいためプッシュ時スキャンが適しています。

試算例

例えば、5つのリポジトリに対して月間100回プッシュし、本番では各リポジトリに2イメージを保持(計10イメージ)するケースで試算します。再スキャン回数は月にどれくらいの頻度で対象の CVE が新たに公開されるかに依存しますが、ここでは月15回程度を見込みました。

項目 計算式 コスト
初回スキャン 100 push × $0.09 $9.00
再スキャン 10 images × 15回 × $0.01 $1.50
月額合計 $10.50

実際のコストはリポジトリ数・プッシュ頻度・保持イメージ数によって変わるので、自社の運用に合わせて試算してみてください。

Basic Scanning(無料)と比較するとコストはかかりますが、言語パッケージの脆弱性検知や新規 CVE の自動再スキャンが得られることを考えると、検討する価値はあると思います。

Terraform による構築

1. ECR スキャン設定

まず ECR レジストリに対して Enhanced Scanning を有効化します。

resource "aws_ecr_registry_scanning_configuration" "this" {
  scan_type = "ENHANCED"

  rule {
    scan_frequency = "CONTINUOUS_SCAN"
    repository_filter {
      filter      = "*"
      filter_type = "WILDCARD"
    }
  }
}

filter = "*" でレジストリ内のすべてのリポジトリをスキャン対象にしています。リポジトリを個別に指定する方法もありますが、新しいリポジトリを追加した際にスキャン対象への追加を忘れるリスクがあるため、ワイルドカードで全体を対象にしています。

scan_frequency は環境によって使い分けています。本番環境では CONTINUOUS_SCAN、開発環境では SCAN_ON_PUSH を設定しています。

2. EventBridge ルール

resource "aws_cloudwatch_event_rule" "ecr_scan_finding" {
  name = "ecr-scan-finding-notification"
  event_pattern = jsonencode({
    "source" : ["aws.inspector2"],
    "detail-type" : ["Inspector2 Finding"],
    "detail" : {
      "status" : ["ACTIVE"],
      "severity" : ["CRITICAL"],
      "resources" : {
        "type" : ["AWS_ECR_CONTAINER_IMAGE"]
      }
    }
  })
  state = "ENABLED"
}

resource "aws_cloudwatch_event_target" "ecr_scan_finding_sns" {
  rule = aws_cloudwatch_event_rule.ecr_scan_finding.name
  arn  = var.ecr_scan_finding_sns_topic_arn
}

Enhanced Scanning では Inspector2 がスキャンエンジンとなるため、イベントソースは aws.inspector2 になります。 Basic Scanning の場合は aws.ecr になるので注意が必要です。

3. SNS トピック

EventBridge から受け取ったイベントを AWS Chatbot に渡すための SNS トピックを作成します。

resource "aws_sns_topic" "ecr_scan_finding_topic" {
  name = "ecr-scan-finding-topic"
}

resource "aws_sns_topic_policy" "ecr_scan_finding_topic_policy" {
  arn    = aws_sns_topic.ecr_scan_finding_topic.arn
  policy = data.aws_iam_policy_document.sns_ecr_scan_finding_topic_policy.json
}

data "aws_iam_policy_document" "sns_ecr_scan_finding_topic_policy" {
  # EventBridge からの Publish を許可
  statement {
    sid    = "AllowEventBridgeToPublishSNS"
    effect = "Allow"
    actions   = ["sns:Publish"]
    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
    resources = [aws_sns_topic.ecr_scan_finding_topic.arn]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = ["arn:aws:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:rule/ecr-scan-finding-notification"]
    }
  }

  # Chatbot からの Subscribe を許可
  statement {
    sid    = "AllowChatbotToSubscribe"
    effect = "Allow"
    actions   = ["sns:Subscribe"]
    principals {
      type        = "Service"
      identifiers = ["chatbot.amazonaws.com"]
    }
    resources = [aws_sns_topic.ecr_scan_finding_topic.arn]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = ["arn:aws:chatbot::${data.aws_caller_identity.current.account_id}:chat-configuration/slack-channel/alert-to-slack"]
    }
  }
}

SNS トピックポリシーでは、EventBridge からの Publish と Chatbot からの Subscribe のみを許可しています。 condition で発信元を絞ることで、意図しないリソースからの操作を防いでいます。

4. AWS Chatbot(Slack 通知)

最後に、SNS トピックのメッセージを Slack に転送する Chatbot の設定です。

resource "aws_chatbot_slack_channel_configuration" "chatbot_alert_to_slack" {
  configuration_name = "alert-to-slack"
  slack_channel_id   = "XXXXXXXXX" # 通知先の Slack チャンネル ID
  slack_team_id      = "XXXXXXXXX" # Slack ワークスペース ID
  iam_role_arn       = var.chatbot_role_arn
  sns_topic_arns = [
    var.ecr_scan_finding_topic_arn,
    # 他の通知用 SNS トピックもここに追加できる
  ]
  guardrail_policy_arns = [
    "arn:aws:iam::aws:policy/ReadOnlyAccess"
  ]
  logging_level = "ERROR"
}

これで CRITICAL な脆弱性が検知された際に、Slack チャンネルに通知が届くようになります。

なお、AWS Chatbot では同じ Slack チャンネルに対して複数の configuration を作成できません。そのため configuration_namealert-to-slack のように汎用的な名前にしています。こうしておけば、今後 WAF のアラートなど別の通知を追加したくなっても sns_topic_arns にトピックを足すだけで済みます。

実際の通知と運用

実際に届く通知は以下のような形式です。

最初は CVE の詳細まで Slack で確認できるものだと思っていたのですが、実際に届く通知には Inspector2 Finding というイベント名と対象の ECR イメージの ARN が表示されるだけで、CVE 名もパッケージ名も表示されませんでした。

そのため、EventBridge の input_transformer を使い、Chatbot のカスタム通知で通知内容を改善しました。

resource "aws_cloudwatch_event_target" "ecr_scan_finding_sns" {
  rule      = aws_cloudwatch_event_rule.ecr_scan_finding.name
  target_id = "SendToSNS"
  arn       = var.ecr_scan_finding_sns_topic_arn

  input_transformer {
    input_paths = {
      "severity"    = "$.detail.severity"
      "title"       = "$.detail.title"
      "description" = "$.detail.description"
      "repository"  = "$.detail.resources[0].details.awsEcrContainerImage.repositoryName"
    }

    input_template = <<TEMPLATE
{
  "version": "1.0",
  "source": "custom",
  "content": {
    "textType": "client-markdown",
    "title": ":rotating_light: ECR <severity> 脆弱性検出 [環境名 (AWSアカウントID)]",
    "description": "*重要度*: <severity>\n*リポジトリ*: <repository>\n*脆弱性*: <title>\n*詳細*: <description>"
  }
}
TEMPLATE
  }
}

ポイントは input_paths でイベントから必要な項目を抽出し、カスタム通知フォーマットで整形している点です。改善後の通知は以下のような形式です。

CVE-ID やパッケージ名、リポジトリ名が表示されるようになり、Slack 上で脆弱性の概要を把握できるようになりました。詳細な対応判断が必要な場合は Inspector2 のダッシュボードを確認する運用ですが、通知を見ただけで対応要否がわかることが増えました。

さらに通知内容を自由にカスタマイズしたい場合は、EventBridge → SNS → Chatbot の経路ではなく、EventBridge → Lambda で整形する方法もあります。

導入してみて

CRITICAL に絞った判断はうまくいきました。最初の通知が来たときも「これは本当に対応が必要なものだ」と落ち着いて対処できたので、狙い通りでした。

一方で、Chatbot のデフォルトの通知では CVE の詳細が出ず、正直もう少し情報が出ると思っていました。実際に使ってみて初めて気づいた部分で、 input_transformer を使ってカスタマイズできることも後から知りました。

Terraform での複数環境展開やスキャン頻度の使い分けはすんなりいきました。

まとめ

今回は、フレームワークやライブラリの脆弱性に素早く対応できる体制づくりの一環として、ECR の Enhanced Scanning を導入した事例を紹介しました。

構成としては ECR Enhanced Scanning → EventBridge → SNS → Chatbot → Slack というシンプルなパイプラインですが、Terraform でコード化することで再現性のある形で複数環境に展開できました。

まず検知できる状態を作ることが第一歩、そこさえ超えれば運用しながら精度を上げていけます。本記事がその一歩を踏み出すきっかけになれば嬉しいです。

最後まで読んでいただきありがとうございました!