CI/CDパイプラインにContainer Security Scanningを組み込む実装ガイド

本記事では、CI/CDパイプラインにコンテナセキュリティスキャンを統合し、デプロイ前に脆弱性を検出・ブロックする実装方法を解説します。Trivy、Anchore、Snykなどのツールを使った段階的な導入から、実務での運用ポイントまでカバーします。

Container Security Scanningが必要な背景

Dockerコンテナの普及に伴い、セキュリティリスクも急速に増加しています。実務では、毎日数千件の新しい脆弱性が報告され、これらのCVE(Common Vulnerabilities and Exposures)がコンテナイメージに含まれていないかを自動的に確認する必要があります。

Container Security Scanningとは、コンテナイメージ内に含まれるOSパッケージやアプリケーション依存関係の脆弱性を検出するプロセスです。CI/CDパイプラインに組み込むことで、以下のメリットが得られます:

  • 早期検出:本番環境へのデプロイ前に脆弱性を発見
  • 自動化:手動レビューが不要になり、開発速度が向上
  • コンプライアンス:セキュリティポリシーの一貫性を確保
  • リスク低減:セキュリティインシデントの可能性を大幅に削減

主要なContainer Security Scanningツール比較

市場に複数のツールが存在します。筆者の実務経験では、以下3つが最も実用的です:

ツール 強み 向いている場面
Trivy 軽量、高速、無料。OS向けVulnerability DBが充実 小規模チーム、起動が速い必要がある場合
Anchore 詳細なポリシーエンジン、エンタープライズ対応 大規模組織、厳密なセキュリティポリシーが必要
Snyk SCA(Software Composition Analysis)が強い。開発者向けUI アプリケーション依存関係の脆弱性にも対応したい場合

本記事ではTrivyを基軸に解説しますが、Anchoreへの応用も示します。

Trivyを使ったCI/CD統合の実装

Step 1: ローカル環境でTrivyのセットアップ

まずはローカルで動作確認します。macOS 14 / Docker Desktop / Trivy 0.50.1での動作を確認しています。

# Homebrewでインストール
brew install trivy

# バージョン確認
trivy version

# 簡単なスキャンテスト
trivy image --severity HIGH,CRITICAL nginx:latest

このコマンドでnginxイメージの高リスク脆弱性が一覧表示されます。出力例:

2025-01-15T10:23:45.123Z	INFO	Vulnerability Scanner	image="nginx:latest" platform="linux/amd64"
2025-01-15T10:23:52.456Z	INFO	[nginx:latest] 32 vulnerabilities detected

nginx:latest (debian 12.2)
============================
HIGH: 5 vulnerabilities

  libc6 (Debian)
  └─ CVE-2024-2961 High
     Description: A heap overflow vulnerability in the glibc
     Severity: HIGH
     Affected Version: 2.36-9+deb12u3

CRITICAL: 1 vulnerability

  openssl (Debian)
  └─ CVE-2024-0567 Critical
     Description: OpenSSL vulnerability
     Severity: CRITICAL

Step 2: GitHub Actionsへの統合

実務ではGitHub ActionsやGitLab CIなどのCI/CDサービスとの連携が必須です。以下は実際に動作するGitHub Actionsのワークフロー定義です:

name: Container Security Scan

on:
  push:
    branches: [ main, develop ]
    paths:
      - 'Dockerfile'
      - 'docker-compose.yml'
  pull_request:
    branches: [ main ]

jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    
    steps:
      # ステップ1: リポジトリをチェックアウト
      - name: Checkout code
        uses: actions/checkout@v4
      
      # ステップ2: Dockerイメージをビルド
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      
      # ステップ3: Trivyでスキャン実行
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
      
      # ステップ4: 結果をGitHubにアップロード
      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'
      
      # ステップ5: CRITICAL脆弱性がある場合は失敗
      - name: Fail if CRITICAL vulnerabilities found
        run: |
          if grep -q '"level": "CRITICAL"' trivy-results.sarif; then
            echo "❌ CRITICAL vulnerabilities detected!"
            exit 1
          else
            echo "✅ No CRITICAL vulnerabilities found"
          fi

このワークフローの実行フロー:


flowchart LR
    A[コード push] --> B[ワークフロー開始]
    B --> C[Dockerイメージビルド]
    C --> D[Trivy実行]
    D --> E{CRITICAL
脆弱性あり?} E -->|Yes| F[❌ ビルド失敗] E -->|No| G[✅ 次のステップへ] D --> H[結果をGitHub
Securityに報告]

Step 3: GitLab CIへの統合

GitLabを使用している場合は、.gitlab-ci.ymlで以下のように統合します:

stages:
  - build
  - scan
  - deploy

build_image:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

trivy_scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    # ローカルレジストリからイメージをプル
    - trivy image --severity HIGH,CRITICAL 
      --format json 
      --output trivy-report.json
      $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    
    # JUnitレポート形式で出力(GitLabダッシュボード表示用)
    - trivy image --severity HIGH,CRITICAL 
      --format sarif 
      --output trivy-sarif.json
      $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  
  artifacts:
    reports:
      sast: trivy-sarif.json
    paths:
      - trivy-report.json
  
  allow_failure: false  # 脆弱性検出でビルド失敗

deploy:
  stage: deploy
  script:
    - echo "Deploying $CI_COMMIT_SHA"
    - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main
  when: on_success

Anchoreを使った高度なポリシー管理

Anchoreのポリシーエンジンとは

Trivyは脆弱性検出に特化していますが、Anchoreは「ポリシーエンジン」を備えています。これは「このイメージがデプロイ可能か」をビジネス要件に基づいて判定します。

例えば、以下のようなルールを定義できます:

  • CRITICAL脆弱性は絶対に許さない
  • HIGH脆弱性は最大3つまで許容(ただし60日以内に修正予定なら可)
  • MITERライセンスのパッケージは許さない
  • パッケージの署名検証に失敗したら拒否

Anchoreのセットアップと実装

Docker ComposeでAnchoreをローカルで試してみます:

version: '3.8'
services:
  anchore-engine:
    image: anchore/anchore-engine:latest
    ports:
      - "8228:8228"
      - "8338:8338"
    environment:
      ANCHORE_DB: "postgresql://anchore:anchore@postgres:5432/anchore"
      ANCHORE_LOG_LEVEL: "INFO"
    volumes:
      - ./anchore-config.yaml:/etc/anchore/config.yaml:ro
    depends_on:
      - postgres
  
  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: anchore
      POSTGRES_PASSWORD: anchore
      POSTGRES_DB: anchore
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

起動後、Anchore CLIで手動スキャンを実行:

# Anchore CLIをインストール
pip install anchorecli

# Anchoreサーバーに接続設定
export ANCHORE_CLI_URL="http://localhost:8228/v1"
export ANCHORE_CLI_USER=admin
export ANCHORE_CLI_PASS=foobar

# イメージをスキャン対象に追加
anchore-cli image add nginx:latest

# スキャン完了まで待機
anchore-cli image wait nginx:latest

# 脆弱性レポート取得
anchore-cli image vuln nginx:latest all

# ポリシー評価(GO/FAILを判定)
anchore-cli image policy nginx:latest

カスタムポリシーの定義

anchore-policy.jsonでビジネス要件に基づくポリシーを定義します:

{
  "version": "2_0",
  "policies": [
    {
      "name": "strict-security-policy",
      "version": "1",
      "description": "本番環境向け厳密なセキュリティポリシー",
      "rules": [
        {
          "gate": "vulnerabilities",
          "trigger": "critical",
          "action": "STOP",
          "params": [
            {
              "name": "max_days_since_sync",
              "value": 1  // 脆弱性DBが1日以上古い場合は失敗
            }
          ]
        },
        {
          "gate": "vulnerabilities",
          "trigger": "high",
          "action": "WARN",
          "params": [
            {
              "name": "max_days_since_sync",
              "value": 1
            }
          ]
        },
        {
          "gate": "licenses",
          "trigger": "blacklisted",
          "action": "STOP",
          "params": [
            {
              "name": "license_terms",
              "value": "GPL,AGPL"  // GPL系ライセンスは許さない
            }
          ]
        }
      ]
    }
  ]
}

このポリシーをCI/CDで適用:

anchore-cli policy add anchore-policy.json

# デフォルトポリシーとして設定
anchore-cli policy activate strict-security-policy

# 以降のスキャンでこのポリシーが自動適用される
anchore-cli image policy nginx:latest

実装時によくあるハマりポイントと解決策

問題1: スキャン結果の誤検知(False Positives)

Trivyなどのツールは、時に脆弱性があると誤判定することがあります。例えば、パッケージのバージョンが正確に検出されなかったり、実は修正されているのに古い脆弱性ベースで検出されたりします。

解決策:

# .trivyignoreファイルで特定のCVEを無視
# ファイル例:
AVD-2024-1234
CVE-2024-5678

# CI/CDパイプラインで参照
trivy image --ignorefile .trivyignore myapp:latest

ただし、むやみに無視するのは禁物です。無視する際は理由をコメントに記述し、定期的に見直すようにしましょう。

問題2: スキャン時間が長くなる

大規模なレイヤー数を持つイメージや、古いOSベースイメージをスキャンすると、数分以上かかることがあります。実務では、CI/CDのタイムアウトに引っかかることもあります。

解決策1: イメージの最適化

# Dockerfile例:マルチステージビルドで不要なレイヤーを削減
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y build-essential
COPY . /src
WORKDIR /src
RUN make

FROM ubuntu:22.04
# ビルド結果のみコピー(中間レイヤーは除外)
COPY --from=builder /src/output /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*  # キャッシュを削除してサイズ削減

解決策2: スキャン対象の絞り込み

# CRITICAL+HIGHのみスキャン(中程度の脆弱性は無視)
trivy image --severity CRITICAL,HIGH myapp:latest

# 特定のコンポーネントのみスキャン
trivy rootfs --skip-files /usr/share/doc /

解決策3: スキャンキャッシュの活用

# Trivyのスキャン結果をキャッシュ
trivy image --cache-dir /tmp/trivy-cache myapp:latest

問題3: CI/CDパイプラインのブロック率が高すぎる

脆弱性検出が厳しすぎると、正当なイメージまでがブロックされ、開発チームのフラストレーションが溜まります。

解決策:段階的な導入

# フェーズ1: 最初は警告のみ(ビルド失敗なし)
trivy image --severity CRITICAL myapp:latest || echo "⚠️ Warning but continuing..."

# フェーズ2: 1ヶ月後、CRITICAL+HIGHで失敗させる
trivy image --severity CRITICAL,HIGH myapp:latest

# フェーズ3: さらに1ヶ月後、全脆弱性で失敗
trivy image --severity LOW,MEDIUM,HIGH,CRITICAL myapp:latest

パフォーマンス・コスト最適化の考慮事項

スキャン頻度の最適化

毎回フルスキャンするのではなく、以下のような戦略を採用します:

  • PR時:Dockerfileや依存関係の変更がある場合のみスキャン
  • マージ時:フルスキャン実行
  • 定期実行:毎日深夜に全イメージの再スキャン(脆弱性DBの更新に対応)
name: Scheduled Full Scan

on:
  schedule:
    # 毎日深夜2時(UTC)に実行
    - cron: '0 2 * * *'

jobs:
  full-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:latest'
          format: 'json'
          output: 'full-scan-results.json'
      
      # スキャン結果をSlackに通知
      - name: Notify to Slack
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "🔒 Daily Container Security Scan Complete",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "結果: ${{ job.status }}\nイメージ: myapp:latest"
                  }
                }
              ]
            }

レジストリスキャンとの連携

プライベートレジストリ(Amazon ECR、Azure Container Registry)のスキャン機能も活用すると効果的です。

# AWS ECRのスキャン有効化
aws ecr put-image-scanning-configuration \
  --repository-name myapp \
  --image-scanning-configuration scanOnPush=true

# スキャン結果を取得
aws ecr describe-image-scan-findings \
  --repository-name myapp \
  --image-id imageTag=latest \
  --region us-east-1

実務ユースケース:マイクロサービス環境での運用

筆者の実務経験では、30個以上のマイクロサービスを運用するチームでContainer Security Scanningを導入した際、以下のアプローチが最も効果的でした:

集約型スキャンダッシュボード

複数のマイクロサービスのスキャン結果を一元管理するダッシュボード構築:

import requests
import json
from datetime import datetime

def aggregate_scan_results():
    """全マイクロサービスのスキャン結果を集約"""
    services = [
        'auth-service',
        'payment-service',
        'notification-service',
        'data-processor'
    ]
    
    results = {
        'timestamp': datetime.now().isoformat(),
        'services': {}
    }
    
    for service in services:
        # 各サービスのスキャン結果をTrivyサーバーから取得
        response = requests.get(
            f'http://trivy-server:4954/v1/image/json',
            params={'image-ref': f'myregistry/{service}:latest'}
        )
        
        if response.status_code == 200:
            scan_data = response.json()
            
            # 脆弱性をカウント
            critical_count = len([
                v for v in scan_data.get('Results', [])
                if v.get('Severity') == 'CRITICAL'
            ])
            high_count = len([
                v for v in scan_data.get('Results', [])
                if v.get('Severity') == 'HIGH'
            ])
            
            results['services'][service] = {
                'status': 'PASS' if critical_count == 0 else 'FAIL',
                'critical': critical_count,
                'high': high_count,
                'last_scanned': datetime.now().isoformat()
            }
    
    return results

# 結果をJSONで出力(ダッシュボードに表示)
results = aggregate_scan_results()
print(json.dumps(results, indent=2))

この結果をWebダッシュボード(例:Grafana)に統合することで、セキュリティ状況を一目で把握できます:


graph TD
    A[複数マイクロサービス] -->|Trivy| B[スキャン実行]
    B -->|結果集約| C[集約API]
    C -->|メトリクス取得| D[Prometheus]
    D -->|可視化| E[Grafana Dashboard]
    E -->|アラート| F[Slack通知]
    F -->|CRITICAL検出| G[緊急対応チーム]
  

使うべき場面と使うべきでない場面

✅ Container Security Scanningを導入すべき場合

  • 本番環境でコンテナを使用しており、セキュリティが重要
  • 複数のチームがコンテナイメージを開発・管理している
  • コンプライアンス要件(SOC2、PCI-DSS等)がある
  • 開発速度とセキュリティのバランスをとりたい

❌ 導入が過剰かもしれない場合

  • プロトタイプ環境やローカル開発環境のみでの使用
  • スキャンオーバーヘッドが開発速度を大幅に低下させている
  • 完全にエアギャップ環境で運用しており、外部脅威がない

よくある質問

すべての脆弱性が同じリスクではありません。以下の観点で判断してください:

Trivy:オープンソース、ローカルで実行可能、CI/CDパイプラインに統合しやすい、漏洩認証情報検出も可能

はい。Kubernetes Admission Controllerを使うことで、ポリシー違反のイメージのデプロイを防げます:

以下の方法で監査可能な形式で保存できます:

まとめ

  • Container Security Scanningの目的:CI/CDパイプラインに脆弱性検出を自動統合し、本番環境へのリスク軽減
  • ツール選択:Trivyは軽量・高速、Anchoreはポリシー管理に特化。規模と要件に応じて選択
  • 実装方法:GitHub ActionsやGitLab CIに統合し、CRITICAL脆弱性でビルド
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →