逆向解析Docker镜像文件


了解 Docker 镜像的分层存储原理和对应机制!

我们在学习了 Docker 相关的知识和使用之后,肯定很好奇 Docker 镜像中文件到底是怎么分的呢?它又是怎么存储起来的呢?我们怎么在不启动镜像的情况找到镜像中的对应文件呢?咳咳咳,那我们就带着这些问题,一起来看看吧!

逆向解析Docker镜像文件


1. 探究问题起因

想要了解一个东西肯定是因为某些原理作为出发点的!

我们设想一下,我们最近刚刚面试完,进入了一个新的公司。在刚入职的第一天,直属上司分配给我们一个简单的任务,就是:编写某应用程序对应镜像的 Dockerfile 文件

我们的公司(A)之前让另一家公司(B)开发了一个 Web 应用程序,而公司(B)后来该项目的托管和维护交给了外包公司(C)来负责。外包公司(C)将其分配给了(C1)来负责,而其定制化了一个 Docker 镜像并创建了对应的 Dockerfile 文件,但是并没有提交上去。

恰好在你入职的时候,C1 这个人早已不在职了,现在公司继续更新一版对应服务,所以该任务就到了你的手里了。咳咳咳,开始你的表演吧!

幸运的是,Docker 镜像格式的透明度要高得多,所以我们只需要进行一些简单的“侦查”工作,就可以大致分析出来镜像文件的构成方式,并可以得出很多有趣的结论。

我们知道 Docker 镜像是分成一层一层的,堆叠在一起组成的。而 Dockerfile 文件中几乎每条指令都会变成了一个描述该指令对应变化的层。比如,如果你的 Dockerfile 文件中有这样一条 RUN script.sh 指令的话,它用于创建一个真正的大文件,然后你用 RUN rm really big 文件删除它,实际上在 Docker 镜像中得到是两个层。


2. 镜像存储方式

任务:编写某应用程序对应镜像的 Dockerfile 文件!

  • 首先,在我们本地拉去对应的镜像,并将其导出到一个文件中去。
# 拉取镜像
vagrant@vagrant:~/test$ sudo docker pull tmknom/prettier:2.0.5
2.0.5: Pulling from tmknom/prettier
cbdbe7a5bc2a: Pull complete
a0ab3bb12e81: Pull complete
e2ff8799a99a: Pull complete
Digest: sha256:85a60938fa30459683c7dd2bec2e80998d57227bf2c6da7bb541458d080b28fc
Status: Downloaded newer image for tmknom/prettier:2.0.5
docker.io/tmknom/prettier:2.0.5

# 导出镜像
vagrant@vagrant:~/test$ docker save tmknom/prettier:2.0.5 > prettier.tar
  • 导出镜像之后,我们再分析其文件目录结构,可以看到其是以 tar 文件进行归档的。
# 解压镜像
vagrant@vagrant:~/test$ tar -xf prettier.tar

# 目录结构
vagrant@vagrant:~/test$ tree .
.
├── repositories
├── manifest.json
├── 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json
├── a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97
│   ├── json
│   ├── layer.tar
│   └── VERSION
├── d4f612de5397f1fc91272cfbad245b89eac8fa4ad9f0fc10a40ffbb54a356cb4
│   ├── json
│   ├── layer.tar
│   └── VERSION
└── 6c37da2ee7de579a0bf5495df32ba3e7807b0a42e2a02779206d165f55f1ba70
    ├── json
    ├── layer.tar
    └── VERSION

  • manifest.json
    • Config -> 对应镜像相关信息的主配置文件
    • RepoTags -> 对应镜像的名称和对应版本号
    • Layers -> 对应镜像包的分层目录列表信息

逆向解析Docker镜像文件 - repositories

  • repositories

逆向解析Docker镜像文件 - manifest.json

  • 88f38be28f05f38dba94ce0c1328ebe2b963b65848ab96594f8172a9c3b0f25b.json
    • history -> 历史列表中列出了镜像创建的全部过程(created by)
    • 大多数层并不会更改文件系统镜像,所以用 empty_layer: true 标识出来

逆向解析Docker镜像文件 - a9c..c97

  • 我们可以按照时间戳来对 Dockerfile 的层进行分组,大多数层的时间戳都在一分钟之内,表示每一层构建所花费的时间。然而,我们发现其前两个层是 2020-04-24,其余层是 2020-04-29 这个时间。这是因为前两个层来自 baseDocker 镜像。理想情况下,我们应该找出一个 FROM 语句来获得这个映像,这样我们就有了一个可维护的 Dockerfile 文件了。

逆向解析Docker镜像文件 - a9c..c97


  • 查看对应文件中的内容,我们就可以了解其镜像分层到底里面存储了什么东西,做了什么操作。
vagrant@vagrant:~/test/a9c..c97$ cat VERSION
1.0
vagrant@vagrant:~/test/a9c..c97$ jq . json
{
  "id": "a9cc4ace48cd792ef888ade20810f82f6c24aaf2436f30337a2a712cd054dc97",
  "created": "1970-01-01T00:00:00Z",
  "container_config": {
    "Hostname": "",
    "Domainname": "",
    "User": "",
    "AttachStdin": false,
    "AttachStdout": false,
    "AttachStderr": false,
    "Tty": false,
    "OpenStdin": false,
    "StdinOnce": false,
    "Env": null,
    "Cmd": null,
    "Image": "",
    "Volumes": null,
    "WorkingDir": "",
    "Entrypoint": null,
    "OnBuild": null,
    "Labels": null
  },
  "os": "linux"
}

  • 查看第一个 layer.tar 之后,我们会发现其看起来像是一个操作系统基本目录文件结构。是的,你没有猜错,这真是 Alpine 镜像,也就是改 Dockerfile 文件的基础镜像。
vagrant@vagrant:~/test$ cd a9c..c97/
vagrant@vagrant:~/test/a9c..c97$ tar -tf layer.tar | head
bin/
bin/arch
bin/ash
bin/base64
bin/bbconfig
bin/busybox
bin/cat
bin/chgrp
bin/chmod
bin/chown

vagrant@vagrant:~/test/a9c..c97$ cat etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.11.6
PRETTY_NAME="Alpine Linux v3.11"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
  • 我们可以解压对应 tar 包,来查看其中包含的内容和文件。而另外的两个层,分别包含了一堆依赖项和证书更新,以及 Prettier 的源代码文件。
vagrant@vagrant:~/test/a9c..c97$ tar -xf layer.tar
vagrant@vagrant:~/test/a9c..c97$ tree etc/apk/
etc/apk/
├── arch
├── keys
│   ├── [email protected]
│   ├── [email protected]
│   └── [email protected]
├── protected_paths.d
├── repositories
└── world

3. 镜像压缩工具 - docker-squash

Docker image squashing tool

  • 一般来说,在构建 Docker 镜像的时候,都会创建许多层。但有时,冗余的指令会导致层数的不必要增加,从而消耗时间和资源,导致镜像变大。而 docker-squash 工具就是可以将多层合并成一层。
# 安装
$ pip install docker-squash

# 查看原始镜像分层
$ docker history jboss/wildfly:latest

# 压缩多合一
$ docker-squash -f 10 -t jboss/wildfly:squashed jboss/wildfly:latest
$ docker-squash -f 4bb15f3b6977 -t jboss/wildfly:squashed jboss/wildfly:latest

# 查看效果
$ docker history jboss/wildfly:squashed

4. 镜像压缩工具 - docker-slim

在不改变容器的功能的前提下,缩减容器体积!

逆向解析Docker镜像文件 - docker-slim

逆向解析Docker镜像文件 - docker-slim


5. 思考总结陈述

送人玫瑰,手有余香!

在弄清楚 Docker 中的分层逻辑以及对应层文件变动的存储方式之后,我们就可以做很多事情了。比如,对于我们镜像中的代码如何进行加密处理且不怕将镜像公开出去泄露隐私呢?如何去有效的缩减镜像打包过程中产生的冗余层和对应文件及目录呢?如何对镜像进行快速更新(通过对层的补丁增量更新的方式)呢?这些都是值得我们思考的事情!


文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录