kubernetes入门到进阶(4)

发布时间:2023年12月26日

创建容器镜像:如何编写正确高效的Docker?file

早上好,趁现在手头工作比较轻松,就再来写一篇续集,估计都发出去就又要到下班的时间咯

在上一个章节中我们一起学习了容器化的应用,也就是被打包成镜像的应用程序,然后再用各种docker?命令来运行,管理它们

这里呢,我就又一个疑问了,这些镜像是怎么创建出来的?我们能不能自己制作属于自己的镜像呢?所以今天,我就来说一下镜像的内部机制,还有搞笑的编写dockerfile制作容器镜像的方法

镜像的内部机制是什么

现在你应该知道,镜像就是一个打包文件,里面包含了应用程序还有它运行所以来的环境,例如文件系统,环境变量,配置参数等等;

环境变量,配置参数这些东西还是比较简单的,随便用一个manifest清单就可以管理,真正麻烦的是文件系统,为了保证容器运行环境的一致性,镜像必须把应用程序所在的操作系统的根目录,也就是rootfs,都包含进来

虽然这些文件里不包含系统内核(因为容器共享了宿主机的内核),但如果每个镜像都重复做这样的打包操作,仍然会导致大量的冗余,可以想象,如果有一千个镜像,都基于centos系统打包,那么这些景象里就会重复一千次centos根目录,对磁盘存储,网络传输都是很大的浪费

很自然的,我们就会想到,应该把重复的部分抽取出来,只存放一份ubuntu根目录文件,然后让这一千个镜像以某种方式共享这部分数据

这个思路,也正是容器镜像的一个重大创新点:分层,术语叫layer

容器镜像内部并不是一个平坦的结构,而是由许多个镜像层组成的,每层都是只读不可修改的一组文件,相同的层可以在镜像之间共享,然后U盾讴歌层像搭积木一样堆叠起来,再使用一种叫Union?FS联合文件系统的技术把他们合并在一起,就形成了容器最终看到的文件系统

这里就来拿千层蛋糕来做个比喻:千层糕也是有很多层叠加在一起的,从最上面可以看懂每层里面镶嵌的葡萄干,核桃,行人,青丝等等,每一层糕就相当于一个layer,干果就好比是layer里的各个文件,如果两层的同一个文职都有干果,也就是有文件同名,那么我们就只能看懂上层的文件,而下层就被屏蔽了

你可以用命令docker?inspect?来查看镜像的分层信息,比如nginx:alpine镜像:

docker?inspect?nginx:alpine?

他的分层信息在RootFS部分:

通过这张截图可以看到,nginx:alpine镜像里一共有6个layer

相信你现在也就明白了,之前在使用docker?pull?、docker?rmi?等命令操作镜像的时候,那些奇怪的输出信息是什么了,其实就是镜像里的各个layer,docker会检查是否有重复的层,如果本地已经存在就不会重复下载,如果层被其他镜像共享就不会删除,这样就可以节约磁盘和网络成本

Dockerfile是什么

知道了容器镜像的内部结构和基本原理,我们就可以来学习如何自己动手制作容器镜像了,也就是自己打包应用

在之前的章节里我们说容器的时候曾经说过容器就是小板房,镜像就是样板间,那么,1要造出这个样板间,就必然要有一个施工图纸,由他来规定如何建造低级,铺设水电,开窗搭门等动作,这个施工图纸就是docker?file

比起容器,镜像来说,docker?file?非常普遍,他就是一个纯文本,里面记录了一系列的构建指令,比如选择基础镜像,可拷贝文件、运行脚本等等,每个指令都会生成一个layer,而docker顺序执行这个文件里的所有步骤,最后都会创建出一个新的镜像来

我们来看一个最简单的docker?file?实例

vim busbox.file?

#?Dockerfile.busybox

FROM??busybox??#选择基础镜像

CMD?echo?"hello?world"??#启动容器时默认运行的命令

这个文件里有两条指令

第一条指令是FROM?,所有的docker?file?都要从它开始,表示选择构建使用的基础镜像,相当于打地基,这我么你是用的是busybox?

第二条指令是CMD,他制定了docker?run?启动容器时默认运行的命令,这里我们使用了echo命令,输出“hello?world”字符串

现在有了docker?file?这张施工图纸,我们就可以请出施工队了,用docker?build?命令来创建出镜像:

docker?build?-t?busybox?-f?dockerfile.busybox?/opt/dockerfile/

docker?build:?这是构建?Docker?镜像的命令。

-t?busybox:?这个选项用于指定构建出的镜像的名称(tag)。在这个例子中,镜像的名称为?busybox。

-f?dockerfile.busybox:?这个选项用于指定?Dockerfile?的文件名,默认情况下?Docker?会在当前目录下查找名为?Dockerfile?的文件,但这里通过?-f?选项明确指定使用名为?dockerfile.busybox?的文件。

/opt/dockerfile/:?这是指定?Docker?上下文路径的位置,也就是构建时?Docker?所在的目录。Docker?会将这个目录及其子目录下的文件作为构建上下文,可以在?Dockerfile?中使用这些文件。在这个例子中,dockerfile.busybox?文件应该位于?/opt/dockerfile/?目录下。

最终,这个命令的作用是在?/opt/dockerfile/?目录下使用名为?dockerfile.busybox?的?Dockerfile?构建一个名为?busybox?的?Docker?镜像。?Dockerfile?是一个包含构建指令的文本文件,它定义了镜像的构建过程。在这个例子中,Dockerfile?中的指令告诉?Docker?使用?busybox?基础镜像,并在容器启动时运行?echo?"hello?world"?这个命令。

你需要特别注意命令的格式,用-f参数指定docker?file文件名,后面必须跟一个文件路径,叫做“构建上下文”(build?context),这里只是一个简单的点号,表示当前路径的意思

接下来,你就会看到docker?会逐行的读取并执行dockerfile里的指令,依次创建镜像层,在生成完整的镜像

新的精选好难过暂时还没有名字*(用docker?images会看到是《none》),但我们可以之久使用image?id来查看或者运行。

怎样编写正确,高效的dockerfile

大概了解了docker?file?之后,我们再来讲讲dockerfile的一些常用指令和最佳时间,帮助大家在今后的工作中把它写好?,用好

胡搜西安因为构建镜像的第一条必须是from,所以基础镜像的选择非常关键,如果关注的是镜像的安全和大小,那么一般水选择alpine,如果关注的是应用的运行稳定性,那么可能会选择ubuntu,Debian,centos。

FROM?alpine:3.15??????#选择alpine镜像

FROM?ubuntu:bionic?????#选择ubuntu镜像

我们在本机上开发测试时会产生一些源码,配置等文件,需要打包进镜像里,这是可以使用copy命令,他的用法和linux的cp差不多,不过拷贝源文件必须是构建上下文路径里的,不能随意指定文件,也就是说,如果要从本机向镜像拷贝文件,就必须把这些文件放到一个专门的目录,然后在docker?build里指定构建上下文到这个目录才行

这里有两个copy命令示例,大家可以看一下:

COPY?./a.txt?/tmp/a.txt??????#把构建上下文里的a.txt拷贝到镜像的/tmp目录

COPY?/etc/hosts??/tmp?????#错误!不能使用构建上下文之外的文件?

接下来要说的就是dockerfile里最重要的一个指令RUN,他可以执行任意的shell命令,比如更新系统,安装应用,下载文件,创建目录,编译程序等等,实现任意的镜像构建步骤,非常灵活。

RUN通常会是dockerfile里最复杂的指令,会包含很多的shell命令,但docke?file里只有一条指令只能是一行,所有的RUN指令会在每行的末尾使用续行符\,命令之间也会用&&来连接,这样保证在逻辑上是一行,就想下面这样

RUN?yum?update?\

&&?yum?-y?install?\

???????build-essential?\?

???????curl??\

???????make??\?

???????unzip???\

????&&?cd?/tmp?\

????&&?curl?-fSL?xxx.tar.gz?-o?xxx.tar.gz\

????&&?tar?xzf?xxx.tar.gz?\

????&&?cd?xxx?\

????&&?./config?\

????&&?make?\

????&&?make?clean

有的时候在dockerfile里写这种超长的RUN指令很不美观,而且一旦写错了,每次调试都要重新构建也很麻烦,所以你可以采用一种变通的技巧:把这些shell命令几种到一个脚本文件里,用COPY命令拷贝进去再用run来执行

COPY?setup.sh??/tmp/????????????????#?拷贝脚本到/tmp目录

RUN?cd?/tmp?&&?chmod?+x?setup.sh?\??#?添加执行权限

????&&?./setup.sh?&&?rm?setup.sh????#?运行脚本然后再删除

RUN指令世界上就是shell编程,如果你对它有所了解,就应该知道它有变量的概念,可以实现参数化运行,这在dockerfile里也可以做到,需要使用两个指令ARG和ENV。

他们区别在于ARG创建的变量只在镜像构建过程中课件,荣区运行时不可见,而ENV创建的变量不仅能够在构建镜像的过程中使用,在容器运行时也能够以环境变量的形式被应用程序使用

下面是一个简单的例子,使用ARG定义了基础镜像的名字(可以用在FROM指令里),使用ENV定义了两个环境变量:

ARG?IMAGE_BASE="node"

ARG?IMAGE_TAG="alpine"

ENV?PATH=$PATH:/tmp?

ENV?DEBUG=OFF

还有一个重要的指令是EXPOSE,他用来声明容器对外服务的端口号,对现在基于Node.js、Tomcat、Nginx、Go等开发的微服务系统来说非常有用:

EXPOSE?443???????#默认的是tcp协议

EXPOSE?53/udp???#可以指定udp协议

讲了这些dockerfile指令后,我还要特别强调一下,因为每个指令都会生成一个镜像层,所以dockerfile里最好不要滥用指令,尽量精简合并,否则太多的层会导致镜像臃肿不堪

docker?build是怎么工作的

dockerfile必须要经过docker?build才能生效,所以我们再看看看docker?build的详细用法,刚才在构建镜像的时候,你是否对构建上下文这个词干到有些困扰呢?他到底是什么含义呢?

我觉得用docker?的官方架构图来理解会比较清楚(注意图中与docker?build?关联的虚线)

以为命令行docker是一个简单的客户端,真正的镜像构建工作是由服务器端的docker?daemon来完成的,所以docker客户端就只能吧构建上下文目录打包上传(显示信息Sending?build?context?to?docker?daemon),这样服务器才能够获取本地的这些文件。

明白了这一点,你就会知道,构建上下文其实与dockerfile并没有直接的关系,他其实制定了要打包进镜像的一些依赖文件,而COPY命令也只能使用基于构建上下文的相对路径,因为docker?daemon看不到本地环境,只能看到打包上传的那些文件。

但这个机制也会导致一些麻烦,如果目录里有的文件(例如readme/.git/.svn等)不需要拷贝进镜像,docker也会一股脑的打包上传,效率极低

为了避免这种问题,你可以在构建上下文目录里再建立一个.dockerignore文件,语法与.gitignore类似,排除那些不需要的文件

下面是一个简单的示例,表示不打包上传后缀是swp???sh的文件

#docker?ignore?

*.swp

*.sh

另外关于dockerfile,一般应该在命令行使用-f来显示指定,但如果省略这个参数,docker?build就会在当前目录下找名字是docker?file的文件,所以,如果只有一个构件目标的话,文件直接叫dockerfile是最省事的

现在我们使用docker?build应该就没有什么难点了,不过构建出来的镜像只有IMAGE?ID没有名字,不是很方便

为此你可以加上一个-t参数,也就是指定镜像的标签(tag),这样docker就会在构建完成后自动给镜像添加名字,当然,名字必须要符合上一篇文章里的命名规范,用:分隔名字和标签,如果不提供标签默认就是latest

小结

好了,今天我们一起学了容器镜像的内部结构,重点理解容器镜像是由多个只读的layer构成的,同一个layer可以被不同的镜像共享,减少了存储和传输的成本。

如何编写dockerfile内容稍微多一点,我再简单做个小结:

1-创建镜像需要编写dockerfile,写清楚创建镜像的步骤,每个指令都会生成一个layer

2-dockerfile里,第一个指令必须是from,用来选择基础镜像,常用的有alpine,ubuntu等,其他常用的指令有:COPY、RUN、ECPOSE、分别是拷贝文件,运行shell命令,声明服务端口号

3-docker?build需要用-f来指定dockerfile,如果不指定就是用当前目录下名字是dockerfile的文件

4-docker?build需要指定构建上下文,其中的文件会打包上传到docker?daemon,所以尽量不要在构建上下文中存放多余的文件

5-创建镜像的时候应当尽量使用-t参数,为镜像起一个有意义的名字,方便管理

今天的章节说的不少,但关于创建镜像还有很多高级技巧等待你去探索,比如使用缓存,多阶段构建等等,你可以再参考docker官方文档(https://docs.docker.com/engine/reference/builder/),或者一些知名应用的镜像(如Nginx、Redis、Node.js等)进一步学习。

接下来还是继续问大家几个问题,去练习一下

这里有一个完整的Dockerfile示例,你可以尝试着去解释一下它的含义,然后再自己构建一下:

#?Dockerfile

#?docker?build?-t?ngx-app?.

#?docker?build?-t?ngx-app:1.0?.

ARG?IMAGE_BASE="nginx"

ARG?IMAGE_TAG="1.21-alpine"

FROM?${IMAGE_BASE}:${IMAGE_TAG}

COPY?./default.conf?/etc/nginx/conf.d/

RUN?cd?/usr/share/nginx/html?\

????&&?echo?"hello?nginx"?>?a.txt

EXPOSE?8081?8082?8083

当然还有两个思考题:

镜像里的层都是只读不可修改的,但容器运行的时候经常会写入数据,这个冲突应该怎么解决呢?(答案在本期找)

你能再列举一下镜像的分层结构带来了哪些好处吗?

本篇就到这里吧,马上要下班咯,继续期待下一篇!

文章来源:https://blog.csdn.net/pengpeng0121/article/details/135212764
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。