UbuntuでChiselを使う:セキュリティを強化する超軽量コンテナイメージの作り方

Ubuntu tutorial - IT technology blog
Ubuntu tutorial - IT technology blog

コンテナイメージの「肥大化」——よくある問題なのに意外と見過ごされている

よく見かけるパターンがある。アプリを書き終えたdevが ubuntu:22.04 からDockerイメージをビルドし、50KBのPythonバイナリを動かすだけなのにイメージが400〜500MBにもなる、というケースだ。コンテナを触り始めた頃は、これが問題だとは思わなかった——「動けばいい」という感覚だった。でも、使ってもいないパッケージがイメージに大量に含まれていてCVEスキャナーが赤信号を出し続けるのを見て、「slim」や「distroless」がなぜこれほど語られるのか、ようやく腑に落ちた。

根本的な問題はこうだ。たとえば libssl3 をインストールすると、SSLライブラリだけでなく、man page、ドキュメント、ロケールファイル、ヘッダーまでついてくる。それらは容量を占有するだけでなく、脆弱性が発生したときの潜在的なアタックサーフェスになる。

Chiselとは何か、そしてAlpineやDistrolessとどう違うのか

ChiselはCanonicalが開発したツールで、Ubuntuパッケージを「スライス(slice)」できる——アプリに必要なファイルだけを正確に取り出し、残りはすべて除外する。この概念はパッケージスライシング(package slicing)と呼ばれる。

Alpine(musl libcを使用し、glibc向けにコンパイルされたアプリと非互換になることがある)やDistroless(固定的でカスタマイズが難しい)とは異なり、Chiselでは以下が実現できる:

  • 正規のUbuntuパッケージを使用——glibcの非互換を気にしなくていい
  • 必要なスライスを正確に選択:runtimeファイルのみ、ドキュメントやヘッダーは不要
  • Docker multi-stage buildと組み合わせて超軽量イメージを作成

Chiselでは各パッケージが「スライス」で定義されている——パッケージ内のファイルをグループ化したものだ。たとえば libssl3 パッケージには libssl3_libs(.soファイルのみ)と libssl3_dev(開発用ヘッダー)というスライスがある。スライスの構文は package_slicename だ。

UbuntuにChiselをインストールする

ChiselはGoのバイナリで、複雑な依存関係なしにすぐインストールできる:

# 最新バイナリをダウンロード
CHISEL_VERSION=$(curl -s https://api.github.com/repos/canonical/chisel/releases/latest | grep tag_name | cut -d'"' -f4)
curl -sSL "https://github.com/canonical/chisel/releases/download/${CHISEL_VERSION}/chisel_${CHISEL_VERSION}_linux_amd64.tar.gz" | tar xz
sudo mv chisel /usr/local/bin/

# バージョン確認
chisel --version

Ubuntuデスクトップ/サーバーを使っているなら、snap経由でもインストールできる:

sudo snap install chisel --channel=latest/stable

パッケージの利用可能なスライスを確認するには:

# libssl3のスライスを確認
chisel info --release ubuntu-22.04 libssl3

# python3.10のスライスを確認
chisel info --release ubuntu-22.04 python3.10

実践:PythonアプリケーションのMinimal Imageを作る

シンプルなFlask APIを例に、2つのビルド方法を直接比較してみよう。

通常の方法(ubuntu:22.04ベース)

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY app.py .
CMD ["python3", "app.py"]

結果:イメージは約250MB、Flaskとは無関係なパッケージが数百個含まれる。

Chiselとmulti-stage buildを使う方法

ChiselはDocker multi-stage buildと組み合わせると最も効果的だ——最初のstageでスライスし、最後のstageでは必要なものだけをコピーする:

# Stage 1: Chiselで最小限のrootfsを作成
FROM ubuntu:22.04 AS chisel-stage
RUN apt-get update && apt-get install -y curl
RUN CHISEL_VERSION=$(curl -s https://api.github.com/repos/canonical/chisel/releases/latest | grep tag_name | cut -d'"' -f4) && \
    curl -sSL "https://github.com/canonical/chisel/releases/download/${CHISEL_VERSION}/chisel_${CHISEL_VERSION}_linux_amd64.tar.gz" | tar xz -C /usr/local/bin/

# Python runtimeに必要なスライスだけを切り出す
RUN chisel cut --release ubuntu-22.04 --root /rootfs \
    base-files_base \
    base-passwd_data \
    libc6_libs \
    libssl3_libs \
    python3.10_minimal \
    python3-minimal_minimal

# Stage 2: 専用ディレクトリにPythonパッケージをインストール
FROM ubuntu:22.04 AS pip-stage
RUN apt-get update && apt-get install -y python3 python3-pip
WORKDIR /app
COPY requirements.txt .
RUN pip3 install --target=/app/packages -r requirements.txt

# Stage 3: Final image - scratchから、必要なものだけ
FROM scratch
COPY --from=chisel-stage /rootfs /
COPY --from=pip-stage /app/packages /app/packages
COPY app.py /app/
ENV PYTHONPATH=/app/packages
CMD ["/usr/bin/python3", "/app/app.py"]

Chisel適用後のイメージサイズ:約45〜60MB、ベースのubuntuイメージより75〜80%削減。

実践例:GoバイナリのMinimal Image

静的コンパイルされたGoアプリケーションの場合、Python runtimeが不要なのでさらに小さなイメージになる:

# Stage 1: Goバイナリをビルド
FROM golang:1.22 AS build-stage
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# Stage 2: Chisel - ca-certificatesとタイムゾーンデータのみ
FROM ubuntu:22.04 AS chisel-stage
RUN apt-get update && apt-get install -y curl
RUN CHISEL_VERSION=$(curl -s https://api.github.com/repos/canonical/chisel/releases/latest | grep tag_name | cut -d'"' -f4) && \
    curl -sSL "https://github.com/canonical/chisel/releases/download/${CHISEL_VERSION}/chisel_${CHISEL_VERSION}_linux_amd64.tar.gz" | tar xz -C /usr/local/bin/

RUN chisel cut --release ubuntu-22.04 --root /rootfs \
    base-files_base \
    ca-certificates_data \
    tzdata_zoneinfo

# Stage 3: Final image
FROM scratch
COPY --from=chisel-stage /rootfs /
COPY --from=build-stage /app/server /server
USER 65534:65534
ENTRYPOINT ["/server"]

静的Goバイナリ+Chisel rootfs:イメージはわずか10〜15MBになる。そして FROM scratch をベースにしているため、シェルもパッケージマネージャーも存在せず、アタックサーフェスはほぼゼロだ。

実測データ:Chisel適用前後のCVE数

CentOSからUbuntuに移行したばかりの頃、パッケージ管理に慣れるまで1週間ほどかかった。当時は ubuntu:22.04 をベースイメージとして何にでも使っていた——便利だし慣れていたから。でも、「クリーンなはず」のイメージをTrivyでスキャンして47個のCVEが検出されたとき、さすがに真剣に見直すことにした。

# TrivyでCVEスキャン - 適用前後の比較
trivy image my-app:ubuntu-full
# Total: 47 (LOW: 21, MEDIUM: 18, HIGH: 7, CRITICAL: 1)

trivy image my-app:chisel-minimal
# Total: 2 (LOW: 2, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

CVE数が47から2に減少した——主な理由は、アプリに不要なパッケージをすべて排除したからだ。余分なパッケージがなければ、余分な脆弱性もない。

Chiselを使う際の実践的な注意点

  • すべてのパッケージにスライスがあるわけではない:chisel-releasesリポジトリはまだ開発中だ。必要なパッケージにスライス定義がなければ、自分で書くか別の方法を使うしかない。
  • デバッグが難しくなる:イメージにはシェルも lscat もない。デバッグが必要なときは docker export でファイルシステムをinspectするか、デバッグビルドに busybox_musl スライスを一時的に追加する。
  • FROM scratchは/etc/passwdが必要:アプリがUID/GIDの解決を必要とする場合、Chiselのリストに base-passwd_data スライスを追加すること。
  • 依存関係は自動解決:スライスAがスライスBに依存している場合、ChiselはBを自動的に取り込む——手動で列挙する必要はない。

まとめ

Chiselはすべてのケースに対応できる万能ツールではない——スライス定義のない複雑なパッケージが多く必要なアプリでは、追加の手間がかかる。しかし、高いセキュリティが求められるプロダクションサービスには、投資する価値のあるツールだ。

今の自分がよく使うパターンはこうだ:ビルドにはubuntuのフルイメージ → Chiselスライスでruntimeのrootfsを作成 → FROM scratch にコピー。この3ステップにより、Ubuntuエコシステム全体(Alpineのようなglibc非互換の心配なし)を維持しながら、Distrolessに近いイメージサイズとセキュリティプロファイルを実現できる。

コンテナのベースイメージにUbuntuを使っていてまだChiselを検討していないなら、2時間ほど実際に試してTrivyスキャンを走らせてみるといい。どんな記事よりも明確に、その差を実感できるはずだ。

Share: