万字长文:彻底搞懂容器镜像构建 原 荐

大家好,我是张晋涛。

我将在这篇文章中深入 Docker 的源码,与你聊聊镜像构建的原理。

文章过长,目录如下:

万字长文:彻底搞懂容器镜像构建
                                                原
                                                    荐

Docker 架构

这里我们先从宏观上对 Dockedocker和虚拟机的区别r 有个大概的认识,它整体上是个 C/S 架构;我们dockerhub平时使用的 docker 命令docker面试题及答案就是它的 CLI 客户端,而它的服务端是docker常用命令 dockerd 在 Linux 系统中,通常我们是使用 systemd 进行管理,所以我们可以使用 systemctldocker和虚拟机的区别 start docker 来启动服务。(但是请注意,dockerd 是否能运行与 systemd 并无任何关系,你可以像平时执行一个普通的二进制docker和虚拟机的区别程序一样,直接通过 dockerd 来启动docker服务,注意需要 root 权限)

实际上也就是

万字长文:彻底搞懂容器镜像构建
                                                原
                                                    荐

Docker 架构

(图片来源:docker overview)

docker CLI 与 dockerd 的交互是通docker和虚拟机的区别过 REST API 来完成的,当我们执行 docker version 的时候过滤 API 可以看到如下输出:

➜~dockerversion|grepAPI
APIversion:1.41
APdockerhubIvdocker容器ersion:1.41(minimumvdocker面试题及答案ersion1.12)

上面一行是 docker CLI 的 API 版本,下面则代表了 dodocker安装部署ckerd 的 API 版本,它的后面还有个括号,是因为 Docker 具备了很良好的兼容性,这里表示它最小可兼容的 API 版本是 1.12docker和虚拟机的区别

对于我们进行 C/S 架构的项目开发而言,一般都是 API 先行, 所以我们先来看下 API 的部分。

当然,本文的主体docker是干什么的构建系统相关的,所以docker常用命令我们就直接来看构建相关的 API 即可。

接下来会说 CLI,docker面试题及答案代码以 v20.10.5 为准。最后说服务端 Dockerd 。

API

Docker 维护团队在每个版本正式发布之后,都会将 API 文档发布docker怎么读出来,可以通过 Docker Engine API 在线浏览docker是干什么的,也可以自行构建 API 文档。

首先 clone Docker 的源docker是干什么的代码仓库, 进入项目仓库内执行 make swagger-docs 即可在启动一个容docker菜鸟教程器同时将端口暴露至本地的 9000 端口, 你可以直接通过 htdocker怎么读tp://127.0.0.1:9000 访问本地的 API 文档。

(MoeLove)➜gitclonehttps:docker容器//github.comdocker是干什么的/docker/docker.gitdocker常用命令docker
(MoeLove)➜cddocker
(MoeLove)➜dockergit:(mastdocker和虚拟机的区别er)gitcheckout-bv20.10.5v20.10.5
(MoeLovedockerhub)➜dockergit:(v20.10.5)makeswagger-docs
APIdocdocker常用命令sprevidocker安装部署ewwillberunningathttp:docker面试题及答案//dockerhublocalhost:9000

打开 http://127.0.0.1:9000/#operation/ImageBuild 这个地址就可以看到 1.41 版本的构建镜像所需的 API 了。我们对此 API 进dockerhub行下分析。

请求地址和方法

接口地址是 /v1.41/build 方法是 POST ,我们可以使用一个较新docker常用命令版本的 cudocker常用命令rl 工具来验证下此docker常用命令接口(需要使用 --unix-socket 连接docker怎么读 Docker 监听的 UNIX Domain Socket )。dockerdocker和虚拟机的区别d 默认情况下监听在 /var/run/dockerdockerhub.sock ,当然你也可以给 dockerd 传递 --host 参数用于监听 HTTP 端口或者其他路径的 unix socket .

/#curl-XPOST--unix-socket/var/run/docker.socklocalhost/v1.41/build
{"message":"Cadocker是干什么的nnotlocatespecifidocker怎么读edDockerfile:Dockerfile"}

docker常用命令上面的输出我们可以看到,我们确实访问到了该接口,同时该接口的响应是提示需要 Dockerfile .

请求体docker容器

请求体是一个 tar 归档文件,可选docker菜鸟教程择无压缩、gzipbzip2xz 压缩等形式。关于这几种压缩格式就不再展开介绍了,但值得注docker容器意的是 如果使用了压缩,则传输体积会变小,即网络消耗会相应减少。但压缩/解压缩需要耗费 CPU 等docker和虚拟机的区别计算资源 这在我们对大规模镜像构建做优化时是个值得权衡的点。

请求头

因为要发送的是个 tar 归档文件,Content-type 默认是 application/x-tar 。另一个会发送的头是 X-Registry-Config,这是一dockerhub个由 Base64 编码后的 Docker Registry 的配置信息,内容与 $HOME/.docker/config.json 中的 auths 内的信息一致。

这些配置信息,在你执行 ddocker常用命令ocker login 后会自动写入到 $HOME/.docker/config.jsondocker常用命令 文件内的。这些信息被传输到 dockdocker和虚拟机的区别erd 在构建过程中作为拉取镜像的认证信息使用。

请求参数

最后就是请求参数了,参数有很多,通过 docker build --help 基本都可以docker面试题及答案看到对应含义的,这里不再一一展开了docker面试题及答案,后面会有一些关键参数的介绍。

小结

上面我们介绍了 Docdocker容器ker 构建镜像相关的docker是干什么的 API,我们可以直接访问Docker Engine 的 API 文档。dockerfile或者通过源码仓库,自己来构建一个本地的 API 文档服务,使dockerfile用浏览器进行访问。

通过 API 我们也知道了该接口所需的请求体是一个 tar 归档文件(可选择压缩算法进行压缩),同时它的请求头中会携带用户在镜像docker安装部署仓库中的认证信息。这提醒我们, 如果在使用远程 Dockerd 构建时,请注意安全,尽量使用 tls 进行加密,以免数据泄漏docker是干什么的

CLI

API 已经介绍完了,我们来看下 docker CLI,我以前的文章中介绍过现在 Docker 中有两个构建系统,一个是 v1 版本的 builder 另一个是 v2 版本的即 BuildKit 我们来分别深入源码来看看在构建镜像时,他们各自的行为吧。

准备代码

CLI 的代码仓库在 htdocker容器tps://github.com/docker/cli 本文的代码以 v20.10.5 为准。

通过以下步骤使用此版本的代码:

(MoeLove)➜gdockeritclonehttpdockerhubs://github.com/docker/cli.git
(MoeLove)➜cdcli
(MoeLove)➜cligit:(master)gitcheckout-bv20.10.docker是干什么的5v20.10.5

逐步分解

docker 是我们所使用的客户端工具,用于与 dodocker菜鸟教程ckerd 进行交互。关于构建相关的部分, 我们所熟知的便是 ddocker怎么读ocker build 或者是 dockerdocker常用命令 image bdocker怎么读uild,在 19.03 中新增的是 docker builder build ,但其实他们都是同一个只是做了个 alias 罢了:

//cmd/docker面试题及答案docker/docker.dockerfilego#L237
ifv,ok:=aliadocker容器sMap["builder"];ok{
aliases=append(aliases,
[2][]string{{"build"},{v,"build"}},
[2][]stridocker和虚拟机的区别ng{{"image","build"},{v,"buildocker和虚拟机的区别d"}},
)
}

真正的入口函数其实在 cli/command/image/build.go;区分如何调用的逻辑如下:

funcrunBuild(dockerClicommadocker和虚拟机的区别nd.Cli,optionsbuildOptiodocker和虚拟机的区别ns)error{
buildocker是干什么的dkitEnabled,err:dockerfile=command.BuildKitEnabled(dockerCli.ServerIdockerhubnfo())
iferr!=nil{
returnerr
}
ifbuilddocker和虚拟机的区别kitEnabled{
returnrunBuildBuildKit(dockerCli,optdockerhubions)
}
//省略掉了对于buildocker怎么读der的实际逻辑
}

这里就是判断下是否支持 buildkit

//cli/command/cli.go#Ldockerfile176
funcBuildKitEnabled(siServerInfo)(bool,error){
buildkitEnabled:=si.BuildkitVersion==types.BuilderBuildKit
ifbuildkitEnv:=osdocker和虚拟机的区别.Getenv("DOCdocker和虚拟机的区别KER_BUILDdocker安装部署KIT");buildkitEnv!=""{
varerrerror
buildkitEnabled,errdockerhub=strconv.ParseBdockerfileool(buildkitEnv)
iferr!=nil{
returnfalse,errors.Wrap(err,"DOCdocker常用命令KER_BUILDKdockerhubITenvironmentvariableexpectsbooleanvalue")
}
}
returnbuildkitEnabled,nil
}

当然,从docker安装部署这里可以得到两个信息:

  • 通过
    dodocker常用命令ckerddocker常用命令配置可开启
    buildkit 。在
    /etc/dockerdocker/ddocker怎么读aemon.json 中添加如下内容,并重docker菜鸟教程
    dockerd 即可:
{
"features":{
"buildkit":true
}
}

  • docker CLI 上也可开启
    buildkit 的支持,并且 CLI 的配置可覆盖服务端配置。通过
    export DOCKER_BUILDKIT=1 即可开启
    buildkit 的支持,设置为 0 则关闭(0/false/f/F 之的也都是相同的结果)

从上面的介绍也看到了,对于原本默认的 builder 而言, 入口逻docker是干什么的辑在 runBuild 中, 而对于使用 buildkit 的则是 rudocker常用命令nBuildBuildKit 接下来,我们对两者进行逐步分解。

builder v1

runBuild 函数中,大致经dockerfile历了以下阶段:

参数处理

最开始的部分是一些对参数的处docker是干什么的理和校验。

  • streamcompress 不可同时使用。

因为如果我们指定了 compress 的话,则 CLI 会使用 gzip 将构建上下文进行压缩,这样也docker容器就没法很好的通过 stream 的模式来处理构建的上下文了。

当然你也可能会想,从技docker常用命令术上来讲,压缩和流式没有什么必然的冲突,是可实现的。事实的确如此,如果从技术的角度上来讲两者并非完全不能一起存在,无非就是增加解压缩的动作。但是当开启 stream 模式,对每个文件都进行压缩和解压的操作那将会是很大的资源浪费,同时也增加了其复杂度,所以在 CLI 中便直接进行了限制,不允许同时使用 compressstdocker和虚拟机的区别ream

  • 不可同时使用 stdin 读取 Dockerfilebuild context

在进行构建时,如果我们将 Dockerfildocker常用命令e 的名字传递为 - 时,表示从 stdin 读取其内容。

例如,某个目录下有三个文件 foo barDockerfile,通过管道将 Dockerfile 的内容通过 stdin 传递给 docker build

(MoeLove)➜xls
barDockerfilefoo
(MoeLove)➜xcatDockerfile|DOCKER_BUILDKdocker容器IT=0dockerbuild-f-.
Sedocker是干什么的ndingbuildcontexttoDockerdaemon15.41kB
Step1/3:FROMsdockerfilecratch
---&dockergt;
Step2/3:docker面试题及答案COPYfoofoo
--dockerhub->a2af45d66bb5
Step3/3:COPYbarbar
--->cc803c675dd2
Successfullybuiltcc803c675dd2

可以看到通过 stdin 传递 Dockerfile 的方式能成功的构建镜像。接下来我们尝试通过 stdinbuild context 传递进去。

(MoeLove)➜xtar-cvfx.tarfoobarDockerfile
foo
bar
Dodocker菜鸟教程ckerfile
(MoeLove)➜xcatx.tar|DOCKER_BUILDKIT=0dockerbuild-fDockerfdocker面试题及答案ile-
SendingdockerfilebuildcontexttoDockerdaedocker菜鸟教程mon10.24kB
Step1/3:FROMscradocker怎么读tch
--->
Step2/3:COPYdocker面试题及答案foofoodocker
--->09319712e220
Step3/docker是干什么的3:Cdocker容器OPYbarbar
--->ce88644a7395
Successfullybuiltdocker是干什么的ce88644a7395

可以看到通过 stdin 传递 build context 的方式也可以成功构建镜像。

但如果 Dockerfile 的名称dockerfile与构建的上下文都指定为 -docker build -f - - 时,会发生什么呢?

(MoeLove)➜xDOdocker常用命令CKER_BUILDKIT=0dockerbuild-f--
invalidargument:can'docker安装部署tusestdinfordocker安装部署bothbuildcontextanddockerfile

就会报错了。所以, 不能同时使docker是干什么的stdin 读取 Dockerfiledockerbuidocker常用命令ld context

  • build context 支持四种行为。
switch{
casedocker常用命令options.contextFromStdin():
//省略
caseisLocalDir(specifiedContext):
//省略
caseurlutil.IsGitURL(specifiedContextdocker容器):
//省略
caseurlutidocker容器l.IsURL(specifiedContext):
//省略
defadocker面试题及答案ult:
returnerrors.Errorf("unabletopreparecontext:path%qnotfound",specifiedContext)
}

stddocker常用命令in 传入,上文已经演示过了,传递给 stdin 的是 tar 归档文件。当然也可以是指定一个具体的 PATH,我们通常使用的 docker build . 便是这种用法;

或者可以指定一个 git 仓库的地址,CLI 会调用 git 命令将仓库 clone 至一个临时目录,进行使用;

最后一种是,给定一个 URL 地址,该地址可以是 一个具体的 Dockerfile 文件地址 或者是 一个 tar 归档文件的下载地址

这几种基本docker容器就是字面上的区别,至于 CLI 的行为差异,主要是最后一种,当 URL 地址是一个具体的 Dockerfile 文件地址,在这种情况下 build context 相当于只有 Dockerfile 自身,所以并不能使用 COPY的指定,至于 ADD 也只能使用可访问的外docker容器部地址。

  • 可使用 .dockerignore 忽略不需要的文件

我在之前的文章中有分享过相关的dockerfile内容。这里我们看看它的实现逻辑。

//cli/command/image/build/dockerignore.go#L13
funcReadDockerignore(contextDirstring)([]string,error){
varexcludes[]string

f,err:=os.Opdocker面试题及答案en(filepath.Join(contextDir,".dockerignore"))
switch{
caseos.IsNotExist(erdocker安装部署r):
returnexcludes,nil
caseerr!=nil:
returnnil,err
}
deferf.Close()

redockerfileturndockerignore.ReadAll(f)
}
  • .dockerignore 是一个固定的文件名,并且需要放在 build context 的根目录下。似前面提到的,使用一个 Dockerfile 文件的 URL 地址作为 build context 传入docker和虚拟机的区别的方式,便无法使用 .dockeridocker安装部署gnore

  • .dockerigndockerhubore 文件可以不存在,但在读取的时候如果遇到错误,便会抛出错docker和虚拟机的区别误。

  • 通过 .dockerignore 将会过docker菜鸟教程滤掉不希望加入到镜像内,或者过滤掉与镜像无关的内容。

最后 CLI 会将 build contexdockerhubt 中的内容经过 .dockerigndocker面试题及答案ore 过滤后,打包成为真正的 build context 即真正的构建上下文。这也是为什么有时候你发现自己明明在 Dockerfile 里面写了 COPY xx xx 但是最后没有发现该文件的情况。很可能就是被 .dockerignore 给忽略掉了。这样有利于优化 CLI 与 dockerd 之间的传输docker压力之类的。

  • docker CLI 还会去读取
    ~/.docker/config.json 中的内容。

这与前docker常用命令面 API 部分所描述的docker常用命令内容基本是一致的。将认证信息通过 X-Registry-Config 头传递给 dockerd 用于docker菜鸟教程在需要拉取镜像时进行身份校验。

  • 调用 API 进行实际构建任务

当一切所需的校docker安装部署验和信息都docker是干什么的准备就绪之后,则开始调用 dockerCli.Client 封装的 API 接口,将dockerhub请求发送至 dockerd,进行实际的构建任务。

response,err:=dockerCli.Client().ImageBuild(ctx,body,buildOptions)
iferr!=nil{
ifoptions.quiet{
fmtdocker菜鸟教程.Fprintf(dockerCli.Err(),"%s",progBuff)
}
cancel()
returnerr
}
deferresponse.Body.Closedocker和虚拟机的区别()

到这里其实一次构建的过程中 CLI 所处理的流程就基本结束了docker和虚拟机的区别,之后便是按照传递的参数进行进度的输出或是将镜像 ID 写入到文件之类的。这部分就不进行展开了。

小结

整个过程大致如下图:

docker builder 处理流程

从入口函数 runBuild 开始,经过判断是否支持 buildkit ,如果不支持 buildkit 则继续使用 v1 的 buildedocker容器r。接下来读取各类参数,按照不同的参数执行各类不同的docker和虚拟机的区别处理逻辑。这里需要注意的就是 Dockerfilebuild context 都可支持从文件或者 stdin 等读入,具体使用时,需要注意docker怎么读。另外 .dockerignodocker是干什么的re 文件可过滤掉 buildockerfiled contdocker是干什么的ext 中的一些文件,在使用时,可通过此方法进行构建效率的优化,当然也需要注意,在通过 URL 获取 Dockerfile 的时候,是不存在 build context 的,所以类似 COPY 这样的命dockerhub令也就无法使用了。当所有的 build context 和参数都准备就绪后,接下来调用封装好的客户端,将这些请求按照本文docker和虚拟机的区别开始之初介绍的 API 发送给 dockerd ,由其进行真正的构建逻辑。

最后当构建结束后,CLI 根据参数决定是否要显示docker和虚拟机的区别构建进度或dockerfile者结果。

buidocker和虚拟机的区别ldkdocker常用命令it

接下来我们来看看 buildkit 如何来执行构建,方法入口与 builder 一致,但是在 buildkitEnabled 处,由于开启了 buildkit 支持,所以跳转到了 runBuildBuildKit

funcrunBuild(dockerClicdocker面试题及答案ommand.Cli,optionsbuildOptions)error{
buildkitEnabled,err:=command.BuildKitEnabled(dockerCli.ServerInfo())
iferr!=nil{
returnedocker安装部署rr
}
ifbuidocker是干什么的ldkitEnabled{
returnrudockerfilenBuildBuildKdocker安装部署it(dockerCli,options)
}
//省略掉了对于builder的实际逻辑docker
}

创建会话

但是与 builder 不同的是,这里先执行了一次 trySessiondockerhub 函数docker是干什么的

//cli/command/image/budockerhubild_buildkit.go#L50
s,err:=trySessiodocker安装部署n(dockerCli,options.context,false)
iferr!=nil{
returnerr
}
ifs==nil{
returnerrors.Errorf("buildkitnotsupportedbydaemon")
}

这个docker容器函数是用来做什么的呢?我们来找docker菜鸟教程到该函数所在的文件 cli/command/image/build_session.go

//cli/cdocker是干什么的ommand/image/build_session.go#L29
functrydocker安装部署Session(dockerClicodocker常用命令mmand.Cli,contextDirstring,forStreambool)(*session.Session,error){
if!isSessionSuppdockerorted(dockerCli,forStream){
returnnil,nil
}
sharedKey:=gdocker安装部署etBuildSharedKey(contextDir)docker怎么读
s,err:=session.NewSession(context.Background(),filepath.Base(contextDir),sharedKey)
iferr!=ndockerhubil{
retdockerhuburnnil,dockerfileerrors.dockerhubWrap(err,"failedtocreatesession")
}
redocker是干什么的turns,nil
}

当然还包括它其中最主要的 isSessdocker和虚拟机的区别ionSupported 函数:

//cli/command/image/build_session.go#L22
funcisSessionSupported(dockerClicommand.Cli,forStreambool)bool{
if!forStream&&versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(),"1.39"){
redocker安装部署turntrue
}docker是干什么的
returndockedocker和虚拟机的区别rCli.ServerInfo()docker容器.HasExperimental&&versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(),"1.31")
}

isSessionSupported 很明显是用于判断是docker菜鸟教程否支持 Session,这里由于我们会传入 forStreamfalse ,而且当前的 API 版本是 1.41 比 1.39 大,所以此函数会返回 true 。其docker和虚拟机的区别实在 builder 中也执行过相同的逻辑,只不过是在传递了 --stream 参数后,使用 Session 获取一个长连接以达到 stream 的处理能力。

这也就是为什么会有下面 dockerCli.ServerInfo().HasExdockerperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().Clidocker安装部署entVersion(), "1.31") 这个判断存在的原因了。docker怎么读

当确认支持 Sdocker菜鸟教程ession 时,docker面试题及答案则会调用 session.NewSession 创建一个新的会话。

//github.com/mobydocker是干什么的/buildkit/sessiodockerfilen/session.go#L47
funcNewSession(ctxcontexdocker面试题及答案t.Contexdocker容器t,name,sharedKeystring)(*Session,error){
id:=identity.Nedocker常用命令wID()

varunary[]grpc.UnaryServerIntercdockerhubeptor
varstream[]grpc.StreamServedocker安装部署rInterceptor

serverOpts:docker和虚拟机的区别=[]grpc.ServerOption{}
ifspan :=opentracing.SpanFromContext(ctx)dockerhub;span !=nil{
tracer:=span.Tracer()
unary=append(unary,otgrpc.OpenTracingServerInterceptor(tracer,traceFilter()))
stream=append(stream,otgrpc.OpenTracingStreamServerIntercedocker和虚拟机的区别ptor(span.Tracer(),docker容器tracedocker常用命令Filter()))
}

unary=append(unary,grdocker和虚拟机的区别pcerrors.UnaryServerIdocker和虚拟机的区别nterceptor)
stream=appendocker常用命令d(stream,grpcerrors.StreamServerInterceptor)

iflen(unary)==1{
serverOpts=append(serverOpts,grpc.UnaryInterceptor(unary[0]))
}elseiflen(unary)>1{
serverOpts=appenddocker安装部署(serverOpts,grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unary...)))
}

iflen(stream)==1{
serverOpts=append(serverOpts,grpc.StreamInterceptor(stream[0])docker常用命令)docker面试题及答案
}edocker是干什么的lseiflen(stream)&gdockerhubt;1{
servdocker和虚拟机的区别erOdockerpts=append(serverOpts,grdocker容器pc.StreamIntercedocker容器ptor(grpc_middleware.ChainStreamServer(sdocker菜鸟教程tream...)))
}

s:=&Session{
id:id,
nadocker容器me:name,
sharedKey:sharedKey,
grpcServer:grpc.NewServer(serverOpts...),
}

grpc_health_v1.RegdockerhubisterHealthSdockerfileerver(s.grpcServer,health.NewServerdocker常用命令(docker怎么读))

returns,nil
}

它创建了一个长连接会话,接下来的操作也都会基于这个会话来做。接下来的操作与docker常用命令 builder 大体一致,docker安装部署先判断 context 是以哪种形式提供的;当然它也与 builder 一样,是不允许同时从 stdin 获取 Dockerfilebuild context

switch{
caseoptions.contextdockerfileFromStdin():
//省略处理逻辑
casdocker菜鸟教程eisLocaldockerDir(options.context):
//docker菜鸟教程省略处理逻辑
caseurlutil.IsGitURL(options.contedocker和虚拟机的区别xt):
//省略docker处理逻辑docker安装部署
caseurlutildockerfile.IsURL(options.contexdocker和虚拟机的区别t):
//省略处理逻辑
default:
returnerrors.Edocker是干什么的rrorf("unabletodocker安装部署preparecontext:path%qnotfound",options.context)
}

这里的docker是干什么的处理逻辑与 v1 builder 保持一致的原因,主要在于用户体验上,当前的 CLI 的功能已经基本稳定,用户也已经习惯,所以即使是增加了 BuildKit 也并没有对主体docker安装部署的操作逻辑造成多docker菜鸟教程大改变。

选择输出模式

BuildKidocker怎么读t 支持了三种不同的输出模式 local tar 和正常模式(即存储在 dockerd 中), 格式为 -o type=localdockerhub,dedockerst=path 如果需要将构建的docker常用命令镜像进行分发,或是需要进行镜像内文件浏览的话,使用这个方式也是很方便的。

outputs,err:=parseOutputs(options.outputs)
iferr!=nil{
returnerrors.Wrapf(errdocker怎么读,"failedtoparseoutputs")
}

for_,out:=rangeoutputs{
switchout.docker安装部署Typdocker和虚拟机的区别e{
case"local":
//省略
case"tar":
//省略
}
}

其实它支持的模式还有第 4 种, 名为 cacheonly 但它并不会像前面提到的三种模式一样,有个很直观的输出,而且用的人可能会很少,所以就没有单独写了。

读取认证信息

dockerAuthProvider:=authprovider.NewDockerAuthProvider(os.Stderr)
s.Allow(dockerAuthProvider)

这里的行为与上面提到的 builder 的行为基本一致,这里主要有两个需要注意的点:

  • Allow() 函数
func(sdocker菜鸟教程*Session)Allow(aAttachable){
a.Register(s.grpcServer)
}

这个 Alldockerfileow 函数就是允许通过上面提到的 grpc 会话访问给定的服务。

  • authprovider

authproviderBuildKitdocker和虚拟机的区别 提供的一组抽象接口集合,通过它们docker是干什么的可以访问到机器上的配置文件,进而拿到认证信息,行为与 buidocker容器lder 基本一致。

高阶特性:secretsssh

我其他的文章讲过这两种高阶特性的使用了,本篇中就不再多使用进行过多说明了,只来大体看下该部分的原理和逻辑。

secretsprovidersshprovider 都是 buildkit 在提供的,利用这两种特性可以在 Docker 镜像进行构建时更加安全,且更加灵活。

funcparseSecretSpdocker和虚拟机的区别edocker是干什么的cs(sl[]string)(session.Attdockerhubachable,error){
fs:=make([]secdocker菜鸟教程retsprovider.Source,0,len(sl))
for_,v:=rangesl{
s,err:=parseSecret(v)
iferr!=nil{
returnnil,errdocker安装部署
}
fs=append(fs,*s)
}
store,err:=secretsprovider.NewStore(fs)
ifdocker和虚拟机的区别err!=nil{
returnnil,err
}
returnsecretsprovider.NdockerewSecretProvider(store),nil
}

关于 secrets 方面docker和虚拟机的区别,最终的 parseSecret 会完成格式相关的校验之类的;

funcparseSSHSpdocker安装部署ecs(sl[]string)(session.Attachable,error){
confidocker怎么读gs:=make([]sshprovider.AgentConfig,0,len(sl))
for_,v:=rangesl{
c:=parseSSH(v)
configs=appenddocker(configs,*c)
}
retudocker菜鸟教程rnsshprovider.NewSSHAgentPrdocker面试题及答案ovider(configsdocker是干什么的)
}

而关于 ssh 方面,则与上方的 secrets 基本一致,通过 sshprovider 允许进行 ssh 转发之类的,这里不再深入展开了。

调用 API 发送构建请求

这里主要有两种情况。


  • build context 是从
    stdin 读,并且是一个
    tardocker常用命令 文件时
buildID:=stringid.Genedocker常用命令rateRandomID()
ifbody!=nil{
eg.Go(func()error{
buildOptions:=types.ImageBuildOptions{
Version:types.Bdockerhubuildocker容器derBuildKit,
BuildID:uploadRequestRemote+":"+buildID,
}

response,err:=dockerCli.Client().ImageBuild(codocker是干什么的ntext.Background(),bdocker菜鸟教程ody,buildOptions)
iferr!=nil{
returnerr
}
deferresponse.Body.Close()
returnnil
})
}

它会执行上述这部docker容器分逻辑,但同时也要注意,这是使用的是 Golandocker怎么读g 的 goroutine,到这里也并不是结束,这部分代码之后的代码也同样会被执行。这就说到了另一种情况了(通常情况)。

  • 使用
    doBuild 完成逻辑
eg.Go(fdocker和虚拟机的区别unc()error{
defdocker面试题及答案erfundocker容器c(){
s.Close()
}()

buildOptions:=imageBuildOptions(dockerCli,options)
buildOpdocker安装部署tions.Version=types.BuilderBuildKit
buildOptidocker是干什么的ons.Dockerfile=ddocker和虚拟机的区别ockerfileName
bdocker容器uildOptions.RemoteContext=remote
buildOptions.docker是干什么的SessionID=s.ID()
buildOptions.BuildID=buildID
buildOptions.Outputs=outputs
returndoBuild(ctdocker常用命令x,eg,dockerCli,stdoutUsed,optiondocker常用命令s,buildOptions)
})

doBuild 会做些什么呢?它同样也调用了 API 向 dockerd 发起了构建请求。

funcdoBuild(ctxcondockerfiletext.Context,eg*errgroup.Group,dodockerfileckerdocker容器Clicommadocker怎么读nd.Cli,stdoudocker面试题及答案tUsedbdockerhubool,optionsbuildOptions,buildOptionstypes.ImageBuildOptions,atsession.Attachable)(finalErrerror){
response,err:=dockerCli.Client().ImadockerfilegeBuild(cdockerontext.Backgdocker常用命令round(),nil,buildOptions)
iferr!=nil{
returnerr
}
deferrdocker安装部署esponse.Body.Close()
//省略
}

从以上的介绍我们可以先做个小的总docker安装部署结。docker菜鸟教程build contextstdindocker常用命令 读,并且是个 tar 归档时,实际会向 dockerd 发起两次 /build 请求 而一般情况下只docker常用命令会发送一次请求。

那这里会有什么差别呢?此处先不展开,我们留到下面讲 dockerd 服务端的时候再docker和虚拟机的区别来解释。

小结

这里我们对开启了 buildkitdockerhub 支持的 CLI 构建镜像的过程进行了分析,大致过程如下:

从入docker怎么读口函数 rudocker容器nBuild 开始,判断是否支持 buildkit ,如果支持 buildkdocker容器it 则调用 runBuildBuildKit。与 v1 的 bdocker和虚拟机的区别uilder 不同的是,开启了 buildkit 后,会首先创建一个长连接的会话docker和虚拟机的区别,并一直docker菜鸟教程保持。其次,与 builder 相同,判断 build context 的来源,格式之类的,校验参数等。当然,buildkit 支持三种不同的输docker菜鸟教程出格式 tar, local 或正常的存储于 Dockdockerer 的目录中。另外是在 buildkit 中新增的高docker面试题及答案阶特性,可以配置 secretsssdocker怎么读h 密钥等功能。最后,再调用 API 与 dockerd 交互完成镜像的构建。

服务端:dockerd

上面分别介绍了 API, CLI 的 v1 builderbdocker安装部署uildkit ,接下来我们看看服务端的具体原理和逻辑。

Client 函数

还记得docker是干什么的上面部分中最后通过 API 与服务端交互的 ImageBuild 函数docker和虚拟机的区别吗?在开始 dockerd 的介绍前,我们来看下这个客户端接口的docker具体内容。

//github.com/docker/dockedocker菜鸟教程r/client/image_build.go#L20
func(cli*Client)ImageBuild(ctxcontext.Context,buildContextio.Reader,optionstypes.ImageBuildOptions)(types.ImageBuilddockerhubResponse,error){
query,err:=cli.imageBuildOptidocker是干什么的onsToQuery(options)
iferr!=nil{
returntypes.ImageBuildResponse{},err
}

headers:=http.Header(make(map[string][]string))
buf,err:=json.Marshal(options.AuthConfigs)
iferr!=nil{
returntypes.ImageBuildResponse{},err
}
headers.Add("X-Rdocker和虚拟机的区别egidockerstry-Configdocker和虚拟机的区别",base64.URLEncoding.EncodeToString(bdocker面试题及答案uf))

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

servedocker常用命令rResp,err:=cli.postRaw(ctx,"docker怎么读/build",query,buildContext,headers)
iferr!=nil{
returntypes.ImageBuildResponse{},err
}

osType:=gdocker是干什么的etDockerOS(serverResp.header.Getdocker是干什么的("Server"))

retudocker面试题及答案rntypes.ImageBuildResponse{
Body:servedocker安装部署rResp.body,
OSType:osType,
},nil
}

没有什么太特别的地方,行为与 API 一致。通过这里我们确认它确实访问docker菜鸟教程/build 接口,所以,我们来看看 dockerddockerfile /build 接口,看看它在构建镜像的时候做docker是干什么的了什么。

dockerd

由于本文集中讨论的是构建系统相关的部分docker面试题及答案,所以也就不再过多赘述与构建无关的内容了,我们直接来看,当 CLI 通过 /build 接口发送请求后,会发生什么。

docker常用命令来看该 API 的入口:

//api/server/router/build/build.go#L32
func(r*buildRouter)initRoutes(){
r.routes=[]router.Route{
router.NewPostRoute("/builddocker常用命令",r.postBuild),
router.NewPostRoute("/build/prune",r.postPrune),
router.NewPostRdocker菜鸟教程oute("/build/cancel",r.postCancel),
}
}

dockerd 提供了一套类 RESTful 的后端接口服务,处理逻辑的入口便是上面的 postBuilddocker面试题及答案 函数。

该函数的内容较多,我们来分解下它的主要步骤。

buildOptions,err:=newImageBuildockerfiledOptions(ctx,r)dockerhub
iferr!=nil{
returnerrf(docker是干什么的edocker容器rr)
}

newImageBuildOptions 函数就是构造构建参数的,将通docker容器过 API 提交过来的dockerfile参数转换为docker怎么读构建动作实际需要的参数形式。

buildOptions.AuthConfigs=getAuthConfigs(r.Header)

getAuthConfigs 函数用于从请求头拿到认证信docker是干什么的dockerfile

imgID,err:=br.backend.Build(ctx,backend.BuildCondocker是干什么的fig{
Source:body,
Options:buildOptions,
ProgressWriterdocker是干什么的:buildProgressWriter(out,wantAux,createProgressdocker菜鸟教程Reader)docker面试题及答案,
})
ifedockerfilerr!=nildocker常用命令{
returnerrf(err)
}

这里就需要注意了: 真正的构建过程要开始了。使用 backend 的 Build 函数来完成真正的构建过程

//apdocker常用命令i/servdocker容器er/bacdocker面试题及答案kend/build/backend.go#L53
func(b*Backend)Build(ctxcontext.Context,configbackend.BuildConfig)(string,error){
optiondocker是干什么的s:=config.Options
useBuildKit:=options.Version==types.BuilderBuidockerhubldKit

tagger,edocker容器rr:=NewTagger(b.imageComponent,config.ProgressWriter.StdoutFormatter,optdockerfileions.Tags)
ifedockerhubrr!=nil{
retdocker是干什么的urn"",err
}

varbuild*builder.Result
ifdockerhubuseBuildKit{
build,err=b.buidocker常用命令ldkit.Build(ctx,config)
iferr!=nil{
return"",err
}
}else{
build,err=b.builder.Build(ctx,config)
iferr!=nil{
return"docker",err
}
}

ifbuild==nil{
return"",nil
}

varimageID=build.ImageID
ifdocker和虚拟机的区别options.Squash{
ifimageIDdocker容器,err=squashBuild(build,b.imageComponent);err!=nil{
return"",err
}
ifconfig.ProgressWritdocker面试题及答案er.AuxFormatter!=ndocker怎么读il{
iferrdocker面试题及答案=config.ProgressWriter.Auxdocker容器Formatter.docker是干什么的Emit("moby.image.id",types.docker安装部署BuildResult{ID:imageID});err!=nil{
return"",err
}
}
}

if!useBuildKit{
stdout:=config.ProgressWriter.StdoutFormatter
fmt.Fprintf(stdout,"Successfullybudocker是干什么的ilt%s\n",stringid.Trdocker和虚拟机的区别uncateID(imageID))
}
ifimageID!=""{
err=tagdockerhubger.TagImages(image.ID(imageID))
}
returnimageID,err
}

这个函数看着比较长,但主要功能就以下三点:

  • NewTagger 是用于给镜像打标签,也就是我们的 -t 参数相关的内容,这里不做展开。

  • 通过判断dockerfile是否使用了 buildkidockerhubt 来调用不同的构建后端。

useBuildKit:=options.Version==types.BuilderBuildKit

varbuild*builder.Rdocker是干什么的esult
ifusdocker面试题及答案eBuildKit{
build,docker和虚拟机的区别err=b.bdockeruildkitdocker是干什么的.Build(ctx,condocker和虚拟机的区别fig)
iferr!=nidockerl{
return"",err
}
}else{docker安装部署
build,err=b.bdocker容器uilder.Build(ctx,config)
iferr!=nil{
return"",err
}
}
  • 处理构建完成后的动作。

到这个函数之后,就分别是 v1 builderbuildkitDockerfile 的解析,以及对 build context 的操作了。

这里涉及到的内容与我下一篇文章《高效构建 Docker 镜像的最佳实践》的内部关联比较大,此处就不再进行展开了。敬请期待下一篇文章。

总结

本文首先介绍了 Docker 的 C/S 架构,介绍了构建镜像所用的 API , API 文档可以在线查看或dockerhub者本地构建。之后深入到 Dockerdocker容器 CLdocker容器I 的源码中,逐步分解 v1 builderbuildkit 在构建镜像时执行的过程的差异。最后,我们深入到 dockerd 的源码中,了解到了对不同构建后端的docker怎么读调用。至此,Docker 构建镜像的原理及主体代码就介绍完毕。

但这还并不是结束,我会在后续文章中分享镜像构建的相关实践,敬请期待!


欢迎订阅我的文章公众号【MoeLove】

TheMoeLovedocker常用命令

docker菜鸟教程文分享自微信公众号 - MoeLdocker怎么读ove(TheMoeLovdocker安装部署e)。
如有侵docker和虚拟机的区别权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。