通过前边两篇关于容器文章的介绍,大家应该对容器的概念应该理解比较透彻了,今天我们来继续分析,如何将自己的Spring Cloud应用程序打包,并部署到Docker平台上。
请在阅读本篇文章之前,在自己的机器上下载Docker Desktop应用,并完成安装,因为这个过程并不是太困难,因此笔者不会介绍如何安装Docker到自己的机器上,如果实在不知道怎么安装,建议参考官方文档:http://docs.docker.com/install。
假设读者已经成功将Docker安装到了自己的机器上,那么我们就可以使用docker的CLI客户端命令行工具。首先我们可以从DockerHub上拉取一个镜像并在本地运行起来,Dockerhub是公共开发的镜像仓库,你可以访问到任何以public可见级别上传的镜像,我们今天要使用的是一个叫busybox的进项,我们可以用这个镜像来执行echo 命令。
注解:BusyBox是一个集成了三百多个最常用Linux命令和工具的进项,其中包含了一些简单的工具,如echo、ls、cat等,还包含了一些大的,更复杂的工具,例grep、find、mount以及telnet。很多人将BusyBox称为Linux工具里的瑞士军刀。
那么接下来我们就通过这个叫busybox的瑞士军刀镜像,在笔者的Mac上运行起来,并且借着这个简单的场景来分析一下当我们运行docker run这个命令后,具体发生了什么。
在Docker Desktop启动的前提下,我们可以在自己的机器上运行:docker run busybox echo “yunpan”, 结果如下图所示:
上图看起并不是让人印象深刻,但是你需要从这些日志输出中看到:第一,由于docker没有在本地找到busybox镜像,因此需要先下载这个进项,然后在本地将这个镜像运行起来,并接受输入的命令,最后的输出就是运行结果:yunpan,需要注意的是,如果你的机器上有这个叫busybox的镜像,那么就会跳过下载的这个步骤,因此为了确保你能看到相同的输出,请通过docker images先看看本地是否有busybox这个镜像,如果有的话,就通过docker rmi -f ‘镜像id’ 来从本地删除这个镜像。
大家要注意的另外一点是,这句命令并没有什么安装和部署的步骤,以及应用程序以来的组件部署的步骤,因此通过这个简单的例子,你能看到通过容器打包来在多个环境运行应用程序是如此的便利。
这里有另外一个细节需要大家注意,我们通过docker run运行的应用程序,其实本质上运行在一个特殊的进程中,这个进程和本地机器上的其他进程隔离。为了更直观的展示docker run执行后具体发生了什么,请看下图:
如上图所示,Docker的CLI工具将执行发给Docker daemon来运行busybox镜像,daemon首先检查本地的镜像缓存是否有busybox进项,如果没有,首先从Docker hub仓库下载这个镜像.
镜像下载到我的本地电脑后,Docker daemon基于刚刚下载的镜像启动一个容器实例,并且在启动完成后,在镜像中运行echo命令,这个命令只是简单的将输入字符串打印出来到控制台,执行完成后echo进程就会退出,而进程退出后容器的实例也会停止运行。
由于笔者是在自己的Mac电脑上运行,因此deamon和容器都运行在一个Linux 虚拟机中,而如果读者是在Linux机器上运行这个docker的命令,那么deamon和容器实例都是直接在这台Linux机器上创建对应的容器进程。
注意:我们除了从默认dockerhub上下载本地不存在镜像之外,我们也可以通过docker run abc.io/yunpan/images-sample来指定从这个叫abc.io的仓库地址下载image-sample来运行。
好了,到这里为止,关于Docker的理论性只是就这么多了,接下来我们来通过创建一个Node js的的应用程序,将它打包成一个容器镜像,并在本地运行起来的例子,来向大家详细介绍通过Docker如何将一个web应用运行起来,这个应用程序会返回所运行机器的hostname。
通过这种方式,我们逐步会揭开一些事实:容器运行的进程实例所看到的hostname和宿主机是不一样,即便是从宿主机上来看,容器进程和运行在宿主机上的其他进程没有什么区别。这个例子也会为我们后续在Kubernetes部署多个应用的实例打下基础,因为你可以在部署三个应用实例的情况下,每次访问都会命中不同的容器实例,因为返回的hostname是不同的。
我们的测试应用其实非常简单,由一个js文件组成:application.js包含了这个应用的所有代码,如下图所示:
上边的Node JS代码应该不难理解,我们在8080端口启动了一个HTTP服务器,当请求到达的时候,将请求的信息输出到日志,然后发送响应信息,格式是:’你好, 容器实例运行机器的hostname是:os.hostname()。你的访问IP地址是:clientIP 。‘,特别需要注意的是,请求响应中返回的是服务器的真实hostname,而不是请求中的hostname,大家一定要注意这个区别。
虽然说我们可以通过Node来直接将上边的JS代码运行起来,但是我们有更好的办法。接下来我们会把这个Node JS打成Docker镜像,然后我们就可以在任意安装了Docker的机器上运行这个web应用程序,而不需要提前安装Node js组件,这可真是方便啊。
【将Node JS应用打包成镜像】
为了将我们的Node JS应用打包成镜像, 我们首先要创建一个叫Dockerfile的文件,这个文件的目的就是告诉Docker,如何将我们的Node JS打包成镜像,我们可以将刚才创建的application.js文件保存在本地的目录:/Users/gaopanqi/work/kubernetes/samples/sample1(可以替换为自己本地的实际目录),并在这个目录下创建一个Dockerfile文件,如下图所示:
我们来简单介绍一下文件中的这三句命令。首先FROM指令告诉我们容器镜像会基于node:12这个基础镜像构建,大家应该还记得笔者在前边文章讲的镜像分层和共享的内容,这就是分层共享机制的体现,可以认为有人已经帮我们做好了node:12这个镜像,我们的应用会构建在这个镜像之上,很明显这个镜像中有我们的应用运行所需的Node JS环境,这就是为什么我们不需要在本地单独安装Node环境,才能运行容器实例的原因。
另外我需要强调的是,由于我们的影响是基于这个node:12镜像构建,那么我们打包的时候,如果本地有这个node:12镜像,那么都不需要重新去下载,整个打包过程会非常的迅速和高效,这就会分层和镜像共享给我带来的红利和便利。
注意:node:12代表的是node镜像中,标记有tag12的版本,关于镜像和版本其实有特定的规则,因为比较简单,如果不是很清楚,可以自行学习。
接着我们第二句命令ADD application.js /app.js的作用是将node js代码文件从本地拷贝到镜像的根目录下,并且进行重新命名(app.js),最后一句相比大家都很熟悉了,制定应用的入口函数,也就是镜像启动的时候,执行什么代码来启动应用,你可以看到在上边的例子中,我们通过node app.js来启动我们的应用程序。
注意:给自己的应用选在一个基础镜像远远要比随便在Dockerhub上找一个看起来可用的镜像要复杂的多,特别是对于企业级的用户来讲,考虑到安全性的问题,大家务必和运维团队的同学进行沟通,因为大部分企业都有自己的标准的镜像,以防止从外部下载的镜像中携带恶意的代码。对于上边的例子,因为我们就是单纯朴素的希望有一个安装了Node环境的基础镜像,因此选择了这个node的镜像,当然你也可以基于标准的linux镜像来编写Dockerfile,唯一不同的是要增加一句wget安装NodeJS的指令,为了简化我们的讨论,笔者就直接使用这个node js的镜像了,但是读者可以有自己的选择。
【构建容器镜像】
在准备好构建应用的代码和Dockerfile之后,我们就可以把代码打包成标准的Docker镜像了,在自己的机器上运行:docker build -t qigaopan/web:v1.0 ., Docker工具就开始帮助我们构建镜像了,如下图:
整个构建过程会持续一段时间,具体和你的网络情况有关,特别如果你本地有node:12这个镜像的话,整个过程会非常快,这个镜像在笔者的机器上有800M左右。构建命令中的-t参数指定了镜像的名称和标签,大家打包的时候一定要注意最后的“.",这是指定包含了docker build需要的Dockerfile的目录,很多初学者很容易这个点号,需要特别注意。
当整个打包过程完成后,在最后会输出打包好的镜像的名称,我们可以在自己的机器上执行docker images命令,列出所有本地的镜像,其中就包含了刚才打包好的镜像,由于我们通过-t参数指定了具体的名称,大家应该不难找到这个刚刚打包好的镜像,一般都在最上边,这个清单是按更新时间来排序的。
是不是感觉很神奇,那么这个过程具体是如何发生的呢?我们其实只是简单的告诉Docker工具:请帮基于提供的Dockerfile来构建一个叫web":v1.0的镜像,并提供了Dockerfile和应用程序源代码的路径,Docker就会读取Dockerfile中的内容,然后要做的就是基于这个文件中包含的指令,来帮我们把镜像构建出来,如下图所示:
如上图所示,真正执行build操作的并不是CLI工具,而是将包含Dockerfile和源代码的整个目录上传到Docker deamon,通过deamon来进行镜像的打包,因此你可以看到,Docker deamon和CLI客户端工具根本不需要必须在一台机器上,如果你是在mac上运行打包的命令,那么CLI工具就运行在这台Mac宿主机上,而Docker deamon运行在一台Linux虚拟机中,当然我们完全可以专门找一台机器,在上边运行Docker deamon,这样一台机器就可以当做专门的build机器来对外提供服务。这种配置方式在CI/CD流水线配置中是主流。
注意:我们在对应打包的时候,请不要将无关的辅助性的配置,说明和中间结果等文件打包到Docker镜像中,这不光会让整个镜像的尺寸变大,也会让整个构建的流程变慢,同时会消耗额外的网络流量。
在build镜像的时候,Docker deamon首先将基础镜像node:12从Dockerhub这个公共的镜像仓库下载到本地,然后会基于这个这基础镜像来创建我们的应用程序镜像web:v1.0,这个构建的过程由编写在Dockerfile的三行命令来驱动,每一行命令都会在基础镜像上增加一层,三个命令执行完后,最后的镜像会被命名为-t制定的名字和标签。说到这里,你可能会很好奇,打包过程中增减的每一层文件看起来长啥样啊?请继续阅读。
【组成镜像的层具体是啥样?】
笔者在前文中多次强调,Docker的镜像是由多个层组成,那么你可能会觉得我们刚才创建的node js的应用镜像应该由两层组成,其中最底层是基础镜像,而最上边就是Dockerfile中的三行命令。然而事实并非如此,当我们构建镜像的时候,Dockerfile中的”每一行命令“都会创建一个新的层,并且先后顺序叠加到基础镜像上。
具体来说,在构建web:1.0的过程中,Docker deamon首先将基础镜像层加载到内存,然后在这个基础镜像层之上创建新的一层来讲application.js代码文件保存进去,最后再新建的代码层之上创建另外一层来保存启动脚本,最后这一层会被打上web:v1.0的标记。口说无凭,我们来验证一下,在自己的机器可以通过命令:docker history web:v1.0来查看组成镜像的各个层,如下图所示:
如上图所示,我们可以看到web:v1.0这个镜像由16层组成(是不是远远大于你的预期?),大部分的层都是由基础镜像构成,其中最上边两层是我们的Dockerfile文件中的最后两条命令,而剩余部分就是基础镜像提供的层,笔者在图上进行了标记说明,可以参考上图对比Dockerfile文件和实际打包的镜像。
另外上图有个非常有意思的点,Create BY列向我展示了相关层是执行什么命令产生的,除了使用ADD命令之外,我们还可以使用RUN来构建镜像的时候执行某个命令,如果你仔细看上图,你会发现还有很多apt-get的命令,这些命令就是通过网络来给镜像安装必须的软件,也就是说通过apt-get,我们可以在打包的时候,给镜像安装某些应用需要在运行的时候使用的依赖。
好了,相信通过上边的内容学习,你已经对通过Docker工具打包这个事情理解的比较透彻了,你是不是已经按奈不住内心的激动,想赶紧把这个应用运行起来,俗话说,是骡子是马,拉出来溜溜。接下来,在把这个web:v1.0的镜像在本地运行起来吧。
【将镜像运行起来】
有了打包好的镜像,那么把他运行起来就非常的容易了,在自己的机器上运行:docker run --name yunpan-container -p 8080:8080 -d qigaopan/web:v1.0, 这句命令会输出一行UUID,这个就是本地运行的容器实例的唯一标识ID。
➜ sample1 docker run --name yunpan-container -p 8080:8080 -d qigaopan/web:v1.0
e48ca2f1460d130ce95a532c1dca7c86fdcbd25cb204a419192c86503c464b12
docker run命令后边的几个参数值得我们稍微说一下,我们可以通过--name来为启动的容器实例制定一个名字,方便查看;通过-d标记来让这个容器运行在后台,而通过-p来进行端口映射,在笔者的mac环境中,容器运行在创建的Linux虚拟机中,因此容器默认是无法从从外部访问,因此我们需要通过端口映射来从笔者的终端访问到这个容器提供的服务。
好了,了解了这些必要的细节之后,我们来通过自己的浏览器,或者命令行curl工具,curl命令行工具来访问我们刚刚在Docker上部署的应用服务,如下所示:
➜ sample1 curl http://localhost:8080
你好, 容器实例运行机器的hostname是: e48ca2f1460d。 你的访问IP地址是: ::ffff:172.17.0.1。
漂亮,如果你能看到预期的返回,那就说明我们的应用被成功打包,并部署到本地Docker环境中了,并且也成功运行起来了。接下来,我们来通过下图来聊聊端口映射,这是很多初学者不太容易理解的地方,笔者最近在现场给不同的同学讲过客户端,宿主机,容器,扩容器访问的网络机制,觉得这部分才是很多人学习Kubernetes的最大的拦路虎,因为你需要有操作系统,计算机网络的知识,才能真正理解。
上图的信息都已经很完整了,笔者就不做累述了。
如果你还记得我们的代码中其实有把请求端的IP地址写到日志中,那么你的可能会问,我去哪里看这个容器运行的日志呢?好问题,我们可以通过命令:docker logs yunpan-container(请替换成你自己的容器名称)来查看容器运行过程中输出的日志,如下图所示:
好了,这篇文章就写到这里了。接下来笔者会继续沿着容器的思路,来揭示组成容器的三大支柱,以及通过一个Spring Cloud的java程序的例子,来看看如何将主流的微服务打包成容器镜像,并启动起来,敬请期待。
如果你想直接体验这个例子,可以从本地直接拉笔者已经打包上传到Dockerhub的镜像:”docker pull qigaopan/web:1.0“。