コンテナイメージの軽量化は、起動の速さやコスト以上に「攻撃面の縮小」に直結します。マルチステージビルドからdistroless、非rootユーザ、脆弱性スキャン、署名、SBOMまで、本番運用で効く勘所を判断軸つきで整理します。
01なぜ「軽量化」がそのまま「セキュリティ」になるのか
コンテナイメージの軽量化を、起動時間の短縮やレジストリ転送コストの話だと捉えている現場は多いです。それも正しいのですが、本質はもう一つあります。イメージに含まれるパッケージが少ないほど、既知脆弱性(CVE)の母数が減り、攻撃者が侵入後に使える道具も減るという点です。シェルやパッケージマネージャが入っていなければ、侵入後の探索・権限昇格・横展開の難易度は跳ね上がります。
実際、Googleのdistrolessプロジェクトは「ランタイムに必要なものだけを残し、シェルやパッケージマネージャを排除して攻撃面を縮小する」という思想で公開されています。最小構成のgcr.io/distroless/static系はおおよそ2MiB前後で、Debianフルイメージ(100MiB超)の2%未満に収まります(サイズはバージョンで変わるため公式で最新を確認してください)。軽量化とハードニングは、別々の作業ではなく同じ作業の裏表です。
02マルチステージビルド:ビルド道具を本番に持ち込まない
軽量化の第一歩はマルチステージビルドです。コンパイラやビルドツール、テスト依存関係はビルド用ステージに閉じ込め、最終ステージには成果物(バイナリや必要ファイル)だけをCOPY --from=で持ち込みます。Docker公式も「最終ステージには最小ベースを使い、重いツールは前段ステージで完結させ、成果物だけをコピーする」ことを推奨しています。
Goのような静的バイナリでは効果が劇的で、ビルド道具込みで800MB〜1GB級だったものが、最終イメージ10〜20MB級まで落ちることも珍しくありません。JavaやPython、Node.jsでも、ビルドステージでの依存解決と本番ステージの分離は同じ発想で効きます。
# build stage
FROM golang:1.22 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
# runtime stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
ポイントは、COPY --fromで「必要なものだけを名指しで」持ち込むことです。COPY . .を最終ステージでやってしまうと、ビルドキャッシュや.gitまで混入し、軽量化もハードニングも台無しになります。
.dockerignoreを必ず整備してください。.git、node_modules(ビルドで再取得する場合)、*.env、認証情報の混入はここで止めます。マルチステージでも、ビルドコンテキストに秘密情報を渡すとレイヤやビルドログに残るリスクがあります。03ベースイメージの選定:distroless / alpine / slim の決定軸
「最小ベースにすればいい」は正しいものの、現場では言語ランタイムやglibc依存、デバッグ容易性とのトレードオフがあります。よくある選択肢を判断軸で整理します。
- scratch / distroless/static:静的バイナリ向け。最小・最安全だが、シェルもca証明書も無いので
tzdataや証明書は明示的に入れる。デバッグは:debugタグや一時的なephemeralコンテナで。 - distroless(言語別 java/python/nodejs):ランタイムは入るがパッケージマネージャ・シェルは無い。glibc依存アプリの本命。
:nonrootタグで非root起動が標準。 - -slim(debian slim):シェルや最小限のツールは残る。段階的移行や、どうしてもデバッグ性が要る場面の落とし所。
- alpine:小さいがmusl libc採用のため、glibc前提のバイナリやネイティブ拡張で相性問題が出ることがある。採用前に実挙動を検証する。
kubectl debugのephemeralコンテナやサイドカーでツールを注入する運用が定石です。本番イメージにデバッグ道具を常駐させないことがハードニングの肝です。04非rootユーザで動かす:一行で効く最重要対策
ハードニングの費用対効果が最も高いのが非root実行です。多くのベースイメージはデフォルトでroot(UID 0)実行のため、コンテナブレイクアウトやマウント誤設定と組み合わさると被害が拡大します。distrolessの:nonrootタグはUID 65532の非rootユーザを用意しており、USER nonroot:nonrootを書くだけで適用できます。
あわせて、実行基盤側でも多層で縛ります。KubernetesならsecurityContextでrunAsNonRoot: true、readOnlyRootFilesystem: true、allowPrivilegeEscalation: falseを設定します。ECS/Fargateならタスク定義のuser指定とreadonlyRootFilesystemを使います。イメージ側とオーケストレータ側の両方で担保するのが実務の基本です。ECSのタスク定義の勘所はECSタスク定義の実践で整理しています。
bind 80が失敗する事故が定番です。1024未満のポートは避け、コンテナは8080等で待ち受けてロードバランサ側でマッピングしてください。ファイル書き込みが必要な箇所はemptyDirやボリュームに逃がし、ルートFSは読み取り専用を保ちます。05脆弱性スキャン:ECR拡張スキャンとTrivyの使い分け
どれだけ最小化しても、依存パッケージのCVEはゼロにはなりません。継続的にスキャンし、検知して直す仕組みが要ります。AWS内では二層で考えると整理しやすいです。
ECR拡張スキャン(Amazon Inspector連携)は、OSパッケージと言語パッケージの両方を対象に、プッシュ時スキャンと継続スキャンを提供します。新しいCVEが公表されると既存イメージの結果も更新され、EventBridgeにイベントが飛ぶため、通知・自動対応につなげられます。さらに拡張スキャンは、そのイメージがEKS/ECSでどれだけ使われているかを可視化し、修正の優先順位付けに使えます。注意点として、拡張スキャン有効化時にInspectorが認識するのは直近14日以内にプッシュされたイメージで、古いものは再プッシュが必要です(仕様は公式で最新を確認してください)。ECRからの追加料金は無く、Inspector側の料金が発生します。
Trivy(Aqua Security製OSS)は、CI/CDのビルド段階で「マージ前に落とす」シフトレフト用途に向きます。コンテナイメージ・ファイルシステム・IaC・シークレット・SBOM生成まで一つのバイナリでカバーし、CIへの組み込みが軽いのが利点です。
--exit-code 0で可視化から入り、対応方針(許容CVEの.trivyignore管理と期限)を決めてから段階的にゲート化してください。基盤側の監視設計はGuardDuty/Security Hub運用とも連携させると全体像が締まります。06イメージ署名とサプライチェーン:AWS SignerとNotation
スキャンは「中身が安全か」を見ますが、サプライチェーン攻撃では「そもそも正規のイメージか」が問われます。ここで効くのがイメージ署名です。AWSはAWS Signerを提供し、CNCFのNotaryプロジェクト由来のOSSNotationと統合しています。署名はイメージマニフェストに対してECDSA-SHA-384で行われ、署名はOCIアーティファクトとしてイメージと並べてECRに保存されます(ECRはOCI 1.1のreferrersに対応)。
ECRにはマネージド署名(プッシュ時に自動署名、推奨)と手動署名(NotationCLI + Signerプラグインでクライアント側署名)の2方式があります。デプロイ側では、EKSならKyvernoやRatifyといったadmissionコントローラで「署名済みイメージ以外はPod起動を拒否」でき、ECS/Fargateでも検証パターンが用意されています。署名の生成・鍵ローテーション・失効はSignerが管理し、CloudTrailで監査できます。
07SBOM:脆弱性が出た「後」に効く一覧
Log4Shellのような事案で効いたのは、「どのイメージにどのライブラリが入っているか」を即座に引ける状態でした。それを機械可読で残すのがSBOM(Software Bill of Materials)です。標準フォーマットはSPDXとCycloneDXの2つで、主要ツールは両対応です。生成にはSyft(-o spdx-json / -o cyclonedx-json)やTrivyが使え、AWSではAmazon Inspectorが監視対象リソース(ECRのイメージ含む)のSBOMを組織横断で集中生成でき、SPDX/CycloneDX両形式をサポートします。
- いつ作るか:CIのビルド直後にイメージダイジェスト単位で生成し、成果物として保管する。
- どこに置くか:署名と同様、OCIアーティファクトとしてイメージに添付する運用が主流。イメージと台帳が乖離しない。
- 何に使うか:新規CVE公表時に「影響イメージの棚卸し」を数分で終わらせる。EKS上の稼働イメージ照合はAWSのブログでも実例が示されています。
latest等)は動くのでダイジェスト(sha256:...)を主キーにして保管してください。監査対応でも、この一覧の有無が工数を大きく分けます。08レイヤキャッシュとサイズ削減:速さと安全を両立する
ハードニングと軽量化を進めても、ビルドが遅くCIが詰まると運用が回りません。レイヤキャッシュの効かせ方でビルド時間は大きく変わります。基本は「変わりにくいものを先に、変わりやすいものを後に」書くことです。
- 依存解決を先に固定:
COPY go.* ./→go mod download、あるいはCOPY package*.json ./→npm ciをソースコピーの前に置く。ソース変更のたびに依存を再取得しない。 - BuildKitのキャッシュマウント:
RUN --mount=type=cacheでパッケージキャッシュを再利用。イメージには残さないので軽さと速さを両立できる。 - リモートキャッシュ:CIでは
--cache-to/--cache-fromでECR等にキャッシュを外出しし、ランナー間で共有する。 - 不要物を残さない:
apt-get後のrm -rf /var/lib/apt/lists/*を同一RUN内で。別RUNだと前レイヤに残りサイズが減らない。
マルチステージなら、そもそも最終イメージにビルドキャッシュが混入しないため、キャッシュを潤沢に使いつつ本番は小さく保てます。速さのためのキャッシュと、安全のための最小化は、ステージ分割で無理なく共存します。
latestではなくダイジェスト固定(FROM ...@sha256:...)を推奨します。再現性が上がり、供給元すり替えにも強くなります。更新はRenovate等で自動PR化し、更新のたびにスキャン+署名を通す運用にすると、固定と鮮度を両立できます。—まとめ
コンテナイメージのハードニングは、単発の設定ではなく「軽量化・非root・スキャン・署名・SBOM」を層で積む取り組みです。優先順位で言えば、(1)マルチステージ+最小ベースで攻撃面を削り、(2)非root+読み取り専用FSで爆発半径を絞り、(3)ECR拡張スキャン/Trivyで継続的に検知し、(4)署名+admission検証で正規性を担保し、(5)SBOMで事後の棚卸しを速くする——この順で入れると効果を実感しやすいです。
いずれも一度に完璧を目指すと形骸化します。監査モードや可視化から始め、社内の網羅率が上がってからゲート化する段階移行が、エンタープライズでは現実的です。既存のCI/CDや基盤標準への組み込みでお困りでしたら、導入事例もあわせてご覧ください。
—参考(一次情報)
- Scan images for OS and programming language package vulnerabilities in Amazon ECR(Amazon ECR User Guide)
- Announcing Container Image Signing with AWS Signer and Amazon EKS(AWS Containers Blog)
- Using SBOM to find vulnerable container images running on Amazon EKS clusters(AWS Containers Blog)
- Multi-stage builds(Docker Docs)
- GoogleContainerTools/distroless(GitHub)
- aquasecurity/trivy(GitHub)