Dockerfile差分だけ“厳格モード”で落とすCI設計:既存負債を爆発させずに品質を積み上げる(Hadolint+Conftest運用編)

はじめに

Dockerfile の lint/ポリシー検査を CI に入れると、品質は確実に上がります。
一方で、導入直後にほぼ必ず起きる問題があります。

  • 既存の Dockerfile がルール違反だらけで、CI が全部落ちる
  • 直す量が多すぎて導入が止まる
  • “とりあえず無効化” のまま形骸化する

これ、仕組み自体が悪いのではなく、導入の仕方が難しいだけです。

そこで現実解として強いのが、タイトルどおりの運用です。

  • Dockerfile が変更されたときだけ厳格モード(fail)
  • 変更されていないときは “緩いモード”(warn)でレポートのみ

こうすると「既存負債を一気に返さない」まま、将来の負債増加を止めて、変更が入るたびに少しずつ品質を上げられます。
この記事では、GitHub Actions を例に、差分検査→厳格/緩和の切り替え→実務で破綻しない設計まで、手を動かして作っていきます。


座学

1) “全部厳格”が失敗しやすい理由

CI を厳格にするときの落とし穴は、技術ではなくプロセスです。

  • ルール違反の総量が大きいほど、PR が通らなくなる
  • 通らないと「CIうざい」「ルールが現場を分かってない」になりがち
  • 結果、例外だらけ or 無効化で終わる

つまり、品質向上のための仕組みが、開発の摩擦になって壊れるのが典型失敗です。

差分だけ厳格にするのは、この摩擦を最小化しながら “増える負債” を止める戦略です。


2) 差分だけ厳格にするとは何を意味するか

ここで言う「差分だけ」とは2段階あります。

A. ファイル単位の差分

  • Dockerfile が変更された PR だけ厳格
  • Dockerfile を触らない PR はレポートのみ(落とさない)

これは導入が簡単で、効果が高いです。

B. 行(差分)単位の厳格

  • Dockerfile の “変更された行” に関連する違反だけ fail
  • 変更されていない行の違反は warn

B は理想ですが、実装が一気に難しくなります(パースが必要、ツール出力とのマッピングが必要)。
多くのチームでは A だけでも十分に効きます。この記事ではまず A を完成させ、余裕があれば B に拡張できる設計に寄せます。


3) 厳格/緩和モードの設計パターン

CI の切り替えは主にこの3つのどれかになります。

  1. exit code で制御
    • 緩和:失敗しても continue-on-error: true
    • 厳格:通常どおり落とす
  2. ルールセットを切り替え
    • 緩和:重要ルールだけ(最低限)
    • 厳格:全部
  3. 同じルールだが “severity” で扱い分け
    • Rego で denywarn を分ける
    • もしくは出力を加工して「警告扱い」へ

おすすめは、最初は 1(exit code) が最短で運用に乗ります。
ポリシーが育ってきたら 2/3 へ進むのが自然です。


4) 差分検出の実務ポイント

差分検出でハマりやすいのはここです。

  • pull_request の場合、比較対象は base shahead sha
  • shallow clone(fetch-depth)だと git diff が取れない
  • monorepo だと Dockerfile が複数ある(検索が必要)

対策としては:

  • checkout を fetch-depth: 0 にする
  • git diff --name-only を base..head で取る
  • Dockerfile を “列挙” する処理を用意する(後で拡張可能)

ハンズオン

ここでは GitHub Actions で「Dockerfile が変更されたら厳格、されてなければ緩和」を作ります。
ツールは例として Hadolint + Conftest(OPA)を想定しますが、考え方は他の検査にも使えます。

0) 前提のファイル構成(例)

repo/
Dockerfile
policy/
dockerfile.rego
scripts/
dockerfile_to_json.py

(Conftest のために Dockerfile を JSON へ変換するスクリプトは、前回までの流れをそのまま利用する想定です)


1) 差分で Dockerfile 変更有無を判定する(最小)

まずは「Dockerfile が触られたか?」を CI で判断できるようにします。

.github/workflows/dockerfile-policy.yml(骨組み)

name: Dockerfile Policy (Diff-aware)on:
pull_request:
push:jobs:
detect:
runs-on: ubuntu-latest
outputs:
dockerfile_changed: ${{ steps.diff.outputs.dockerfile_changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 - id: diff
shell: bash
run: |
set -euo pipefail # push と pull_request で比較対象が違うので分岐
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
# push は直前コミットとの差分でOK(必要なら前のshaの取り方を調整)
BASE="${{ github.event.before }}"
HEAD="${{ github.sha }}"
fi echo "BASE=$BASE"
echo "HEAD=$HEAD" CHANGED=$(git diff --name-only "$BASE" "$HEAD" || true)
echo "$CHANGED" if echo "$CHANGED" | grep -E '(^|/)(Dockerfile|.*\.Dockerfile)$' >/dev/null; then
echo "dockerfile_changed=true" >> "$GITHUB_OUTPUT"
else
echo "dockerfile_changed=false" >> "$GITHUB_OUTPUT"
fi

ポイント:

  • fetch-depth: 0 が重要(差分が取れない事故を防ぐ)
  • monorepo を想定して Dockerfile だけでなく *.Dockerfile も拾う例にしています

2) 検査ジョブに “厳格/緩和” を注入する

次に、検査ジョブで dockerfile_changed を見て挙動を変えます。
最も簡単なのは、緩和モードでは continue-on-error: true を使う方法です。

続き:lint_and_policy ジョブ

  lint_and_policy:
runs-on: ubuntu-latest
needs: detect steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # --- Hadolint ---
- name: Hadolint (strict if changed)
continue-on-error: ${{ needs.detect.outputs.dockerfile_changed != 'true' }}
run: |
docker run --rm -i hadolint/hadolint < Dockerfile # --- Dockerfile -> JSON ---
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12" - name: Install parser
run: pip install dockerfile-parse - name: Convert Dockerfile to JSON
run: python scripts/dockerfile_to_json.py Dockerfile > dockerfile.json # --- Conftest ---
- name: Setup Conftest
uses: instrumenta/conftest-action@v0.4.0
with:
version: "0.56.0" - name: Conftest policy (strict if changed)
continue-on-error: ${{ needs.detect.outputs.dockerfile_changed != 'true' }}
run: |
conftest test dockerfile.json -p policy

挙動はこうなります:

  • Dockerfile が変更されている PR
    → Hadolint/Conftest で違反が出たら ジョブが落ちる(厳格)
  • Dockerfile が変更されていない PR
    → 違反が出ても ジョブは落ちない(緩和)

これだけで導入の摩擦が大きく下がります。


3) “緩和でも無視しない”ためのレポート表示

緩和モードでただ通すだけだと、誰も見ない問題が起きます。
そこで、緩和時は サマリーに出す のが効きます(PR の UI に残るので気づきやすい)。

例として、Conftest の出力を GitHub Actions Summary に書く例です(雑に全出力を貼るだけでも効果があります)。

      - name: Conftest policy (write summary)
if: always()
run: |
set -euo pipefail
echo "## Dockerfile Policy Report" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- dockerfile_changed: ${{ needs.detect.outputs.dockerfile_changed }}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY" # conftest を一度実行してログを残す(失敗しても続行)
conftest test dockerfile.json -p policy > conftest.log 2>&1 || true echo '```' >> "$GITHUB_STEP_SUMMARY"
tail -n 200 conftest.log >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"

ポイント:

  • 緩和時も “見える” 形で残す
  • ただし貼りすぎると読みづらいので tail 等で上限を付ける

4) ルールセット切り替え(成長させる運用)

さらに一歩進めるなら、緩和では最低限ルールだけにするのが現実的です。
例:

  • 緩和ルール:latest禁止 / USER必須 のような “重大なやつだけ”
  • 厳格ルール:ベストプラクティス含む全部

構成例:

policy/
strict/
dockerfile.rego
soft/
dockerfile.rego

CIでポリシーのパスを切り替える:

      - name: Conftest policy
continue-on-error: ${{ needs.detect.outputs.dockerfile_changed != 'true' }}
run: |
if [ "${{ needs.detect.outputs.dockerfile_changed }}" = "true" ]; then
conftest test dockerfile.json -p policy/strict
else
conftest test dockerfile.json -p policy/soft
fi

これで、緩和時でも「最低限の地雷だけは踏ませない」設計にできます。


まとめ

Dockerfile の lint/ポリシー検査は、CI に入れるだけで勝てるわけではありません。
既存資産の負債がある現場ほど、導入の仕方が重要です。

  • いきなり全PRを厳格にすると、既存違反で詰まりやすい
  • “Dockerfile が変更されたときだけ厳格”にすると、負債を爆発させずに品質を上げられる
  • 緩和モードでもレポートを可視化して、少しずつ直す文化を作る
  • 慣れてきたら「ルールセット切り替え」で段階導入を洗練させる

この運用にすると、CI が「邪魔者」ではなく「地雷除去装置」になり、レビューも建設的になります。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です