Docker Image

先简单介绍下 Docker Image, 通常情况下我们将其称之为镜像,镜像是由多个层组成的文件,这些层用于在容器内执行代码(命令)等。每个镜像基本上都是根据应用程序完整的可执行版本进行构建的,并且需要注意的是,它会依赖于主机的系统内核。当用户在运行镜像时,这将会创建一个或者多个容器实例。

Dockerd

Dockerd 是 Docker 的服务端,默认情况下提供 Unix Domain Socket 连接,当然也可以监听某个端口,用于对外提供服务。 所以有时候,我们也可以使用服务器上的 Docker daemon 来提供服务,以加快构建速度及解决一些网络问题之类的。

好的,基础概念了解了, 那我们开始进入正题。

使用 Dockerfile

我们知道构建镜像的方法有多种,本文中我们只介绍使用 Dockerfile 通过 docker build 的方式构建镜像。

为了简便,我们以一个简单的 Dockerfile 开始。构建一个容器内使用的 kubectl 工具 (当然选择它的原因在于 kubectl 足够大,并不考虑可用性,这个稍后解释)

FROM scratch

LABEL maintainer='Jintao Zhang <moelove.info>'

ADD kubectl /kubectl
ENTRYPOINT [ "/kubectl" ]

Dockerfile 足够简单,只是将 kubectl 的二进制文件拷贝进去,并将 Entrypoint 设置为 kubectl 。

Dockerd in Docker

我个人一般为了避免环境的污染,大多数的事情都在容器内完成。包括 dockerd 我也启在容器内。其中的原理不再介绍,可以参考我之前的文章或分享。使用起来很简单:

docker run --privileged -d -P docker:stable-dind
复制代码

注意这里使用了 -P 所以本地会随机映射一个端口,当然你也可以直接指定映射到容器内的 2375 端口。

(Tao) ➜  build git:(master) docker ps                                                       
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                     NAMES
b56f6483614d        docker:stable-dind          "dockerd-entrypoint.…"   9 hours ago         Up 9 hours          0.0.0.0:32769->2375/tcp   trusting_babbage

构建

我们直接使用启动在容器内的 dockerd 进行构建,通过上面的 docker ps 命令可以看到是映射到了本地的 32769 端口。所以我们使用以下命令进行构建:

(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 images                                                              
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE                                      
(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 build -t local/kubectl .                                            
Sending build context to Docker daemon  55.09MB
Step 1/4 : FROM scratch
 --->
Step 2/4 : LABEL maintainer='Jintao Zhang <moelove.info>'
 ---> Running in ebcf44071bf0
Removing intermediate container ebcf44071bf0
 ---> eb4ea1725ff2
Step 3/4 : ADD kubectl /kubectl
 ---> 1aad06c4dbb4
Step 4/4 : ENTRYPOINT [ "/kubectl" ]
 ---> Running in 2fc78fe974e3
Removing intermediate container 2fc78fe974e3
 ---> 457802d4bf3e
Successfully built 457802d4bf3e
Successfully tagged local/kubectl:latest
(Tao) ➜  kubectl git:(master) docker -H 0.0.0.0:32769 images                  
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
local/kubectl       latest              457802d4bf3e        3 seconds ago       55.1MB

看日志及结果,可以看到我们已经成功的构建了我们所需的镜像。说了这么多,其实我们今天的内容才刚刚开始。

深入原理

Dockerd 服务

在本文一开始,我已经提过 Dockerd 是 Docker 的后端服务,通过上面的

docker -H 0.0.0.0:32769 images                                                              

这条命令可以看到我们通过 -H 指定了本地 32679 端口的 dockerd 服务,这其实是个 HTTP 服务,我们来验证下。

(Tao)   kubectl git:(master) curl -i   0.0.0.0:32769/_ping
HTTP/1.1 200 OK
Api-Version: 1.37
Docker-Experimental: false
Ostype: linux
Server: Docker/18.03.1-ce (linux)
Date: Tue, 04 Sep 2018 17:20:51 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK%                  

可以看到几条关键的信息 Api-Version: 1.37 这个表明了当前使用的 API 版本,本文的内容也是以 1.37 为例进行介绍,这是当前的稳定版本。我们也可以通过 docker version 进行查看。

(Tao)   kubectl git:(master) docker -H 0.0.0.0:32769 version                                                             
Client:
 Version:           18.06.0-ce
 API version:       1.37 (downgraded from 1.38)
 Go version:        go1.10.3
 Git commit:        0ffa825
 Built:             Wed Jul 18 19:11:45 2018
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.03.1-ce
  API version:      1.37 (minimum version 1.12)
  Go version:       go1.9.5
  Git commit:       9ee9f40
  Built:            Thu Apr 26 07:23:03 2018
  OS/Arch:          linux/amd64
  Experimental:     false
复制代码

可以看到我本地在用的 docker cli 版本较高,当连接到低版本的 dockerd 时,API 版本降级至与 dockerd 版本保持一致。

当然,你可能会问,如果是 dockerd 版本高会如何呢?其实我日常中的开发环境就是这样,大多数 API 都没什么影响, 不过这并不是今天的重点。

root@bdcdac73ee20:/# docker version
Client:
 Version:      17.06.0-ce
 API version:  1.30
 Go version:   go1.8.3
 Git commit:   02c1d87
 Built:        Fri Jun 23 21:15:15 2017
 OS/Arch:      linux/amd64

Server:
 Version:      dev
 API version:  1.39 (minimum version 1.12)
 Go version:   go1.10.3
 Git commit:   e8cc5a0b3
 Built:        Tue Sep  4 10:00:36 2018
 OS/Arch:      linux/amd64
 Experimental: false

build context

回到我们上面的构建过程中。我们可以看到日志内容的第一行:

...
Sending build context to Docker daemon  55.09MB

从这条日志,我们可以得到两个信息:

  • 构建的过程是将 build context 发送给 dockerd , 实际的构建压力在 dockerd 上
  • 发送了 55.09 MB

第一条结论,我们在上一小节已经讨论过了,我们来重点看下第二条结论。

(Tao) ➜  kubectl git:(master) ls -al 
总用量 53808
drwxrwxr-x. 2 tao tao     4096 95 01:00 .
drwxrwxr-x. 3 tao tao     4096 95 00:57 ..
-rw-rw-r--. 1 tao tao      109 95 01:00 Dockerfile
-rwxrwxr-x. 1 tao tao 55084063 95 00:53 kubectl
(Tao) ➜  kubectl git:(master) du -sh .
53M     .
(Tao) ➜  kubectl git:(master) du -sh kubectl Dockerfile 
53M     kubectl
4.0K    Dockerfile

按照我们 Dockerfile 的内容,我们需要将 kubectl 的二进制包放入镜像内,所以 build context 虽然比二进制文件多出来 2M 左右的大小你也不会很意外。

但我这里做了另一个例子,不多赘述,代码可以在我的 GitHub 中找到。这里贴出来结果:

(Tao) ➜  text git:(master) ls -al                                                                                          
总用量 16                                                                                                                  
drwxrwxr-x. 2 tao tao 4096 9月   5 01:45 .                                                                                
drwxrwxr-x. 4 tao tao 4096 9月   5 01:44 ..                             
-rw-rw-r--. 1 tao tao   77 9月   5 01:45 Dockerfile                       
-rw-rw-r--. 1 tao tao   61 9月   5 01:45 file  
(Tao) ➜  text git:(master) du -b Dockerfile file
77      Dockerfile
61      file                                                                                                              
(Tao) ➜  text git:(master) docker -H 0.0.0.0:32769 build --no-cache=true -t local/file .                                  
Sending build context to Docker daemon  3.072kB
...

相信你看到这个结果已经明白我想表达的意思,我们继续探索下这个过程。

/build 请求

前面我们已经说过,这就是个普通的 HTTP 请求,所以我们当然可以直接抓包来看看到底发生了什么?

很简单,通过 dockerd 的地址,使用 POST 方法,访问 /build 接口, 当然实际情况是会增加前缀,即我在上面提到的版本号,在目前的环境中使用的是 /v1.37/build 这个接口。

而这个请求携带了一些很有用的参数,和头信息。这里我来简单说下:

Header

build 请求的头部,主要有以下两个

  • Content-Type 默认值为 application/x-tar,表明自己是一个归档。
  • X-Registry-Config 这个头部信息中包含着 registry 的地址及认证信息,并且以 base64 进行编码。对 docker 熟悉的朋友或者看过我之前文章的朋友应该知道, Docker cli 在 login 成功后,会将认证信息保存至本地,密码做 base64 保存。而 build 的时候则会将此信息再次 base64 进行编码。通过这里也可以看出来,在使用远端 Dockerd 的时候, 应该尽量配置 TLS 以防止中间人攻击,造成密码泄漏等情况。

Parameters

请求参数中,列几个比较有意义的:

  • t 这其实就是我们 docker build -t 时候指定的参数,并且,我们可以同时指定多个 -t 同时构建多个不同名称的镜像。
  • memory cpusetcpus 这些主要用于资源限制
  • buildargs 如果想要了解这个参数,可以回忆下 Dockerfile 中的 ARG 指令的用法

当然,我们想要探索的过程其实重点就在于请求头部了, 整个请求的输入流,必须是一个 tar 压缩包,并且支持 identity (不压缩), gzip, bzip2, xz 等压缩算法。

实现

我们来看下基本的实现:

func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
	query, err := cli.imageBuildOptionsToQuery(options)
	if err != nil {
		return types.ImageBuildResponse{}, err
	}

	headers := http.Header(make(map[string][]string))
	buf, err := json.Marshal(options.AuthConfigs)
	if err != nil {
		return types.ImageBuildResponse{}, err
	}
	headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))

	headers.Set("Content-Type", "application/x-tar")

	serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
	if err != nil {
		return types.ImageBuildResponse{}, err
	}

	osType := getDockerOS(serverResp.header.Get("Server"))

	return types.ImageBuildResponse{
		Body:   serverResp.body,
		OSType: osType,
	}, nil
}