kubernetes入门到进阶(4)

创建容器镜像:如何编写正确高效的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

当然还有两个思考题:

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

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

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

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值