一、说明

        Docker 镜像是通过构建 Dockerfiles 创建的。构建过程执行 Dockerfile 中的指令来创建构成最终镜像的文件系统层。 如果给出已有图像,您可以检索构建它的 Dockerfile 吗?在本文中,我们将研究两种可以实现此目的的方法。

二、逆向的需求

        当您构建自己的 Docker 映像时,您应该将 Dockerfile 作为版本控制文件存储在源存储库中。这种做法可确保您始终可以检索用于组装镜像的说明。

        如果您使用公共Registoy中的镜像,您将无法访问它的Dockerfile。或者您可能正在使用不直接提供 Dockerfile 的镜像快照。在这些情况下,您需要一种可以从计算机上的镜像提取 Dockerfile 的技术。

        Docker 不提供任何内置功能来实现这一点。您可以自己设法进行逆向工程,以按需生成图像 Dockerfile 的近似版本。

三、使用Docker的history命令

        docker history 命令显示镜像的层历史。它显示了用于构建每个连续文件系统层的命令,使其成为复制 Dockerfile 时的一个很好的起点。

        这是一个用于 Node.js 应用程序的简单 Dockerfile:

FROM node:16
COPY app.js .
RUN app.js --init
CMD ["app.js"]

编译镜像:docker build:

$ docker build -t node-app:latest .

现在使用 docker history 检查图像的层历史:

$ docker history node-app:latest
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
c06fc21a8eed   8 seconds ago    /bin/sh -c #(nop)  CMD ["app.js"]               0B        
74d58e07103b   8 seconds ago    /bin/sh -c ./app.js --init                      0B        
22ea63ef9389   19 seconds ago   /bin/sh -c #(nop) COPY file:0c0828d0765af4dd...   50B       
424bc28f998d   4 days ago       /bin/sh -c #(nop)  CMD ["node"]                 0B        
<missing>      4 days ago       /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry...   0B        
...

        历史包括图像中层的完整列表,包括从 node:16 基本镜像继承的层。层是有序的,所以最近的层在第一层。您可以根据创建时间确定示例 Dockerfile 创建的层的起始位置。这些显示了 Dockerfile 中使用的 COPY 和 CMD 指令的内部表示。

        当表格仅限于显示每一层的命令时,docker history 输出更有用。您也可以禁用截断以查看与每一层关联的完整命令:

$ docker history node-app:latest --format "{{.CreatedBy}}" --no-trunc
/bin/sh -c #(nop)  CMD ["app.js"]
/bin/sh -c ./app.js --init
/bin/sh -c #(nop) COPY file:0c0828d0765af4dd87b893f355e5dff77d6932d452f5681dfb98fd9cf05e8eb1 in . 
/bin/sh -c #(nop)  CMD ["node"]
/bin/sh -c #(nop)  ENTRYPOINT ["docker-entrypoint.sh"]
...

      从此命令列表中,您可以大致了解组装镜像的步骤。对于像这样的简单镜像,这些信息完全可以准确地重现 Dockerfile。

四、使用 Whaler自动提取图层

        从 docker 历史记录中复制命令是一个费力的过程。基本是手工处理。幸运的是,有可用的社区工具可以根据镜像的层历史自动创建 Dockerfile。另一个选择是,由 Alpine 组织打包到 alpine/dfimage (Dockerfile-from-Image) Docker 镜像中的 Whaler工具。

4.1 什么是Whaler

        Whaler 是一个 Go 程序,旨在将 docker 镜像逆向工程到创建它的 Dockerfile 中。它当前执行以下操作

  • 从图像生成 Dockerfile

  • 搜索添加的文件名以查找潜在的秘密文件

  • 提取由 Docker ADD/COPY 指令添加的文件

  • 它还显示杂项。信息,如端口打开、它运行的用户和环境变量。

4.2 Whaler使用方法示例

1)安装whaler

# 安装whaler到/usr/local/bin/目录下
wget -cO /usr/local/bin/whaler https://github.com/P3GLEG/Whaler/releases/download/1.0/Whaler_linux_amd64# 赋予可执行权限
chmod +x /usr/local/bin/whaler

2)常用参数

# 将docker客户端ID设置为特定版本-sV
whaler -sV=1.36 # 打印有关图像的所有详细信息 -v 
whaler -v 镜像名# 将镜像曾保存到当前目录-x 

3)将镜像逆向为dockfile

# 逆向镜像
whaler 镜像名# 示例
[master root ~]# whaler -sV=1.36 nginx
Analyzing nginx
Docker Version: 20.10.7
GraphDriver: overlay2
Environment Variables
|PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|NGINX_VERSION=1.21.5
|NJS_VERSION=0.7.1
|PKG_RELEASE=1~bullseyeOpen Ports
|80Image user
|User is rootPotential secrets:
Dockerfile:
CMD ["bash"]
LABEL maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>
ENV NGINX_VERSION=1.21.5
ENV NJS_VERSION=0.7.1
ENV PKG_RELEASE=1~bullseye
RUN set -x  \&& addgroup --system --gid 101 nginx  \&& adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx  \&& apt-get update  \&& apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates  \&& NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; found=''; for server in hkp://keyserver.ubuntu.com:80 pgp.mit.edu ; do echo "Fetching GPG key $NGINX_GPGKEY from $server"; apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY"  \&& found=yes  \&& break; done; test -z "$found"  \&& echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY"  \&& exit 1; apt-get remove --purge --auto-remove -y gnupg1  \&& rm -rf /var/lib/apt/lists/*  \&& dpkgArch="$(dpkg --print-architecture)"  \&& nginxPackages=" nginx=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} "  \&& case "$dpkgArch" in amd64|arm64) echo "deb https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list  \&& apt-get update ;; *) echo "deb-src https://nginx.org/packages/mainline/debian/ bullseye nginx" >> /etc/apt/sources.list.d/nginx.list  \&& tempDir="$(mktemp -d)"  \&& chmod 777 "$tempDir"  \&& savedAptMark="$(apt-mark showmanual)"  \&& apt-get update  \&& apt-get build-dep -y $nginxPackages  \&& ( cd "$tempDir"  \&& DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" apt-get source --compile $nginxPackages )  \&& apt-mark showmanual | xargs apt-mark auto > /dev/null  \&& { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; }  \&& ls -lAFh "$tempDir"  \&& ( cd "$tempDir"  \&& dpkg-scanpackages . > Packages )  \&& grep '^Package: ' "$tempDir/Packages"  \&& echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list  \&& apt-get -o Acquire::GzipIndexes=false update ;; esac  \&& apt-get install --no-install-recommends --no-install-suggests -y $nginxPackages gettext-base curl  \&& apt-get remove --purge --auto-remove -y  \&& rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list  \&& if [ -n "$tempDir" ]; then apt-get purge -y --auto-remove  \&& rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; fi  \&& ln -sf /dev/stdout /var/log/nginx/access.log  \&& ln -sf /dev/stderr /var/log/nginx/error.log  \&& mkdir /docker-entrypoint.d
COPY file:65504f71f5855ca017fb64d502ce873a31b2e0decd75297a8fb0a287f97acf92 in /docker-entrypoint.shCOPY file:0b866ff3fc1ef5b03c4e6c8c513ae014f691fb05d530257dfffd07035c1b75da in /docker-entrypoint.ddocker-entrypoint.d/docker-entrypoint.d/10-listen-on-ipv6-by-default.shCOPY file:0fd5fca330dcd6a7de297435e32af634f29f7132ed0550d342cad9fd20158258 in /docker-entrypoint.ddocker-entrypoint.d/docker-entrypoint.d/20-envsubst-on-templates.shCOPY file:09a214a3e07c919af2fb2d7c749ccbc446b8c10eb217366e5a65640ee9edcc25 in /docker-entrypoint.ddocker-entrypoint.d/docker-entrypoint.d/30-tune-worker-processes.shENTRYPOINT ["/docker-entrypoint.sh"]
EXPOSE 80
STOPSIGNAL SIGQUIT
CMD ["nginx" "-g" "daemon off;"]

4.3 容器板的Whaler使用方法示例 

尝试使用whaler,首先拉取whaler的镜像。

    docker pull pegleg/whaler

启动whaler容器,分析nginx:latest镜像

docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock:ro pegleg/whaler -sV=1.36 nginx:latest
docker build --rm -t pegleg/whaler .
alias whaler="docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock:ro pegleg/whaler"
whaler -sV=1.36 nginx:latest

这个工具会自动拉取目标 docker 镜像。参数 -sV=1.36 并不总是必需的。

        

五、使用 Dfimage 自动提取图层

        运行 dfimage 镜像并提供 Docker 标签将输出一个 Dockerfile,可用于复制引用的图像。您必须将主机的 Docker 套接字绑定到 dfimage 容器中,以便它可以访问您的图像列表并在需要时拉取标签。

$ docker run --rm 
    -v /var/run/docker.sock:/var/run/docker.sock 
    alpine/dfimage node-app:latest

Analyzing node-app:latest
Docker Version: 20.10.13
GraphDriver: overlay2
Environment Variables
|PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|NODE_VERSION=16.14.2
|YARN_VERSION=1.22.18

Image user
|User is root

Dockerfile:
...
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node"]
COPY file:bcbc3d5784a8f1017653685866d30e230cae61d0da13dae32525b784383ac75f in .
    app.js

RUN ./app.js --init
CMD ["app.js"]

        创建的 Dockerfile 包含从头开始(空文件系统)到指定镜像的最后一层所需的一切。它包括来自基础镜像的所有层。您可以在上面示例输出的第一个 ENTRYPOINT 和 CMD 指令中看到这些(为简洁起见,其他基础镜像层已被省略)。

        除了 COPY 之外,我们镜像的特定指令与原始 Dockerfile 中的内容相匹配。您现在可以将这些指令复制到一个新的 Dockerfile 中,使用整个 dfimage 输出或只获取与最终镜像相关的部分。后一种选择只有在您知道原始基础图像的轮廓时才有可能,因此您可以在文件顶部添加 FROM 指令。

六、逆向工程的局限性

        在许多情况下,dfimage 将能够组装一个可用的 Dockerfile。尽管如此,它并不完美,也不能保证完全匹配。与镜像的原始 Dockerfile 相比的差异程度将根据所使用的指令而有所不同。

        并非所有指令都在层历史记录中捕获。不受支持的将丢失,您无法确定它们是什么。使用 RUN、ENV、WORKDIR、ENTRYPOINT 和 CMD 等命令和元数据指令可获得最佳准确性。如果他们的命令没有导致文件系统更改,则 RUN 指令仍然可能会丢失,这意味着没有创建新的镜像层。

        当您到达最终目的地时,这足以帮助您了解复制的内容以及原因。然后,您可以使用此信息将新的源路径插入到 Dockerfile 中,您可以将其用于未来的构建。在其他情况下,检查图像中的文件可能有助于揭示副本的用途,以便您可以为主机路径确定一个有意义的文件名。

七、总结

        Docker 不提供直接从image反推出Dockerfile 的方法。不过,仍然可以将构建过程拼凑起来。对于指令很少的简单图像,您通常可以通过查看 docker history 命令输出中的 CREATED BY 列来手动恢复成指令。

        具有更复杂构建过程的更大图像最好使用 dfimage 等工具进行分析。这会为您解析复杂的 dockerfile代码,生成一个新的 Dockerfile,且与可能的原始 Dockerfile 尽量匹配。

        逆向工程工作并不完美,一些 Dockerfile 指令在构建过程中丢失或损坏。因此,您不可假定以这种方式创建的 Dockerfile 是原始文件的准确恢复。您可能还必须对 ADD 和 COPY 指令进行一些手动调整,恢复已转换为上下文引用的宿主机文件路径。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐