はじめに
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つのどれかになります。
- exit code で制御
- 緩和:失敗しても
continue-on-error: true - 厳格:通常どおり落とす
- 緩和:失敗しても
- ルールセットを切り替え
- 緩和:重要ルールだけ(最低限)
- 厳格:全部
- 同じルールだが “severity” で扱い分け
- Rego で
denyとwarnを分ける - もしくは出力を加工して「警告扱い」へ
- Rego で
おすすめは、最初は 1(exit code) が最短で運用に乗ります。
ポリシーが育ってきたら 2/3 へ進むのが自然です。
4) 差分検出の実務ポイント
差分検出でハマりやすいのはここです。
pull_requestの場合、比較対象は base sha と head 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 が「邪魔者」ではなく「地雷除去装置」になり、レビューも建設的になります。
コメントを残す