【小ネタ】コンテナイメージのレイヤーからファイルを回収する方法
コンテナイメージのレイヤーからファイルを回収する方法
コンテナイメージのビルド時に一度COPYし、後の命令で削除したファイルを、しばらく後になってコンテナイメージから取り出したくなったことはないでしょうか?私はあります。[1]
こんな時、ビルド済みのコンテナイメージからファイルを回収できる可能性があります。この記事ではコンテナのlayerからファイルを回収する手法について記載します。
結論
コンテナイメージビルド時にファイルをコピーする操作と削除する操作が別々のlayerとなっている場合、イメージをアーカイブ形式で出力してあげることでファイルを取り出すことが可能です。
コンテナイメージの構造について
いろいろなところで解説されているので少しの概要だけ記載します。
コンテナイメージはlayerと呼ばれるファイルシステムを重ね合わせる(積み上げる)ことで作られます。Dockerfileの一部の命令ごとに新しいlayerが作られ[2]、そこではファイルの追加・変更・削除などの処理が発生します。
積み上げられたlayerは下位のlayerのファイルを継承します。つまり、上位のlayerは下位のlayerに対する差分のファイルシステムとなります。
最終的に、基となったコンテナイメージに対する差分の集合が新しいイメージとなります。
検証
シナリオ
今回は以下のケースを想定して検証します。
- AlmaLinux 9.6のコンテナイメージに対して自己署名証明書を登録したコンテナイメージから自己署名証明書を取り出す。
- 自己署名証明書はCOPY命令でコンテナイメージに組み込まれている。
コンテナイメージ作成
- 自己署名証明書作成
次のコマンドで256 ビットの楕円曲線デジタル署名アルゴリズム (ECDSA) キーと、その鍵で署名された自己署名証明書を作成します。
証明書作成時の質問には適当に回答しています。
$ openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 -out private.key
$ openssl req -key private.key -new -x509 -days 3650 -out server.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JA
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) [Default City]:Tokyo
Organization Name (eg, company) [Default Company Ltd]:Hoge
Organizational Unit Name (eg, section) []:Fuga
Common Name (eg, your name or your server's hostname) []:Test For Container Layer
Email Address []:
- Dockerfileの用意
次に、以下のDockerfileを用意します。
FROM almalinux:9.6-minimal
COPY server.crt /tmp/server.crt
RUN trust anchor /tmp/server.crt && rm /tmp/server.crt
Dockerfileと同じディレクトリに先ほど作成したserver.crtという自己署名証明書を配置しておきます。
- コンテナイメージのビルド
イメージ名はtest-file1で作成しました。
$ docker build -t test-file1 .
コンテナの確認
証明書がインストールされていることと、コンテナ内の/tmp/配下にserver.crtが存在しないことを確認します。
ホストサーバーで以下のコマンドを実行
$ docker run -it test-file1:latest /bin/bash
コンテナに入れるので、以下を実行
# ls /tmp/
⇒何もなし
# trust list | grep -C 3 "Test For Container Layer"
pkcs11:id=%6E%6B%7C%89%F1%C1%AF%A1%8C%32%2E%ED%EE%B9%77%F6%75%DB%E6%EE;type=cert
type: certificate
label: Test For Container Layer
trust: anchor
category: authority
⇒証明書インストール済み
コンテナイメージからファイルの取り出し
layerからserver.crtファイルを取り出してみましょう。
その前にコンテナイメージがどのように作成されたかを確認します。
$ docker image history test-file1:latest
IMAGE CREATED CREATED BY SIZE COMMENT
c228ba78cd96 28 hours ago RUN /bin/sh -c trust anchor /tmp/server.crt … 2.59MB buildkit.dockerfile.v0
<missing> 28 hours ago COPY server.crt /tmp/server.crt # buildkit 12.3kB buildkit.dockerfile.v0
<missing> 3 weeks ago CMD ["/bin/bash"] 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ADD almalinux-9-minimal-amd64.tar.xz / # bui… 100MB buildkit.dockerfile.v0
コンテナイメージは4つの命令で作成されたことがわかりました。またベースイメージとして使ったalmalinux:9.6-minimalは2つの命令から構成されていることもわかります。
上記のCMD命令はイメージのメタデータのみを更新しておりlayerは作成しておりません。[3] サイズが0Bとなっており、ファイルシステムとしての実体は存在しないとわかります。したがって今回作成したコンテナイメージは3つのlayerから成り立つとわかります。
そして、その中でもserver.crtをCOPYするlayerが一番サイズが小さいことがわかります。
次に、コンテナイメージをtarアーカイブ形式で出力し、展開します。
$ docker save -o test-image1.tar test-file1:latest
$ tar xvf test-image1.tar
$ tree
.
├── blobs
│ └── sha256
│ ├── 0314125dcb23155573a7a82bdb451831255511518bb2c5607551e426a51fe0fb
│ ├── 17c600215b50e243dc5bf3a666a193840ff4e68310b66bde3003f10366401abc
│ ├── 2b1071f7deac7631c1a9de9378237b74f4654a380a7c4b4b9ceeb6e78e2f99cf
│ ├── 37da69bd02c3220597afbbfdd49922e08b9f89cfcf5e29067cb46ee393f6fb70
│ ├── 5ab4867ca216bd36f064f616e11af25749f013448e7e9824cb6e98e9855d5d8f
│ ├── 9d153035aad48be5b2a3116f4e69a6f01db78a4698a4f640c38038a657607f1a
│ ├── c228ba78cd96288c514065e6c55acea2b50ab725bc4139a8c874d1c10f74b858
│ ├── d0a899768c3c96bb235feb30e125bad7ef8b9d6498f3dfd3ffef204609ced811
│ └── d2275536f4d25bc83cead2de1a6968b264533bea554fab03cf3ce5512f574e37
├── index.json
├── manifest.json
├── oci-layout
└── test-image1.tar
このblobs/sha256配下にlayerのデータが格納されております。ここにはコンテナイメージのメタデータを記述したJSONも含まれているため、layer数よりも多くのファイルが格納されています。
layerはtar.gzで圧縮されていますので、fileコマンドを使えばlayerデータを判別できます。
$ cd blobs/sha256
$ file *
0314125dcb23155573a7a82bdb451831255511518bb2c5607551e426a51fe0fb: gzip compressed data, original size modulo 2^32 94177280
17c600215b50e243dc5bf3a666a193840ff4e68310b66bde3003f10366401abc: JSON data
2b1071f7deac7631c1a9de9378237b74f4654a380a7c4b4b9ceeb6e78e2f99cf: JSON data
37da69bd02c3220597afbbfdd49922e08b9f89cfcf5e29067cb46ee393f6fb70: gzip compressed data, original size modulo 2^32 2376704
5ab4867ca216bd36f064f616e11af25749f013448e7e9824cb6e98e9855d5d8f: JSON data
9d153035aad48be5b2a3116f4e69a6f01db78a4698a4f640c38038a657607f1a: JSON data
c228ba78cd96288c514065e6c55acea2b50ab725bc4139a8c874d1c10f74b858: JSON data
d0a899768c3c96bb235feb30e125bad7ef8b9d6498f3dfd3ffef204609ced811: gzip compressed data, original size modulo 2^32 3072
d2275536f4d25bc83cead2de1a6968b264533bea554fab03cf3ce5512f574e37: JSON data
gzip compressed dataが特定できました。また、その中でもd0a899768c3c96bb235feb30e125bad7ef8b9d6498f3dfd3ffef204609ced811が一番ファイルサイズが小さいことがわかります。
server.crtをCOPYしたlayerが一番ファイルサイズが小さかったので、このファイルが対象のlayerだと判断できます。
$ tar xvf d0a899768c3c96bb235feb30e125bad7ef8b9d6498f3dfd3ffef204609ced811
tmp/
tmp/server.crt
当たりです。
$ cd tmp/
$ ls
server.crt
このserver.crtがコンテナイメージに格納した自己署名証明書となります。
この方法が使えないケース
この方法は取り出したいファイルをCOPY命令で追加している(layerとして残っている)場合に使えます。以下のようなDockerfileの場合はlayerが残らないので使えません。
FROM almalinux:9.6-minimal
RUN --mount=type=secret,id=server.crt \
trust anchor /run/secrets/server.crt
上記のDockerfileからコンテナイメージを作成し、確認します。
コンテナイメージビルドでは以下のようにsecretを渡します。
$ docker build --secret id=server.crt,src=server.crt -f RUN.Dockerfile -t test-file2 .
COPY命令でファイルを追加したときと比較してレイヤ数は減ります。
$ docker image history test-file2:latest
IMAGE CREATED CREATED BY SIZE COMMENT
dee5d880039d 3 minutes ago RUN /bin/sh -c trust anchor /run/secrets/ser… 2.58MB buildkit.dockerfile.v0
<missing> 3 weeks ago CMD ["/bin/bash"] 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ADD almalinux-9-minimal-amd64.tar.xz / # bui… 100MB buildkit.dockerfile.v0
コンテナを立ち上げて、中身を確認します。
$ docker run -it --rm test-file2:latest /bin/bash
証明書は残っていません。
# ls /run/
lock
証明書はちゃんとインストール済みです。
# trust list | grep -C 3 "Test For Container Layer"
pkcs11:id=%6E%6B%7C%89%F1%C1%AF%A1%8C%32%2E%ED%EE%B9%77%F6%75%DB%E6%EE;type=cert
type: certificate
label: Test For Container Layer
trust: anchor
category: authority
ちゃんとtarアーカイブに出力し、layerを見てみます。
$ docker save -o test-image2.tar test-file2:latest
$ tar xvf test-image2.tar
$ tree
.
├── blobs
│ └── sha256
│ ├── 0314125dcb23155573a7a82bdb451831255511518bb2c5607551e426a51fe0fb
│ ├── 3e40baffd015881c5051785d7e982fdd23a0439c4882cb2e2ad7eee72295aa8f
│ ├── 418dd1691817eaeb86cd3c11663b10dee089ee2dcd6c43879e7ea506f7494015
│ ├── 4364e021bcd1fb14069012fe65182b5b6e41314f7908fa8a3d56e94c4b203a50
│ ├── 7ed0f91a9653adca0bd7ece3dfa16a744ad7160ae4de4681f6d3faedaf11cf23
│ ├── cd4318217b215a6b2982dbd769f7ddc90993c568bdac591ba428e1b50eced1c7
│ ├── dee5d880039db5130a4fbcbb0d067af2a8f9747ab21469ccc94e51d0f62c6ee4
│ └── f8a8c5b5de2707fa9a0cb58fc4cae92c989daeafb7200314656930c10d0b6c57
├── index.json
├── manifest.json
├── oci-layout
└── test-image2.tar
$ cd blobs/sha256/
$ file *
0314125dcb23155573a7a82bdb451831255511518bb2c5607551e426a51fe0fb: gzip compressed data, original size modulo 2^32 94177280
3e40baffd015881c5051785d7e982fdd23a0439c4882cb2e2ad7eee72295aa8f: JSON data
418dd1691817eaeb86cd3c11663b10dee089ee2dcd6c43879e7ea506f7494015: JSON data
4364e021bcd1fb14069012fe65182b5b6e41314f7908fa8a3d56e94c4b203a50: gzip compressed data, original size modulo 2^32 2375680
7ed0f91a9653adca0bd7ece3dfa16a744ad7160ae4de4681f6d3faedaf11cf23: JSON data
cd4318217b215a6b2982dbd769f7ddc90993c568bdac591ba428e1b50eced1c7: JSON data
dee5d880039db5130a4fbcbb0d067af2a8f9747ab21469ccc94e51d0f62c6ee4: JSON data
f8a8c5b5de2707fa9a0cb58fc4cae92c989daeafb7200314656930c10d0b6c57: JSON data
分かっていたことですが、layer数が少ないです。
このコンテナイメージを作成する際に追加されたlayerを見ても、証明書は残っていません。
$ tar tf 4364e021bcd1fb14069012fe65182b5b6e41314f7908fa8a3d56e94c4b203a50
etc/
etc/pki/
etc/pki/ca-trust/
etc/pki/ca-trust/extracted/
etc/pki/ca-trust/extracted/edk2/
etc/pki/ca-trust/extracted/edk2/cacerts.bin
etc/pki/ca-trust/extracted/java/
etc/pki/ca-trust/extracted/java/cacerts
etc/pki/ca-trust/extracted/openssl/
etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt
etc/pki/ca-trust/extracted/pem/
etc/pki/ca-trust/extracted/pem/directory-hash/
etc/pki/ca-trust/extracted/pem/directory-hash/002c0b4f.0
~略~
etc/pki/ca-trust/extracted/pem/directory-hash/ffa7f1eb.0
etc/pki/ca-trust/extracted/pem/directory-hash/vTrus_ECC_Root_CA.pem
etc/pki/ca-trust/extracted/pem/directory-hash/vTrus_Root_CA.pem
etc/pki/ca-trust/extracted/pem/email-ca-bundle.pem
etc/pki/ca-trust/extracted/pem/objsign-ca-bundle.pem
etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
etc/pki/ca-trust/source/
etc/pki/ca-trust/source/Test_For_Container_Layer.p11-kit
というわけで、RUN --mountの方法でファイルを追加されていた場合はファイルを回収できません。
最後に
ちょっとした小ネタでした。Union file systemなどのコンテナイメージを構成する技術を知っておくと、このような手段をとれるようになります。
後ろの方で軽く触れたRUN --mountを使ったファイルの追加ですが、COPYコマンドの使い分けなどはベストプラクティスにも記載がありますので勉強してみてください。
-
先人がイメージを作った際にGitで管理していない証明書をコンテナに配置し、コンテナ内のキーストア格納後に証明書をイメージから消去していたのですが、私がコンテナを作り直す際にその証明書の原本が見つからなかったという経験があります。 ↩︎
-
一部の命令はメタデータの更新のみが発生し、その場合はlayerは作られません。 ↩︎
-
the CMD instruction specifies what command to run within the container, which only modifies the image's metadata, which doesn't produce an image layer.
https://docs.docker.com/engine/storage/drivers/#images-and-layers ↩︎
Discussion