Skip to content

Latest commit

 

History

History
1328 lines (949 loc) · 65.8 KB

File metadata and controls

1328 lines (949 loc) · 65.8 KB

六、在容器中部署应用

在过去的几章中,我们重点介绍了 Go 应用的实际开发。然而,软件工程不仅仅是编写代码。通常,您还需要考虑如何将应用部署到其运行时环境中。特别是在微服务架构中,每个服务可能构建在完全不同的技术堆栈上,部署可能很快成为一个挑战。

当您部署使用不同技术的服务时(例如,当您使用 Go、Node.js 和 Java 编写服务时),您将需要提供一个环境,所有这些服务都可以在其中实际运行。使用传统的虚拟机或裸机服务器,这可能会变得相当麻烦。尽管现代云提供商可以轻松快速地生成和处理虚拟机,但为各种可能的服务维护基础设施仍然是一项运营挑战。

这就是现代集装箱技术的发展方向,如 60;码头工人RKT闪亮。使用容器,您可以将应用及其所有依赖项打包到容器映像中,然后使用该映像在可以运行这些容器的任何服务器上快速生成运行您的应用的容器。唯一需要在服务器上运行的软件(无论是虚拟化的还是裸机的)是容器运行时环境(通常是 Docker 或 RKT)。

在本章中,我们将向您展示如何在容器映像中打包 MyEvents 应用(我们在过去几章中构建了该应用),以及如何部署这些映像。由于我们考虑的是大问题,我们还将研究集群管理器,例如Kubernetes,它允许您在多个服务器上同时部署容器,从而使您的应用部署更具弹性和可扩展性。

在本章中,我们将介绍以下主题:

  • 使用 Docker 构建和运行容器映像
  • 使用 Docker Compose 设置复杂的多容器应用
  • 使用 Kubernetes 的容器云基础设施

什么是集装箱?

Docker 等容器技术使用现代操作系统提供的隔离功能,如 Linux 中的名称空间控制组cgroups)。使用这些功能允许操作系统在很大程度上将多个正在运行的进程彼此隔离。例如,容器运行时可能提供两个进程,它们具有两个完全独立的 filmount 名称空间,或者使用网络名称空间提供两个独立的网络堆栈。除了名称空间之外,还可以使用 cgroup 来确保每个进程使用的资源不会超过先前分配的资源量(例如 CPU 时间、内存或 I/O 以及网络带宽)。

与传统虚拟机不同,容器完全在主机环境的操作系统中运行;没有虚拟化的硬件和操作系统在上面运行。此外,在许多容器运行时中,您甚至没有在常规操作系统中找到的所有典型进程。例如,Docker 容器通常不像常规 Linux 系统那样具有 init 进程;相反,容器中的根进程(PID 1)将是您的应用(同样,由于容器仅在其PID 1进程存在时才存在,因此一旦您的应用存在,它将停止存在)。

当然,这并不适用于所有容器运行时。例如,LXC 将在您的容器中为您提供一个完整的 Linux 系统(至少是其中的用户空间部分),包括一个作为 PID 1 的初始化进程。

大多数容器运行时还附带了容器映像的概念。这些包含预打包的文件系统,您可以从中生成新的容器。许多基于容器的部署实际上使用容器映像作为部署构件,其中实际构建构件(例如,编译的 Go binary、Java 应用或 Node.js 应用)与其运行时依赖项打包在一起(对于已编译的 Go 二进制文件来说,这并不多;但是,对于其他应用,容器映像可能包含 Java 运行时、Node.js 安装或应用工作所需的任何其他内容)。为应用创建容器映像也有助于使应用具有可伸缩性和弹性,因为从应用映像生成新容器很容易。

Docker 等容器运行时也倾向于将容器视为不可变(这意味着容器在启动后通常不会以任何方式发生更改)。在容器中部署应用时,部署应用新版本的典型方法是构建一个新的容器映像(包含应用的更新版本),然后从该新映像创建一个新容器,并删除运行应用旧版本的容器。

Docker 简介

目前,应用容器运行时的事实标准是Docker,尽管还有其他运行时,例如 RKT(发音为 rocket)。在本章中,我们将重点介绍 Docker。然而,许多容器运行时是可互操作的,并且是基于通用标准构建的。例如,可以很容易地从 Docker 图像生成 RKT 容器。这意味着,即使您决定使用 Docker 映像部署应用,也不会遇到供应商锁定。

运行简单容器

我们曾在第 4 章中与 Docker 合作,使用消息队列的异步微服务架构,快速建立 RabbitMQ 和 Kafka 消息代理;然而,我们并没有详细介绍 Docker 实际上是如何工作的。我们将假定您已经在本地计算机上安装了一个可正常工作的 Docker。如果没有,请查看官方安装说明,了解如何在操作系统上安装 Docker:https://docs.docker.com/engine/installation/

要测试 Docker 安装是否正常工作,请在命令行上尝试以下命令:

$ docker container run --rm hello-world 

前面的命令使用 Docker 1.13 中引入的新 Docker 命令结构。如果您运行的是旧版本的 Docker,请使用docker run而不是docker container run。您可以使用docker version命令测试当前 Docker 版本。另外,请注意 Docker 在 1.13 版之后更改了其版本控制方案,因此 1.13 版之后的下一个版本将是 17.03。

Docker run 命令遵循docker container run [flags...] [image name] [arguments...]模式。在这种情况下,hello-world是要运行的映像的名称,--rm标志表示容器在完成运行后应立即移除。运行上述命令时,您应该会收到与以下屏幕截图中类似的输出:

docker 容器运行输出

实际上,docker run命令在这里做了很多事情。首先,它检测到hello-world映像在本地计算机上不存在,并从 Docker 官方映像注册表下载了它(如果再次运行相同的命令,您将注意到该映像将不会被下载,因为它已经存在于本地计算机上)。

然后,它从刚刚下载的hello-world图像创建了一个新容器,并启动了该容器。容器映像仅由一个小程序组成,该程序将一些文本打印到命令行,然后立即存在。

请记住,Docker 容器没有 init 系统,通常只有一个进程在其中运行。一旦该进程终止,容器将停止运行。由于我们创建了带有--rm标志的容器,Docker 引擎也会在容器停止运行后自动删除容器。

接下来,让我们做一些更复杂的事情。执行以下命令:

$ docker container run -d --name webserver -p 80:80 nginx 

此命令将下载nginx映像并从中生成一个新容器。与hello-world映像不同,此映像将运行一个无限期运行的 web 服务器。为了不无限期地阻塞 shell,-d标志(简称--detach)用于在后台启动新容器。--name标志负责为新容器提供实际名称(如果省略,将为容器生成随机名称)。

默认情况下,容器内运行的NGINXweb 服务器侦听 TCP 端口 80。但是,每个 Docker 容器都有自己独立的网络堆栈,因此您不能仅通过导航到http://localhost来访问此端口。-p 80:80标志告诉 Docker 引擎将容器的 TCP 端口 80 转发到本地主机的端口 80。要检查容器现在是否正在实际运行,请运行以下命令:

$ docker container ls 

前面的命令列出了当前运行的所有容器、从中创建它们的映像以及它们的端口映射。您应该会收到与以下屏幕截图中类似的输出:

docker 容器 ls 输出

当容器运行时,您现在可以通过http://localhost访问刚刚启动的 web 服务器。

建立自己的形象

到目前为止,您已经使用了 Docker Hub 中公开的预制图像,例如nginx图像(或者第 4 章中的 RabbitMQ 和 Spotify/Kafka 图像,使用消息队列的异步微服务架构。然而,有了 Docker,也很容易构建自己的图像。通常,Docker 映像是从Dockerfile构建的。Dockerfile 是一种新 Docker 映像的构造手册,它描述了如何从给定的基本映像开始构建 Docker 映像。由于从一个完全空的文件系统开始(甚至在 shell 或标准库中都没有这样的空文件系统)是没有意义的,所以映像通常构建在包含流行 Linux 发行版的用户空间工具的发行版映像上。流行的基础图像包括 Ubuntu*、Debian或 CentOS。*

让我们构建一个简短的示例Dockerfile。出于演示目的,我们将构建自己的hello-world映像版本。为此,创建一个新的空目录,并创建一个名为Dockerfile的新文件,其内容如下:

FROM debian:jessie 
MAINTAINER You <you@example.com> 

RUN echo 'Hello World' > /hello.txt 
CMD cat /hello.txt 

FROM开头的行表示您正在构建自定义图像的基础图像。它总是需要是Dockerfile的第一行。MAINTAINER语句只包含元数据。

RUN语句是在构建容器映像时执行的(这意味着最终的容器映像将在其文件系统中有一个包含内容Hello World/hello.txt文件)。Dockerfile可能包含许多此类RUN语句。

与此相反,CMD语句是在运行从映像创建的容器时执行的。此处指定的命令将是从映像创建的容器的第一个主进程(PID 1)。

您可以使用docker image build命令(docker build在 1.13 之前的版本中)构建实际的 Docker 映像,如下所示:

$ docker image build -t test-image .

docker 映像生成输出

-t test-image标志包含新图像应获得的名称。创建图像后,您可以使用docker image ls命令找到它:

docker 图像 ls 输出

使用-t指定的名称,您可以使用已知的docker container run命令从前面的图像创建和运行新容器:

$ docker container run --rm test-image

与前面一样,这个命令将创建一个新的容器(这次是从我们新创建的图像),启动它(实际上,启动由Dockerfile中的CMD语句指定的命令),然后在命令完成后移除容器(感谢--rm标志)。

网络容器

通常,您的应用由多个相互通信的进程组成(从相对简单的情况开始,例如应用服务器与数据库通信,到复杂的微服务架构)。使用容器管理所有这些流程时,通常每个流程都有一个容器。在本节中,我们将了解如何让多个 Docker 容器通过其网络接口相互通信。

为了实现容器到容器的通信,Docker 提供了一个网络管理功能。命令行允许您创建新的虚拟网络,然后向这些虚拟网络添加容器。一个网络中的容器可以相互通信,并通过 Docker 的内置 DNS 服务器解析其内部 IP 地址。

让我们通过使用docker network create命令与 Docker 创建一个新网络来测试这一点:

$ docker network create test

之后,您将能够看到新网络正在运行docker network ls

docker 网络 ls 输出

创建新网络后,可以将容器附加到此网络。首先,从nginx图像创建一个新容器开始,并使用--network标志将其连接到测试网络:

$ docker container run -d --network=test --name=web nginx 

接下来,在同一网络中创建一个新容器。由于我们已经启动了一个 web 服务器,我们的新容器将包含一个 HTTP 客户端,我们将使用该客户端连接到新的 web 服务器(注意,我们没有像以前那样使用-p标志将容器的 HTTP 端口绑定到本地主机)。为此,我们将使用适当的/curl 图像。这是一个基本上包含 cURL 命令行实用程序的容器化版本的图像。由于我们的 web 服务器容器的名称为 web,我们现在可以简单地使用该名称来建立网络连接:

$ docker container run --rm --network=test appropriate/curl http://web/

此命令只需将 web 服务器的索引页打印到命令行:

docker 容器运行输出

这表明从适当的/cURL 映像创建的 cURL 容器能够通过 HTTP 访问 web 容器。建立连接时,您只需使用容器的名称(在本例中为web。Docker 将自动将此名称解析为容器的 IP 地址。

有了 Docker 映像和网络知识,您现在可以将 MyEvents 应用打包到容器映像中,并在 Docker 上运行它们。

使用卷

单个 Docker 容器通常寿命很短。部署应用的新版本可能会导致大量容器被删除并生成新容器。如果您的应用在云环境中运行(我们将在本章后面介绍基于云的容器环境),那么您的容器可能会出现节点故障,并将在另一个云实例上重新调度。这对于无状态应用(在我们的示例中是事件服务和预订服务)是完全可以容忍的。

但是,对于有状态容器(在我们的示例中,这将是 MessageBroker 和数据库容器),这会变得很困难。毕竟,如果删除 MongoDB 容器并创建一个具有类似配置的新容器,那么数据库管理的实际数据将消失。这就是发挥作用的地方。

卷是 Docker 使数据在单个容器的生命周期之外持久化的方法。它们包含文件并独立于单个容器存在。每个卷可以装入到任意数量的容器中,允许您在容器之间共享文件。

要对此进行测试,请使用docker volume create命令创建一个新卷:

$ docker volume create test 

这将创建一个名为测试的新卷。您可以使用docker volume ls命令再次找到该卷:

$ docker volume ls 

创建卷后,可以使用docker container run命令的-v标志将其装入容器:

$ docker container run --rm -v test:/my-volume debian:jessie 
/bin/bash -c "echo Hello > /my-volume/test.txt" 

此命令创建一个新容器,该容器将测试卷装载到/my-volume目录中。容器的命令将是一个 bashshell,它在这个目录中创建一个test.txt文件。在此之后,容器将终止并被删除。

要确保卷中的文件仍然存在,现在可以将此卷装载到第二个容器中:

$ docker container run -rm -v test:/my-volume debian:jessie 
cat /my-volume/test.txt

此容器将把test.txt文件的内容打印到命令行。这表明测试卷仍然包含其所有数据,即使最初填充数据的容器已被删除。

建筑容器

我们将从为 MyEvents 应用的组件构建容器映像开始。到目前为止,我们的应用由三个组件组成:两个后端服务(活动和预订服务)和 React 前端应用。虽然前端应用本身不包含任何类型的后端逻辑,但我们至少需要一个 web 服务器将此应用交付给用户。这使得我们总共需要构建三个容器映像。让我们从后端组件开始。

为后端服务构建容器

事件和预订服务都是 Go 应用,它们被编译成单个可执行二进制文件。因此,在 Docker 映像中不必包含任何类型的源文件,甚至不必包含 Go 工具链。

在这一点上需要注意的是,在接下来的步骤中,您将需要 Go 应用的编译 Linux 二进制文件。在 macOS 或 Windows 上,调用go build时需要设置GOOS环境变量:

$ GOOS=linux go build 

在 macOS 和 Linux 上,您可以使用file命令检查正确的二进制类型。对于 LinuxELF二进制,file命令应打印类似以下内容的输出:

$ file eventservice 
eventservice: ELF 64-bit executable, x86-64, version 1 (SYSV),  
statically linked, not stripped 

首先,为事件服务和预订服务编译 Linux 二进制文件。

编译完这两个服务后,通过为事件服务定义 Docker 映像生成过程继续。为此,在事件服务的根目录中创建一个名为Dockerfile的新文件:

FROM debian:jessie 

COPY eventservice /eventservice 
RUN  useradd eventservice 
USER eventservice 

ENV LISTEN_URL=0.0.0.0:8181 
EXPOSE 8181 
CMD ["/eventservice"] 

此 Dockerfile 包含一些我们以前未涉及的新语句。COPY语句将文件从主机的本地文件系统复制到容器映像中。这意味着我们假设您在开始 Docker 构建之前已经使用go build构建了 Go 应用。USER命令导致所有后续的RUN语句和CMD语句作为该用户(而不是 root 用户)运行。ENV命令设置应用可用的环境变量。最后,EXPOSE语句声明从该映像创建的容器将需要 TCP 端口8181

使用docker image build命令继续构建容器映像:

$ docker image build -t myevents/eventservice .

接下来,在bookingservice中添加一个类似的 Docker 文件:

FROM debian:jessie 

COPY bookingservice /bookingservice 
RUN  useradd bookingservice 
USER bookingservice 

ENV LISTEN_URL=0.0.0.0:8181 
EXPOSE 8181 
CMD ["/bookingservice"] 

再次使用docker image build构建图像:

$ docker image build -t myevents/bookingservice .

为了测试我们的新图像,我们现在可以生成相应的容器。但是,在启动实际的应用容器之前,我们需要为这些容器和所需的持久性服务创建一个虚拟网络。事件和预订服务都需要一个 MongoDB 实例和一个共享 AMQP(或 Kafka)消息代理。

让我们从创建容器网络开始:

$ docker network create myevents

接下来,将 RabbitMQ 容器添加到网络:

$ docker container run -d --name rabbitmq --network myevents 
rabbitmq:3-management

继续添加两个 MongoDB 容器:

$ docker container run -d --name events-db --network myevents mongo 
$ docker container run -d --name bookings-db --network myevents mongo 

最后,您可以启动实际的应用容器:

$ docker container run \ 
    --detach \ 
    --name events \ 
    --network myevents \ 
    -e AMQP_BROKER_URL=amqp://guest:guest@rabbitmq:5672/ \ 
    -e MONGO_URL=mongodb://events-db/events \ 
    -p 8181:8181 \ 
    myevents/eventservice 
$ docker container run \ 
    --detach \ 
    --name bookings \ 
    --network myevents \ 
    -e AMQP_BROKER_URL=amqp://guest:guest@rabbitmq:5672/ \ 
    -e MONGO_URL=mongodb://bookings-db/bookings \ 
    -p 8282:8181 \
    myevents/bookingservice 

注意端口映射。目前,两个服务都在 TCP 端口8181上侦听 REST API。只要这两个 API 在不同的容器中运行,它就完全有效。但是,当将这些端口映射到主机端口时(例如,出于测试目的),我们会遇到端口冲突,我们在这里通过将预订服务的端口8181映射到8282来解决。

另外,请注意如何使用-e标志将环境变量传递到正在运行的容器中。例如,使用MONGO_URL环境变量,可以很容易地将两个应用容器连接到不同的数据库。

启动所有这些容器后,您将能够从本地机器通过http://localhost:8181到达活动服务,通过http://localhost:8282到达预订服务。下面的docker container ls命令现在应该向您显示五个正在运行的容器:

docker 容器 ls 输出

对较小的图像使用静态编译

目前,我们正在debian:jessie图像之上构建应用图像。此图像包含典型 Debian 安装的用户空间工具和库,大小约为 123MB(您可以使用docker image ls命令来查找)。为要添加到该基本映像的已编译 Go 应用再添加 10 MB,每个生成的映像大小约为 133 MB(这并不意味着我们用于活动服务和预订服务的两个映像将一起占用 266 MB 的磁盘空间。它们都构建在相同的基本映像上,Docker 在优化容器映像的磁盘空间使用方面非常高效)。

但是,我们的应用不使用这些工具和库中的大多数,因此我们的容器映像可能会小得多。通过这一点,我们可以优化本地磁盘空间使用(尽管 Docker 引擎在这方面已经相当高效),优化从图像存储库下载图像时的传输时间,并减少针对恶意用户的攻击面。

通常,编译后的 Go 二进制文件具有很少的依赖项。您不需要任何类型的运行库或 VM,并且您在项目中使用的所有 Go 库都直接嵌入到生成的可执行文件中。但是,如果您在 Linux 中编译应用,Go 编译器会将生成的二进制文件链接到一些通常在任何 Linux 系统上都可用的 C 标准库。如果您使用的是 Linux,那么您可以通过调用ldd二进制文件(其中一个编译过的 Go 二进制文件作为参数)轻松找到您的程序链接到的库。如果二进制文件链接到 C 标准库,则将收到以下输出:

$ ldd ./eventservice 
    linux-vdso.so.1 (0x00007ffed09b1000) 
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fd523c36000) 
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd52388b000) 
    /lib64/ld-linux-x86-64.so.2 (0x0000564d70338000)  

这意味着您的 Go 应用实际上需要这些 Linux 库才能运行,您不能随意从映像中删除它们以使其变小。

如果使用GOOS=linux环境变量在 Windows 或 macOS 上交叉编译应用,则可能不会出现此问题。由于这些系统上的编译器无法访问 Linux 标准 C 库,因此默认情况下,它将生成一个没有任何依赖关系的静态链接二进制文件。当使用这样的二进制文件调用时,ldd将呈现以下输出:

$ ldd ./eventservice 
    not a dynamic executable 

在 Linux 上,通过为 Go build 命令设置CGO_ENABLED=0环境变量,可以强制 Go 编译器创建静态链接的二进制文件:

$ CGO_ENABLED=0 go build 
$ ldd ./eventservice 
    not a dynamic executable 

使用完全静态链接的二进制文件可以创建更小的容器映像。您现在可以使用划痕图像,而不是将debian:jessie作为基础图像。scratch图像是一种特殊的图像。它直接内置在 Docker 引擎中,您无法从 Docker Hub 下载它。scratch图像的特殊之处在于它是完全空的,因为它不包含单个文件。这意味着没有标准库,没有系统实用程序,甚至连一个 shell 都没有。尽管这些属性通常会使 scratch 映像的使用变得麻烦,但它非常适合为静态链接的应用构建最小的容器映像。

将事件服务的Dockerfile更改如下:

FROM scratch 

COPY eventservice /eventservice 

ENV LISTEN_URL=0.0.0.0:8181 
EXPOSE 8181 
CMD ["/eventservice"] 

接下来,以类似的方式更改预订服务的Dockerfile。使用前面代码中的docker image build命令再次构建两个容器映像。之后,使用docker image ls命令验证图像大小:

docker 图像 ls 输出

为前端构建容器

现在我们有了后端应用的容器映像,我们可以将注意力转向前端应用。由于此应用在用户的浏览器中运行,因此我们并不真正需要容器化的运行时环境。但我们真正需要的是一种将应用交付给用户的方法。由于整个应用由一些 HTML 和 JavaScript 文件组成,因此我们可以构建一个容器映像,其中包含一个简单的 NGINX web 服务器,为用户提供这些文件。

为此,我们将以nginx:1.11-alpine图像为基础。此映像包含构建在 Alpine Linux 上的 NGINX web 服务器的最低版本。Alpine 是一个针对小规模优化的 Linux 发行版。整个nginx:1.11-alpine图像的大小只有 50MB。

将以下Dockerfile添加到前端应用目录:

FROM nginx:1.11-alpine 

COPY index.html /usr/share/nginx/html/ 
COPY dist /usr/share/nginx/html/dist/ 
COPY node_modules/bootstrap/dist/css/bootstrap.min.css /usr/share/nginx/html/node_modules/bootstrap/dist/css/bootstrap.min.css 
COPY node_modules/react/umd/react.production.min.js /usr/share/nginx/html/node_modules/react/umd/react.production.min.js
COPY node_modules/react-dom/umd/react-dom.production.min.js /usr/share/nginx/html/node_modules/react-dom/umd/react-dom.production.min.js
COPY node_modules/promise-polyfill/promise.min.js /usr/share/nginx/html/node_modules/promise-polyfill/promise.min.js 
COPY node_modules/whatwg-fetch/fetch.js /usr/share/nginx/html/node_modules/whatwg-fetch/fetch.js 

显然,我们的 web 服务器将需要在dist/bundle.js为用户提供index.html和已编译的 web 包包包服务,因此这些包将通过COPY复制到容器映像中。然而,从安装到node_modules/目录中的所有依赖项来看,我们的用户只需要一个非常特定的子集。出于这些原因,我们将这五个文件显式复制到容器映像中,而不是对整个node_modules/目录使用COPY

在实际构建容器映像之前,请确保已安装应用的最新网页包构建和所有依赖项。您还可以使用-p标志触发 Webpack 来创建应用的生产版本,该版本将针对大小进行优化:

$ webpack -p 
$ npm install 

在此之后,构建容器:

$ docker container build -t myevents/frontend . 

现在可以使用以下命令启动此容器:

$ docker container run --name frontend -p 80:80 myevents/frontend

请注意,在本例中,我们没有传递--network=myevents标志。这是因为前端容器实际上不需要直接与后端服务通信。所有通信都是从用户的浏览器启动的,而不是从实际的前端容器中启动的。

-p 80:80标志将容器的 TCP 端口 80 绑定到本地 TCP 端口 80。这允许您现在在浏览器中打开http://localhost并查看 MyEvents 前端应用。如果您仍在运行前几节中的后端容器,那么应用应该是开箱即用的。

使用 Docker Compose 部署应用

到目前为止,从现有容器映像实际部署 MyEvents 应用涉及许多docker container run命令。尽管这在测试中效果相当好,但一旦应用在生产环境中运行,尤其是当您想要部署更新或扩展应用时,它就会变得非常乏味。

一种可能的解决方案是Docker Compose。Compose 是一个工具,允许您以声明方式描述由多个容器组成的应用(在本例中,是一个 YAML 文件,用于描述构建应用的组件)。

Docker Compose 是常规 Docker 安装包的一部分,因此,如果您在本地计算机中安装了 Docker,那么也应该提供 Docker Compose。通过在命令行上调用以下命令,可以轻松地测试这一点:

$ docker-compose -v 

如果您的本地机器上没有 Compose,请参阅上的安装手册 https://docs.docker.com/compose/install 获取有关如何设置 Compose 的详细说明。

每个撰写项目都由一个docker-compose.yml文件描述。撰写文件稍后将包含应用所需的所有容器、网络和卷的描述。然后,Compose 将尝试将 Compose 文件中表示的所需状态与本地 Docker 引擎的实际状态相协调(例如,通过创建、删除、启动或停止容器)。

在项目目录的根目录下创建一个包含以下内容的文件:

version: "3" 
networks: 
  myevents: 

注意撰写文件中的version: "3"声明。Compose 支持多种声明格式,最新版本为版本 3。在一些文档、示例或开源项目中,您很可能会偶然发现为旧版本编写的 Compose 文件。完全不声明版本的组合文件将被解释为版本 1 文件。

现在,前面的 Compose 文件只声明您的应用需要一个名为myevents的虚拟网络。但是,您可以通过运行以下命令,使用 Compose 来协调所需的状态(必须存在一个名为myevents的网络):

$ docker-compose up 

现在,前面的命令将打印一条警告消息,因为我们正在声明一个没有被任何容器使用的容器网络。

容器在services下的 Compose 文件中声明。每个容器都有一个名称(用作 YAML 结构中的键),并且可以具有各种属性(例如要使用的图像)。让我们继续向 Compose 文件添加一个新容器:

version: "3" 
networks: 
  myevents:

services: 
  rabbitmq: 
    image: rabbitmq:3-management 
    ports: 
      - 15672:15672 
    networks: 
      - myevents 

这是您先前使用docker container run -d --network myevents -p 15672:15672 rabbitmq:3-management命令手动创建的 RabbitMQ 容器。

现在可以通过运行以下命令创建此容器:

$ docker-compose up -d 

-d标志与 docker 容器运行命令具有相同的效果;它将导致容器在后台启动。

一旦 RabbitMQ 容器开始运行,您就可以调用docker-compose up任意次数。由于已经运行的 RabbitMQ 容器与 Compose 文件中的规范匹配,Compose 将不采取任何进一步的操作。

让我们继续向 Compose 文件添加两个 MongoDB 容器:

version: "3" 
networks: 
  - myevents

services: 
  rabbitmq: #... 

  events-db: 
    image: mongo 
    networks: 
      - myevents 

  bookings-db: 
    image: mongo 
    networks: 
      - myevents 

下次再跑docker-compose up -d。Compose 仍然不会触及 RabbitMQ 容器,因为它仍然符合规范。但是,它将创建两个新的 MongoDB 容器。

接下来,我们可以添加两个应用服务:

version: "3" 
networks: 
  - myevents

services: 
  rabbitmq: #... 
  events-db: #... 
  bookings-db: #... 
  events: 
    build: path/to/eventservice 
    ports: 
      - "8181:8181" 
    networks: 
      - myevents 
    environment: 
      - AMQP_BROKER_URL=amqp://guest:guest@rabbitmq:15672/ 
      - MONGO_URL=mongodb://events-db/events 
  bookings: 
    build: path/to/bookingservice 
    ports: 
      - "8282:8181" 
    networks: 
      - myevents 
    environment: 
      - AMQP_BROKER_URL=amqp://guest:guest@rabbitmq:15672/ 
      - MONGO_URL=mongodb://bookings-db/bookings 

请注意,我们不是为这两个容器指定一个image属性,而是一个build属性。这将导致 Compose 根据相应目录中的 Dockerfile 按需实际构建这些容器的映像。

重要的是要注意 Docker 构建不会编译 Go 二进制文件。相反,它将依赖于它们已经存在。在第 9 章持续交付中,您将学习如何使用 CI 管道来自动化这些构建步骤。

您还可以使用docker-compose命令单独触发此管道的各个步骤。例如,使用docker-compose pull从 Docker Hub 下载Compose文件中使用的所有图像的最新版本:

$ docker-compose pull 

对于不使用预定义图像的容器,请使用docker-compose build重建所有图像:

$ docker-compose build 

使用另一个docker-compose up -d创建新容器。

确保已停止以前创建的任何可能绑定到 TCP 端口 8181 或 8282 的容器。使用docker container lsdocker container stop命令定位并停止这些容器。

您还可以使用docker-compose ps命令获取与当前 Compose 项目关联的当前正在运行的容器的概览:

docker 合成 ps 输出

最后,将前端应用添加到撰写文件:

version: "3" 
networks: 
  - myevents

services: 
  rabbitmq: #... 
  events-db: #... 
  bookings-db: #... 
  events: #... 
  bookings: #... 

  frontend: 
    build: path/to/frontend 
    ports: 
      - "80:80" 

正如您在本节中所了解的,Docker Compose 使您能够以声明的方式描述应用的架构,从而可以在支持 Docker 实例的任何服务器上轻松部署和更新应用。

到目前为止,我们一直在一台主机上工作(很可能是您的本地计算机)。这有利于开发,但对于生产设置,您需要考虑将应用部署到远程服务器。此外,由于云架构都是关于规模的,在接下来的几节中,我们还将研究如何大规模管理容器化应用。

发布您的图像

现在,您可以从应用组件构建容器映像,并在本地计算机上从这些映像运行容器。然而,在生产环境中,构建容器映像的机器很少是运行它的机器。要真正能够将应用部署到任何云环境,您需要一种将构建的容器映像分发到任意数量的主机的方法。

这就是容器注册表发挥作用的地方。事实上,您已经在本章前面使用了容器注册表,即 Docker Hub。当您使用本地计算机上不存在的 Docker 映像(例如,nginx映像)时,Docker 引擎会将该映像从 Docker Hub 拉到本地计算机上。但是,也可以使用容器注册表(如 Docker Hub)发布自己的容器图像,然后从另一个实例中提取它们。

在 Docker Hub(您可以通过在浏览器中访问)https://hub.docker.com ),您可以注册为用户,然后上传自己的图片。为此,请在登录后单击“创建存储库”,并为图像选择一个新名称。

要将新映像推送到新创建的存储库中,首先需要使用本地计算机上的 Docker Hub 帐户登录。为此使用以下docker login命令:

$ docker login 

现在,您将能够将图像推送到新的存储库中。图像名称需要以 Docker Hub 用户名开头,后跟斜杠:

$ docker image build -t martinhelmich/test . 
$ docker image push martinhelmich/test 

默认情况下,推送到 Docker Hub 的图像将公开可见。Docker Hub 还提供了将私人图像作为付费功能推送的可能性。只有在使用docker login命令成功进行身份验证后,才能提取私有映像。

当然,您不必使用 Docker Hub 来分发自己的图像。还有其他供应商,如码头(https://quay.io ),所有主要的云提供商也为您提供托管托管容器注册的可能性。但是,当使用 Docker Hub 以外的注册表时,前面的一些命令将略有更改。对于初学者,您必须告诉注册表中的docker login命令您将要登录:

$ docker login quay.io 

此外,要推送的容器映像不仅需要以 Docker Hub 用户名开头,还需要以整个注册表主机名开头:

$ docker image build -t quay.io/martinhelmich/test .
$ docker image push quay.io/martinhelmich/test 

如果您不想将容器映像委托给第三方提供商,您还可以推出自己的容器注册表。您可以使用 Docker 映像快速设置自己的注册表:

$ docker volume create registry-images 
$ docker container run \ 
    --detach \ 
    -p 5000:5000 \ 
    -v registry-images:/var/lib/registry \ 
    --name registry \ 
    registry:2.6.1 

这将设置一个容器注册表,可在以下位置访问:http://localhost:5000。您可以将其视为任何其他第三方注册表:

$ docker image build -t localhost:5000/martinhelmich/test . 
$ docker image push localhost:5000/martinhelmich/test 

让私有容器注册中心监听localhost:5000对于开发来说很好,但是对于生产设置,您需要额外的配置选项。例如,您需要为注册表配置 TLS 传输加密(默认情况下,Docker 引擎将拒绝本地主机以外的任何未加密 Docker 注册表),并且还需要设置身份验证(除非您明确打算运行可公开访问的容器注册表)。查看注册表的官方部署指南,了解如何设置加密和身份验证:https://docs.docker.com/registry/deploying/

将应用部署到云

为了结束本章,我们将了解如何将容器化应用部署到云环境。

容器引擎(如 Docker)允许您在隔离的环境中提供多个服务,而无需为单个服务提供单独的虚拟机。然而,与云应用的典型情况一样,我们的容器架构需要易于扩展,并且具有故障恢复能力。

这就是 Kubernetes 等容器编排系统发挥作用的地方。这些系统允许您在整个主机集群上部署容器化应用。它们允许轻松扩展,因为您可以轻松地将新主机添加到现有集群(之后可以自动在其上安排新的容器工作负载),并使您的系统具有弹性;可以快速检测节点故障,从而允许在其他地方启动这些节点上的容器,以确保其可用性。

库伯内特斯简介

最著名的容器编曲之一是 Kubernetes(希腊语中的舵手)。Kubernetes 是一个开源产品,最初由谷歌开发,现在由云计算计算基金会拥有。

下图显示了 Kubernetes 群集的基本架构:

每个 Kubernetes 群集的中心组件是主服务器(当然,主服务器不必是实际的单个服务器。在生产设置中,您通常会有多个配置为高可用性的主服务器)。主服务器将整个集群状态存储在终端数据存储中。API 服务器是提供 REST API 的组件,内部组件(如调度器、控制器或 Kubelets)和外部用户(您!)都可以使用 REST API。调度器跟踪各个节点上的可用资源(例如内存和 CPU 使用情况),并决定应调度集群中的新容器中的哪个节点。控制器是管理高级概念(如复制控制器或自动缩放组)的组件。

Kubernetes 节点是启动主服务器管理的实际应用容器的地方。每个节点运行一个 Docker 引擎和一个Kubelet。Kubelet 连接到主服务器的 RESTAPI,并负责实际启动调度程序为此节点调度的容器。

在库伯内特斯,容器被组织成豆荚。Pod 是 Kubernetes 最小的调度单元,由一个或多个 Docker 容器组成。Pod 中的所有容器都保证在同一台主机上运行。每个 Pod 将接收一个在整个集群内唯一且可路由的 IP 地址(这意味着运行在一台主机上的 Pod 将能够通过其 IP 地址与运行在其他节点上的 Pod 通信)。

Kube 代理是确保用户能够实际访问应用的组件。在 Kubernetes 中,您可以定义将多个 POD 分组的服务。Kube 代理为每个服务分配一个唯一的 IP 地址,并将网络流量转发给与服务匹配的所有 POD。这样,当一个应用的多个实例在多个 pod 中运行时,Kube 代理还实现了一个非常简单但有效的负载平衡。

你可能已经注意到库伯内特斯的架构相当复杂。建立 Kubernetes 集群是一项具有挑战性的任务,本书将不详细介绍。对于本地开发和测试,我们将使用 Minikube 工具,它会自动在本地机器上创建虚拟化的 Kubernetes 环境。当您在公共云环境中运行应用时,还可以使用自动为您设置生产就绪的 Kubernetes 环境的工具。一些云提供商甚至为您提供托管 Kubernetes 集群(例如,谷歌容器引擎Azure 容器服务都是基于 Kubernetes 构建的)。

与 Minikube 建立本地 Kubernetes

要开始使用 Minikube,您需要在本地机器上使用三个工具:Minikube 本身(它将在您的机器上处理虚拟 Kubernetes 环境的设置)、VirtualBox(它将用作虚拟化环境)和 kubectl(它是使用 Kubernetes 的命令行客户机)。尽管我们在本例中使用的是 Minikube,但我们在以下各节中展示的每个 kubectl 命令几乎都适用于每个 Kubernetes 集群,而不管它是如何设置的。

从设置 VirtualBox 开始。为此,请从官方下载页面下载安装程序 https://www.virtualbox.org/wiki/Downloads 并按照操作系统的安装说明进行操作。

接下来,下载 Minikube 的最新版本。您可以在以下网址找到所有版本:https://github.com/kubernetes/minikube/releases (撰写本文时,最新版本为 0.18.0)。同样,请按照操作系统的安装说明进行操作。或者,使用以下命令快速下载并设置 Minikube(分别将linux替换为darwinwindows

$ curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.18.0/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/ 

最后,建立 kubectl。您可以在以下位置找到安装说明:https://kubernetes.io/docs/tasks/kubectl/install 。或者,使用以下命令(同样,根据需要将linux替换为darwinwindows

curl -LO https://storage.googleapis.com/kubernetes-release/release/1.6.1/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin 

设置好所有要求后,您可以使用minikube start命令启动本地 Kubernetes 环境:

$ minikube start 

此命令将下载一个 ISO 映像,然后从此映像启动一个新的虚拟机,并安装各种 Kubernetes 组件。喝杯咖啡,如果这需要几分钟,不要感到惊讶:

minikube 启动输出

minikube start命令还为 kubectl 创建了一个配置文件,使您能够将 kubectl 与 minikube VM 一起使用,而无需进行任何进一步的配置。您可以在~/.kube/config的主目录中找到此文件。

要测试整个设置是否按预期工作,请运行kubectl get nodes命令。此命令将打印属于 Kubernetes 群集的所有节点的列表。在 Minikube 设置中,您应该只看到一个节点:

$ kubectl get nodes 

kubectl 获取节点输出

库伯内特斯的核心概念

在回到我的事件之前,让我们更深入地看一下 Kubernetes 的一些核心概念。我们将首先创建一个新的 Pod,其中包含一个简单的 nginxweb 服务器。

Kubernetes 资源(如 POD 和服务)通常在 YAML 文件中定义,这些文件声明性地描述集群的所需状态(类似于您以前使用过的 Docker Compose 配置文件)。对于我们的新 NGINX Pod,在本地文件系统中的任意位置创建一个名为nginx-pod.yaml的新文件:

apiVersion: v1 
kind: Pod 
metadata: 
  name: nginx-test 
spec: 
  containers: 
  - name: nginx 
    image: nginx 
    ports: 
      - containerPort: 80 
        name: http 
        protocol: TCP 

这个所谓的清单文件描述了新 Pod 的外观。在metadata部分,您可以设置基本元数据,例如 Pod 的名称或任何附加标签(我们稍后需要这些标签)。spec部分包含 Pod 外观的实际规格。如您所见,spec.containers部分的格式为列表;理论上,你可以在这里添加额外的容器,然后在同一个吊舱内运行。

创建此文件后,使用kubectl apply命令创建 Pod:

$ kubectl apply -f nginx-pod.yaml 

在此之后,您可以使用kubectl get pods命令验证您的 Pod 是否已成功创建。请注意,Pod 将其状态从ContainerCreating更改为Running可能需要几秒钟到几分钟:

$ kubectl get pods 

kubectl 获得吊舱输出

请注意,kubectl命令直接与 Kubernetes API 服务器通信(尽管在使用 Minikube 时,这不是一个很大的区别,因为所有组件都运行在同一个虚拟机上),而不是与集群节点通信。理论上,Kubernetes 集群可以由许多主机组成,Kubernetes 调度程序将自动选择最适合在其上运行新 Pod 的主机。

你可以为一个吊舱配置更多的东西。例如,您可能希望限制应用的内存或 CPU 使用。在这种情况下,您可以将以下设置添加到新创建的 Pod 清单中:

# ... 
spec: 
  containers: 
  - name: nginx 
    image: nginx 
    resources: 
      limits: 
        memory: 128Mi 
        cpu: 0.5 
    ports: # ... 

resources.limits部分将指示 Kubernetes 创建一个内存限制为 128 MB、CPU 限制为一半 CPU 核心的容器。

关于 Kubernetes 豆荚,需要注意的重要一点是,它们不是持久的。POD 可能会在接到通知后立即终止,并且可能在节点出现故障时丢失。因此,建议使用 Kubernetes 控制器(如部署控制器)为您创建吊舱。

继续之前,使用kubectl delete命令删除 Pod:

$ kubectl delete pod nginx-test 

接下来,创建一个新的nginx-deployment.yaml文件:

apiVersion: apps/v1beta1 
kind: Deployment 
metadata: 
  name: nginx-deployment 
spec: 
  replicas: 2 
  template: 
    metadata: 
      labels: 
        app: nginx 
    spec: 
      containers: 
      - name: nginx 
        image: nginx 
        ports: 
        - containerPort: 80 
          name: http 
          protocol: TCP 

此清单将为您创建一个所谓的部署控制器。部署控制器将确保给定配置的给定数量的 Pod 在任何时候都在运行。在这种情况下,spec.template字段描述的两个 Pod(在spec.replicas字段中指定)(请注意,spec.template字段与我们之前编写的 Pod 定义相匹配,减去名称)。

与前面一样,使用kubectl apply命令创建部署:

$ kubectl apply -f nginx-deployment.yaml 

使用kubectl get pods命令验证您的操作是否成功。您应该注意,将安排两个 POD(名称如nginx-deployment-1397492275-qz8k5

kubectl 获得吊舱输出

您可以通过部署做更多的事情。对于初学者,尝试使用kubectl delete命令删除自动生成的一个 Pod(请记住,在您的机器上,您将有一个不同的 Pod 名称):

$ kubectl delete pod nginx-deployment-1397492275-qz8k5 

删除 Pod 后,再次调用kubectl get pods。您将注意到,部署控制器几乎立即创建了一个新的 Pod。

此外,您可能认为应用的两个实例不够,需要进一步扩展应用。为此,您可以简单地增加部署控制器的spec.scale属性。要增加(或减少)比例,您可以编辑现有 YAML 文件,然后再次调用kubectl apply。或者,您可以使用kubectl edit命令直接编辑资源:

$ kubectl edit deployment nginx-deployment 

特别是对于spec.scale属性,还可以使用一个特殊的kubectl scale命令:

$ kubectl scale --replicas=4 deployment/nginx-deployment 

kubectl 获得吊舱输出

服务

目前,我们有四个 NGINX 容器正在运行,但无法实际访问它们。这就是服务发挥作用的地方。创建名为nginx-service.yaml的新 YAML 文件:

apiVersion: v1 
kind: Service 
metadata: 
  name: nginx 
spec: 
  type: NodePort 
  selector: 
    app: nginx 
  ports: 
  - name: http 
    port: 80 

请注意,spec.selector属性与部署清单中指定的metadata.labels属性匹配。部署控制器创建的所有 pod 都将有一组给定的标签(实际上只是任意键/值映射)。服务的spec.selector属性现在指定 Pod 必须由该服务识别的标签。另外,请注意type: NodePort属性,这将在以后很重要。

创建文件后,照常使用kubectl apply创建服务定义:

kubectl 应用输出

$kubectl apply-f nginx-service.yaml

接下来,调用kubectl get services检查新创建的服务定义。

kubectl get services输出中,您将找到新创建的nginx服务(以及始终存在的 Kubernetes 服务)。

还记得创建服务时指定的type: NodePort属性吗?此属性的效果是,每个节点上的 Kube 代理现在打开了一个 TCP 端口。此端口的端口号是随机选择的。在前面的示例中,这是 TCP 端口 31455。您可以使用此端口从 Kubernetes 群集外部(例如,从本地计算机)连接到您的服务。此端口上接收到的任何和所有流量都将转发到服务规范中指定的selector匹配的 POD 之一。

服务的特殊之处在于,通常它们的使用寿命(远)比你的平均吊舱长。添加新 POD 时(可能是因为您增加了部署控制器的副本计数),这些 POD 将自动添加。此外,当 Pod 被删除时(同样,可能是由于副本计数更改,但也可能是由于节点故障,或者仅仅是因为 Pod 被手动删除),它们将停止接收流量。

如果您正在使用 Minikube,现在可以使用minikube service命令快速查找节点的公共 IP 地址,以便在浏览器中打开此服务:

$ minikube service nginx 

除了节点端口,还要注意前面输出中的集群 IP 属性;这是一个 IP 地址,您可以在群集中使用它来访问与此服务匹配的任何 Pod。因此,在本例中,您可以启动一个运行您自己的应用的新 Pod,并使用 IP 地址10.0.0.223访问此应用中的nginx服务。此外,由于使用 IP 地址很麻烦,您还可以使用服务名称(nginx,在本例中)作为 DNS 名称。

持久卷

通常,您需要一个地方以持久的方式存储文件和数据。由于单个 POD 在 Kubernetes 环境中的寿命相当短,因此将文件直接存储在容器的文件系统中通常不是一个好的解决方案。在 Kubernetes 中,这个问题是通过使用持久卷来解决的,持久卷基本上是您以前使用过的 Docker 卷的更灵活的抽象。

要创建一个新的持久卷,请创建一个包含以下内容的新example-volume.yaml文件:

apiVersion: v1 
kind: PersistentVolume 
metadata: 
  name: volume01 
spec: 
  capacity: 
    storage: 1Gi 
  accessModes: 
  - ReadWriteOnce 
  - ReadWriteMany 
  hostPath: 
    path: /data/volume01 

使用kubectl apply -f example-volume.yaml创建卷。之后,您可以通过运行kubectl get pv再次找到它。

前面的清单文件创建了一个新卷,该卷将其文件存储在使用该卷的主机上的/data/volume01目录中。

除了在本地开发环境中,对持久数据使用 hostPath 卷是一个糟糕的想法。如果使用此持久卷的 Pod 在另一个节点上被重新调度,那么它将无法访问以前拥有的相同数据。Kubernetes 支持多种卷类型,您可以使用这些类型跨多台主机访问卷。

例如,在 AWS 中,可以使用以下体积定义:

apiVersion: v1 
kind: PersistentVolume 
metadata: 
  name: volume01 
spec: 
  capacity: 
    storage: 1Gi 
  accessModes: 
  - ReadWriteOnce 
  awsElasticBlockStore: 
    volumeID: <volume-id> 
    fsType: ext4 

在 Pod 中使用持久卷之前,您需要声明它。Kubernetes 对创建持久卷和在容器中使用持久卷进行了重要区分。这是因为创建持久卷的人和使用(声明)持久卷的人通常是不同的。此外,通过解耦卷的创建和使用,Kubernetes 还将 POD 中卷的使用与实际的底层存储技术解耦。

接下来,通过创建一个example-volume-claim.yaml文件,然后调用kubectl apply -f example-volume-claim.yaml来创建一个PersistentVolumeClaim

apiVersion: v1 
kind: PersistentVolumeClaim 
metadata: 
  name: my-data 
spec: 
  accessModes: 
    - ReadWriteOnce 
  resources: 
    requests: 
      storage: 1Gi 

再次调用kubectl get pv时,您会发现volume01卷的状态字段已更改为Bound。现在,您可以在创建 Pod 或部署时使用新创建的持久卷声明:

apiVersion: v1 
kind: Pod 
spec: 
  volumes: 
  - name: data 
    persistentVolumeClaim: 
      claimName: my-data 
  containers: 
  - name: nginx 
    image: nginx 
    volumeMounts: 
    - mountPath: "/usr/share/nginx/html" 
      name: data 

当您在云环境中操作 Kubernetes 群集时,Kubernetes 还能够通过与云提供商的 API 对话自动创建新的持久卷,例如,创建新的 EBS 设备。

将 MyEvents 部署到 Kubernetes

现在,您已经对 Kubernetes 迈出了第一步,我们可以将 MyEvents 应用部署到 Kubernetes 集群中。

创建 RabbitMQ 代理

让我们从创建 RabbitMQ 代理开始。由于 RabbitMQ 不是无状态组件,我们将使用 Kubernetes 提供的特殊控制器StatefulSet控制器。这类似于部署控制器,但将创建具有持久标识的 POD。

要创建新的StatefulSet,请创建一个名为rabbitmq-statefulset.yaml的新文件:

apiVersion: apps/v1beta1 
kind: StatefulSet 
metadata: 
  name: rmq 
spec: 
  serviceName: amqp-broker 
  replicas: 1 
  template: 
    metadata: 
      labels: 
        myevents/app: amqp-broker 
    spec: 
      containers: 
      - name: rmq 
        image: rabbitmq:3-management 
        ports: 
        - containerPort: 5672 
          name: amqp 
        - containerPort: 15672 
          name: http 

不过,这个定义遗漏了一件重要的事情,那就是持久性。当前,如果 RabbitMQ Pod 因任何原因失败,则将调度一个新的 Pod,而不包含代理以前具有的任何状态(在本例中为交换、队列和尚未调度的消息)。出于这个原因,我们还应该声明一个可供此StatefulSet使用的持久卷。我们不需要手动创建一个新的PersistentVolume和一个新的PersistentVolumeClaim,只需为StatefulSet声明一个volumeClaimTemplate并让 Kubernetes 自动设置新卷即可。在 Minikube 环境中,这是可能的,因为 Minikube 附带了用于此类卷的自动供应器。在云环境中,您会发现类似的卷资源调配器。

StatefulSet中增加以下部分:

apiVersion: apps/v1beta1 
kind: StatefulSet 
metadata: 
  name: rmq 
spec: 
  serviceName: amqp-broker 
  replicas: 1 
  template: # ... 
  volumeClaimTemplates: 
  - metadata: 
      name: data 
      annotations: 
        volume.alpha.kubernetes.io/storage-class: standard 
    spec: 
      accessModes: ["ReadWriteOnce"] 
      resources: 
        requests: 
          storage: 1Gi 

volumeClaimTemplate将指示StatefulSet控制器为StatefulSet的每个实例自动提供一个新的PersistentVolume和一个新的PersistentVolumeClaim。如果增加副本计数,控制器将自动创建更多卷。

剩下要做的最后一件事是实际使用rabbitmq容器中的体积声明。为此,请按如下方式修改容器等级库:

containers: 
- name: rmq 
  image: rabbitmq:3-management 
  ports: # ... 
  volumeMounts: 
  - name: data 
    mountPath: /var/lib/rabbitmq 

使用kubectl apply -f rabbitmq-statefulset.yaml创建StatefulSet。在此之后,当您运行kubectl get pods时,您应该会看到一个名为rmq-0的新 Pod 启动。在分别运行kubectl get pvkubectl get pvc时,您还应该看到自动生成的持久卷和相应的声明。

接下来,创建一个Service以允许其他 POD 访问您的 RabbitMQ 代理:

apiVersion: v1 
kind: Service 
metadata: 
  name: amqp-broker 
spec: 
  selector: 
    myevents/app: amqp-broker 
  ports: 
  - port: 5672 
    name: amqp 

像往常一样,使用kubectl apply -f rabbitmq-service.yaml创建服务。创建服务后,您将能够使用主机名amqp-broker(或其长格式amqp-broker.default.svc.cluster.local)通过 DNS 解析服务。

创建 MongoDB 容器

接下来,让我们创建 MongoDB 容器。从概念上讲,它们与您在上一节中创建的 RabbitMQ 容器没有太大区别。与前面一样,我们将使用带有自动配置卷的StatefulSet。将以下内容放入名为events-db-statefulset.yaml的新文件中,然后在此文件上调用kubectl apply

apiVersion: apps/v1beta1 
kind: StatefulSet 
metadata: 
  name: events-db 
spec: 
  serviceName: events-db 
  replicas: 1 
  template: 
    metadata: 
      labels: 
        myevents/app: events 
        myevents/tier: database 
    spec: 
      containers: 
      - name: mongo 
        image: mongo:3.4.3 
        ports: 
        - containerPort: 27017 
          name: mongo 
        volumeMounts: 
        - name: database 
          mountPath: /data/db 
  volumeClaimTemplates: 
  - metadata: 
      name: data 
      annotations: 
        volume.alpha.kubernetes.io/storage-class: standard 
    spec: 
      accessModes: ["ReadWriteOnce"] 
      resources: 
        requests: 
          storage: 1Gi 

接下来,通过创建新文件events-db-service.yaml并调用kubectl apply来定义与此StatefulSet匹配的服务:

apiVersion: v1 
kind: Service 
metadata: 
  name: events-db 
spec: 
  clusterIP: None 
  selector: 
    myevents/app: events 
    myevents/tier: database 
  ports: 
  - port: 27017 
    name: mongo 

现在,我们需要对预订服务的 MongoDB 容器重复这一点。您可以重用上面几乎相同的定义;只需将events替换为bookings即可创建StatefulSetbookings-db服务。

将图像提供给 Kubernetes

在您现在可以部署实际的微服务之前,您需要确保 Kubernetes 集群可以访问您的映像。通常,您需要在容器注册表中提供您的自建图像。如果您正在使用 Minikube 并希望省去设置自己的图像注册表的麻烦,您可以执行以下操作:

$ eval $(minikube docker-env) 
$ docker image build -t myevents/eventservice .

第一个命令将指示本地 shell 不连接到本地 Docker 引擎,而是连接到 Minikube VM 中的 Docker 引擎。然后,使用常规的docker container build命令,您可以直接在 Minikube VM 上构建要使用的容器映像。

如果您的映像在私有注册表(例如 Docker Hub、Quay.io 或自托管注册表)中可用,则需要配置 Kubernetes 群集,以便授权它实际访问这些映像。为此,您将把注册表凭据添加为Secret对象。为此,请使用kubectl create secret命令:

$ kubectl create secret docker-registry my-private-registry \
 --docker-server https://index.docker.io/v1/ \
 --docker-username <your-username> \
 --docker-password <your-password> \
 --docker-email <your-email>

在上面的代码示例中,my-private-registry是为 Docker 凭证集任意选择的名称。--docker-server标志https://index.docker.io/v1/指定官方 Docker Hub 的 URL。如果您使用的是第三方注册表,请记住相应地更改此值。

您现在可以在创建新 Pod 时使用这个新创建的Secret对象,方法是在 Pod 规范中添加一个imagePullSecrets属性:

apiVersion: v1
kind: Pod
metadata:
  name: example-from-private-registry
spec:
  containers:
  - name: secret
    image: quay.io/martins-private-registry/secret-application:v1.2.3
  imagePullSecrets:
  - name: my-private-registry

当您使用StatefulSet或 Deploymet 控制器创建 POD 时,使用imagePullSecrets属性也有效。

部署 MyEvents 组件

现在您的 Kubernetes 集群上有了可用的容器映像(通过在 Minikube VM 上本地构建它们,或者通过将它们推送到注册表并授权您的集群访问该注册表),我们就可以开始部署实际的事件服务了。由于事件服务本身是无状态的,我们将使用常规部署对象来部署它,而不是使用StatefulSet

继续创建一个新文件-events-deployment.yaml-包含以下内容:

apiVersion: apps/v1beta1 
kind: Deployment 
metadata: 
  name: eventservice 
spec: 
  replicas: 2 
  template: 
    metadata: 
      labels: 
        myevents/app: events 
        myevents/tier: api 
    spec: 
      containers: 
      - name: api 
        image: myevents/eventservice 
        imagePullPolicy: Never 
        ports: 
        - containerPort: 8181 
          name: http 
        environment: 
        - name: MONGO_URL 
          value: mongodb://events-db/events 
        - name: AMQP_BROKER_URL 
          value: amqp://guest:guest@amqp-broker:5672/ 

注意imagePullPolicy: Never属性。如果您直接在 Minikube 虚拟机上构建了myevents/eventservice映像,这是必要的。如果您有一个实际的容器注册表,您可以将图像推送到其中,那么您应该忽略此属性(并添加一个imagePullSecrets属性)。

接下来,通过创建一个新文件events-service.yaml来创建相应的服务:

apiVersion: v1 
kind: Service 
metadata: 
  name: events 
spec: 
  selector: 
    myevents/app: events 
    myevents/tier: api 
  ports: 
  - port: 80 
    targetPort: 8181 
    name: http 

使用相应的kubectl apply调用创建部署和服务。此后不久,您将在kubectl get pods输出中看到相应的容器。

对预订服务进行类似操作。您可以在本书的代码示例中找到预订服务的完整清单文件。

最后,让我们部署前端应用。使用以下清单创建另一个部署:

apiVersion: apps/v1beta1 
kind: Deployment 
metadata: 
  name: frontend 
spec: 
  replicas: 2 
  template: 
    metadata: 
      labels: 
        myevents/app: frontend 
    spec: 
      containers: 
      - name: frontend 
        image: myevents/frontend 
        imagePullPolicy: Never 
        ports: 
        - containerPort: 80 
          name: http 

使用以下清单创建相应的Service

apiVersion: v1 
kind: Service 
metadata: 
  name: frontend 
spec: 
  selector: 
    myevents/app: frontend 
  ports: 
  - port: 80 
    targetPort: 80 
    name: http 

配置 HTTP 入口

此时,您已经拥有在 Kubernetes 集群中运行的 MyEvents 应用所需的所有服务。但是,目前还没有方便的方法从集群外部访问这些服务。使它们可访问的一个可能的解决方案是使用节点端口服务(我们在前面的一节中已经做过)。但是,这将导致您的服务暴露在一些随机选择的高 TCP 端口上,这对于生产设置是不可取的(HTTP(S)服务应该在 TCP 端口80443上可用)。

如果您的 Kubernetes 群集运行在公共云环境中(更准确地说,是 AWS、GCE 或 Azure),您可以创建一个LoadBalancer``Service,如下所示:

apiVersion: v1 
kind: Service 
metadata: 
  name: frontend 
spec: 
  type: LoadBalancer 
  selector: 
    myevents/app: frontend 
  # ... 

这将提供适当的云提供商资源(例如,AWS 中的弹性负载平衡器,以使您的服务可以在标准端口上公开访问。

然而,Kubernetes 还提供了另一项功能,允许您处理称为入口的传入 HTTP 流量。入口资源为您提供了一个更细粒度的控制,可以控制如何从外部世界访问 HTTP 服务。例如,我们的应用由两个后端服务和一个前端应用组成,这三个应用都需要通过 HTTP 公开访问。虽然可以为这些组件中的每一个创建单独的LoadBalancer服务,但这将导致这三个服务中的每一个都接收自己的 IP 地址并需要自己的主机名(例如,在https://myevents.example上为前端应用提供服务,在https://events.myevents.examplehttps://bookings.myevents.example上为两个后端服务)。使用起来可能会很麻烦,在许多微服务架构中,通常需要为外部 API 访问提供一个入口点。使用入口,我们可以声明到服务映射的路径,例如,使所有后端服务可以在https://api.myevents.example处访问。

https://github.com/kubernetes/ingress/blob/master/controllers/nginx/README.md.

在使用入口资源之前,您需要为 Kubernetes 群集启用入口控制器。这是非常具体的个人环境;一些云提供商提供处理 Kubernetes 入口流量的特殊解决方案,而在其他环境中,您需要运行自己的解决方案。然而,使用 Minikube,启用入口是一个简单的命令:

$ minikube addons enable ingress 

相反,如果您打算在 Kubernetes 上运行自己的入口控制器,请查看 NGINX 入口控制器的官方文档。开始时它可能看起来很复杂,但正如许多内部 Kubernetes 服务一样,入口控制器也只包含部署和服务资源。

在 Minikube 中启用入口控制器后,您的 Minikube VM 将开始响应端口80443上的 HTTP 请求。要确定需要连接到哪个 IP 地址,请运行minikube ip命令。

为了让开放世界能够访问我们的服务,请在新文件ingress.yaml中创建一个新的 Kubernetes 资源,该文件包含以下内容:

apiVersions: extensions/v1beta1 
kind: Ingress 
metadata: 
  name: myevents 
spec: 
  rules: 
  - host: api.myevents.example 
    http: 
      paths: 
      - path: /events 
        backend: 
          serviceName: events 
          servicePort: 80 
      - path: /bookings 
        backend: 
          serviceName: bookings 
          servicePort: 80 
  - host: www.myevents.example 
    http: 
      paths: 
      - backend: 
          serviceName: frontend 
          servicePort: 80 

使用kubectl apply -f ingress.yaml创建入口资源。当然,myevents.example域不会公开访问(这是.example顶级域的全部要点);因此,要实际测试此设置,您可以在主机文件中添加一些条目(macOS 和 Linux 上为/etc/hosts;Windows 上为C:\Windows\System32\drivers\etc\hosts

192.168.99.100 api.myevents.example 
192.168.99.100 www.myevents.example

通常,192.168.99.100应该是 Minikube VM 的(仅本地可路由的)IP 地址。与minikube ip命令的输出进行交叉核对,确认无误。

总结

在本章中,您学习了如何使用容器技术(如 Docker)将应用(包括其所有依赖项)打包到容器映像中。您学习了如何从应用构建容器映像,并将它们部署到基于 Kubernetes 构建的生产容器环境中。

我们将在第 9 章中回到构建容器映像,在这里您将学习如何进一步自动化容器构建工具链,使您能够完全自动化应用部署,从 git push 命令开始,到 Kubernetes 云中运行更新的容器映像结束。

到目前为止,我们对云是相当不可知的。到目前为止,我们看到的每一个示例都将在任何主要的公共或私有云中工作,无论是 AWS、Azure、GCE 还是 OpenStack。事实上,容器技术通常被认为是从云提供商的个人怪癖中抽象出来并避免(潜在成本高昂的)供应商锁定的一种极好的方法。

在接下来的两章中,所有这些都将发生变化,我们将关注一个主要的云提供商——亚马逊网络服务AWS)。您将了解这些提供者的复杂性,如何将 MyEvents 应用部署到这些平台上,以及如何使用它们提供的独特功能。