· 19 分で読める · 9,567 文字
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脆弱性でビルド