distrolessイメージの中身を直接見てみる
Dockerやpodmanなどで使うコンテナイメージを作成するとき、GoogleContainerTools/distrolessイメージ:
が選択肢に上がってくることがあるが、
(本番実行用のイメージは)shellすら含んでいないためそのまま実行することができず、
特に初心者だとどういうものなのかのイメージが掴みづらい。
そこで、イメージに含まれるファイルやディレクトリ構造をある程度見れるだけでもだいぶマシになる気がするのでメモしておく。
※あまり高尚なやり方にならなかったが、特別な技能がほとんど不要でハードルが低い(はず)というところで良しとしている
distroless imagesについて
リポジトリのリンクについては冒頭に貼った通りなので、詳細はそちらを参照。
概要としては、
"Distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution.
(訳)「Distroless」イメージには、アプリケーションとそのランタイム依存関係のみが含まれます。これらには、標準の Linux ディストリビューションに含まれるパッケージ マネージャー、シェル、その他のプログラムは含まれていません。
とのこと。
コンテナの実行に不必要なものがことごとく削られているため、うまく使いこなせれば
- セキュリティの向上
- コンテナイメージサイズの軽量化
などの効果が期待できる。
ここで、distrolessという名前がついてはいるが、
Linuxの既存のdistributionと無関係といったことではなく、
むしろdebianをベースに構築されている。
Distroless images are based on Debian 12 (bookworm).
また、distroless imagesは1つの種類のコンテナイメージを指しているものではなく、
用途に合わせていくつか種類がある。
例えば、pythonやnodejs, javaのランタイムを搭載されているものもあれば、libcすら積んでいないものまである。
(各イメージの詳細)
-
gcr.io/distroless/static
- glibcやlibsslすら含まない、最も小さくて軽量なイメージ
-
gcr.io/distroless/base-nossl
-
gcr.io/distroless/static
の内容は全て含みつつ、glibcを含む(libsslは持たない)
-
-
gcr.io/distroless/base
-
gcr.io/distroless/base-nossl
の内容は全て含みつつ、libsslも含む
-
-
gcr.io/distroless/cc
-
gcr.io/distroless/base
の内容は全て含みつつ、libgcc1とその依存関係を含む
-
-
gcr.io/distroless/java
-
gcr.io/distroless/base
の内容は全て含みつつ、指定したバージョンのJavaのランタイムとその依存関係を含む
-
-
gcr.io/distroless/nodejs
-
gcr.io/distroless/base
の内容は全て含みつつ、指定したバージョンのNodejsのランタイムとその依存関係を含む
-
-
gcr.io/distroless/python
-
gcr.io/distroless/base
の内容は全て含みつつ、Python3のランタイムとその依存関係を含む
-
また、非rootのユーザーが作成されているものや、デバッグ用にshellが入っているものなどはタグ名で識別されている。
ざっくりまとめると、
- debianをベースとしつつ、含まれる内容が最小限となるように(あれば)必要なものを追加し、不要なものはことごとく削ったもの
という感じになっている。
distrolessイメージ内容の確認その1: 適当なコンテナに中身を全コピーする
本番用のイメージは先述の通りshellすらないので実行が困難になるため、
別の適当なイメージを持ってきてそちらをベースイメージとし、
distrolessイメージの中身はDockerfileを記述することで適当にコンテナに含めるようにする。
例:
# ↓ベースイメージはshellが使えれば何でも良いので適当に変える
FROM docker.io/library/alpine:3.20.3
ENV DISTROLESS_CONTENTS=/distroless-contents
# ↓例としてstatic-debian12:nonrootの内容を持ってきているが、確認したいものに応じて指定するイメージは変える
COPY / $DISTROLESS_CONTENTS/
# ↓distrolessイメージの内容を格納したディレクトリをデフォルトにしておく
WORKDIR $DISTROLESS_CONTENTS
# ↓起動時にshellが起動するようにする
CMD ["/bin/sh"]
上記だと適当なバージョンのalpineをベースに、
/distroless-contents/
にgcr.io/distroless/static-debian12:nonroot
に含まれる内容をそのまま突っ込んでいる。
(見たいものに応じて、distrolessのイメージ・タグは適宜変更する)
イメージが軽くて手軽に実行しやすいという理由で上記の例ではalpineを使っているが、
深い理由も無いので適当にubuntuやdebianなどに置き換えてもOK。
上記のようなDockerfileに対して、例えば以下のようにイメージビルド、コンテナ実行:
IMAGE_NAME="distroless-example" # 適当に変える
# イメージビルド
docker build --tag $IMAGE_NAME .
# コンテナ実行
docker run --rm -it $IMAGE_NAME
とすると、shellからdistrolessの中身を見ることができる。
(上記の例: `gcr.io/distroless/static-debian12:nonroot`)
含まれるディレクトリ, ファイル:
tree .
.
├── bin
├── boot
├── dev
├── etc
│ ├── debian_version
│ ├── default
│ ├── dpkg
│ │ └── origins
│ │ └── debian
│ ├── ethertypes
│ ├── group
│ ├── host.conf
│ ├── issue
│ ├── issue.net
│ ├── nsswitch.conf
│ ├── os-release -> ../usr/lib/os-release
│ ├── passwd
│ ├── profile.d
│ ├── protocols
│ ├── rpc
│ ├── services
│ ├── skel
│ ├── ssl
│ │ └── certs
│ │ └── ca-certificates.crt
│ └── update-motd.d
│ └── 10-uname
├── home
│ └── nonroot
├── lib
├── proc
├── root
├── run
├── sbin
├── sys
├── tmp
├── usr
│ ├── bin
│ ├── games
│ ├── include
│ ├── lib
│ │ ├── os-release
│ │ └── ssl
│ │ └── cert.pem -> /etc/ssl/certs/ca-certificates.crt
│ ├── sbin
│ ├── share
│ │ ├── base-files
│ │ │ ├── dot.bashrc
│ │ │ ├── dot.profile
│ │ │ ├── dot.profile.md5sums
│ │ │ ├── info.dir
│ │ │ ├── motd
│ │ │ ├── profile
│ │ │ ├── profile.md5sums
│ │ │ └── staff-group-for-usr-local
│ │ ├── common-licenses
│ │ │ ├── Apache-2.0
│ │ │ ├── Artistic
│ │ │ ├── BSD
│ │ │ ├── CC0-1.0
│ │ │ ├── GFDL -> GFDL-1.3
│ │ │ ├── GFDL-1.2
│ │ │ ├── GFDL-1.3
│ │ │ ├── GPL -> GPL-3
│ │ │ ├── GPL-1
│ │ │ ├── GPL-2
│ │ │ ├── GPL-3
│ │ │ ├── LGPL -> LGPL-3
│ │ │ ├── LGPL-2
│ │ │ ├── LGPL-2.1
│ │ │ ├── LGPL-3
│ │ │ ├── MPL-1.1
│ │ │ └── MPL-2.0
│ │ ├── dict
│ │ ├── doc
│ │ │ ├── base-files
│ │ │ │ ├── FAQ -> README
│ │ │ │ ├── README
│ │ │ │ ├── README.FHS
│ │ │ │ ├── changelog.gz
│ │ │ │ └── copyright
│ │ │ ├── ca-certificates
│ │ │ │ └── copyright
│ │ │ ├── netbase
│ │ │ │ ├── changelog.gz
│ │ │ │ └── copyright
│ │ │ └── tzdata
│ │ │ ├── README.Debian
│ │ │ ├── changelog.Debian.gz
│ │ │ ├── changelog.gz
│ │ │ └── copyright
│ │ ├── info
│ │ ├── lintian
│ │ │ └── overrides
│ │ │ ├── base-files
│ │ │ └── tzdata
│ │ ├── man
│ │ ├── misc
│ │ └── zoneinfo
│ │ ├── Africa
│ │ │ └── (長いので詳細省略)
│ │ ├── America
│ │ │ └── (長いので詳細省略)
│ │ ├── (...)
│ │ ├── UCT -> Etc/UTC
│ │ ├── US
│ │ │ └── (長いので詳細省略)
│ │ ├── UTC -> Etc/UTC
│ │ ├── Universal -> Etc/UTC
│ │ ├── W-SU -> Europe/Moscow
│ │ ├── WET
│ │ ├── Zulu -> Etc/UTC
│ │ ├── iso3166.tab
│ │ ├── leap-seconds.list
│ │ ├── leapseconds
│ │ ├── localtime -> /etc/localtime
│ │ ├── posix
│ │ │ ├── Africa -> ../Africa
│ │ │ ├── America -> ../America
│ │ │ ├── (...)
│ │ │ └── Zulu -> ../Etc/UTC
│ │ ├── posixrules -> America/New_York
│ │ ├── right
│ │ │ ├── Africa
│ │ │ │ └── (長いので詳細省略)
│ │ │ ├── America
│ │ │ │ └── (長いので詳細省略)
│ │ │ ├── (...)
│ │ │ ├── UCT -> Etc/UTC
│ │ │ ├── US
│ │ │ │ └── (長いので詳細省略)
│ │ │ ├── UTC -> Etc/UTC
│ │ │ ├── Universal -> Etc/UTC
│ │ │ ├── W-SU -> Europe/Moscow
│ │ │ ├── WET
│ │ │ └── Zulu -> Etc/UTC
│ │ ├── tzdata.zi
│ │ ├── zone.tab
│ │ └── zone1970.tab
│ └── src
└── var
├── backups
├── cache
├── lib
│ ├── dpkg
│ │ └── status.d
│ │ ├── base-files
│ │ ├── base-files.md5sums
│ │ ├── netbase
│ │ ├── netbase.md5sums
│ │ ├── tzdata
│ │ └── tzdata.md5sums
│ └── misc
├── local
├── lock
├── log
├── run
├── spool
└── tmp
99 directories, 1325 files
ディレクトリ毎の大まかな容量:
du -h -d 3
4.0K ./var/backups
4.0K ./var/local
4.0K ./var/lock
4.0K ./var/run
4.0K ./var/log
4.0K ./var/spool
4.0K ./var/lib/misc
92.0K ./var/lib/dpkg
100.0K ./var/lib
4.0K ./var/tmp
4.0K ./var/cache
136.0K ./var
4.0K ./home/nonroot
8.0K ./home
4.0K ./usr/games
4.0K ./usr/src
4.0K ./usr/sbin
4.0K ./usr/bin
4.0K ./usr/include
4.0K ./usr/lib/ssl
12.0K ./usr/lib
4.0K ./usr/share/misc
264.0K ./usr/share/common-licenses
4.0K ./usr/share/info
4.0K ./usr/share/dict
16.0K ./usr/share/lintian
4.0K ./usr/share/man
176.0K ./usr/share/doc
3.8M ./usr/share/zoneinfo
36.0K ./usr/share/base-files
4.3M ./usr/share
4.4M ./usr
4.0K ./dev
4.0K ./sbin
4.0K ./run
4.0K ./bin
4.0K ./lib
4.0K ./boot
4.0K ./tmp
4.0K ./sys
216.0K ./etc/ssl/certs
220.0K ./etc/ssl
4.0K ./etc/profile.d
4.0K ./etc/default
4.0K ./etc/skel
8.0K ./etc/dpkg/origins
12.0K ./etc/dpkg
8.0K ./etc/update-motd.d
312.0K ./etc
4.0K ./root
4.0K ./proc
4.9M .
distrolessのstaticだと全体でもたったの5MB程度しかないが、大部分(3.8MBくらい)は/usr/share/zoneinfo
が占めていることなどがわかる
非rootユーザー:
root:x:0:0:root:/root:/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin
nonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin
タグをnonroot
にしたので非rootのユーザー: nonroot
ができており(tree
などの実行結果からホームディレクトリ: home/nonroot
ができていることも確認可能)、
UID/GIDが65532に設定されていることなどがわかる
先述のようにdistrolessでも種類がたくさんあり、それぞれ内容が異なるため1つの例についてのみ記載したが、
他のイメージでも同様にして含まれるファイルやディレクトリ構造などを確認できる。
distrolessイメージ内容の確認その2: ホスト側にdistrolessの中身を持ってくる
先ほどの例だとalpineのbusybox
で使えるコマンドくらいしか使えないので、場合によっては不便かもしれない。
あるいはベースイメージをubuntuなどにして、apt install **
などとしてツールを増やせばもう少し色々できる可能性があるが、
いっそホストに持ってきてしまう方が楽な場合もあるので一応メモしておく。
やり方はいくつかあると思われるが、docker container cp
コマンド:
などを使うことができる。
まずは、適当な名前で先程のコンテナを実行しておく:
# コンテナの起動
CONTAINER_NAME="distroless-example-container" # 適当に編集する
IMAGE_NAME="distroless-example" # 適当に編集する(前節で作成したイメージを指定)
docker run --name $CONTAINER_NAME $IMAGE_NAME
# コンテナからホストへのコピー
docker container cp -a $CONTAINER_NAME:/distroless-contents ./distroless-contents # 先の手順で作成したイメージでは、/distroless-contentsにdistrolessイメージの中身を入れている
# コンテナの削除
# ホストに必要なファイルを持ってきたらコンテナは用済みなので削除する
docker container rm $CONTAINER_NAME
上記のようにしておくと、ホスト側のdistroless-contents
ディレクトリ以下に対象のdistrolessイメージの内容が全て格納され、
あとは好きなように中身を見ることができる。
※UID/GIDの対応はうまく反映されない場合がありそうなので注意
なお、上記のやり方を見ると、わざわざ前の手順のようにDockerfileなどを書かなくても直接
CONTAINER_NAME="distroless-example-container"
docker run --name $CONTAINER_NAME gcr.io/distroless/static-debian12:nonroot
docker container cp -a $CONTAINER_NAME:/ ./distroless-contents
などのようにできそうな気がする(かもしれない)が、
shellすら含まないdistrolessイメージを直接docker run
によって(上記のようなやり方で)起動することは一般にはできないため、
仕方なく前の節のようなDockerfileを書いてイメージを作り直すことで対応している。
最後に
基本的には「公式リポジトリの説明をよく読みましょう」だったりするのだが、
多少制約があるとしてもshellでコマンドを実行しながら中身のファイルを直接見れるのは理解を促進する(気がする)し、
あるいは自分はわかっていても、他の人に説明するときにコマンドを打ってディレクトリやファイルの中身をその場で直接見せながらやるとやりやすいことがあるかもしれない。
という訳で、やっている内容は技術的にはしょうもなかったりするが一応メモしておいた。
(参考)
そもそもどういうときにdistrolessイメージが嬉しい/使用可能なのか、
他の選択肢にどういうのがあるのか、
などはこの記事に書いていないので↓なども参照
なお、distrolessのstaticよりも更に削ぎ落としたい場合、
完全にゼロの状態からイメージを作成していくscratch
があったりする:
distroless以外の選択肢を検討する場合も、distrolessの各イメージに何が入っている/入っていないが確認できると比較・検討が捗る(かもしれない)
Discussion