Hadolint+OPA/ConftestでDockerfileを“ポリシー化”する実務ガイド:レビュー運ゲーをやめてCIで品質を担保する

はじめに

Dockerfile は小さなテキストファイルですが、ここが雑だと運用は一気に不安定になります。たとえば、

  • latest を使ってしまい、ある日突然ビルドが壊れる
  • apt-get update の後に掃除がなくてイメージが肥大化する
  • root 実行のまま本番に入ってしまう
  • curl | sh のような危険パターンが紛れ込む
  • 重要なルールが「人の記憶」や「レビュー担当の好み」に依存して揺れる

こういう事故は、個々人が注意深くなるだけでは防ぎきれません。人は忙しいし、レビュー観点は増えるし、知識差もあります。
そこで効くのが “ポリシー化” です。

  • Hadolint:Dockerfile の静的解析(ベストプラクティス・危険パターン・スタイル)
  • OPA/Conftest:組織ルール(例:USER 必須、latest 禁止、特定ベースイメージのみ許可)を コードとして 強制

この2つを組み合わせると、Dockerfile レビューが「指摘大会」から「ルールに沿っているかの確認」になり、品質が安定します。
この記事では、“CIで落ちる仕組み” まで含めて、座学→ハンズオンで手を動かしながら導入していきます。


座学

1) Hadolint と OPA/Conftest は役割が違う

まずここが肝です。どちらも “チェック” をしますが、得意領域が違います。

Hadolint(Dockerfile専用Linter)

  • Dockerfile の一般的ベストプラクティスを広くカバー
  • ありがちなミス(apt-get update の扱い、sudo、危険な ADD、ピン留め不足など)を早期に検出
  • “良い書き方”を教えてくれる

OPA/Conftest(ポリシーエンジン)

  • 会社・チームの規約を 明文化 して強制
  • “一般論”ではなく、あなたの現場のルールに寄せられる
    • 例:FROM は社内ミラーのみ許可
    • 例:本番は USER 必須、EXPOSE の番号制限
    • 例:apk add には --no-cache 必須
  • 例外(許可リスト)や環境別ルールも表現できる

結論:
Hadolintで一般的な品質を底上げし、Conftestで組織ルールを“ブレなく”強制する、が最も運用しやすいです。


2) 「レビューで見るべきこと」を機械にやらせる

Dockerfile レビューで毎回見る観点は、だいたい固定です。

  • 再現性:latest を使っていないか、依存が固定されているか
  • 安全性:root 実行か、危険コマンドがないか、秘密情報を埋め込んでいないか
  • サイズ:キャッシュ・掃除、不要ファイルの混入(.dockerignore
  • 保守性:意味のある LABEL、分かりやすい順序、コメント

これを人が毎回 “目視” でやると、見落としと属人化が起きます。
CIで落ちるようにすると、レビューは「設計や意図」に集中できるようになります。


3) ポリシー化の現実解:厳しすぎると嫌われる

導入初期にありがちな失敗は、最初からガチガチにして PR が通らなくなることです。
おすすめは段階導入:

  1. 警告(Lintはレポートだけ):まず現状を可視化
  2. 新規・変更分だけ必須:既存資産を一気に直さず、将来の負債増加を止める
  3. 本番向けDockerfileだけ厳格化:dev 用は緩め、本番だけ強制

この順番でやると反発が少なく、ルールが定着します。


4) Conftestのイメージ:Dockerfileを“構造化”してから判定する

Conftest(OPA/Rego)は JSON/YAML など構造化データに対して強いです。
Dockerfileはテキストなので、そのままだと扱いにくいのですが、ここで使うのが dockerfile-parse(Python)などのパーサです。

流れはこうです:

  1. Dockerfile をパースして JSON を作る(命令一覧、FROM、USER、RUN など)
  2. Conftest が JSON に対して Rego ルールで判定する
  3. ルール違反なら CI を落とす

つまり、Dockerfileを“検査できる形”に変換してからポリシー適用するのが実務的です。


ハンズオン

ここでは最小構成で「DockerfileをチェックしてCIで落とす」ところまで作ります。
以下の構成を想定します。

repo/
  Dockerfile
  policy/
    dockerfile.rego
  scripts/
    dockerfile_to_json.py
  .github/
    workflows/
      dockerfile-policy.yml

0) 例として“あえて微妙な”Dockerfileを用意する

まず、違反を検出できるかを確認するためのDockerfileです。

Dockerfile

FROM ubuntu:latest

RUN apt-get update
RUN apt-get install -y curl

CMD ["bash", "-lc", "echo hello"]

このDockerfileには問題が複数あります:

  • latest 使用(再現性が揺れる)
  • apt-get updateapt-get install が分離(キャッシュ・整合性・セキュリティ的にも微妙)
  • rm -rf /var/lib/apt/lists/* が無くて肥大化しやすい
  • USER 指定なし(rootのまま動く)

1) Hadolint をローカルで回してみる(まずは可視化)

Hadolint はコンテナでも実行できます。ローカルで試すと理解が早いです。

docker run --rm -i hadolint/hadolint < Dockerfile

ここで出る指摘は“一般的ベストプラクティス”です。
ただし、あなたの現場の事情(社内ベースイメージ縛り等)はHadolintだけでは表現しづらい。そこで次が Conftest です。


2) Dockerfile を JSON に変換するスクリプトを用意する

Conftest は JSON/YAML を扱うのが得意なので、Dockerfile をパースして JSON にします。
ここでは Python の dockerfile-parse を使った最小スクリプト例を置きます。

scripts/dockerfile_to_json.py

import json
import sys
from dockerfile_parse import DockerfileParser

def main():
    path = sys.argv[1] if len(sys.argv) > 1 else "Dockerfile"
    dfp = DockerfileParser(path=path)

    # 代表的に使いやすい形へ整形
    instructions = []
    for entry in dfp.structure:
        # entry: {instruction, value, original, startline, endline, ...}
        instructions.append({
            "instruction": entry.get("instruction", "").upper(),
            "value": entry.get("value", ""),
            "original": entry.get("original", ""),
            "startline": entry.get("startline"),
            "endline": entry.get("endline"),
        })

    doc = {
        "path": path,
        "baseimage": dfp.baseimage,     # FROM
        "instructions": instructions,
    }

    print(json.dumps(doc, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    main()

ローカルで実行するには(python環境がある場合):

pip install dockerfile-parse
python scripts/dockerfile_to_json.py Dockerfile > dockerfile.json
cat dockerfile.json

JSON になれば、Conftest 側で柔軟に判定できます。


3) Conftest(OPA/Rego)で“組織ルール”を定義する

次に、あなたのチームのルールを “コード” にします。ここでは例としてよく効く3つを入れます。

  • latest を禁止(FROMタグが latest ならNG)
  • USER が必須(本番想定)
  • apt-get updateapt-get install は同一RUNで、かつ掃除を要求(基本のサイズ・安定性)

policy/dockerfile.rego

package dockerfile.policy

default deny = []

# 1) FROM latest 禁止
deny[msg] {
  endswith(lower(input.baseimage), ":latest")
  msg := "FROM で :latest を使用しています(再現性が揺れるため禁止)"
}

# 2) USER 必須(少なくとも1回はUSER命令が必要)
deny[msg] {
  not has_user
  msg := "USER が指定されていません(root実行を避けるため USER を必須にしてください)"
}

has_user {
  some i
  input.instructions[i].instruction == "USER"
}

# 3) apt-get update/install の分離禁止 + 掃除必須(簡易チェック)
deny[msg] {
  some i
  inst := input.instructions[i]
  inst.instruction == "RUN"
  contains(inst.original, "apt-get update")
  not contains(inst.original, "apt-get install")
  msg := "apt-get update が単独RUNです(updateとinstallは同一RUNにまとめてください)"
}

deny[msg] {
  some i
  inst := input.instructions[i]
  inst.instruction == "RUN"
  contains(inst.original, "apt-get install")
  not contains(inst.original, "rm -rf /var/lib/apt/lists")
  msg := "aptのキャッシュ掃除がありません(rm -rf /var/lib/apt/lists/* を同一RUNに含めてください)"
}

注:この例は“分かりやすさ優先”の簡易判定です。厳密にやるなら命令の分解や許可パターン(例外)も入れられます。まずは運用で回る最小から始めるのがコツです。

Conftest をローカル実行(Conftest が入っている場合):

conftest test dockerfile.json -p policy

違反があると deny が出て終了コードが非0になります。CIで落とせます。


4) GitHub Actionsで “Dockerfile変更時に必ず検査” する

最後にCIに繋ぎます。ここでは

  • Hadolint はコンテナで実行
  • Conftest は公式セットアップ+PythonでJSON生成
    という形の例を出します。

.github/workflows/dockerfile-policy.yml

name: Dockerfile Policy

on:
  pull_request:
    paths:
      - "Dockerfile"
      - "policy/**"
      - "scripts/**"
  push:
    paths:
      - "Dockerfile"
      - "policy/**"
      - "scripts/**"

jobs:
  lint-and-policy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 1) Hadolint(Dockerfileの一般Lint)
      - name: Hadolint
      - run: |
          docker run --rm -i hadolint/hadolint < Dockerfile

      # 2) Python環境(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

      # 3) Conftest(OPA/Regoで組織ルールを強制)
      - name: Setup Conftest
        uses: instrumenta/conftest-action@v0.4.0
        with:
          version: "0.56.0"

      - name: Conftest policy check
        run: conftest test dockerfile.json -p policy

これで、PRで Dockerfile を触ったら 必ず lint/policy が走り、違反があれば落ちます。
レビュー担当が「言う/言わない」で揺れないのが最大の価値です。


5) ルール違反を直して“通る”Dockerfileにする

先ほどのDockerfileを、最低限ポリシーを満たす形に直します。

Dockerfile(修正版の例)

FROM ubuntu:24.04

RUN apt-get update \
  && apt-get install -y --no-install-recommends curl ca-certificates \
  && rm -rf /var/lib/apt/lists/*

# 最小権限ユーザー(例)
RUN useradd -m -u 10001 appuser
USER appuser

CMD ["bash", "-lc", "echo hello"]
  • latest をやめて固定タグへ
  • apt は1RUNにまとめ、掃除を同じRUNで実施
  • USER を指定して root 実行を避ける

この時点で Hadolint/Conftest が通るようになり、ルールが仕組みとして機能していることが確認できます。


まとめ

Dockerfile の品質は、プロダクトの安定性・セキュリティ・開発体験に直結します。にもかかわらず、レビューだけに頼ると 属人化見落とし が避けられません。

  • Hadolintで一般的ベストプラクティスを網羅的に検出
  • OPA/Conftestで組織ルールを “コード化” し、CIで強制
  • Dockerfile を JSON にパースしてから判定する構成にすると、運用が現実的
  • 導入は段階的(警告→変更分→本番のみ厳格)にすると定着しやすい

この仕組みが入ると、レビューは「ルール指摘」から「設計・意図の議論」へ移り、チームの生産性が上がります。
ポリシーは “縛る” ためではなく、事故の確率を下げるための自動化です。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

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