一次 Docker 鏡像的逆向工程
這要從一次諮詢的失誤說起:政府組織 A 讓政府組織 B 開發一個 Web 應用程序。政府機構 B 把部分工作外包給某個人。後來,項目的託管和維護被外包給一家私人公司 C。C 公司發現,之前外包的人(已經離開很久了)構建了一個自定義的 Docker 鏡像,並將其成為系統構建的依賴項,但這個人沒有提交原始的 Dockerfile。C 公司有合同義務管理這個 Docker 鏡像,可是他們他們沒有源代碼。C 公司偶爾叫我進去做各種工作,所以處理一些關於這個神秘 Docker 鏡像的事情就成了我的工作。
幸運的是,Docker 鏡像的格式比想像的透明多了。雖然還需要做一些偵查工作,但只要解剖一個鏡像文件,就能發現很多東西。例如,這裡有一個 Prettier 代碼格式化 的鏡像可供快速瀏覽。
首先,讓 Docker 守護進程 拉取鏡像,然後將鏡像提取到文件中:
docker pull tmknom/prettier:2.0.5
docker save tmknom/prettier:2.0.5 > prettier.tar
是的,該文件只是一個典型 tarball 格式的歸檔文件:
$ tar xvf prettier.tar
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/VERSION
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/json
6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar
88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/VERSION
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/json
a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/VERSION
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/json
d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/layer.tar
manifest.json
repositories
如你所見,Docker 在命名時經常使用 哈希 。我們看看 manifest.json
。它是以難以閱讀的壓縮 JSON 寫的,不過 JSON 瑞士軍刀 jq 可以很好地列印 JSON:
$ jq . manifest.json
[
{
"Config": "88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json",
"RepoTags": [
"tmknom/prettier:2.0.5"
],
"Layers": [
"a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar",
"d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4/layer.tar",
"6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar"
]
}
]
請注意,這三個 層 對應三個以哈希命名的目錄。我們以後再看。現在,讓我們看看 Config
鍵指向的 JSON 文件。它有點長,所以我只在這裡轉儲第一部分:
$ jq . 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json | head -n 20
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"--help"
],
"ArgsEscaped": true,
"Image": "sha256:93e72874b338c1e0734025e1d8ebe259d4f16265dc2840f88c4c754e1c01ba0a",
最重要的是 history
列表,它列出了鏡像中的每一層。Docker 鏡像由這些層堆疊而成。Dockerfile 中幾乎每條命令都會變成一個層,描述該命令對鏡像所做的更改。如果你執行 RUN script.sh
命令創建了 really_big_file
,然後用 RUN rm really_big_file
命令刪除文件,Docker 鏡像實際生成兩層:一個包含 really_big_file
,一個包含 .wh.really_big_file
記錄來刪除它。整個鏡像文件大小不變。這就是為什麼你會經常看到像 RUN script.sh && rm really_big_file
這樣的 Dockerfile 命令鏈接在一起——它保障所有更改都合併到一層中。
以下是該 Docker 鏡像中記錄的所有層。注意,大多數層不改變文件系統鏡像,並且 empty_layer
標記為 true
。以下只有三個層是非空的,與我們之前描述的相符。
$ jq .history 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json
[
{
"created": "2020-04-24T01:05:03.608058404Z",
"created_by": "/bin/sh -c #(nop) ADD file:b91adb67b670d3a6ff9463e48b7def903ed516be66fc4282d22c53e41512be49 in / "
},
{
"created": "2020-04-24T01:05:03.92860976Z",
"created_by": "/bin/sh -c #(nop) CMD ["/bin/sh"]",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:06.617130538Z",
"created_by": "/bin/sh -c #(nop) ARG BUILD_DATE",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:07.020521808Z",
"created_by": "/bin/sh -c #(nop) ARG VCS_REF",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:07.36915054Z",
"created_by": "/bin/sh -c #(nop) ARG VERSION",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:07.708820086Z",
"created_by": "/bin/sh -c #(nop) ARG REPO_NAME",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:08.06429638Z",
"created_by": "/bin/sh -c #(nop) LABEL org.label-schema.vendor=tmknom org.label-schema.name=tmknom/prettier org.label-schema.description=Prettier is an opinionated code formatter. org.label-schema.build-date=2020-04-29T06:34:01Z org
.label-schema.version=2.0.5 org.label-schema.vcs-ref=35d2587 org.label-schema.vcs-url=https://github.com/tmknom/prettier org.label-schema.usage=https://github.com/tmknom/prettier/blob/master/README.md#usage org.label-schema.docker.cmd=do
cker run --rm -v $PWD:/work tmknom/prettier --parser=markdown --write '**/*.md' org.label-schema.schema-version=1.0",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:08.511269907Z",
"created_by": "/bin/sh -c #(nop) ARG NODEJS_VERSION=12.15.0-r1",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:08.775876657Z",
"created_by": "/bin/sh -c #(nop) ARG PRETTIER_VERSION",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:26.399622951Z",
"created_by": "|6 BUILD_DATE=2020-04-29T06:34:01Z NODEJS_VERSION=12.15.0-r1 PRETTIER_VERSION=2.0.5 REPO_NAME=tmknom/prettier VCS_REF=35d2587 VERSION=2.0.5 /bin/sh -c set -x && apk add --no-cache nodejs=${NODEJS_VERSION} nodejs-np
m=${NODEJS_VERSION} && npm install -g prettier@${PRETTIER_VERSION} && npm cache clean --force && apk del nodejs-npm"
},
{
"created": "2020-04-29T06:34:26.764034848Z",
"created_by": "/bin/sh -c #(nop) WORKDIR /work"
},
{
"created": "2020-04-29T06:34:27.092671047Z",
"created_by": "/bin/sh -c #(nop) ENTRYPOINT ["/usr/bin/prettier"]",
"empty_layer": true
},
{
"created": "2020-04-29T06:34:27.406606712Z",
"created_by": "/bin/sh -c #(nop) CMD ["--help"]",
"empty_layer": true
}
]
太棒了!所有的命令都在 created_by
欄位中,我們幾乎可以用這些命令重建 Dockerfile。但不是完全可以。最上面的 ADD
命令實際上沒有給我們需要添加的文件。COPY
命令也沒有全部信息。我們還失去了 FROM
語句,因為它們擴展成了從基礎 Docker 鏡像繼承的所有層。
我們可以通過查看 時間戳 ,按 Dockerfile 對層進行分組。大多數層的時間戳相差不到一分鐘,代表每一層構建所需的時間。但是前兩層是 2020-04-24
,其餘的是 2020-04-29
。這是因為前兩層來自一個基礎 Docker 鏡像。理想情況下,我們可以找出一個 FROM
命令來獲得這個鏡像,這樣我們就有了一個可維護的 Dockerfile。
manifest.json
展示第一個非空層是 a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/layer.tar
。讓我們看看它:
$ cd a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97/
$ tar tf layer.tf | head
bin/
bin/arch
bin/ash
bin/base64
bin/bbconfig
bin/busybox
bin/cat
bin/chgrp
bin/chmod
bin/chown
看起來它可能是一個 操作系統 基礎鏡像,這也是你期望從典型 Dockerfile 中看到的。Tarball 中有 488 個條目,如果你瀏覽一下,就會發現一些有趣的條目:
...
dev/
etc/
etc/alpine-release
etc/apk/
etc/apk/arch
etc/apk/keys/
etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub
etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub
etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub
etc/apk/protected_paths.d/
etc/apk/repositories
etc/apk/world
etc/conf.d/
...
果不其然,這是一個 Alpine 鏡像,如果你注意到其他層使用 apk
命令安裝軟體包,你可能已經猜到了。讓我們解壓 tarball 看看:
$ mkdir files
$ cd files
$ tar xf ../layer.tar
$ ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ cat etc/alpine-release
3.11.6
如果你拉取、解壓 alpine:3.11.6
,你會發現裡面有一個非空層,layer.tar
與 Prettier 鏡像基礎層中的 layer.tar
是一樣的。
出於興趣,另外兩個非空層是什麼?第二層是包含 Prettier 安裝包的主層。它有 528 個條目,包含 Prettier、一堆依賴項和證書更新:
...
usr/lib/libuv.so.1
usr/lib/libuv.so.1.0.0
usr/lib/node_modules/
usr/lib/node_modules/prettier/
usr/lib/node_modules/prettier/LICENSE
usr/lib/node_modules/prettier/README.md
usr/lib/node_modules/prettier/bin-prettier.js
usr/lib/node_modules/prettier/doc.js
usr/lib/node_modules/prettier/index.js
usr/lib/node_modules/prettier/package.json
usr/lib/node_modules/prettier/parser-angular.js
usr/lib/node_modules/prettier/parser-babel.js
usr/lib/node_modules/prettier/parser-flow.js
usr/lib/node_modules/prettier/parser-glimmer.js
usr/lib/node_modules/prettier/parser-graphql.js
usr/lib/node_modules/prettier/parser-html.js
usr/lib/node_modules/prettier/parser-markdown.js
usr/lib/node_modules/prettier/parser-postcss.js
usr/lib/node_modules/prettier/parser-typescript.js
usr/lib/node_modules/prettier/parser-yaml.js
usr/lib/node_modules/prettier/standalone.js
usr/lib/node_modules/prettier/third-party.js
usr/local/
usr/local/share/
usr/local/share/ca-certificates/
usr/sbin/
usr/sbin/update-ca-certificates
usr/share/
usr/share/ca-certificates/
usr/share/ca-certificates/mozilla/
usr/share/ca-certificates/mozilla/ACCVRAIZ1.crt
usr/share/ca-certificates/mozilla/AC_RAIZ_FNMT-RCM.crt
usr/share/ca-certificates/mozilla/Actalis_Authentication_Root_CA.crt
...
第三層由 WORKDIR /work
命令創建,它只包含一個條目:
$ tar tf 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70/layer.tar
work/
原始 Dockerfile 在 Prettier 的 git 倉庫中。
via: https://theartofmachinery.com/2021/03/18/reverse_engineering_a_docker_image.html
作者:Simon Arneaud 選題:lujun9972 譯者:DCOLIVERSUN 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive