2.1 创建、运行及共享容器镜像
![](https://book.img.zhangyue01.com/group61/M00/77/93/CmQUOF8KrwOEGX6fAAAAAB76RfE908976629.png?v=ywDc_OF-&t=CmQUOF8KrwM.)
正如在之前章节所介绍的,在Kubernetes中运行应用需要打包好的容器镜像。本节将会对Docker的使用做简单的介绍。接下来的几节中将会介绍:
1.安装Docker并运行第一个 “Hello world” 容器
2.创建一个简单的Node.js应用并部署在Kubernetes中
3.把应用打包成可以独立运行的容器镜像
4.基于镜像运行容器
5.把镜像推送到Docker Hub,这样任何人在任何地方都可以使用
2.1.1 安装Docker并运行Hello World容器
首先,需要在Linux主机上安装Docker。如果使用的不是Linux操作系统,就需要启动Linux虚拟机(VM)并在虚拟机中运行Docker。如果使用的是Mac或Windows系统,Docker将会自己启动一个虚拟机并在虚拟机中运行Docker守护进程。Docker客户端可执行文件可以在宿主操作系统中使用,并可以与虚拟机中的守护进程通信。
根据操作系统的不同,按照http://docs.docker.com/engine/installation/上的指南安装Docker。安装完成后,可以通过运行Docker客户端可执行文件来执行各种Docker命令。例如,可以试着从Docker Hub的公共镜像仓库拉取、运行镜像,Docker hub中有许多随时可用的常见镜像,其中就包括 busybox,可以用来运行简单的 echo"Hello world" 命令。
运行Hello World容器
busybox是一个单一可执行文件,包含多种标准UNIX命令行工具,如:echo、ls、gzip 等。除了包含 echo 命令的 busybox 命令,也可以使用如Fedora、Ubuntu等功能完备的镜像。
如何才能运行 busybox 镜像呢?无须下载或者安装任何东西。使用 docker run 命令然后指定需要运行的镜像的名字,以及需要执行的命令(可选),如下面这段代码。
代码清单2.1 使用Docker运行一个Hello world容器
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru6EFhrnAAAAALAp7s4880680406.jpg?v=IYh9lmIh&t=CmQUOF8Kru4.)
这或许看起来并不那么令人印象深刻,但非常棒的是仅仅使用一个简单的命令就下载、运行一个完整的“应用”,而不用安装应用或是做其他的事情。目前的应用是单一可执行文件(busybox),但也可以是一个有许多依赖的复杂应用。整个配置运行应用的过程是完全一致的。同样重要的是应用是在容器内部被执行的,完全独立于其他所有主机上运行的进程。
背后的原理
图 2.1 展示了执行 docker run 命令之后发生的事情。首先,Docker会检查busybox:latest 镜像是否已经存在于本机。如果没有,Docker会从http://docker.io的Docker镜像中心拉取镜像。镜像下载到本机之后,Docker基于这个镜像创建一个容器并在容器中运行命令。echo 命令打印文字到标准输出流,然后进程终止,容器停止运行。
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru6EXWiEAAAAAE3po2A652809197.jpg?v=iEzgKd3o&t=CmQUOV8Kru4.)
图2.1 在一个基于busybox镜像的容器中运行echo “Hello world”
运行其他镜像
运行其他的容器镜像和运行busybox镜像是一样的,甚至可能更简单,因为你可以不需要指定执行命令。就像例子中的 echo "Hello world",被执行的命令通常都会被包含在镜像中,但也可以根据需要进行覆盖。在浏览器中搜索http://hub.docker.com或其他公开的镜像中心的可用镜像之后,可以像这样在Docker中运行镜像:
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru6ET-tlAAAAAJB38P0650834642.jpg?v=aPDX7rlM&t=CmQUOF8Kru4.)
容器镜像的版本管理
当然,所有的软件包都会更新,所以通常每个包都不止一个版本。Docker支持同一镜像的多个版本。每一个版本必须有唯一的tag名。当引用镜像没有显式地指定tag时,Docker会默认指定tag为latest。如果想要运行别的版本的镜像,需要像这样指定镜像的版本:
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru6EP9ErAAAAAPaGSuc068530318.jpg?v=hTVyrXHp&t=CmQUOF8Kru4.)
2.1.2 创建一个简单的 Node.js 应用
现在有了一个可以工作的Docker环境来创建应用。接下来会构建一个简单的Node.js Web应用,并把它打包到容器镜像中。这个应用会接收HTTP请求并响应应用运行的主机名。这样,应用运行在容器中,看到的是自己的主机名而不是宿主机名,即使它也像其他进程一样运行在宿主机上。这在后面会非常有用,当应用部署在Kubernetes上并进行伸缩时(水平伸缩,复制应用到多个节点),你会发现HTTP请求切换到了应用的不同实例上。
应用包含一个名为app.js的文件,详见下面的代码清单。
代码清单2.2 一个简单的Node.js应用:app.js
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru6EZHpwAAAAAMk9Kbw427141906.jpg?v=IZ9_41NU&t=CmQUOF8Kru4.)
代码清晰地说明了实现的功能。这里在8080端口启动了一个HTTP服务器。服务器会以状态码 200 OK 和文字 "You've hit <hostname>"来响应每个请求。请求handler会把客户端的IP打印到标准输出,以便日后查看。
注意 返回的主机名是服务器真实的主机名,不是客户端发出的HTTP请求中头的 Host 字段。
现在可以直接下载安装Node.js来测试代码了,但是这不是必需的,因为可以直接用Docker把应用打包成镜像,这样在需要运行的主机上就无须下载和安装其他的东西(当然不包括安装Docker来运行镜像)。
2.1.3 为镜像创建Dockerfile
为了把应用打包成镜像,首先需要创建一个叫Dockerfile的文件,它包含了一系列构建镜像时会执行的指令。Dockerfile文件需要和app.js文件在同一目录,并包含下面代码清单中的命令。
代码清单2.3 构建应用容器镜像的Dockerfile
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru6EehG9AAAAAJWIo1w764144383.jpg?v=wvHVSNPf&t=CmQUOV8Kru4.)
From 行定义了镜像的起始内容(构建所基于的基础镜像)。这个例子中使用的是 node 镜像的tag 7 版本。第二行中把app.js文件从本地文件夹添加到镜像的根目录,保持app.js这个文件名。最后一行定义了当镜像被运行时需要被执行的命令,这个例子中,命令是 node app.js。
选择基础镜像
你或许在想,为什么要选择这个镜像作为基础镜像。因为这个应用是Node.js应用,镜像需要包含可执行的 node 二进制文件来运行应用。你也可以使用任何包含这个二进制文件的镜像,或者甚至可以使用Linux发行版的基础镜像,如 fedora或ubuntu,然后在镜像构建的时候安装Node.js。但是由于 node镜像是专门用来运行Node.js应用的,并且包含了运行应用所需的一切,所以把它当作基础镜像。
2.1.4 构建容器镜像
现在有了Dockerfile和app.js文件,这是用来构建镜像的所有文件。运行下面的Docker命令来构建镜像:
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru6EUk_3AAAAAOwwlZQ595130096.jpg?v=gWsx3-U1&t=CmQUOV8Kru4.)
图2.2展示了镜像构建的过程。用户告诉Docker需要基于当前目录(注意命令结尾的点)构建一个叫kubia的镜像,Docker会在目录中寻找Dockerfile,然后基于其中的指令构建镜像。
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru6EVyagAAAAAA6CwQU296953430.jpg?v=C-HpydAC&t=CmQUOV8Kru8.)
图2.2 基于Dockerfile构建一个新的容器镜像
镜像是如何构建的
构建过程不是由Docker客户端进行的,而是将整个目录的文件上传到Docker守护进程并在那里进行的。Docker客户端和守护进程不要求在同一台机器上。如果你在一台非Linux操作系统中使用Docker,客户端就运行在你的宿主操作系统上,但是守护进程运行在一个虚拟机内。由于构建目录中的文件都被上传到了守护进程中,如果包含了大量的大文件而且守护进程不在本地运行,上传过程会花费更多的时间。
提示 不要在构建目录中包含任何不需要的文件,这样会减慢构建的速度——尤其当Docker守护进程运行在一个远端机器的时候。
在构建过程中,Docker首次会从公开的镜像仓库(Docker Hub)拉取基础镜像(node:7),除非已经拉取过镜像并存储在本机上了。
镜像分层
镜像不是一个大的二进制块,而是由多层组成的,在运行busybox例子时你可能已经注意到(每一层有一行Pull complete),不同镜像可能会共享分层,这会让存储和传输变得更加高效。比如,如果创建了多个基于相同基础镜像(比如例子中的 node:7)的镜像,所有组成基础镜像的分层只会被存储一次。拉取镜像的时候,Docker会独立下载每一层。一些分层可能已经存储在机器上了,所以Docker只会下载未被存储的分层。
你或许会认为每个Dockerfile只创建一个新层,但是并不是这样的。构建镜像时,Dockerfile中每一条单独的指令都会创建一个新层。镜像构建的过程中,拉取基础镜像所有分层之后,Docker在它们上面创建一个新层并且添加app.js。然后会创建另一层来指定镜像被运行时所执行的命令。最后一层会被标记为kubia:latest。图2.3 展示了这个过程,同时也展示另外一个叫 other:latest 的镜像如何与我们构建的镜像共享同一层Node.js镜像。
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru6ERBuQAAAAANPxcKY454634030.jpg?v=e3mmSGTP&t=CmQUOF8Kru8.)
图2.3 容器镜像是由多层组成的,每一层可以被不同镜像复用
构建完成时,新的镜像会存储在本地。下面的代码展示了如何通过Docker列出本地存储的镜像:
代码清单2.4 列出本地存储的镜像
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru-EXZG4AAAAAPGvMwE935863516.jpg?v=CLK75Mv4&t=CmQUOF8Kru8.)
比较使用Dockerfile和手动构建镜像
Dockerfile是使用Docker构建容器镜像的常用方式,但也可以通过运行已有镜像容器来手动构建镜像,在容器中运行命令,退出容器,然后把最终状态作为新镜像。用Dockerfile构建镜像是与此相同的,但是是自动化且可重复的,随时可以通过修改Dockerfile重新构建镜像而无须手动重新输入命令。
2.1.5 运行容器镜像
以下的命令可以用来运行镜像:
![](https://book.img.zhangyue01.com/group61/M00/77/8E/CmQUOF8Kru-EWSiaAAAAAEwj3JE421010166.jpg?v=qaxNl5tL&t=CmQUOF8Kru8.)
这条命令告知Docker基于 kubia 镜像创建一个叫 kubia-container 的新容器。这个容器与命令行分离(-d 标志),这意味着在后台运行。本机上的8080端口会被映射到容器内的8080端口(-p 8080:8080 选项),所以可以通过http://localhost:8080 访问这个应用。
如果没有在本机上运行Docker守护进程(比如使用的是Mac或Windows系统,守护进程会运行在VM中),需要使用VM的主机名或IP来代替localhost运行守护进程。可以通过 DOCKER_HOST 这个环境变量查看主机名。
访问应用
现在试着通过http://localhost:8080 访问你的应用(确保使用Docker主机名或IP替换localhost):
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru-ENNgEAAAAAKC_gOw373672132.jpg?v=_84EvOBA&t=CmQUOV8Kru8.)
这是应用的响应。现在应用运行在容器中,与其他东西隔离。可以看到,应用把 44d76963e8e1 作为主机名返回,这并不是宿主机的主机名。这个十六进制数是Docker容器的ID。
列出所有运行中的容器
下面的代码清单列出了所有的运行中的容器,可以查看列表(为了更好的可读性,列表被分成了两行显示)。
代码清单2.5 列出运行中的容器
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru-ELzW5AAAAACnU1i8745473192.jpg?v=aXxxPTSd&t=CmQUOV8Kru8.)
有一个容器在运行。Docker会打印出每一个容器的ID和名称、容器运行所使用的镜像,以及容器中执行的命令。
获取更多的容器信息
docker ps 只会展示容器的大部分基础信息。可以使用 docker inspect查看更多的信息:
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EbdSvAAAAAFD3-NA033122525.jpg?v=2ZnnxbIi&t=CmQUOF8Kru8.)
Docker会打印出包含容器底层信息的长JSON。
2.1.6 探索运行容器的内部
我们来看看容器内部的环境。由于一个容器里可以运行多个进程,所以总是可以运行新的进程去看看里面发生了什么。如果镜像里有可用的shell二进制可执行文件,也可以运行一个shell。
在已有的容器内部运行shell
镜像基于的Node.js镜像包含了bash shell,所以可以像这样在容器内运行shell:
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru-EAXZjAAAAADqLbro131125653.jpg?v=grNT5soH&t=CmQUOV8Kru8.)
这会在已有的kubia-container容器内部运行bash。bash 进程会和主容器进程拥有相同的命名空间。这样可以从内部探索容器,查看Node.js和应用是如何在容器里运行的。-it 选项是下面两个选项的简写:
-i,确保标准输入流保持开放。需要在shell中输入命令。
-t,分配一个伪终端(TTY)。
如果希望像平常一样使用shell,需要同时使用这两个选项(如果缺少第一个选项就无法输入任何命令。如果缺少第二个选项,那么命令提示符不会显示,并且一些命令会提示 TERM 变量没有设置)。
从内部探索容器
下面的代码展示了如何使用shell查看容器内运行的进程。
代码清单2.6 从容器内列出进程
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EUMuwAAAAANCQQ7E564196139.jpg?v=-4g9dzLW&t=CmQUOF8Kru8.)
只看到了三个进程,宿主机上没有看到其他进程。
容器内的进程运行在主机操作系统上
如果现在打开另一个终端,然后列出主机操作系统上的进程,连同其他的主机进程依然会发现容器内的进程,如代码清单2.7所示。
注意 如果使用的是Mac或者Windows系统,需要登录到Docker守护进程运行的VM查看这些进程。
代码清单2.7 运行在主机操作系统上的容器进程
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EPjzkAAAAAGU48ng357910913.jpg?v=7CC13-fr&t=CmQUOF8Kru8.)
这证明了运行在容器中的进程是运行在主机操作系统上的。如果你足够敏锐,会发现进程的ID在容器中与主机上不同。容器使用独立的PID Linux命名空间并且有着独立的系列号,完全独立于进程树。
容器的文件系统也是独立的
正如拥有独立的进程树一样,每个容器也拥有独立的文件系统。在容器内列出根目录的内容,只会展示容器内的文件,包括镜像内的所有文件,再加上容器运行时创建的任何文件(类似日志文件),如下面的代码清单所示。
代码清单2.8 容器拥有完整的文件系统
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru-EcZS_AAAAAJ0MWNs324509173.jpg?v=JemPRyRH&t=CmQUOV8Kru8.)
其中包含app.js文件和其他系统目录,这些目录是正在使用的 node:7 基础镜像的一部分。可以使用 exit 命令来退出容器返回宿主机(类似于登出ssh session)。
提示 进入容器对于调试容器内运行的应用来说是非常有用的。出错时,需要做的第一件事是查看应用运行的系统的真实状态。需要记住的是,应用不仅拥有独立的文件系统,还有进程、用户、主机名和网络接口。
2.1.7 停止和删除容器
可以通过告知Docker停止 kubia-container 容器来停止应用:
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EVmrtAAAAAEo6jcU489000256.jpg?v=ig5UAkim&t=CmQUOF8Kru8.)
因为没有其他的进程在容器内运行,这会停止容器内运行的主进程。容器本身仍然存在并且可以通过 docker ps-a 来查看。-a 选项打印出所有的容器,包括运行中的和已经停止的。想要真正地删除一个容器,需要运行 docker rm :
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru-EMtU4AAAAANuWR5E031310134.jpg?v=GrVRaWn3&t=CmQUOV8Kru8.)
这会删除容器,所有的内容会被删除并且无法再次启动。
2.1.8 向镜像仓库推送镜像
现在构建的镜像只可以在本机使用。为了在任何机器上都可以使用,可以把镜像推送到一个外部的镜像仓库。为了简单起见,不需要搭建一个私有的镜像仓库,而是可以推送镜像到公开可用的Docker Hub(http://hub.docker.com)镜像中心。另外还有其他广泛使用的镜像中心,如Quay.io和Google Container Registry。
在推送之前,需要重新根据Docker Hub的规则标注镜像。Docker Hub允许向以你的Docker Hub ID开头的镜像仓库推送镜像。可以在http://hub.docker.com上注册Docker Hub ID。下面的例子中会使用笔者自己的ID(luksa),请在每次出现时替换自己的ID。
使用附加标签标注镜像
一旦知道了自己的ID,就可以重命名镜像,现在镜像由 kubia 改为luksa/kubia(用自己的Docker Hub ID代替 luksa):
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EcPmGAAAAALHCDGY388203338.jpg?v=NlxrBeX1&t=CmQUOF8Kru8.)
这不会重命名标签,而是给同一个镜像创建一个额外的标签。可以通过docker images 命令列出本机存储的镜像来加以确认,如下面的代码清单所示。
代码清单2.9 一个容器镜像可以有多个标签
![](https://book.img.zhangyue01.com/group61/M00/77/AE/CmQUOV8Kru-EaRjOAAAAAPz3EMs811223356.jpg?v=zceGoVSA&t=CmQUOV8Kru8.)
正如所看到的,kubia 和 luksa/kubia 指向同一个镜像ID,所以实际上是同一个镜像的两个标签。
向Docker Hub推送镜像
在向Docker Hub推送镜像之前,先需要使用 docker login 命令和自己的用户ID登录,然后就可以像这样向Docker Hub推送 yourid/kubia 镜像:
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EOnjNAAAAAERhnMM020443192.jpg?v=0yEiNyMs&t=CmQUOF8Kru8.)
在不同机器上运行镜像
在推送完成之后,镜像便可以给任何人使用。可以在任何机器上运行下面的命令来运行镜像:
![](https://book.img.zhangyue01.com/group61/M00/77/8F/CmQUOF8Kru-EIVTqAAAAAIyIZtE027784222.jpg?v=hl0LIfxe&t=CmQUOF8Kru8.)
这非常简单。最棒的是应用每次都运行在完全一致的环境中。如果在你的机器上正常运行,也会在所有的Linux机器上正常运行。无须担心主机是否安装了Node.js。事实上,就算安装了,应用也并不会使用,因为它使用的是镜像内部安装的。