原文:
annas-archive.org/md5/951792ab738574a4713e2995dc6f7c0c
译者:飞龙
第十六章:16
介绍 Kubernetes
本章介绍当前最流行的容器编排工具。它介绍了用于定义和运行分布式、弹性、稳健和高度可用应用程序的 Kubernetes 核心对象。最后,它介绍了 minikube 作为本地部署 Kubernetes 应用程序的一种方式,以及 Kubernetes 与 Docker Desktop 的集成。
我们将讨论以下主题:
-
理解 Kubernetes 架构
-
Kubernetes 主节点
-
集群节点
-
Play with Kubernetes 简介
-
Kubernetes 在 Docker Desktop 中的支持
-
Pod 简介
-
Kubernetes ReplicaSets
-
Kubernetes 部署
-
Kubernetes 服务
-
基于上下文的路由
-
比较 SwarmKit 和 Kubernetes
阅读完本章后,你应该掌握以下技能:
-
在餐巾纸上草拟 Kubernetes 集群的高级架构
-
解释 Kubernetes Pod 的三到四个主要特性
-
用两到三句话描述 Kubernetes ReplicaSets 的作用
-
解释 Kubernetes 服务的两到三个主要职责
-
在 minikube 中创建一个 Pod
-
配置 Docker Desktop 以使用 Kubernetes 作为编排工具
-
在 Docker Desktop 中创建一个 Deployment
-
创建一个 Kubernetes 服务来暴露应用程序服务(内部或外部)到集群中
技术要求
在本章中,如果你想跟随代码示例,你需要安装 Docker Desktop 和一个代码编辑器——最好是 Visual Studio Code:
-
请导航到你克隆示例代码库的文件夹。通常,这应该是
~/The-Ultimate-Docker-Container-Book
:$ cd ~/The-Ultimate-Docker-Container-Book
-
创建一个名为
ch16
的新子文件夹,并进入该文件夹:$ mkdir ch16 && cd ch16
本章讨论的所有示例的完整解决方案可以在sample-solutions/ch16
文件夹中找到,或者直接访问 GitHub:github.com/PacktPublishing/The-Ultimate-Docker-Container-Book/tree/main/sample-solutions/ch16
。
理解 Kubernetes 架构
一个 Kubernetes 集群由一组服务器组成。这些服务器可以是虚拟机或物理服务器,后者也叫裸金属服务器。集群的每个成员都可以有两种角色之一。它要么是 Kubernetes 主节点,要么是(工作)节点。前者用于管理集群,而后者则运行应用程序工作负载。我将工作节点放在括号中,因为在 Kubernetes 术语中,只有在谈到运行应用程序工作负载的服务器时才会提到节点。但在 Docker 和 Swarm 的术语中,相当于工作节点。我认为“工作节点”这一概念更好地描述了服务器的角色,而不仅仅是一个简单的节点。
在一个集群中,你有一个小且奇数数量的主节点,以及根据需要的多个工作节点。小型集群可能只有几个工作节点,而更现实的集群可能有几十个甚至上百个工作节点。从技术上讲,集群可以拥有的工作节点数量没有限制。然而,实际上,当处理成千上万个节点时,某些管理操作可能会出现显著的性能下降。
在 Kubernetes 工作节点上,我们运行的是 Pods。这是一个在 Docker 或 Docker Swarm 中没有的概念。Pod 是 Kubernetes 集群中的原子执行单元。在很多情况下,一个 Pod 只包含一个容器,但一个 Pod 也可以由多个容器共同运行。我们将在本节稍后对 Pods 进行更详细的描述。
集群中的所有成员需要通过物理网络连接,所谓的底层网络。Kubernetes 为整个集群定义了一个平面网络。Kubernetes 本身并不提供任何网络实现,而是依赖于第三方的插件。
Kubernetes 只是定义了容器网络接口(CNI),并将实现留给其他人。CNI 非常简单。它规定,集群中运行的每个 Pod 必须能够与集群中任何其他 Pod 相互连接,而不会发生任何网络地址转换(NAT)。同样的要求也适用于集群节点和 Pods 之间,即,直接在集群节点上运行的应用程序或守护进程必须能够访问集群中的每个 Pod,反之亦然。
下图展示了 Kubernetes 集群的高级架构:
图 16.1 – Kubernetes 的高级架构图
上图的解释如下:
在顶部框中间,我们有一个 etcd
节点集群。etcd
节点是一个分布式键值存储,在 Kubernetes 集群中用于存储集群的所有状态。etcd
节点的数量必须是奇数,正如 Raft 一致性协议所要求的那样,该协议指定了哪些节点用于相互协调。我们谈论集群状态时,并不包括由运行在集群中的应用程序产生或消耗的数据。相反,我们指的是关于集群拓扑、运行的服务、网络设置、使用的密钥等所有信息。也就是说,这个 etcd
集群对整个集群至关重要,因此,我们永远不应在生产环境或任何需要高可用性的环境中仅运行单个 etcd
服务器。
然后,我们有一个 Kubernetes 主节点集群,它们也在彼此之间形成一个共识组,类似于etcd
节点。主节点的数量也必须是奇数。我们可以运行一个单主节点的集群,但在生产环境或关键任务系统中,我们绝不应该这样做。在这种情况下,我们应该始终至少有三个主节点。由于主节点用于管理整个集群,因此我们也在讨论管理平面。
主节点使用etcd
集群作为其后端存储。将负载均衡器(LB)放在主节点前面,并使用一个知名的完全限定域名(FQDN),如admin.example.com
,是一种良好的实践。所有用于管理 Kubernetes 集群的工具应该通过这个负载均衡器访问,而不是直接使用其中一个主节点的公共 IP 地址。这在前面图的左上方有展示。
在图的底部,我们有一个工作节点集群。节点的数量可以少到一个,并且没有上限。
Kubernetes 主节点和工作节点彼此通信。这是一种双向通信方式,不同于我们在 Docker Swarm 中看到的那种通信方式。在 Docker Swarm 中,只有管理节点与工作节点通信,而不会有反向通信。所有访问集群中运行的应用程序的入口流量都应该通过另一个负载均衡器。
这是应用程序负载均衡器或反向代理。我们永远不希望外部流量直接访问任何工作节点。
现在我们对 Kubernetes 集群的高层架构有了一个大致的了解,让我们更深入地探讨 Kubernetes 主节点和工作节点。
Kubernetes 主节点
Kubernetes 主节点用于管理 Kubernetes 集群。以下是这样的主节点的高层次图示:
图 16.2 – Kubernetes 主节点
在前面的图的底部,我们有基础设施,它可以是本地或云端的虚拟机,或者本地或云端的服务器(通常称为裸金属)。
目前,Kubernetes 主节点仅在 Linux 上运行。支持最流行的 Linux 发行版,如 RHEL、CentOS 和 Ubuntu。在这台 Linux 机器上,我们至少有以下四个 Kubernetes 服务在运行:
-
kubectl
用于管理集群和集群中的应用程序。 -
控制器:控制器,或者更准确地说是控制器管理器,是一个控制循环,它通过 API 服务器观察集群的状态并进行更改,尝试将当前状态或有效状态调整为所需状态,如果它们不同的话。
-
调度器:调度器是一项服务,它尽力在工作节点上调度 Pods,同时考虑各种边界条件,如资源需求、策略、服务质量要求等。
-
用于存储集群状态所有信息的
etcd
。更准确地说,作为集群存储的etcd
不一定需要与其他 Kubernetes 服务安装在同一个节点上。有时,Kubernetes 集群配置为使用独立的etcd
服务器集群,如图 16.1所示。但选择使用哪种变体是一个高级管理决策,超出了本书的范围。
我们至少需要一个主节点,但为了实现高可用性,我们需要三个或更多主节点。这与我们在学习 Docker Swarm 的管理节点时学到的非常相似。在这方面,Kubernetes 主节点相当于 Swarm 管理节点。
Kubernetes 主节点从不运行应用工作负载。它们的唯一目的是管理集群。Kubernetes 主节点构建了一个 Raft 共识组。Raft 协议是一种标准协议,通常用于一组成员需要做出决策的场景。它被许多知名的软件产品所使用,如 MongoDB、Docker SwarmKit 和 Kubernetes。有关 Raft 协议的更详细讨论,请参见进一步阅读部分中的链接。
在主节点上运行工作负载
有时,特别是在开发和测试场景中,使用单节点 Kubernetes 集群是有意义的,这样它自然就成了主节点和工作节点。但这种场景应该避免在生产环境中使用。
如前所述,Kubernetes 集群的状态存储在etcd
节点中。如果 Kubernetes 集群需要高可用性,则etcd
节点也必须配置为 HA 模式,通常意味着我们至少在不同的节点上运行三个etcd
实例。
我们再一次声明,整个集群状态存储在etcd
节点中。这包括所有关于集群节点的信息、所有 ReplicaSets、Deployments、Secrets、网络策略、路由信息等。因此,拥有一个强大的备份策略来保护这个键值存储至关重要。
现在,让我们看看将实际运行集群工作负载的节点。
集群节点
集群节点是 Kubernetes 调度应用工作负载的节点。它们是集群的工作马。一个 Kubernetes 集群可以有几个、几十个、几百个,甚至几千个集群节点。Kubernetes 从一开始就为高扩展性而构建。别忘了,Kubernetes 是以 Google Borg 为模型构建的,后者已经运行了数万个容器多年:
图 16.3 – Kubernetes 工作节点
工作节点——它是集群节点,就像主节点一样——可以在虚拟机、裸金属、内部部署或云中运行。最初,工作节点只能配置在 Linux 上。但自 Kubernetes 1.10 版本以来,工作节点也可以在 Windows Server 2010 或更高版本上运行。拥有包含 Linux 和 Windows 工作节点的混合集群是完全可以接受的。
在每个节点上,我们需要运行以下三项服务:
-
YAML
或JSON
格式,它们声明性地描述了一个 Pod。我们将在下一部分了解 Pod 是什么。PodSpecs
主要通过 API 服务器提供给 Kubelet。 -
从版本 1.9 起,
containerd
被用作容器运行时。在此之前,它使用的是 Docker 守护进程。还可以使用其他容器运行时,如rkt
或CRI-O
。容器运行时负责管理和运行 Pod 中的各个容器。 -
kube-proxy:最后是 kube-proxy。它作为一个守护进程运行,是一个简单的网络代理和负载均衡器,用于所有在该节点上运行的应用服务。
现在我们已经了解了 Kubernetes 的架构以及主节点和工作节点,接下来是介绍我们可以用来开发针对 Kubernetes 应用的工具。
Play with Kubernetes 介绍
Play with Kubernetes 是一个由 Docker 赞助的免费沙盒,用户可以在其中学习如何使用 Docker 容器并将其部署到 Kubernetes:
-
使用您的 GitHub 或 Docker 凭据登录。
-
成功登录后,通过点击屏幕左侧的**+ 添加新实例**按钮,创建第一个集群节点或实例。
-
按照屏幕上的指示创建您的 Kubernetes 沙盒集群的第一个主节点。
-
使用终端窗口中步骤 1中指示的命令初始化集群主节点。最好直接从那里复制命令。命令应如下所示:
$ kubeadm init --apiserver-advertise-address \ $(hostname -i) --pod-network-cidr 10.5.0.0/16
第一个命令参数使用主机名称来广告 Kubernetes API 服务器的地址,第二个命令定义了集群应使用的子网。
-
接下来,如控制台中的步骤 2 所示,在我们的 Kubernetes 集群中初始化网络(注意,以下命令应为单行):
$ kubectl apply -f https://raw.githubusercontent.com/cloudnativelabs/kube-router/master/daemonset/kubeadm-kuberouter.yaml
-
通过再次点击添加新实例按钮,创建第二个集群节点。
-
一旦节点准备就绪,运行在步骤 4中输出的
join
命令,其中<token-1>
和<token-2>
是特定于您集群的:$ kubeadm join 192.168.0.13:6443 --token <token-1> \> --discovery-token-ca-cert-hash <token-2>
最好直接从 Play with Kubernetes 中的命令行复制正确的命令。
-
一旦第二个节点加入集群,请在第一个节点上运行以下命令,该节点是您初始化集群的地方,用于列出新集群中的节点集合:
$ kubectl get nodes
输出应该类似于以下内容:
NAME STATUS ROLES AGE VERSIONnode1 Ready control-plane,master 6m28s v1.20.1
node2 Ready <none> 32s v1.20.1
请注意,撰写本文时,Play with Kubernetes 使用的是 Kubernetes 1.20.1 版本,这个版本现在已经比较旧了。目前可用的最新稳定版本是 1.27.x。但不用担心,我们示例使用的 1.20.x 版本已经足够。
现在,让我们尝试在这个集群上部署一个 pod。暂时不用担心 pod 是什么,我们将在本章后面详细讲解。此时,只需要按现状理解即可。
-
在你的章节代码文件夹中,创建一个名为
sample-pod.yaml
的新文件,并添加以下内容:apiVersion: v1kind: Podmetadata: name: nginx labels: app: nginxspec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 - containerPort: 443
-
现在,为了在 Play with Kubernetes 上运行前述的 pod,我们需要复制前面的
yaml
文件内容,并在我们集群的node1
上创建一个新文件: -
使用
vi
创建一个名为sample-pod.yaml
的新文件。 -
按下 I(字母 “i”)进入
vi
编辑器的插入模式。 -
将复制的代码片段用 Ctrl + V(或在 Mac 上使用 Command + V)粘贴到此文件中。
-
按下 Esc 键进入
vi
的命令模式。 -
输入
:wq
并按 Enter 键保存文件并退出vi
。
提示
为什么在示例中使用 Vi 编辑器?它是任何 Linux(或 Unix)发行版中都已安装的编辑器,因此始终可用。你可以在这里找到 Vi 编辑器的快速教程:www.tutorialspoint.com/unix/unix-vi-editor.htm
。
-
现在让我们使用名为
kubectl
的 Kubernetes CLI 来部署这个 pod。kubectl
CLI 已经安装在你 Play with Kubernetes 集群的每个节点上:$ kubectl create -f sample-pod.yaml
这样做会产生以下输出:
pod/nginx created
-
现在列出所有的 pods:
$ kubectl get pods
我们应该看到以下内容:
NAME READY STATUS RESTARTS AGEnginx 1/1 Running 0 51s
-
为了能够访问这个 pod,我们需要创建一个 Service。让我们使用
sample-service.yaml
文件,其中包含以下内容:apiVersion: v1kind: Servicemetadata: name: nginx-servicespec: type: NodePort selector: app: nginx ports: - name: nginx-port protocol: TCP port: 80 targetPort: http-web-svc
再次提醒,不必担心此时Service究竟是什么,我们稍后会解释。
-
让我们创建这个 Service:
$ kubectl create -f sample-service.yaml
-
现在让我们看看 Kubernetes 创建了什么,并列出集群上定义的所有服务:
$ kubectl get services
我们应该看到类似这样的内容:
图 16.4 – 服务列表
请注意 PORT(S)
列。在我的情况下,Kubernetes 将 Nginx 的 80
容器端口映射到 31384
节点端口。我们将在下一条命令中使用这个端口。确保你使用的是系统上分配的端口号!
-
现在,我们可以使用
curl
访问该服务:$ curl -4 http://localhost:31384
我们应该收到 Nginx 欢迎页面作为回应。
-
在继续之前,请删除你刚才创建的两个对象:
$ kubectl delete po/nginx$ kubectl delete svc/nginx-service
请注意,在前述命令中,po
快捷方式相当于 pod
或 pods
。kubectl
工具非常灵活,允许使用这样的缩写。同样,svc
是 service
或 services
的缩写。
在接下来的部分,我们将使用 Docker Desktop 及其对 Kubernetes 的支持,运行与本部分相同的 pod 和服务。
Docker Desktop 中的 Kubernetes 支持
从 18.01-ce 版本开始,Docker Desktop 开始支持开箱即用的 Kubernetes。开发人员如果希望将容器化应用程序部署到 Kubernetes 中,可以使用这个编排工具,而不是 SwarmKit。Kubernetes 支持默认是关闭的,需要在设置中启用。第一次启用 Kubernetes 时,Docker Desktop 需要一些时间来下载创建单节点 Kubernetes 集群所需的所有组件。与 minikube(它也是单节点集群)不同,Docker 工具提供的版本使用了所有 Kubernetes 组件的容器化版本:
图 16.5 – Docker Desktop 中的 Kubernetes 支持
上述图表大致展示了 Kubernetes 支持是如何被添加到 Docker Desktop 中的。macOS 上的 Docker Desktop 使用 hyperkit 来运行基于 LinuxKit 的虚拟机。Windows 上的 Docker Desktop 使用 Hyper-V 来实现这一结果。在虚拟机内部,安装了 Docker 引擎。引擎的一部分是 SwarmKit,它启用了 Swarm 模式。Docker Desktop 使用 kubeadm
工具在虚拟机中设置和配置 Kubernetes。以下三个事实值得一提:Kubernetes 将其集群状态存储在 etcd
中;因此,我们在这个虚拟机上运行了 etcd
。接着,我们有构成 Kubernetes 的所有服务,最后,还有一些支持从 Docker CLI 部署 Docker 堆栈到 Kubernetes 的服务。这个服务不是 Kubernetes 官方发行版的一部分,但它是特定于 Docker 的。
所有 Kubernetes 组件都在 LinuxKit 虚拟机中的容器中运行。这些容器可以通过 Docker Desktop 中的设置进行隐藏。稍后我们将在本节中提供一份完整的 Kubernetes 系统容器列表,前提是你已启用 Kubernetes 支持。
启用 Kubernetes 的 Docker Desktop 相对于 minikube 的一个大优势是,前者允许开发人员使用单一工具构建、测试和运行面向 Kubernetes 的容器化应用程序。甚至可以使用 Docker Compose 文件将多服务应用部署到 Kubernetes 中。
现在让我们动手操作:
- 首先,我们需要启用 Kubernetes。在 macOS 上,点击菜单栏中的 Docker 图标。在 Windows 上,前往任务栏并选择首选项。在弹出的对话框中,选择Kubernetes,如以下截图所示:
图 16.6 – 在 Docker Desktop 中启用 Kubernetes
-
然后,勾选启用 Kubernetes复选框。还需要勾选**显示系统容器(**高级)**复选框。
-
然后,点击应用并重启按钮。安装和配置 Kubernetes 需要几分钟时间。是时候休息一下,享受一杯好茶了。
-
安装完成后(Docker 会通过在
kubectl
中显示绿色状态图标来通知我们),以便访问后者。 -
首先,让我们列出我们拥有的所有上下文。我们可以使用以下命令来完成:
$ kubectl config get-contexts
在作者的笔记本电脑上,我们得到以下输出:
图 16.7 - kubectl 的上下文列表
在这里,我们可以看到,在作者的笔记本电脑上,我们有三个上下文,其中两个来自于他使用kind
。目前,名为kind-demo
的kind
上下文仍然处于活动状态,通过CURRENT
列中的星号标记。
-
我们可以使用以下命令切换到
docker-desktop
上下文:$ kubectl config use-context docker-desktop
执行此操作后,会得到以下输出:
Switched to context "docker-desktop"
-
现在我们可以使用
kubectl
访问 Docker Desktop 刚创建的集群:$ kubectl get nodes
我们应该看到类似以下的内容:
NAME STATUS ROLES AGE VERSIONnode1 Ready control-plane 6m28s v1.25.9
好的,这看起来很熟悉。它与我们在使用 Play with Kubernetes 时看到的几乎相同。作者的 Docker Desktop 使用的 Kubernetes 版本是 1.25.9。我们还可以看到节点是一个master
节点,由control-plane
角色指示。
-
如果我们列出当前在 Docker Desktop 上运行的所有容器,我们会得到以下截图所示的列表(注意,我们使用了
--format
参数来输出容器 ID 和容器名称):$ docker container list --format "table {{.ID}\t{{.Names}}"
这将导致以下输出:
图 16.8 - Kubernetes 系统容器列表
在前面的列表中,我们可以识别出所有组成 Kubernetes 的现在熟悉的组件,如下所示:
-
API 服务器
-
etcd
-
kube-proxy
-
DNS 服务
-
kube-controller
-
kube-scheduler
通常,我们不希望将这些系统容器混入我们的容器列表中。因此,我们可以在 Kubernetes 的设置中取消选中**显示系统容器(高级)**复选框。
现在,让我们尝试将 Docker Compose 应用程序部署到 Kubernetes。
-
进入我们
~/``The-Ultimate-Docker-Container-Book
文件夹的ch16
子文件夹。 -
将
docker-compose.yml
文件从示例解决方案复制到此位置:$ cp ../sample-solutions/ch16/docker-compose.yml .
-
按照 https://kompose.io/installation/上的说明,在你的机器上安装
kompose
工具:-
在 Mac 上,可以通过
$ brew
install kompose
安装 -
在 Windows 上,使用
$ choco
install kubernetes-kompose
-
-
按照以下方式运行
kompose
工具:$ kompose convert
该工具应该创建四个文件:
-
db-deployment.yaml
-
pets-data-persistentvolumeclaim.yaml
-
web-deployment.yaml
-
web-service.yaml
-
打开
web-service.yaml
文件,在第 11 行(spec
条目)后,添加NodePort
条目类型,使其如下所示:...spec: type: NodePort ports: - name: "3000"...
-
现在我们可以使用
kubectl
将这四个资源部署到我们的 Kubernetes 集群:$ kubectl apply –f '*.yaml'
我们应该看到这个:
deployment.apps/db createdpersistentvolumeclaim/pets-data created
deployment.apps/web created
service/web created
-
我们需要找出 Kubernetes 将
3000
服务端口映射到哪个主机端口。使用以下命令来实现:$ kubectl get service
你应该看到类似以下内容:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10d
web NodePort 0.111.98.154 <none> 3000:32134/TCP 5m33s
在我的例子中,我们可以看到服务 web 将 3000
端口映射到 32134
主机(或节点)端口。在下面的命令中,我必须使用这个端口。在你的情况下,端口号可能会不同。使用你从上一条命令中得到的数字!
-
我们可以使用
curl
测试应用程序:$ curl localhost:32134/pet
我们将看到它按预期运行:
图 16.9 – 宠物应用程序在 Docker Desktop 上的 Kubernetes 环境中运行
现在,让我们看看在前面的部署之后,Kubernetes 上到底有哪些资源。
-
我们可以使用
kubectl
来查看:$ kubectl get all
这给我们带来了以下输出:
图 16.10 – 列出 Docker stack deploy 创建的所有 Kubernetes 对象
Docker 为 web
服务和 db
服务创建了一个 Deployment。它还自动为 web
创建了一个 Kubernetes 服务,以便在集群内访问。
这可以说相当酷,极大地减少了面向 Kubernetes 作为编排平台的团队在开发过程中遇到的摩擦。
-
在继续之前,请从集群中删除该堆栈:
$ kubectl delete –f '*.yaml'
现在,我们已经了解了可以用来开发最终将在 Kubernetes 集群中运行的应用程序的工具,接下来是时候了解所有重要的 Kubernetes 对象,这些对象用于定义和管理这样的应用程序。我们将从 pod 开始。
Pod 介绍
与 Docker Swarm 中的可能性相反,你不能直接在 Kubernetes 集群中运行容器。在 Kubernetes 集群中,你只能运行 pod。Pod 是 Kubernetes 中 Deployment 的基本单元。一个 pod 是一个或多个共址容器的抽象,这些容器共享相同的内核命名空间,例如网络命名空间。Docker SwarmKit 中没有类似的概念。多个容器可以共址并共享相同的网络命名空间是一个非常强大的概念。下图展示了两个 pod:
图 16.11 – Kubernetes pods
在前面的图示中,我们有两个 pod,10.0.12.3
和 10.0.12.5
。这两个 pod 都是由 Kubernetes 网络驱动管理的私有子网的一部分。
一个 pod 可以包含一个或多个容器。所有这些容器共享相同的 Linux 内核命名空间,特别是它们共享网络命名空间。这一点通过围绕容器的虚线矩形表示。由于在同一个 pod 中运行的所有容器共享网络命名空间,每个容器需要确保使用自己的端口,因为在一个网络命名空间中不允许重复端口。在这种情况下,在 Pod 1 中,主容器使用的是 80
端口,而辅助容器使用的是 3000
端口。
来自其他 Pod 或节点的请求可以使用 Pod 的 IP 地址结合相应的端口号来访问单个容器。例如,你可以通过 10.0.12.3:80
访问运行在 Pod 1 主容器中的应用程序。
比较 Docker 容器和 Kubernetes Pod 网络
现在,让我们比较 Docker 的容器网络与 Kubernetes 的 Pod 网络。在下面的图示中,左边是 Docker,右边是 Kubernetes:
图 16.12 – Pod 中共享同一网络命名空间的容器
当创建 Docker 容器且未指定特定网络时,Docker Engine 会创建一个虚拟以太网(veth
)端点。第一个容器获得 veth0
,下一个获得 veth1
,以此类推。这些虚拟以太网端点连接到 Docker 在安装时自动创建的 Linux 桥接器 docker0
。流量从 docker0
桥接器路由到每个连接的 veth
端点。每个容器都有自己的网络命名空间。没有两个容器使用相同的命名空间。这是故意的,目的是将容器内运行的应用程序彼此隔离。
对于 Kubernetes Pod,情况则不同。当创建一个新 Pod 时,Kubernetes 首先创建一个所谓的 pause
容器,其目的是创建和管理 Pod 将与所有容器共享的命名空间。除此之外,它没有做任何实际的工作;它只是处于休眠状态。pause
容器通过 veth0
连接到 docker0
桥接器。任何后续加入 Pod 的容器都会使用 Docker Engine 的特殊功能,允许它重用现有的网络命名空间。实现的语法如下所示:
$ docker container create --net container:pause ...
重要部分是 --net
参数,其值为 container:<container name>
。如果我们以这种方式创建一个新容器,那么 Docker 不会创建一个新的 veth
端点;该容器将使用与暂停容器相同的 veth
端点。
多个容器共享相同网络命名空间的另一个重要后果是它们相互通信的方式。我们来考虑以下情况:一个 Pod 中包含两个容器,一个监听 80
端口,另一个监听 3000
端口:
图 16.13 – Pod 中的容器通过 localhost 进行通信
当两个容器使用相同的 Linux 内核网络命名空间时,它们可以通过 localhost
相互通信,类似于当两个进程在同一主机上运行时,它们也可以通过 localhost
进行通信。
这一点在前面的图示中得到了说明。从 main
容器中,容器化的应用程序可以通过 http://localhost:3000 访问支持容器内运行的服务。
共享网络命名空间
在了解了这些理论之后,你可能会想知道 Kubernetes 实际上是如何创建一个 Pod 的。
Kubernetes 仅使用 Docker 提供的功能。那么,这种网络命名空间共享是如何工作的呢?首先,Kubernetes 创建了前面提到的所谓 pause
容器。
这个容器的唯一作用就是为该 Pod 保留内核命名空间,并保持它们的存活,即使 Pod 内没有其他容器运行。接下来,让我们模拟创建一个 Pod。我们从创建 pause 容器开始,并使用 Nginx 来实现:
$ docker container run –detach \ --name pause nginx:alpine
现在我们添加第二个容器,命名为 main
,并将其连接到与 pause 容器相同的网络命名空间:
$ docker container run --name main \ -d -it \ --net container:pause \
alpine:latest ash
由于 pause
和示例 containers
都是同一网络命名空间的一部分,它们可以通过 localhost
互相访问。为了证明这一点,我们必须进入主容器执行操作:
$ docker exec -it main /bin/sh
现在我们可以测试连接到运行在 pause 容器中并监听 80
端口的 Nginx。使用 wget
工具进行测试时,我们会得到如下结果:
/ # wget -qO – localhost
这样做会给我们以下输出:
图 16.14 – 两个容器共享相同的网络命名空间
输出结果显示我们确实可以在 localhost
上访问 Nginx。这证明了这两个容器共享相同的命名空间。如果这还不够,我们可以使用 ip
工具在两个容器中显示 eth0
,并且会得到完全相同的结果,具体来说,就是相同的 IP 地址,这是 Pod 的一个特征:所有容器共享相同的 IP 地址:
/ # ip a show eth0
这将显示以下输出:
图 16.15 – 使用 ip 工具显示 eth0 的属性
我们使用以下命令检查 bridge
网络:
$ docker network inspect bridge
之后,我们可以看到只列出了 pause 容器:
[ {
"Name": "bridge",
"Id": "c7c30ad64...",
"Created": "2023-05-18T08:22:42.054696Z",
"Scope": "local",
"Driver": "bridge",
…
"Containers": {
"b7be6946a9b...": {
"Name": "pause",
"EndpointID": "48967fbec...",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
...
}
]
上面的输出已被简化以提高可读性。
由于 main 容器复用了 pause 容器的端点,因此它没有在 Containers
列表中出现。
在继续之前,请删除两个 pause
和 main
容器:
$ docker container rm pause main
接下来,我们将讨论 Pod 的生命周期。
Pod 生命周期
本书前面提到过,容器有生命周期。容器首先初始化,运行,然后最终退出。当容器退出时,它可以通过退出代码零优雅地退出,或者通过非零退出代码终止,后者相当于发生了错误。
同样,Pod 也有生命周期。由于一个 Pod 可以包含多个容器,因此其生命周期比单一容器的生命周期稍微复杂一些。Pod 的生命周期可以在下图中看到:
图 16.16 – Kubernetes Pod 的生命周期
当 Pod 在集群节点上创建时,它首先进入 待处理 状态。一旦 Pod 的所有容器都启动并运行,Pod 就进入 运行中 状态。只有当所有容器成功运行时,Pod 才会进入此状态。如果要求 Pod 终止,它将请求所有容器终止。如果所有容器以退出代码零终止,则 Pod 进入 成功 状态。这是理想路径。
现在,让我们看看一些导致 Pod 处于 失败 状态的场景。可能有三种情况:
-
如果在 Pod 启动期间,至少有一个容器无法运行并失败(即它退出时返回非零退出代码),则 Pod 会从待处理状态转入失败状态。
-
如果 Pod 处于 运行中 状态,而其中一个容器突然崩溃或以非零退出代码退出,则 Pod 将从 运行中 状态转换为 失败 状态。
-
如果要求 Pod 终止,并且在关闭过程中,至少有一个容器以非零退出代码退出,则 Pod 也会进入 失败 状态。
现在让我们看看 Pod 的规范。
Pod 规范
在 Kubernetes 集群中创建 Pod 时,我们可以使用命令式或声明式的方法。我们在本书前面已经讨论过这两种方法的区别,但为了重新表述最重要的方面,使用声明式方法意味着我们编写一个描述我们想要实现的最终状态的清单。我们将省略协调器的细节。我们想要实现的最终状态也被称为期望状态。一般来说,声明式方法在所有成熟的协调器中都受到强烈推荐,Kubernetes 也不例外。
因此,在本章中,我们将专注于声明式方法。Pod 的清单或规范可以使用 YAML
或 JSON
格式编写。在本章中,我们将专注于 YAML
,因为它对我们人类来说更易于阅读。让我们来看一个示例规范。以下是 pod.yaml
文件的内容,该文件可以在我们 labs
文件夹的 ch16
子文件夹中找到:
apiVersion: v1kind: Pod
metadata:
name: web-pod
spec:
containers:
- name: web
image: nginx:alpine
ports:
- containerPort: 80
Kubernetes 中的每个规范都以版本信息开头。Pod 已经存在了一段时间,因此 API 版本是 v1
。第二行指定了我们想要定义的 Kubernetes 对象或资源类型。显然,在这个例子中,我们想要指定一个 pod。接下来是一个包含元数据的块。最基本的要求是给 pod 起个名字。这里我们称其为 web-pod
。接下来的块是 spec
块,包含 pod 的规范。最重要的部分(也是这个简单示例中唯一的部分)是列出所有属于该 pod 的容器。我们这里只有一个容器,但也可以有多个容器。我们为容器选择的名字是 web
,容器镜像是 nginx:alpine
。最后,我们定义了容器暴露的端口列表。
一旦我们编写了这样的规范,就可以使用 Kubernetes CLI kubectl
将其应用到集群中:
-
打开一个新的终端窗口,导航到
ch16
子文件夹:$ cd ~/The-Ultimate-Docker-Contianer-Book/ch16
-
在这个示例中,我们将使用 Docker Desktop 的 Kubernetes 集群。因此,确保你正在使用正确的
kubectl
CLI 上下文:$ kubectl config use-context docker-desktop
这将切换上下文到由 Docker Desktop 提供的 Kubernetes 集群。
-
在此文件夹中,创建一个名为
pod.yml
的新文件,并将提到的 pod 规范添加到该文件中。保存该文件。 -
执行以下命令:
$ kubectl create -f pod.yaml
这将回应 pod "web-pod" created
。
-
然后我们可以列出集群中的所有 pod:
$ kubectl get pods
这样做将为我们提供以下输出:
NAME READY STATUS RESTARTS AGEweb-pod 1/1 Running 0 2m
正如预期的那样,我们有一个处于 Running
状态的 pod,名称为 web-pod
,正如定义的那样。
-
我们可以通过使用
describe
命令来获取有关运行中的 pod 的更详细信息:$ kubectl describe pod/web-pod
这会给我们类似这样的输出:
图 16.17 – 描述运行在集群中的 pod
注意
前面部分的 pod/web-pod
表示法包括 describe
命令。其他变体也是可能的。例如,pods/web-pod
、po/web-pod
、pod
和 po
都是 pods
的别名。
kubectl
工具定义了许多别名,以使我们的生活更加轻松。
describe
命令为我们提供了大量有关 pod 的有价值的信息,其中之一是发生并影响该 pod 的事件列表。该列表会显示在输出的最后。
Containers
部分中的信息与我们在 docker container
inspect
输出中找到的非常相似。
我们还可以看到一个 Volumes
部分,其中有一个 Projected
条目类型。它包含集群的根证书作为机密。我们将在下一章讨论 Kubernetes 的机密。另一方面,卷将在接下来讨论。
Pod 和卷
在关于容器的章节中,我们了解了卷及其作用:访问和存储持久数据。由于容器可以挂载卷,因此 pod 也可以。实际上,真正挂载卷的是 pod 中的容器,但这只是一个语义上的细节。首先,让我们看看如何在 Kubernetes 中定义一个卷。Kubernetes 支持各种卷类型,因此我们不会深入探讨这个话题。
让我们通过定义一个名为my-data-claim
的PersistentVolumeClaim
声明,隐式地创建一个本地卷:
-
创建一个名为
volume-claim.yaml
的文件,并将以下规范添加到文件中:apiVersion: v1kind: PersistentVolumeClaimmetadata: name: my-data-claimspec: accessModes: - ReadWriteOnce resources: requests: storage: 2Gi
我们定义了一个请求 2GB 数据的声明。
-
让我们创建这个声明:
$ kubectl create -f volume-claim.yaml
这将产生如下输出:
persistentvolumeclaim/my-data-claim created
-
我们可以使用
kubectl
列出声明(pvc
是PersistentVolumeClaim
的快捷方式),命令如下:$ kubectl get pvc
这将产生如下输出:
图 16.18 – 集群中 PersistentStorageClaim 对象的列表
在输出中,我们可以看到该声明已经隐式地创建了一个名为pvc-<ID>
的卷。
-
在继续之前,请先移除该 pod:
$ kubectl delete pod/web-pod
或者,使用定义 pod 的原始文件,命令如下:
$ kubectl delete -f pod.yaml
我们现在可以在 pod 中使用该声明创建的卷了。让我们使用之前使用的修改版 pod 规范:
-
创建一个名为
pod-with-vol.yaml
的文件,并将以下规范添加到文件中:apiVersion: v1kind: Podmetadata: name: web-podspec: containers: - name: web image: nginx:alpine ports: - containerPort: 80 volumeMounts: - name: my-data mountPath: /data volumes: - name: my-data persistentVolumeClaim: claimName: my-data-claim
在最后四行的volumes
块中,我们定义了一个我们希望在该 pod 中使用的卷列表。我们在这里列出的卷可以被 pod 的任何容器使用。在我们的例子中,我们只有一个卷。我们指定了一个名为my-data
的卷,它是一个持久卷声明,声明名称就是我们刚刚创建的那个。
然后,在容器规范中,我们有volumeMounts
块,在这里我们定义了要使用的卷,以及容器内卷将挂载的(绝对)路径。在我们的例子中,我们将卷挂载到容器文件系统的/data
文件夹。
-
让我们创建这个 pod:
$ kubectl create -f pod-with-vol.yaml
我们也可以使用声明式的方式:
$ kubectl apply -f pod-with-vol.yaml
-
然后,我们可以
exec
进入容器,通过导航到/data
文件夹,创建一个文件并使用以下命令退出容器,以检查卷是否已经挂载:$ kubectl exec -it web-pod -- /bin/sh/ # cd /data/data # echo "Hello world!" > sample.txt/data # exit
如果我们没错的话,那么这个容器中的数据应该在 pod 的生命周期结束后仍然存在。
-
因此,让我们删除这个 pod:
$ kubectl delete pod/web-pod
-
然后,我们将重新创建它:
$ kubectl create -f pod-with-vol.yaml
-
然后,我们将
exec
进入 pod 的容器:$ kubectl exec -it web-pod -- ash
-
最后,我们输出数据:
/ # cat /data/sample.txt
这是前面命令产生的输出:
Hello world!
这是我们所预期的。
-
按Ctrl + D退出容器。
-
在继续之前,请删除 pod 和持久卷声明。到现在为止,你应该知道怎么做。如果不知道,请回头查看步骤 4。
现在我们已经对 Pods 有了较好的理解,让我们研究一下 ReplicaSets 如何帮助管理这些 Pods。
Kubernetes ReplicaSets
在一个对高可用性有要求的环境中,仅有一个 Pod 是远远不够的。如果 Pod 崩溃了怎么办?如果我们需要更新 Pod 内部的应用程序,但又不能承受任何服务中断怎么办?这些问题表明仅有 Pods 是不足够的,我们需要一个更高级的概念来管理多个相同的 Pod 实例。在 Kubernetes 中,ReplicaSet 用于定义和管理在不同集群节点上运行的多个相同 Pod 的集合。ReplicaSet 定义了容器在 Pod 中运行时使用的容器镜像,以及在集群中运行的 Pod 实例数量等。这些属性以及其他许多属性被称为期望状态。
ReplicaSet 负责始终确保实际状态与期望状态的一致性,如果实际状态偏离期望状态。以下是一个 Kubernetes ReplicaSet:
图 16.19 – Kubernetes ReplicaSet
在前面的示意图中,我们可以看到一个 ReplicaSet 管理着多个 Pods。这些 Pods 被称为pod-api
。ReplicaSet 负责确保在任何给定时间,始终有期望数量的 Pods 在运行。如果某个 Pod 因为某种原因崩溃,ReplicaSet 会在一个有空闲资源的节点上调度一个新的 Pod 替代它。如果 Pod 的数量超过了期望的数量,ReplicaSet 会杀死多余的 Pod。通过这种方式,我们可以说 ReplicaSet 保证了一个自愈且可扩展的 Pod 集合。ReplicaSet 可以包含的 Pod 数量没有上限。
ReplicaSet 规格
类似于我们对 Pods 的学习,Kubernetes 也允许我们以命令式或声明式的方式定义和创建 ReplicaSet。由于在大多数情况下声明式方法是最推荐的方式,我们将专注于这种方法。让我们来看看一个 Kubernetes ReplicaSet 的示例规格:
-
创建一个名为
replicaset.yaml
的新文件,并在其中添加以下内容:apiVersion: apps/v1kind: ReplicaSetmetadata: name: rs-webspec: selector: matchLabels: app: web replicas: 3 template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80
这看起来与我们之前介绍的 Pod 规格非常相似。那么,我们来集中注意其区别。首先,在第 2 行,我们看到的是kind
,之前是 Pod,现在是ReplicaSet
。接着,在第 6 到第 8 行,我们有一个选择器,它决定哪些 Pods 将成为 ReplicaSet 的一部分。在这个例子中,它选择所有标签为app
且值为web
的 Pods。然后,在第 9 行,我们定义了希望运行的 Pod 副本数量;在这个例子中是三个副本。最后,我们有template
部分,它首先定义了元数据,然后定义了规格,其中包含运行在 Pod 内部的容器。在我们的例子中,我们有一个使用nginx:alpine
镜像并暴露80
端口的单一容器。
其中真正重要的元素是副本数和选择器,选择器指定了由 ReplicaSet 管理的 Pod 集合。
-
让我们使用这个文件来创建 ReplicaSet:
$ kubectl create -f replicaset.yaml
这将产生以下结果:
replicaset "rs-web" created
-
现在我们列出集群中所有的 ReplicaSets(
rs
是 ReplicaSet 的快捷方式):$ kubectl get rs
我们得到了以下结果:
NAME DESIRED CURRENT READY AGErs-web 3 3 3 51s
在前面的输出中,我们看到有一个名为rs-web
的 ReplicaSet,其期望状态是三个(Pod)。当前状态也显示了三个 Pod,并告诉我们所有三个 Pod 都已准备就绪。
-
我们还可以列出系统中的所有 Pod:
$ kubectl get pods
这将生成以下输出:
NAME READY STATUS RESTARTS AGErs-web-nbc8m 1/1 Running 0 4m
rs-web-6bxn5 1/1 Running 0 4m
rs-web-lqhm5 1/1 Running 0 4m
在这里,我们可以看到我们预期的三个 Pod。Pod 的名称使用了ReplicaSet
的名称,并附加了一个唯一的 ID。在READY
列中,我们可以看到 Pod 中定义了多少个容器以及它们中有多少个已准备就绪。在我们的案例中,每个 Pod 只有一个容器,并且每个容器都已准备好。因此,Pod 的整体状态是Running
。我们还可以看到每个 Pod 被重启了多少次。在我们的例子中,没有任何 Pod 被重启。
接下来,让我们看看 ReplicaSet 是如何帮助我们实现自愈的。
自愈
现在,让我们通过随机杀死其中一个 Pod 来测试自愈 ReplicaSet 的魔力,并观察会发生什么:
-
让我们删除前面列表中的第一个 Pod。确保将 Pod 的名称(
rs-web-nbc8m
)替换为您自己示例中的名称:$ kubectl delete po/rs-web-nbc8m
上一个命令生成了以下输出:
pod "rs-web-nbc8m" deleted
-
现在,让我们再次列出所有 Pod。我们期望只看到两个 Pod,对吗?你错了:
NAME READY STATUS RESTARTS AGErs-web-4r587 1/1 Running 0 5srs-web-6bxn5 1/1 Running 0 4m30srs-web-lqhm5 1/1 Running 0 4m30
好的,显然,列表中的第一个 Pod 已经被重新创建,正如我们从AGE
列中看到的那样。这是自愈功能在起作用。
-
让我们看看描述 ReplicaSet 时会发现什么:
$ kubectl describe rs
这将给我们以下输出:
图 16.20 – 描述 ReplicaSet
结果,我们在Events
下找到了一个条目,告诉我们 ReplicaSet 创建了一个名为rs-web-4r587
的新 Pod。
-
在继续之前,请删除 ReplicaSet:
$ kubectl delete rs/rs-web
现在是时候讨论 Kubernetes 的 Deployment 对象了。
Kubernetes 部署
Kubernetes 非常重视单一职责原则。所有 Kubernetes 对象都被设计为执行一项任务,而且只执行这一项任务,而且它们的设计目标是非常出色地完成这项任务。在这方面,我们必须理解 Kubernetes 的 ReplicaSets 和 Deployments。正如我们所学,ReplicaSet 负责实现和协调应用服务的期望状态。这意味着 ReplicaSet 管理一组 Pod。
**部署(Deployment)**通过在 ReplicaSet 基础上提供滚动更新和回滚功能来增强 ReplicaSet。在 Docker Swarm 中,Swarm 服务结合了 ReplicaSet 和 Deployment 的功能。从这个角度来看,SwarmKit 比 Kubernetes 更加单体化。以下图示展示了 Deployment 与 ReplicaSet 的关系:
图 16.21 – Kubernetes 部署
在前面的图示中,ReplicaSet 定义并管理一组相同的 pods。ReplicaSet 的主要特点是自我修复、可扩展,并始终尽最大努力使其状态与期望状态一致。而 Kubernetes 部署(Deployment)则在此基础上增加了滚动更新和回滚功能。在这方面,Deployment 是 ReplicaSet 的封装对象。
我们将在 第十七章 中深入学习滚动更新和回滚,使用 Kubernetes 部署、更新和保护应用程序。
在接下来的章节中,我们将深入了解 Kubernetes 服务以及它们如何实现服务发现和路由。
Kubernetes 服务
一旦我们开始处理由多个应用服务组成的应用程序,就需要服务发现。以下图示说明了这个问题:
图 16.22 – 服务发现
在前面的图示中,我们有一个 Web API
服务,需要访问另外三个服务:payments
、shipping
和 ordering
。Web API
服务不应关心如何以及在哪里找到这三个服务。在 API 代码中,我们只需要使用我们想要访问的服务名称和其端口号。一个示例是以下 URL,payments:3000
,它用于访问 payments
服务的一个实例。
在 Kubernetes 中,支付应用服务由一个 ReplicaSet 的 pods 表示。由于高度分布式系统的特性,我们不能假设 pods 拥有稳定的端点。pod 可以随时出现或消失。如果我们需要从内部或外部客户端访问相应的应用服务,这将是个问题。如果我们不能依赖 pod 端点的稳定性,我们还能做什么呢?
这就是 Kubernetes 服务 发挥作用的地方。它们旨在为 ReplicaSets 或 Deployments 提供稳定的端点,如下所示:
图 16.23 – Kubernetes 服务为客户端提供稳定的端点
在前面的图示中,我们可以看到一个 Kubernetes 服务。它提供了一个可靠的集群级 IP 地址,也叫做 app=web
;也就是说,所有具有名为 app
且值为 web
的标签的 pod 都会被代理。
在接下来的章节中,我们将深入了解基于上下文的路由以及 Kubernetes 如何减轻这一任务。
基于上下文的路由
我们经常需要为 Kubernetes 集群配置基于上下文的路由。Kubernetes 提供了多种方式来实现这一点。目前,首选且最具可扩展性的方法是使用 IngressController。以下图示尝试说明这个 ingress 控制器是如何工作的:
图 16.24 – 使用 Kubernetes Ingress 控制器的基于上下文的路由
在前面的图中,我们可以看到当使用 IngressController(如 Nginx)时,基于上下文(或第七层)路由是如何工作的。在这里,我们有一个名为 web
的应用服务的部署。这个应用服务的所有 Pod 都有以下标签:app=web
。然后,我们有一个名为 web
的 Kubernetes 服务,它为这些 Pod 提供一个稳定的端点。该服务的虚拟 IP 是 52.14.0.13
,并且暴露了 30044
端口。也就是说,如果有请求到达 Kubernetes 集群的任何节点,并请求 web
名称和 30044
端口,那么这个请求会被转发到这个服务。然后,服务会将请求负载均衡到其中一个 Pod。
到目前为止,一切顺利,但如何将来自客户端的 ingress 请求路由到 http[s]://example.com/web
URL 并定向到我们的 Web 服务呢?首先,我们必须定义从基于上下文的请求到相应的 <服务名>/<端口>
请求的路由。这是通过 Ingress 对象实现的:
-
在 Ingress 对象中,我们将 Host 和 Path 定义为源,(服务) 名称和端口为目标。当 Kubernetes API 服务器创建这个 Ingress 对象时,作为 sidecar 运行的 IngressController 进程会拾取这个变化。
-
修改 Nginx 反向代理的配置文件。
-
通过添加新路由,要求 Nginx 重新加载其配置,因此,它将能够正确地将任何传入的请求路由到
http[s]://example.com/web
。
在下一节中,我们将通过对比每种调度引擎的一些主要资源,来比较 Docker SwarmKit 和 Kubernetes。
比较 SwarmKit 和 Kubernetes
现在我们已经了解了 Kubernetes 中一些最重要资源的许多细节,接下来比较这两种调度器 SwarmKit 和 Kubernetes 时,通过匹配重要资源来帮助理解。让我们来看看:
SwarmKit | Kubernetes | 描述 |
---|---|---|
Swarm | 集群 | 由各自的调度器管理的服务器/节点集。 |
节点 | 集群成员 | 作为 Swarm/集群成员的单个主机(物理或虚拟)。 |
管理节点 | 主节点 | 管理 Swarm/集群的节点。这是控制平面。 |
工作节点 | 节点 | 运行应用工作负载的 Swarm/集群成员。 |
容器 | 容器** | 运行在节点上的容器镜像实例。**注:在 Kubernetes 集群中,我们不能直接运行容器。 |
任务 | Pod | 运行在节点上的服务实例(Swarm)或副本集(Kubernetes)。一个任务管理一个容器,而一个 Pod 包含一个或多个容器,这些容器共享相同的网络命名空间。 |
服务 | 副本集 | 定义并协调由多个实例组成的应用服务的期望状态。 |
服务 | 部署 | 部署是带有滚动更新和回滚功能的 ReplicaSet。 |
路由网格 | 服务 | Swarm 路由网格提供基于 IPVS 的 L4 路由和负载均衡。Kubernetes 服务是一个抽象,定义了一组逻辑上的 pods 和一种可用于访问它们的策略。它是一个稳定的端点,指向一组 pods |
堆栈 | 堆栈** | 应用程序的定义由多个(Swarm)服务组成。**注意:虽然堆栈在 Kubernetes 中并不是原生支持的,但 Docker 工具 Docker Desktop 会将它们转换为 Kubernetes 集群的部署 |
网络 | 网络策略 | Swarm 软件定义网络(SDNs)用于防火墙容器。Kubernetes 只定义了一个单一的扁平网络。除非显式定义网络策略来约束 pod 之间的通信,否则每个 pod 都可以访问其他 pod 和/或节点 |
这就结束了我们对 Kubernetes 的介绍,它目前是最流行的容器编排引擎。
总结
在本章中,我们学习了 Kubernetes 的基础知识。我们概述了其架构,并介绍了用于在 Kubernetes 集群中定义和运行应用程序的主要资源。我们还介绍了 minikube 和 Docker Desktop 中的 Kubernetes 支持。
在下一章中,我们将把应用程序部署到 Kubernetes 集群中。然后,我们将使用零停机策略更新该应用程序的某个服务。最后,我们将使用密钥对在 Kubernetes 中运行的应用程序服务进行敏感数据的加密。敬请期待!
进一步阅读
以下是包含有关我们在本章中讨论的各种主题的详细信息的文章列表:
-
Raft 共识 算法:
raft.github.Io/
-
Kubernetes 文档:
kubernetes.io/docs/home/
问题
请回答以下问题以评估您的学习进度:
-
Kubernetes 集群的高层次架构是什么?
-
用几句话简要解释 Kubernetes master 的角色。
-
列出每个 Kubernetes(工作节点)节点上需要具备的元素。
-
我们无法在 Kubernetes 集群中运行单独的容器。
-
正确
-
错误
-
-
Kubernetes pod 的三个主要特性是什么?
-
解释为什么 pod 中的容器可以使用
localhost
相互通信。 -
pod 中所谓的
pause
容器的作用是什么? -
Bob 告诉你:“我们的应用程序由三个 Docker 镜像组成:
web
、inventory
和db
。由于我们可以在 Kubernetes pod 中运行多个容器,所以我们打算将应用程序的所有服务部署到一个 pod 中。”列出三到四个原因,解释为什么这是一个不好的主意。 -
用您自己的话解释为什么我们需要 Kubernetes ReplicaSets。
-
在什么情况下我们需要 Kubernetes Deployments?
-
Kubernetes 服务的主要职责是什么?
-
列出至少三种 Kubernetes 服务类型,并解释它们的目的及其差异。
-
如何创建一个 Kubernetes 服务,将应用程序服务内部暴露在集群中?
答案
以下是本章中提出问题的一些示例答案:
-
Kubernetes 集群由控制平面(Kubernetes Master)和多个工作节点组成。控制平面负责维持集群的期望状态,例如正在运行的应用程序和它们使用的容器镜像。工作节点是应用程序部署和运行的服务器。
-
Kubernetes master 负责管理集群。所有创建对象、重新调度 Pod、管理 ReplicaSet 等请求都发生在 master 上。master 不在生产环境或类似生产环境的集群中运行应用程序工作负载。
-
在每个工作节点上,我们有 kubelet、代理和容器运行时。
-
答案是 A. 正确。你不能在 Kubernetes 集群上运行独立的容器。Pod 是该集群中部署的最小单元。
-
Kubernetes Pod 是 Kubernetes 中最小的可部署单元。它可以运行一个或多个共址的容器。以下是三个主要特点:
-
一个 Pod 可以封装多个紧密耦合且需要共享资源的容器。
-
Pod 中的所有容器共享相同的网络命名空间,这意味着它们可以使用
localhost
相互通信。 -
每个 Pod 在集群内都有一个独特的 IP 地址。
-
-
所有在 Pod 内运行的容器共享相同的 Linux 内核网络命名空间。因此,这些容器内运行的所有进程可以通过
localhost
互相通信,类似于在主机上直接运行的进程或应用程序如何通过localhost
进行通信。 -
pause
容器的唯一作用是为在 Pod 中运行的容器保留命名空间。 -
这是一个不好的想法,因为一个 Pod 的所有容器是共址的,这意味着它们运行在同一个集群节点上。而且,如果多个容器运行在同一个 Pod 中,它们只能一起扩展或缩减。然而,应用程序的不同组件(即
web
、inventory
和db
)通常在可扩展性或资源消耗方面有非常不同的需求。web
组件可能需要根据流量进行扩展和缩减,而db
组件则有其他组件没有的存储特殊需求。如果我们将每个组件都运行在各自的 Pod 中,我们在这方面会更具灵活性。 -
我们需要一种机制来在集群中运行多个 Pod 实例,并确保实际运行的 Pod 数量始终与期望数量相符,即使个别 Pod 由于网络分区或集群节点故障而崩溃或消失。ReplicaSet 是提供任何应用程序服务可扩展性和自愈能力的机制。
-
当我们希望在 Kubernetes 集群中更新应用服务而不导致服务停机时,需要使用 Deployment 对象。Deployment 对象为 ReplicaSets 增加了滚动更新和回滚功能。
-
Kubernetes 服务是一种抽象方式,用于将运行在一组 Pods 上的应用暴露为网络服务。Kubernetes 服务的主要职责包括以下几点:
-
为一组 Pods 提供稳定的 IP 地址和 DNS 名称,帮助发现服务,并支持负载均衡。
-
路由网络流量,将其分发到一组 Pods 上,从而提供相同的功能。
-
如有必要,允许将服务暴露给外部客户端。
-
-
Kubernetes 服务对象用于使应用服务参与服务发现。它们为一组 Pods 提供稳定的端点(通常由 ReplicaSet 或 Deployment 管理)。Kubernetes 服务是定义逻辑 Pods 集合和访问策略的抽象。Kubernetes 服务有四种类型:
-
每个集群节点上的
30000
到32767
。 -
LoadBalancer:此类型通过云服务提供商的负载均衡器(如 AWS 上的 ELB)将应用服务暴露到外部。
-
ExternalName:当需要为集群的外部服务(例如数据库)定义代理时使用。
-
-
创建 Kubernetes 服务时,通常会创建一个服务配置文件(
YAML
或JSON
),该文件指定所需的服务类型(例如,ClusterIP 用于内部通信),以及选择器标签以识别目标 Pods 和网络流量的端口。然后使用kubectl apply
命令应用此文件。这将创建一个服务,将流量路由到匹配选择器标签的 Pods。
第十七章:17
使用 Kubernetes 部署、更新和保护应用
在上一章中,我们学习了关于容器编排器 Kubernetes 的基础知识。我们对 Kubernetes 的架构进行了概览,并了解了 Kubernetes 用来定义和管理容器化应用的许多重要对象。
在本章中,我们将学习如何将应用程序部署、更新和扩展到 Kubernetes 集群中。我们还将解释如何实现零停机部署,以便无干扰地更新和回滚关键任务应用。最后,我们将介绍 Kubernetes 秘密,作为配置服务和保护敏感数据的一种手段。
本章涵盖以下主题:
-
部署我们的第一个应用
-
定义存活性和就绪性
-
零停机部署
-
Kubernetes 秘密
完成本章后,你将能够完成以下任务:
-
将一个多服务应用部署到 Kubernetes 集群中
-
为你的 Kubernetes 应用服务定义存活探针和就绪探针
-
更新在 Kubernetes 中运行的应用服务,而不会造成停机
-
在 Kubernetes 集群中定义秘密
-
配置应用服务以使用 Kubernetes 秘密
技术要求
在本章中,我们将使用本地计算机上的 Docker Desktop。有关如何安装和使用 Docker Desktop 的更多信息,请参阅 第二章,设置工作环境。
本章的代码可以在这里找到:main/sample-solutions/ch17
。
请确保你已经按照 第二章 中描述的方式克隆了本书的 GitHub 仓库。
在你的终端中,导航到 ~/The-Ultimate-Docker-Container-Book
文件夹,并创建一个名为 ch17
的子文件夹并进入它:
$ mkdir ch17 & cd ch17
部署我们的第一个应用
我们将把我们的宠物应用——我们在 第十一章 中首次介绍的,使用 Docker Compose 管理容器——部署到 Kubernetes 集群中。我们的集群将使用 Docker Desktop,它提供了一个单节点的 Kubernetes 集群。然而,从部署的角度来看,集群的规模和集群位于云端、公司数据中心或你的工作站并不重要。
部署 Web 组件
提醒一下,我们的应用程序由两个应用服务组成:基于 Node 的 Web 组件和后台 PostgreSQL 数据库。在上一章中,我们学习了需要为每个我们想要部署的应用服务定义一个 Kubernetes 部署对象。我们将首先为 Web 组件执行此操作。和本书中一贯的做法一样,我们将选择声明式方式来定义我们的对象:
- 我们将使用由 Docker Desktop 提供的本地 Kubernetes 单节点集群。确保你的 Docker Desktop 安装中已启用 Kubernetes:
图 17.1 – 在 Docker Desktop 上运行 Kubernetes
- 在您的代码子文件夹(
ch17
)中,添加一个名为web-deployment.yaml
的文件,内容如下:
)
图 17.2 – web
组件的 Kubernetes 部署定义
前面的部署定义可以在sample-solutions/ch17
子文件夹中的web-deployment.yaml
文件中找到。它包含了部署web
组件所需的指令。代码行如下:
-
第 7 行:我们将
Deployment
对象的名称定义为web
。 -
第 9 行:我们声明希望运行一个
web
组件的实例。 -
第 11 到 13 行:通过
Selector
,我们定义了哪些 Pods 将成为我们部署的一部分,即那些具有app
和service
标签,且值分别为pets
和web
的 Pods。 -
第 14 行:在从第 11 行开始的 Pod 模板中,我们定义了每个 Pod 将应用
app
和service
标签。 -
从第 20 行开始:我们定义了将在 Pod 中运行的唯一容器。容器的镜像是我们熟悉的
fundamentalsofdocker/ch11-web:2.0
镜像,容器的名称将为web
。 -
第 23 行和第 24 行:值得注意的是,我们声明容器将端口
3000
暴露给传入流量。
-
请确保您已将
kubectl
的上下文设置为 Docker Desktop。有关如何设置的详细信息,请参见第二章,《设置工作环境》。使用以下命令:$ kubectl config use-context docker-desktop
您将收到以下输出:
Switched to context "docker-desktop".
-
我们可以使用以下命令部署此
Deployment
对象:$ kubectl create -f web-deployment.yaml
前面的命令输出如下信息:
deployment.apps/web created
-
我们可以通过 Kubernetes CLI 再次确认该部署是否已创建:
$ kubectl get all
我们应该看到以下输出:
图 17.3 – 列出所有在 Kind 中运行的资源
在前面的输出中,我们可以看到 Kubernetes 创建了三个对象——部署(deployment)、相关的ReplicaSet
,以及一个 Pod(记住我们指定了只需要一个副本)。当前状态与这三个对象的期望状态一致,所以到目前为止我们没问题。
- 现在,web 服务需要公开给外部访问。为此,我们需要定义一个 Kubernetes 类型为
NodePort
的Service
对象。创建一个名为web-service.yaml
的新文件,并向其中添加以下代码:
图 17.4 – 我们的 web 组件的 Service 对象定义
再次提醒,相同的文件可以在sample-solutions/ch17
子文件夹中的web-service.yaml
文件中找到。
前面的代码行如下:
-
第 7 行:我们将此
Service
对象的名称设置为web
。 -
第 9 行:我们定义了使用的
Service
对象类型。由于web
组件必须能够从集群外部访问,因此不能是ClusterIP
类型的Service
对象,必须是NodePort
或LoadBalancer
类型。在前一章中我们讨论了 Kubernetes 服务的各种类型,因此这里不再详细说明。在我们的示例中,我们使用的是NodePort
类型的服务。 -
第 10 到 13 行:我们指定希望通过 TCP 协议暴露端口
3000
供访问。Kubernetes 会自动将容器端口3000
映射到 30,000 到 32,768 范围内的一个空闲主机端口。Kubernetes 最终选择的端口可以通过在服务创建后使用kubectl get service
或kubectl describe
命令来确定。 -
第 14 到 16 行:我们定义了此服务将作为稳定端点的 pods 的过滤条件。在这种情况下,它是所有具有
app
和service
标签且值分别为pets
和web
的 pods。
-
现在我们已经有了
Service
对象的规格说明,我们可以使用kubectl
来创建它:$ kubectl apply -f web-service.yaml
-
我们可以列出所有服务,以查看前面命令的结果:
$ kubectl get services
上述命令会产生以下输出:
图 17.5 – 为 Web 组件创建的 Service 对象
在前面的输出中,我们可以看到一个名为web
的服务已被创建。该服务被分配了一个唯一的ClusterIP
值10.96.195.255
,并且容器端口3000
已在所有集群节点的端口30319
上发布。
-
如果我们想测试这个部署,可以使用
curl
:$ curl localhost:30319/
这将导致以下输出:
Pets Demo Application
正如我们所看到的,响应是Pets Demo Application
,这是我们预期的结果。Web 服务已在 Kubernetes 集群中启动并运行。接下来,我们将部署数据库。
部署数据库
数据库是一个有状态组件,必须与无状态组件(如我们的 Web 组件)不同对待。我们在第九章《学习分布式应用架构》和第三章《容器编排介绍》中详细讨论了分布式应用架构中有状态和无状态组件的区别。
Kubernetes 为有状态组件定义了一种特殊类型的ReplicaSet
对象,这种对象叫做StatefulSet
。我们使用这种类型的对象来部署数据库。
- 创建一个名为
db-stateful-set.yaml
的新文件,并将以下内容添加到该文件中:
图 17.6 – 用于 DB 组件的 StatefulSet 对象
定义也可以在sample-solutions/ch17
子文件夹中找到。
好的,看起来有点吓人,但其实不是。它比 web 组件的部署定义稍长,因为我们还需要定义一个卷,用于 PostgreSQL 数据库存储数据。卷索赔定义在第 25 至 33 行。
我们想要创建一个名为 pets-data
的卷,其最大大小为 100 MB。在第 22 至 24 行,我们使用此卷,并将其挂载到容器中的 /var/lib/postgresql/data
,这是 PostgreSQL 期望的位置。在第 21 行,我们还声明 PostgreSQL 正在端口 5432
上监听。
-
像往常一样,我们使用
kubectl
部署我们的StatefulSet
:$ kubectl apply -f db-stateful-set.yaml
-
现在,如果我们列出集群中的所有资源,我们将能够看到创建的额外对象:
图 17.7 – StatefulSet 及其 pod
在这里,我们可以看到已创建了 StatefulSet
和一个 pod。对于两者来说,当前状态与期望状态相符,因此系统是健康的,但这并不意味着此时 web
组件可以访问数据库。服务发现不起作用。请记住,web
组件希望使用 db
服务的名称来访问 db
。我们在 server.js
文件中硬编码了 db
主机名。
- 为了使集群内的服务发现正常工作,我们还必须为数据库组件定义一个 Kubernetes
Service
对象。由于数据库应仅能从集群内部访问,因此我们需要的Service
对象类型是ClusterIP
。
创建一个名为 db-service.yaml
的新文件,并将以下规范添加到其中。它可以在 sample-solutions/ch17
子文件夹中找到:
图 17.8 – 为数据库定义的 Kubernetes Service 对象
数据库组件将由此 Service
对象表示。它可以通过名称 db
进行访问,这是服务的名称,如第 4 行所定义。数据库组件不必公开访问,因此我们决定使用 ClusterIP
类型的 Service
对象。第 10 至 12 行的选择器定义了该服务代表具有必要标签的所有 pod 的稳定端点 – 即 app: pets
和 service: db
。
-
让我们使用以下命令部署此服务:
$ kubectl apply -f db-service.yaml
-
现在,我们应该准备好测试该应用程序了。这次我们可以使用浏览器,欣赏肯尼亚马赛马拉国家公园美丽的动物图像:
图 17.9 – 在 Kubernetes 中运行 pets 应用程序的测试
在这种情况下,端口号 30317
是 Kubernetes 自动为我的 web
Service
对象选择的端口号。请将此数字替换为 Kubernetes 分配给您的服务的端口号。您可以使用 kubectl get services
命令获取该数字。
这样,我们就成功将宠物应用程序部署到了 Docker Desktop 提供的单节点 Kubernetes 集群中。我们需要定义四个构件才能完成这一操作,它们如下所示:
-
Deployment
和Service
对象用于web
组件 -
StatefulSet
和Service
对象用于database
组件
要从集群中移除应用程序,我们可以使用以下小脚本:
kubectl delete svc/webkubectl delete deploy/web
kubectl delete svc/db
kubectl delete statefulset/db
kubectl delete pvc/pets-data-db-0
请注意该脚本的最后一行。我们正在删除 Kubernetes 自动为 db
部署创建的持久卷声明。当我们删除 db
部署时,这个声明不会被自动删除!持久卷声明与 Docker 卷有点相似(但请注意,它们并不相同)。
使用 kubectl get pvc
命令查看机器上所有声明的列表。
接下来,我们将优化部署。
精简部署过程
到目前为止,我们已经创建了四个需要部署到集群中的构件。这只是一个非常简单的应用程序,由两个组件组成。试想如果是一个更加复杂的应用程序,它会迅速变成一场维护噩梦。幸运的是,我们有几个方法可以简化部署。我们将在这里讨论的方法是,将组成 Kubernetes 应用程序的所有组件定义在一个文件中。
本书未涉及的其他解决方案包括使用包管理器,例如 Helm(helm.sh/
)或 Kustomize(kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/
),这是 Kubernetes 的原生解决方案。
如果我们的应用程序包含多个 Kubernetes 对象,例如 Deployment
和 Service
对象,那么我们可以将它们都保存在一个文件中,并通过三个破折号分隔各个对象定义。例如,如果我们想在一个文件中包含 web
组件的 Deployment
和 Service
定义,文件内容将如下所示:
图 17.10 – 单个文件中的 web 组件部署和服务
您可以在 sample-solutions/ch17/install-web.yaml
文件中找到此文件。
接下来,我们将所有四个对象定义收集到 sample-solutions/ch17/install-pets.yaml
文件中,并可以一次性部署该应用程序:
$ kubectl apply -f install-pets.yaml
这将给出如下输出:
deployment "web" createdservice "web" created
deployment "db" created
service "db" created
类似地,我们创建了一个名为 sample-solutions/ch17/remove-pets.sh
的脚本,用于从 Kubernetes 集群中删除所有宠物应用程序的构件。请注意,该文件在使用之前已通过 chmod +x ./remove-pets.sh
命令设置为可执行文件。现在,我们可以使用以下命令:
$ ./remove-pets.sh
这将产生如下输出:
deployment.apps "web" deletedservice "web" deleted
statefulset.apps "db" deleted
service "db" deleted
persistentvolumeclaim "pets-data-db-0" deleted
或者,您可以使用以下命令:
$ kubectl delete -f install-pets.yaml
这将删除除持久卷声明外的所有资源,而持久卷声明需要手动删除:
$ kubectl delete pvc/pets-data-db-0
在这一部分,我们已经使用在第十一章《使用 Docker Compose 管理容器》中介绍的宠物应用程序,定义了将此应用程序部署到 Kubernetes 集群中所需的所有 Kubernetes 对象。在每个步骤中,我们确保得到了预期的结果,并且一旦所有的工件存在于集群中,我们展示了运行中的应用程序。
定义存活性和就绪性
像 Kubernetes 和 Docker Swarm 这样的容器编排系统大大简化了部署、运行和更新高度分布式、关键任务应用程序的过程。编排引擎自动化了许多繁琐的任务,例如上下扩展、确保所需状态始终得到维护等。
然而,编排引擎不能自动完成所有事情。有时,我们开发人员需要提供一些只有我们才能了解的信息来支持引擎。那么,我说的是什么意思呢?
我们来看一个单一的应用服务。假设它是一个微服务,我们称之为服务 A。如果我们将服务 A 容器化并运行在 Kubernetes 集群上,那么 Kubernetes 可以确保我们在服务定义中要求的五个实例始终运行。如果一个实例崩溃,Kubernetes 可以快速启动一个新实例,从而保持所需状态。但是,如果一个服务实例没有崩溃,而是不健康或还没有准备好处理请求呢?Kubernetes 应该知道这两种情况。但它不能,因为从应用服务的角度来看,健康与否超出了编排引擎的知识范畴。只有我们应用程序的开发人员知道我们的服务何时健康,何时不健康。
比如,应用服务可能正在运行,但由于某些 bug 其内部状态可能已经损坏,可能处于无限循环中,或者可能处于死锁状态。
类似地,只有我们这些应用程序开发人员才知道我们的服务是否准备好工作,或者它是否还在初始化中。虽然强烈建议将微服务的初始化阶段尽可能缩短,但如果某些服务需要较长的时间才能准备好工作,通常也无法避免。在初始化状态下并不意味着不健康。初始化阶段是微服务或任何其他应用服务生命周期中的预期部分。
因此,如果我们的微服务处于初始化阶段,Kubernetes 不应尝试杀死它。但是,如果我们的微服务不健康,Kubernetes 应该尽快将其杀死并替换为一个新的实例。
Kubernetes 有探针的概念,提供了协调引擎和应用开发者之间的连接。Kubernetes 使用这些探针来获取有关当前应用服务内部状态的更多信息。探针在每个容器内本地执行。服务的健康状况探针(也叫存活探针)、启动探针和服务的就绪探针都有对应的定义。我们逐一来看它们。
Kubernetes 存活探针
Kubernetes 使用存活探针来决定何时杀死一个容器,以及何时启动另一个实例来替代它。由于 Kubernetes 在 Pod 层面上操作,如果至少有一个容器报告为不健康,则相应的 Pod 会被杀死。
或者,我们可以换个角度来说:只有当一个 Pod 中的所有容器都报告健康时,Pod 才会被视为健康。
我们可以在 Pod 的规格说明中定义存活探针,如下所示:
apiVersion: v1kind: Pod
metadata:
…
spec:
containers:
- name: liveness-demo
image: postgres:12.10
…
livenessProbe:
exec:
command: nc localhost 5432 || exit –1
initialDelaySeconds: 10
periodSeconds: 5
相关部分在 livenessProbe
部分。首先,我们定义一个 Kubernetes 会在容器内执行的命令作为探针。在我们的例子中,我们有一个 PostreSQL
容器,使用 netcat
Linux 工具来探测 5432
端口的 TCP。命令 nc localhost 5432
成功时,表示 Postgres 已经开始监听此端口。
另外两个设置项,initialDelaySeconds
和 periodSeconds
,定义了 Kubernetes 在启动容器后应该等待多长时间才执行第一次探针,以及之后探针应该以多频繁的间隔执行。在我们的例子中,Kubernetes 等待 10 秒钟后执行第一次探针,然后每隔 5 秒执行一次探针。
还可以使用 HTTP 端点来替代命令进行探测。假设我们运行一个来自镜像 acme.com/my-api:1.0
的微服务,且该 API 的端点 /api/health
返回状态 200 (OK)
表示微服务健康,返回 50x (Error)
表示微服务不健康。在这种情况下,我们可以这样定义存活探针:
apiVersion: v1kind: Pod
metadata:
…
spec:
containers:
- name: liveness
image: acme.com/my-api:1.0
…
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 3
在上面的代码片段中,我定义了存活探针,使其使用 HTTP 协议,并对 localhost
的 5000
端口上的 /api/health
端点执行 GET
请求。记住,探针是在容器内执行的,这意味着我可以使用 localhost。
我们还可以直接使用 TCP 协议来探测容器上的端口。但稍等一下——我们不就是在第一个例子中使用了基于命令的通用存活探针吗?没错,我们确实使用了,但是我们依赖的是容器中是否存在 netcat
工具。我们不能假设这个工具总是存在。因此,依赖 Kubernetes 本身来为我们执行基于 TCP 的探测会更好。修改后的 Pod 规格如下:
apiVersion: v1kind: Pod
metadata:
…
spec:
containers:
- name: liveness-demo
image: postgres:12.10
…
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 10
periodSeconds: 5
这个看起来非常相似。唯一的变化是,探针的类型从 exec
更改为 tcpSocket
,并且我们不再提供命令,而是提供要探测的端口。
请注意,我们也可以在这里使用 Kubernetes 的 livenessProbe
配置项中的 failureThreshold
。在 Kubernetes 中,livenessProbe
的失败阈值是指容器重启前必须连续发生的最小失败次数。默认值是 3
。最小值是 1
。如果处理程序返回失败代码,kubelet
会杀死容器并重新启动它。任何大于或等于 200
且小于 400
的代码表示成功,其他任何代码表示失败。
让我们试试这个:
-
将
sample-solutions/ch17
文件夹中的probes
子文件夹复制到你的ch17
文件夹中。 -
使用以下命令构建 Docker 镜像:
$ docker image build -t demo/probes-demo:2.0 probes
-
使用
kubectl
部署在probes-demo.yaml
中定义的示例 pod:$ kubectl apply -f probes/probes-demo.yaml
-
描述 pod,并具体分析输出中的日志部分:
$ kubectl describe pods/probes-demo
在大约前半分钟内,你应该看到以下输出:
图 17.11 – 健康 pod 的日志输出
- 等待至少 30 秒,然后再次描述 pod。这时,你应该看到以下输出:
图 17.12 – pod 状态变为不健康后的日志输出
标记的行表示探针失败,并且 pod 即将被重启。
-
如果你获取 pod 列表,你会看到 pod 已经重启了多次:
$ kubectl get pods
这将导致以下输出:
NAME READY STATUS RESTARTS AGEprobes-demo 1/1 Running 5 (49s ago) 7m22s
-
完成示例后,使用以下命令删除 pod:
$ kubectl delete pods/probes-demo
接下来,我们将查看 Kubernetes 的就绪探针(readiness probe)。
Kubernetes 就绪探针(readiness probes)
Kubernetes 使用就绪探针来决定服务实例——即容器——何时准备好接收流量。现在,我们都知道 Kubernetes 部署和运行的是 pod 而非容器,因此讨论 pod 的就绪状态是有意义的。只有当 pod 中的所有容器都报告为“就绪”时,pod 才会被认为是“就绪”的。如果 pod 报告为“未就绪”,Kubernetes 将会把它从服务负载均衡器中移除。
就绪探针的定义与存活探针相同:只需将 pod 配置中的 livenessProbe
键切换为 readinessProbe
。以下是使用我们之前的 pod 配置的示例:
…spec:
containers:
- name: liveness-demo
image: postgres:12.10
…
livenessProbe:
tcpSocket:
port: 5432
failureThreshold: 2
periodSeconds: 5
readinessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 10
periodSeconds: 5
请注意,在这个例子中,由于我们现在有了就绪探针(readiness probe),我们不再需要为存活探针设置初始延迟。因此,我已将存活探针的初始延迟条目替换为一个名为 failureThreshold
的条目,表示在发生故障时 Kubernetes 应该重复探测多少次,直到它认为容器不健康。
Kubernetes 启动探针(startup probes)
对 Kubernetes 来说,知道一个服务实例何时启动通常是很有帮助的。如果我们为容器定义了启动探针,那么只要容器的启动探针未成功,Kubernetes 就不会执行存活探针或就绪探针。一旦所有 Pod 容器的启动探针成功,Kubernetes 就会开始执行容器的存活探针和就绪探针。
鉴于我们已经有了存活探针和就绪探针,什么时候我们需要使用启动探针?可能有一些情况需要考虑异常长的启动和初始化时间,例如在将传统应用程序容器化时。我们本可以通过配置就绪探针或存活探针来解决这个问题,但那样做会违背这些探针的目的。后者的探针旨在快速反馈容器的健康状况和可用性。如果我们配置了长时间的初始延迟或持续时间,反而会影响预期效果。
不出所料,启动探针的定义与就绪探针和存活探针相同。以下是一个示例:
spec: containers:
...
startupProbe:
tcpSocket:
port: 3000
failureThreshold: 30
periodSeconds: 5
...
确保你定义了failureThreshold * periodSeconds
的乘积,以便它足够大,能够应对最差的启动时间。
在我们的示例中,最大启动时间不应超过 150 秒。
零停机部署
在关键任务环境中,应用程序必须始终保持运行。这些天,我们已经无法容忍停机了。Kubernetes 为我们提供了多种实现这一目标的方法。对集群中的应用程序进行更新而不导致停机被称为零停机部署。在本节中,我们将介绍实现这一目标的两种方法,具体如下:
-
滚动更新
-
蓝绿部署
让我们从讨论滚动更新开始。
滚动更新
在上一章中,我们了解到 Kubernetes 的Deployment
对象与ReplicaSet
对象的区别在于,它在后者的功能基础上增加了滚动更新和回滚功能。让我们使用我们的 Web 组件来演示这一点。我们将需要修改 Web 组件的部署清单或描述。
我们将使用与上一节相同的部署定义,唯一的区别是——我们将运行web
组件。以下定义也可以在sample-solutions/ch17/web-deployment-rolling-v1.yaml
文件中找到:
图 17.13 – 带有五个副本的 Web 组件部署
现在,我们可以像往常一样创建这个部署,同时也创建使我们的组件可访问的服务:
$ kubectl apply -f web-deployment-rolling-v1.yaml$ kubectl apply -f web-service.yaml
一旦我们部署了 Pod 和服务,就可以测试我们的 Web 组件。首先,我们可以使用以下命令获取分配的节点端口:
$ PORT=$(kubectl get svc/web -o jsonpath='{.spec.ports[0].nodePort}')
接下来,我们可以在curl
语句中使用$PORT
环境变量:
$ curl localhost:${PORT}/
这将提供预期的输出:
Pets Demo Application
如我们所见,应用程序已经启动并运行,返回了预期的消息,Pets
Demo Application
。
我们的开发人员已经创建了 Web 组件的新版本 2.1。新版本的代码可以在sample-solutions/ch17/web
文件夹中找到,唯一的变化位于server.js
文件的第 12 行:
图 17.14 – Web 组件版本 2.0 的代码更改
我们现在可以按如下方式构建新的镜像(将demo
替换为你的 GitHub 用户名):
$ docker image build -t demo/ch17-web:2.1 web
随后,我们可以将镜像推送到 Docker Hub,步骤如下(将demo
替换为你的 GitHub 用户名):
$ docker image push demo/ch17-web:2.1
现在,我们希望更新由属于web
Deployment
对象的 pod 使用的镜像。我们可以通过使用kubectl
的set image
命令来实现:
$ kubectl set image deployment/web \ web=demo/ch17-web:2.1
如果我们再次测试该应用程序,我们将得到一个确认,证明更新确实已发生:
$ curl localhost:${PORT}/
输出显示现在已经安装了版本 2:
Pets Demo Application v2
那么,我们怎么知道在这次更新过程中没有任何停机时间呢?更新是以滚动方式进行的吗?滚动更新到底是什么意思呢?让我们来探讨一下。首先,我们可以通过使用rollout
status
命令,从 Kubernetes 获取确认,确保部署确实已成功完成:
$ kubectl rollout status deploy/web
命令将返回以下响应:
deployment "web" successfully rolled out
如果我们使用kubectl describe deploy/web
描述web
部署对象,在输出的末尾,我们将看到以下事件列表:
图 17.15 – 在 Web 组件部署描述输出中找到的事件列表
第一个事件告诉我们,在创建部署时,创建了一个名为web-769b88f67
的ReplicaSet
对象,包含五个副本。然后,我们执行了update
命令。事件列表中的第二个事件告诉我们,这意味着创建了一个新的ReplicaSet
对象,名为web-55cdf67cd
,最初只有一个副本。因此,在那个特定时刻,系统上存在六个 pod:五个初始 pod 和一个新的版本的 pod。但是,由于Deployment
对象的期望状态要求只有五个副本,Kubernetes 现在将旧的ReplicaSet
对象缩减为四个实例,这一点可以从第三个事件中看到。
然后,新的ReplicaSet
对象被扩展到两个实例,随后,旧的ReplicaSet
对象被缩减到三个实例,依此类推,直到我们得到了五个新的实例,并且所有旧的实例都被淘汰。尽管我们无法看到发生这些变化的具体时间(除了 3 分钟),但事件的顺序告诉我们,整个更新过程是以滚动方式进行的。
在短时间内,一些 web 服务的调用会从旧版本的组件中得到响应,而另一些调用则会从新版本的组件中得到响应,但服务在任何时候都不会中断。
我们还可以列出集群中的 ReplicaSet
对象,以确认我在前面提到的内容:
图 17.16 – 列出集群中的所有 ReplicaSet 对象
在这里,我们可以看到新的 ReplicaSet
对象有五个实例正在运行,而旧的 ReplicaSet
对象已缩减为零实例。旧的 ReplicaSet
对象仍然存在的原因是 Kubernetes 允许我们回滚更新,在这种情况下,它会重用该 ReplicaSet
。
如果在更新镜像时出现一些未检测到的 bug 渗入新代码,我们可以使用 rollout
undo
命令回滚更新:
$ kubectl rollout undo deploy/web
这将输出以下内容:
deployment.apps/web rolled back
我们可以像这样测试回滚是否成功:
$ curl localhost:${PORT}/
如我们所见,输出显示了这一点:
Pets Demo Application
如果我们列出 ReplicaSet
对象,我们将看到以下输出:
图 17.17 – 回滚后列出 ReplicaSet 对象
这确认了旧的 ReplicaSet
(web-9d66cd994
)对象已被重用,而新的 ReplicaSet
对象已缩减为零实例。
在继续之前,请删除部署和服务:
$ kubectl delete deploy/web$ kubectl delete service/web
但是,有时我们无法或不想容忍旧版本与新版本共存的混合状态。我们希望采取全有或全无的策略。这时,蓝绿部署就派上用场了,我们将在接下来的内容中讨论。
蓝绿部署
如果我们想为宠物应用程序的 web
组件进行蓝绿式部署,可以通过巧妙地使用标签来实现。首先,让我们回顾一下蓝绿部署是如何工作的。以下是一个大致的步骤指南:
-
将
web
组件的第一个版本作为blue
部署。我们将为 pods 添加color: blue
的标签来实现这一点。 -
为这些带有
color: blue
标签的 pods 在selector
部分部署 Kubernetes 服务。 -
现在,我们可以部署版本 2 的 web 组件,但这次,pods 会有一个
color: green
的标签。 -
我们可以测试服务的绿色版本,以检查它是否按预期工作。
-
现在,我们可以通过更新 Kubernetes 服务来将流量从
blue
切换到green
,我们将修改选择器,使其使用color:
green
标签。
让我们为版本 1 定义一个 Deployment
对象,标记为 blue
:
图 17.18 – 为 web 组件指定蓝色部署
上述定义可以在sample-solutions/ch17/web-deployment-blue.yaml
文件中找到。
请注意第 8 行,在那里我们将部署的名称定义为web-blue
,以便与即将到来的web-green
部署区分开来。另外,请注意我们在第 7、15 和 21 行添加了color: blue
标签。其他内容与之前相同。
现在,我们可以为网页组件定义Service
对象。它将与我们之前使用的相同,但有一个小的改动,如下图所示:
图 17.19 – 支持蓝绿部署的网页组件 Kubernetes 服务
关于本章早些时候使用的服务定义,唯一的区别是第 17 行,它将color: blue
标签添加到了选择器中。我们可以在sample-solutions/ch17/web-service-blue-green.yaml
文件中找到上述定义。
然后,我们可以使用以下命令部署蓝色版本的web
组件:
$ kubectl apply -f web-deploy-blue.yaml
我们可以使用此命令部署其服务:
$ kubectl apply -f web-service-blue-green.yaml
一旦服务启动并运行,我们可以确定其 IP 地址和端口号并进行测试:
$ PORT=$(kubectl get svc/web -o jsonpath='{.spec.ports[0].nodePort}')
然后,我们可以使用curl
命令访问它:
$ curl localhost:${PORT}/
这将给我们预期的结果:
Pets Demo Application
现在,我们可以部署web
组件的绿色版本。其Deployment
对象的定义可以在sample-solutions/ch17/web-deployment-green.yaml
文件中找到,内容如下:
图 17.20 – 网页组件绿色部署规范
有趣的行如下:
-
第 8 行:命名为
web-green
,以便与web-blue
区分,并支持并行安装 -
第 7、15 和 21 行:颜色为绿色
-
第 24 行:现在使用的是本章前面构建的网页镜像版本 2.1
请不要忘记在第 24 行将‘’demo‘’
改为你自己的 GitHub 用户名。
现在,我们准备部署这个绿色版本的服务。它应与蓝色服务分开运行:
$ kubectl apply -f web-deployment-green.yaml
我们可以确保两个部署并存,如下所示:
图 17.21 – 显示集群中运行的部署对象列表
如预期的那样,我们有蓝色和绿色两个版本在运行。我们可以验证蓝色仍然是活跃的服务:
$ curl localhost:${PORT}/
我们应该仍然收到以下输出:
Pets Demo Application
现在是有趣的部分:我们可以通过编辑现有的网页组件服务,将流量从blue
切换到green
。为此,请执行以下命令:
$ kubectl edit svc/web
将标签的颜色值从blue
更改为green
。然后,保存并退出编辑器。Kubernetes CLI 将自动更新服务。现在,当我们再次查询网页服务时,将得到如下内容:
$ curl localhost:${PORT}/
这时,我们应该得到以下输出:
Pets Demo Application v2
这证明了流量确实已经切换到web
组件的绿色版本(注意curl
命令响应末尾的v2
)。
注意
如果我们希望坚持声明式的形式,那么最好更新web-service-blue-green.yaml
文件并应用新版本,这样所需的状态仍然保存在文件中,避免现实与文件之间可能的不匹配。然而,为了说明,展示的方式是可以接受的。
如果我们意识到绿色部署出现了问题,新版本有缺陷,我们可以通过再次编辑 web 服务并将color
标签的值替换为蓝色来轻松切换回蓝色版本。这个回滚是瞬时的,并且应该总是有效。然后,我们可以删除有缺陷的绿色部署并修复组件。一旦我们修复了问题,就可以再次部署绿色版本。
一旦组件的绿色版本按照预期运行并且性能良好,我们可以停用蓝色版本:
$ kubectl delete deploy/web-blue
当我们准备部署新版本 3.0 时,这个版本将成为蓝色版本。我们必须相应地更新ch17/web-deployment-blue.yaml
文件并部署它。然后,我们必须将 web 服务从green
切换到blue
,依此类推。
通过这一点,我们成功地展示了如何在 Kubernetes 集群中实现蓝绿部署,使用的是我们宠物应用程序的web
组件。
接下来,我们将学习如何处理 Kubernetes 中应用程序使用的秘密。
Kubernetes 秘密
有时,我们希望在 Kubernetes 集群中运行的服务必须使用机密数据,比如密码、API 密钥或证书,仅举几例。我们希望确保只有授权或专用服务能够查看这些敏感信息。集群中的所有其他服务不应访问这些数据。
出于这个原因,Kubernetes 引入了秘密管理。一个秘密是一个键值对,其中键是秘密的唯一名称,值是实际的敏感数据。秘密存储在etcd
中。Kubernetes 可以配置为在静态存储时加密秘密——也就是在etcd
中——以及在传输时加密秘密——也就是当秘密从主节点传输到工作节点,这些节点上运行着使用此秘密的服务的 pod 时。
手动定义秘密
我们可以像创建 Kubernetes 中的任何其他对象一样声明式地创建一个秘密。以下是这样一个秘密的 YAML 配置:
apiVersion: v1kind: Secret
metadata:
name: pets-secret
type: Opaque
data:
username: am9obi5kb2UK
password: c0VjcmV0LXBhc1N3MHJECg==
上面的定义可以在sample-solutions/ch17/pets-secret.yaml
文件中找到。现在,你可能会想知道这些值是什么。这些是实际的(未加密的)值吗?不是的。它们也不是加密值,而只是base64
编码的值。
因此,它们并不完全安全,因为base64
编码的值可以轻松还原为明文值。我是如何获得这些值的?这很简单——只需按照以下步骤操作:
-
使用
base64
工具按如下方式编码值:$ echo "john.doe" | base64
这将导致以下输出:
am9obi5kb2UK
另外,尝试以下操作:
$ echo "sEcret-pasSw0rD" | base64
这将给我们带来以下输出:
c0VjcmV0LXBhc1N3MHJECg==
-
使用前面的值,我们可以创建密钥:
$ kubectl create -f pets-secret.yaml
在这里,命令输出如下:
secret/pets-secret created
-
我们可以使用以下命令描述密钥:
$ kubectl describe secrets/pets-secret
前面的命令输出如下:
图 17.22 – 创建和描述 Kubernetes 密钥
-
在密钥描述中,值被隐藏,只有它们的长度会显示。所以,也许密钥现在是安全的了。其实不然。我们可以通过
kubectl
的get
命令轻松解码这个密钥:$ kubectl get secrets/pets-secret -o yaml
输出如下:
图 17.23 – 解码 Kubernetes 密钥
如前面的截图所示,我们已经恢复了原始的密钥值。
-
解码你之前得到的值:
$ echo "c0VjcmV0LXBhc1N3MHJECg==" | base64 –decode
这将导致以下输出:
sEcret-pasSw0rD
因此,结果是这种创建 Kubernetes 密钥的方法只适用于开发环境,在那里我们处理的是非敏感数据。在所有其他环境中,我们需要一种更好的方式来处理密钥。
使用 kubectl 创建密钥
定义密钥的更安全方式是使用 kubectl
。首先,我们必须创建包含 base64 编码密钥值的文件,类似于前一节所做的,但这次,我们必须将值存储在临时文件中:
$ echo "sue-hunter" | base64 > username.txt$ echo "123abc456def" | base64 > password.txt
现在,我们可以使用 kubectl
从这些文件创建密钥,如下所示:
$ kubectl create secret generic pets-secret-prod \ --from-file=./username.txt \
--from-file=./password.txt
这将导致以下输出:
secret "pets-secret-prod" created
然后,密钥可以像手动创建的密钥一样使用。
你可能会问,为什么这种方法比另一种更安全?首先,没有 YAML 文件定义密钥,并且它存储在某些源代码版本控制系统中,例如 GitHub,很多人都可以访问这些系统,因此他们可以看到并解码这些密钥。
只有授权了解密钥的管理员才能看到密钥的值,并使用它们直接在(生产)集群中创建密钥。集群本身受到基于角色的访问控制保护,因此没有授权的人无法访问它,也无法解码集群中定义的密钥。
现在,让我们看看如何使用我们定义的密钥。
在 pod 中使用密钥
假设我们要创建一个 Deployment
对象,其中 web
组件使用我们在前一节中介绍的密钥 pets-secret
。我们可以使用以下命令在集群中创建密钥:
$ kubectl apply -f pets-secret.yaml
在 sample-solutions/ch17/web-deployment-secret.yaml
文件中,我们可以找到 Deployment
对象的定义。我们需要将从第 23 行开始的部分添加到原始的 Deployment
对象定义中:
图 17.24 – 带有密钥的 Web 组件的部署对象
在第 29 到 32 行,我们定义了一个名为secrets
的卷,该卷来自我们的密钥pets-secret
。然后,我们按照第 25 到 28 行的描述,在容器中使用该卷。
我们将密钥挂载到容器文件系统的/etc/secrets
路径,并以只读模式挂载该卷。因此,密钥值将作为文件提供给容器,并存放在该文件夹中。文件名将对应于键名,文件内容将是对应键的值。密钥值将以未加密的形式提供给运行在容器内的应用程序。
使用以下命令应用部署:
$ kubectl apply -f web-deployment-secret.yaml
在我们的例子中,由于密钥中有用户名和密码的键,我们将在容器文件系统的/etc/secrets
文件夹中找到两个文件,分别名为username
和password
。username
文件应该包含john.doe
值,password
文件应该包含sEcret-pasSw0rD
值。让我们确认一下:
-
首先,我们将获取 Pod 的名称:
$ kubectl get pods
这将给我们以下输出:
图 17.25 – 查找 Pod 的名称
- 使用 Pod 的名称,我们可以执行以下屏幕截图中显示的命令来获取密钥:
图 17.26 – 确认容器内可以访问密钥
在前面的输出的第 1 行,我们exec
进入运行web
组件的容器。然后,在第 2 到 5 行,我们列出了/etc/secrets
文件夹中的文件,最后,在最后 3 行,我们展示了两个文件的内容,毫无意外地,显示了明文的密钥值。
由于任何语言编写的应用程序都可以读取简单的文件,因此使用密钥的这种机制非常向后兼容。即使是一个旧的 Cobol 应用程序也能从文件系统中读取明文文件。
离开之前,请删除 Kubernetes 部署:
$ kubectl delete deploy/web
然而,有时应用程序期望密钥在环境变量中可用。
让我们看看 Kubernetes 在这种情况下为我们提供了什么。
环境变量中的密钥值
假设我们的 Web 组件期望PETS_USERNAME
环境变量中有用户名,PETS_PASSWORD
环境变量中有密码。如果是这样,我们可以修改部署的 YAML 文件,使其如下所示:
图 17.27 – 部署映射密钥值到环境变量
在第 25 到 35 行,我们定义了两个环境变量PETS_USERNAME
和PETS_PASSWORD
,并将pets-secret
中的相应键值对映射到它们。
应用更新后的部署:
$ kubectl apply -f web-deployment-secret.yaml
请注意,我们不再需要使用卷;相反,我们直接将 pets-secret
的各个密钥映射到容器内有效的环境变量。以下命令序列显示了秘密值确实可用,并且已经映射到相应的环境变量中:
图 17.28 – 秘密值已经映射到环境变量
在本节中,我们展示了如何在 Kubernetes 集群中定义秘密,以及如何在作为部署的一部分运行的容器中使用这些秘密。我们展示了秘密如何在容器内部映射的两种变体——使用文件和使用环境变量。
总结
在本章中,我们学习了如何将应用程序部署到 Kubernetes 集群,并为该应用程序设置应用层路由。此外,我们还学习了如何在不引起任何停机的情况下更新 Kubernetes 集群中运行的应用服务。最后,我们使用秘密为集群中运行的应用服务提供敏感信息。
在下一章中,我们将学习不同的技术,这些技术用于监控在 Kubernetes 集群中运行的单个服务或整个分布式应用程序。我们还将学习如何在不更改集群或服务所在集群节点的情况下,排查生产环境中运行的应用服务问题。敬请期待。
进一步阅读
这里有一些链接,提供了本章讨论主题的更多信息:
-
执行滚动 更新:
bit.ly/2o2okEQ
-
蓝绿 部署:
bit.Ly/2r2IxNJ
-
Kubernetes 中的秘密:
bit.ly/2C6hMZF
问题
为了评估你的学习进度,请回答以下问题:
-
你有一个由两个服务组成的应用程序,第一个是 Web API,第二个是数据库,如 MongoDB。你想将这个应用程序部署到 Kubernetes 集群中。用简短的几句话解释你会如何进行。
-
在 Kubernetes 应用服务的上下文中,liveness 和 readiness 探针是什么?
-
请用自己的话描述你需要哪些组件来为你的应用程序建立第七层(或应用层)路由。
-
简要列出实施蓝绿部署所需的主要步骤。避免过多细节。
-
列举三到四种你会通过 Kubernetes 秘密提供给应用服务的信息类型。
-
列出 Kubernetes 在创建秘密时接受的来源。
-
如何配置应用服务以使用 Kubernetes 秘密?
答案
下面是本章问题的答案:
-
假设我们在注册表中有一个用于两个应用服务的 Docker 镜像——网页 API 和 MongoDB——我们需要做如下操作:
-
使用
StatefulSet
对象定义 MongoDB 的部署;我们将这个部署命名为db-deployment
。StatefulSet
对象应该有一个副本(复制 MongoDB 稍微复杂一些,超出了本书的范围)。 -
定义一个名为
db
的 Kubernetes 服务,类型为ClusterIP
,用于db-deployment
。 -
定义一个网页 API 的部署,命名为
web-deployment
。 -
我们将这个服务扩展为三个实例。
-
定义一个名为
api
的 Kubernetes 服务,类型为NodePort
,用于web-deployment
。 -
如果我们使用密钥,那么直接在集群中通过
kubectl
定义这些密钥。 -
使用
kubectl
部署应用。
-
-
存活探针和就绪探针是 Kubernetes 为容器提供的健康检查。存活探针检查容器是否仍在运行,如果没有,Kubernetes 会自动重启它。就绪探针检查容器是否准备好接受请求。如果容器未通过就绪检查,它不会被移除,但在通过就绪探针之前,不会接收任何传入请求。
-
为了实现应用的第 7 层路由,我们理想情况下使用
IngressController
。这是一种反向代理,如 Nginx,它有一个侧车容器监听 Kubernetes 服务器 API 的相关变化,并在检测到变化时更新反向代理的配置并重启它。然后,我们需要在集群中定义 Ingress 资源,定义路由,例如从基于上下文的路由如https://example.com/pets
到<服务名称>/<端口>
或类似api/32001
的配对。当 Kubernetes 创建或更改此Ingress
对象时,IngressController
的侧车容器会捕捉并更新代理的路由配置。 -
假设这是一个集群内部的库存服务,那么我们做如下操作:
-
部署 1.0 版本时,我们定义一个名为
inventory-deployment-blue
的部署,并将 Pods 标记为color:blue
。 -
我们为前述部署部署一个类型为
ClusterIP
的 Kubernetes 服务,名为inventory
,并且选择器包含color:blue
。 -
当我们准备部署新版本的
payments
服务时,我们定义一个该服务的 2.0 版本的部署,并将其命名为inventory-deployment-green
。我们给 Pods 添加一个color:green
标签。 -
现在我们可以对“绿色”服务进行冒烟测试,当一切正常时,我们可以更新库存服务,使选择器包含
color:green
。
-
-
一些形式的信息是机密的,因此应该通过 Kubernetes 密钥提供给服务,包括密码、证书、API 密钥 ID、API 密钥秘密和令牌。
-
密钥值的来源可以是文件或 base64 编码的值。
-
要配置应用程序使用 Kubernetes 密钥,必须创建一个包含敏感数据的
Secret
对象。然后,必须修改你的Pod
规格,使其包含对Secret
对象的引用。此引用可以作为容器规格中的环境变量,或者作为卷挂载,这样你的应用程序就可以使用这些密钥数据。
第十八章:18
在云中运行容器化应用程序。
在上一章中,我们学习了如何将应用程序部署、更新和扩展到 Kubernetes 集群中。我们了解了如何实现零停机部署,以实现不中断的更新和回滚关键应用程序。最后,我们介绍了 Kubernetes 机密作为配置服务和保护敏感数据的手段。
在本章中,我们将概述在云中运行容器化应用程序的三种最流行的方式。我们将探讨每种托管解决方案,并讨论它们的优缺点。
以下是本章中我们将讨论的主题:
-
为什么选择托管 Kubernetes 服务?
-
在 Amazon Elastic Kubernetes Service (Amazon EKS) 上运行一个简单的容器化应用程序。
-
探索 Microsoft 的 Azure Kubernetes Service (AKS)。
-
理解 Google Kubernetes Engine (GKE)。
阅读完本章后,您将能够执行以下操作:
-
分析托管 Kubernetes 服务与自管理 Kubernetes 集群相比的优缺点。
-
在 Amazon EKS 中部署并运行一个简单的分布式应用程序。
-
部署并在 Microsoft 的 AKS 上运行一个简单的分布式应用程序。
-
在 GKE 上部署并运行一个简单的分布式应用程序。
技术要求
我们将在本章节中使用 亚马逊网络服务 (AWS),Microsoft Azure 和 Google Cloud;因此,每个平台都需要有一个账户。如果您没有现有账户,可以申请这些云服务提供商的试用账户。
我们还将使用我们实验室在 GitHub 存储库的 ~/The-Ultimate-Docker-Container-Book/sample-solutions/ch18
文件夹中的文件,网址为 github.com/PacktPublishing/The-Ultimate-Docker-Container-Book/tree/main/sample-solutions/ch18
。
准备放置您自己代码的文件夹。首先,导航至源文件夹,如下所示:
$ cd ~/The-Ultimate-Docker-Container-Book
然后,创建一个 ch18
子文件夹并导航至该文件夹,如下所示:
$ mkdir ch18 & cd ch18
为什么选择托管 Kubernetes 服务?
目前,AWS、Microsoft Azure 和 Google Cloud 是最受欢迎的三大云服务提供商,每个都提供了托管 Kubernetes 服务,如下所述:
-
Amazon EKS:Amazon EKS 是一个托管服务,使您能够在 AWS 上运行 Kubernetes,无需安装、操作和维护自己的 Kubernetes 控制平面或节点。
-
AKS:AKS 是 Microsoft 的托管 Kubernetes 服务。它提供了与 持续集成和持续部署 (CI/CD) 能力以及 Kubernetes 工具集成的开发人员生产力。它还具有完整的容器 CI/CD 平台的 Azure DevOps 项目。
-
GKE:Google 是 Kubernetes 的原始创造者,GKE 是市场上第一个可用的托管 Kubernetes 服务。它提供了先进的集群管理功能,并与 Google Cloud 服务集成。
其他提供商也提供 Kubernetes 即服务(KaaS),例如 IBM Cloud Kubernetes 服务、Oracle Kubernetes 容器引擎和 DigitalOcean Kubernetes(DOKS)。鉴于云市场发展迅速,查看最新的产品和功能始终是一个好主意。
管理一个 Kubernetes 集群,无论是在本地还是在云中,都涉及相当复杂的操作工作,并且需要专业知识。以下是使用托管 Kubernetes 服务通常是首选解决方案的一些原因:
-
设置和管理的简易性:托管 Kubernetes 服务处理底层基础设施,减少了管理 Kubernetes 集群的操作负担。它们会自动处理 Kubernetes 控制平面的供应、升级、补丁和扩展。
-
高可用性(HA)和高可扩展性:托管服务通常为你的应用提供开箱即用的高可用性和高可扩展性。它们处理必要的协调工作,以将应用分布到不同的节点和数据中心。
-
安全与合规性:托管服务通常包括内置的安全功能,如网络策略、基于角色的访问控制(RBAC)和与云提供商 身份与访问管理(IAM)服务的集成。它们还负责 Kubernetes 软件本身的安全更新。
-
监控与诊断:托管的 Kubernetes 服务通常包括与监控和日志服务的集成,使得观察和排除应用程序故障变得更加容易。
-
成本:虽然使用托管服务会产生一定的费用,但其成本通常低于为高效、安全地运营一个 Kubernetes 集群所需的专职人员和基础设施成本。
-
支持:使用托管 Kubernetes 服务时,你将能够获得云服务提供商的支持。如果你在运行生产工作负载并需要快速解决任何问题,这尤其有价值。
相比之下,运行你自己的 Kubernetes 集群涉及大量的设置和维护工作。从 Kubernetes 的安装和配置,到集群升级、安全补丁、节点供应和扩展的持续任务,再到设置监控和告警,你都需要负责。
管理自己的集群虽然提供了更多的控制和灵活性,但需要大量的时间、资源和专业知识投入。对于许多组织来说,托管服务的好处远远超过了自主管理集群所带来的控制力提升。
在 Amazon EKS 上运行一个简单的容器化应用程序
在这一部分,我们希望在 Amazon EKS 上使用 Fargate 创建一个完全托管的 Kubernetes 集群。创建新集群的过程在 AWS 文档中有详细描述,我们将参考相关页面,以避免重复过多信息。话虽如此,让我们从以下步骤开始。
什么是 Fargate?
AWS Fargate 是由 AWS 提供的无服务器计算引擎,用于容器。它消除了管理底层服务器的需要,让你可以专注于设计和构建应用程序。Fargate 处理容器的部署、扩展和管理,使你可以在无需担心基础设施的情况下启动应用程序。
让我们首先处理一些前提条件,如下所示:
-
确保你可以访问一个 AWS 账户。如果没有,你可以在这里获得一个免费的 1 年试用账户:
aws.amazon.com/free
。 -
登录到你的 AWS 账户。
-
为你的账户创建一对新的访问密钥和访问密钥秘密,你将用它们来配置你的 AWS CLI,从而可以通过命令行访问你的账户。
-
在屏幕右上角找到你的个人资料,从下拉菜单中选择安全凭证。
选择访问密钥(访问密钥 ID 和秘密访问密钥),然后点击创建 访问密钥:
图 18.1 – 将访问密钥 ID 和秘密配对记录在安全位置
-
打开一个新的终端。
-
确保你已安装 AWS CLI。
在 Mac 上,使用以下命令:
$ brew install awscli
在 Windows 上,使用以下命令:
$ choco install awscli
-
在两种情况下,都可以使用以下命令来测试安装是否成功:
$ aws --version
-
配置你的 AWS CLI。为此,你需要你在前面步骤 3中创建的AWS 访问密钥 ID和AWS 秘密访问密钥,以及你的默认区域。
然后,使用以下命令:
$ aws configure
在被询问时输入适当的值。对于默认输出格式,选择 JSON
,如下所示:
图 18.2 – 配置 AWS CLI
-
尝试使用如下命令访问你的账户:
$ aws s3 ls
这应该列出为你的账户定义的所有简单存储服务(S3)存储桶。你的列表可能为空。这里需要注意的是,命令成功执行即可。
-
最后,运行以下命令来再次检查是否已安装
kubectl
:$ kubectl version
现在,我们已经准备好创建 Amazon EKS 集群。按照以下步骤操作:
-
定义几个环境变量,以便后续使用,如下所示:
$ export AWS_REGION=eu-central-1$ export AWS_STACK_NAME=animals-stack$ export AWS_CLUSTER_ROLE=animals-cluster-role
确保将 eu-central-1
替换为离你最近的 AWS 区域。
-
现在,你可以使用以下命令创建所需的 AWS 堆栈,其中包括 VPC、私有和公共子网以及安全组—为了简化操作,使用 AWS 提供的一个示例 YAML 文件:
$ aws cloudformation create-stack --region $AWS_REGION \ --stack-name $AWS_STACK_NAME \ --template-url https://s3.us-west-2.amazonaws.com/amazon-eks/cloudformation/2020-10-29/amazon-eks-vpc-private-subnets.yaml
请花点时间下载并查看前面的 YAML 文件,以了解该命令具体在配置什么内容。
-
在接下来的几个步骤中,您需要定义正确的设置,以授予集群所需的访问权限:
- 首先使用以下命令创建一个 IAM 角色:
$ aws iam create-role \ --role-name $AWS_CLUSTER_ROLE \ --assume-role-policy-document file://"eks-cluster-role-trust-policy.json"
- 继续通过此命令将必要的 Amazon EKS 管理的 IAM 策略附加到刚刚创建的角色:
$ aws iam attach-role-policy \ --policy-arn arn:aws:iam::aws:policy/AmazonEKSClusterPolicy \ --role-name $AWS_CLUSTER_ROLE
-
现在,我们继续进行一些交互步骤,使用 Amazon EKS 控制台:
console.aws.amazon.com/eks/home#/clusters
。
注意
确保控制台右上角显示的 AWS 区域是您要创建集群的 AWS 区域(例如,在作者的案例中是eu-central-1
)。如果不是,请选择 AWS 区域名称旁边的下拉菜单并选择您要使用的 AWS 区域。
-
若要创建集群,请选择添加集群命令,然后选择创建。如果您没有看到此选项,请首先在左侧导航窗格中选择集群。
-
在
animals-cluster
上。 -
选择
animals-cluster-role
。 -
所有其他设置可以保持为默认值。
-
选择下一步。
- 在
vpc-00x0000x000x0x000 | animals-stack-VPC
上。注意名称的后缀,表示它是我们刚刚定义的那个。*同样,您可以保持其他设置为默认值。*选择下一步继续。*我们无需更改配置日志记录页面上的任何内容,因此请选择下一步。*同样的情况适用于选择插件页面,因此选择下一步。*再一次,在配置已选择的插件设置页面上,无需做任何操作,因此请选择下一步。*最后,在审查并创建页面,选择创建。*在集群名称的右侧,集群状态为创建中,持续几分钟,直到集群配置过程完成,如下图所示。在状态变为活动之前,请勿继续进行下一步:
图 18.3 – 创建 EKS 集群
-
不幸的是,我们还没有完成。我们需要创建一个信任策略并将其附加到我们的集群。为此,按照以下步骤操作:
- 首先创建一个
pod-execution-role-trust-policy.json
文件,并将以下内容添加到其中:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Condition": { "ArnLike": { "aws:SourceArn": "arn:aws:eks:<region-code>:<account-no>:fargateprofile/animals-cluster/*" } }, "Principal": { "Service": "eks-fargate-pods.amazonaws.com" }, "Action": "sts:AssumeRole" } ]}
- 首先创建一个
在前面的代码中,将<region-code>
替换为您的 AWS 区域代码(在我的案例中是eu-central-1
),将<account-no>
替换为您的账户号码。您可以在 AWS 控制台左上角的个人资料中找到后者。
- 使用刚刚配置的信任策略,使用以下命令创建一个Pod 执行 IAM 角色:
$ aws iam create-role \ --role-name AmazonEKSFargatePodExecutionRole \
--assume-role-policy-document file://"pod-execution-role-trust-policy.json"
- 最后,使用以下命令将所需的角色和策略连接在一起:
$ aws iam attach-role-policy \ --policy-arn arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy \
--role-name AmazonEKSFargatePodExecutionRole
-
在
animals-cluster
集群上。 -
在
animals-cluster
页面上,执行以下操作:-
选择
animals-profile
。 -
对于在前一步创建的
AmazonEKSFargatePodExecutionRole
角色。 -
选择子网下拉框,并取消选择名称中包含Public的任何子网。仅支持在 Fargate 上运行的 Pods 使用私有子网。
-
选择下一步。
-
- 在
default
下。* 然后选择下一步。* 在审查并创建页面,审查你的 Fargate 配置文件的信息,并选择创建。* 几分钟后,Fargate 配置文件配置部分的状态将从创建中变为活动。在状态变为活动之前,不要继续执行下一步。* 如果你计划将所有 Pods 部署到 Fargate(不使用 Amazon EC2 节点),请按以下步骤创建另一个 Fargate 配置文件并在 Fargate 上运行默认的名称解析器(CoreDNS)。
注意
如果你不这样做,目前将不会有任何节点。
-
在
animals-profile
下。 -
在Fargate 配置文件下,选择添加 Fargate 配置文件。
-
在名称字段中输入CoreDNS。
-
对于你在步骤 13中创建的
AmazonEKSFargatePodExecutionRole
角色。 -
单击其名称中的
Public
。Fargate 仅支持私有子网中的 Pods。 -
选择下一步。
-
在
kube-system
下。 -
选择匹配标签,然后选择添加标签。
-
在值字段中输入
k8s-app
作为kube-dns
。这是必要的,以便将默认的名称解析器(CoreDNS)部署到 Fargate。 -
选择下一步。
-
在审查并创建页面,审查 Fargate 配置文件的信息并选择创建。
-
运行以下命令,删除 CoreDNS Pods 上的默认
eks.amazonaws.com/compute-type : ec2
注解:kubectl patch deployment coredns \ -n kube-system \ --type json \ -p='[{"op": "remove", "path": "/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]'
注意
系统会根据你添加的 Fargate 配置文件标签创建并部署两个节点。你不会在节点组中看到任何列出的内容,因为 Fargate 节点不适用,但你将在计算标签中看到新的节点。
若需更详细的解释,可以按照以下链接中的逐步指南来创建集群:
docs.aws.amazon.com/eks/latest/userguide/getting-started-console.xhtml
(开始使用 Amazon EKS – AWS 管理控制台和 AWS CLI)
当你的集群准备好后,可以继续执行以下步骤:
-
配置
kubectl
以访问 AWS 上的新集群,如下所示:$ aws eks update-kubeconfig --name animals-cluster
响应应类似于以下内容:
Added new context arn:aws:eks:eu-central-...:cluster/animals-cluster to /Users/<user-name>/.kube/config
这里,<user-name>
对应于你正在使用的机器上的用户名。
-
双重检查
kubectl
是否使用了正确的上下文——即刚为 AWS 上的集群创建并添加到你的~/.kube/config
文件中的上下文:$ kubectl config current-context
答案应类似于以下内容:
arn:aws:eks:eu-central-...:cluster/animals-cluster
如果另一个上下文是活动状态,请使用kubectl config use-context
命令,并结合正确的 AWS 上下文。
-
使用
kubectl
列出集群上的所有资源,像这样:$ kubectl get all
此时的答案应如下所示:
图 18.4 – Amazon EKS – kubectl get all
-
要查看集群的节点,请使用以下命令:
$ kubectl get nodes
然后你应该看到类似以下的内容:
图 18.5 – EKS 集群中节点的列表
-
导航到本章的
ch18
文件夹,创建一个aws-eks
子文件夹,然后进入该文件夹:$ cd ~/The-Ultimate-Docker-Container-Book/ch18$ mkdir aws-eks && cd aws-eks
-
在此子文件夹中,创建一个名为
deploy-nginx.yaml
的文件,内容如下:
图 18.6 – 在 Amazon EKS 上部署 nginx 的规范
-
使用
kubectl
将我们的部署部署到集群,如下所示:$ kubectl apply -f deploy-nginx.yaml
-
使用以下命令观察 Pod 的创建过程:
$ kubectl get pods -w
然后等待它们准备就绪:
图 18.7 – 列出部署到 AWS 的 Pods
-
等待它们在
1/1
的值。 -
在 AWS 控制台中,导航到你的集群。
-
在
web
Pods 和两个coredns
Pods 已创建。 -
在计算选项卡中,观察到已创建多个 Fargate 节点。
-
深入到节点以查看已部署到其上的 Pod。
-
进一步深入到 Pod,并观察其详细信息视图中显示的事件列表。
恭喜你——你已在 AWS 上创建了一个完全托管的 Kubernetes 集群,并使用kubectl
在其上创建了第一个部署!正如你所知,这是一项相当了不起的成就。结果表明,在讨论的所有云提供商中,AWS 需要远远比其他更多的步骤才能运行一个 Kubernetes 集群。
在离开之前,并为了避免意外费用,请确保清理掉在此练习期间创建的所有资源。为此,请按照以下步骤操作:
-
使用
kubectl
删除先前的部署:$ kubectl delete -f deploy-nginx.yaml
-
定位你的
animals-cluster
集群并选择它。 -
在
animals-profile
和CoreDNS
配置文件中删除它们。 -
当删除这两个配置文件时(可能需要几分钟),然后点击删除集群按钮以摆脱该集群。
-
删除你创建的 VPC AWS CloudFormation 堆栈。
-
打开AWS CloudFormation控制台,网址为
console.aws.amazon.com/cloudformation
。 -
选择
animals-stack
堆栈,然后选择删除。 -
在删除 animals-stack确认对话框中,选择删除堆栈。
-
删除你创建的 IAM 角色。
-
打开 IAM 控制台,网址为
console.aws.amazon.com/iam/
。 -
在左侧导航窗格中,选择角色。
-
从列表中选择你创建的每个角色(
myAmazonEKSClusterRole
,以及AmazonEKSFargatePodExecutionRole
或myAmazonEKSNodeRole
)。选择删除,输入请求的确认文本,然后选择删除。
或者,按照 AWS 文档中第 5 步:删除资源部分的步骤执行:
docs.aws.amazon.com/eks/latest/userguide/getting-started-console.xhtml
这是一次相当了不起的成就!创建和管理一个 EKS 集群需要比我们预期更多的细节知识。我们将看到,其他提供商在这方面更加用户友好。
现在我们大致了解了 Amazon EKS 的功能,接下来让我们看看全球第二大云服务提供商的产品组合。
探索微软的 AKS
要在 Azure 中实验微软的容器相关服务,我们需要一个 Azure 账户。你可以创建一个试用账户或使用现有账户。你可以在这里获得免费试用账户:azure.microsoft.com/en-us/free/
。
微软在 Azure 上提供了不同的容器相关服务。最易使用的可能是 Azure 容器实例,它承诺是运行容器的最快和最简单方式,无需配置任何虚拟机(VMs),也不需要采用更高级的服务。如果你只想在托管环境中运行单个容器,这项服务非常有用。设置非常简单。在 Azure 门户(portal.azure.com
)中,你首先创建一个新的资源组,然后创建一个 Azure 容器实例。你只需要填写一个简短的表格,填写容器名称、使用的镜像和要打开的端口等属性。容器可以通过公共或私有 IP 地址提供,并且如果容器崩溃,它会自动重启。这里有一个不错的管理控制台,例如用于监控资源消耗,如 CPU 和内存。
第二个选择是Azure 容器服务(ACS),它提供了一种简化集群虚拟机创建、配置和管理的方式,这些虚拟机经过预配置可运行容器化应用。ACS 使用 Docker 镜像,并提供三种编排工具的选择:Kubernetes、Docker Swarm 和分布式云操作系统(DC/OS)(由 Apache Mesos 提供支持)。微软声称其服务能够扩展到数万个容器。ACS 是免费的,只有计算资源会收费。
在这一部分,我们将重点讨论基于 Kubernetes 的最流行的产品。它叫做 AKS,可以在这里找到:azure.microsoft.com/en-us/services/kubernetes-service/
。AKS 使你可以轻松地在云中部署应用并在 Kubernetes 上运行它们。所有复杂和繁琐的管理任务都由微软处理,你可以完全专注于你的应用程序。这意味着你永远不必处理安装和管理 Kubernetes、升级 Kubernetes 或升级底层 Kubernetes 节点操作系统等任务。这些都由 Microsoft Azure 的专家处理。此外,你永远不必处理 etc
或 Kubernetes 主节点。这些都被隐藏起来,你唯一需要交互的是运行你应用的 Kubernetes 工作节点。
准备 Azure CLI
话虽如此,让我们开始。我们假设你已经创建了一个免费试用账户,或者正在使用 Azure 上的现有账户。有多种方式可以与 Azure 账户进行交互。我们将使用在本地计算机上运行的 Azure CLI。我们可以将 Azure CLI 下载并安装到本地计算机,或者在本地 Docker Desktop 上的容器内运行它。由于本书的主题是容器,我们选择后者。
最新版本的 Azure CLI 可以在 Docker Hub 上找到。让我们拉取它:
$ docker image pull mcr.microsoft.com/azure-cli:latest
我们将从这个 CLI 运行一个容器,并在这个容器内部的 shell 中执行所有后续命令。现在,我们需要克服一个小问题——这个容器中没有安装 Docker 客户端。但是我们还需要运行一些 Docker 命令,因此我们必须创建一个从前面提到的镜像派生的自定义镜像,其中包含 Docker 客户端。为此所需的 Dockerfile 可以在 sample-solutions/ch18
子文件夹中找到,其内容如下:
FROM mcr.microsoft.com/azure-cli:latestRUN apk update && apk add docker
在 第 2 行,我们仅使用 Alpine 包管理器 apk
来安装 Docker。然后我们可以使用 Docker Compose 来构建并运行这个自定义镜像。对应的 docker-compose.yml
文件如下:
version: "2.4"services:
az:
image: fundamentalsofdocker/azure-cli
build: .
command: tail -F anything
working_dir: /app
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .:/app
注意
tail -F anything
命令用于保持容器运行,并且用于挂载 Docker 套接字和当前文件夹到 volumes
部分。
提示
如果你在 Windows 上运行 Docker Desktop,则需要定义 COMPOSE_CONVERT_WINDOWS_PATHS
环境变量,才能挂载 Docker 套接字。你可以在 Bash shell 中使用 export COMPOSE_CONVERT_WINDOWS_PATHS=1
,或在运行 PowerShell 时使用 $Env:COMPOSE_CONVERT_WINDOWS_PATHS=1
。更多详情请参见以下链接:github.com/docker/compose/issues/4240
。
现在,让我们构建并运行这个容器,步骤如下:
$ docker compose up --build -d
接下来,让我们进入 az
容器并在其中运行 Bash shell,使用以下命令:
$ docker compose exec az /bin/bash
你应该会看到如下输出:
376f1e715919:/app #
注意,你的哈希码(376f1e...
)代表容器内的主机名将会不同。为了简化后续命令的阅读,我们将省略哈希码。
正如你所注意到的,我们发现自己正在容器内的 Bash shell 中运行。首先,我们来检查 CLI 的版本:
# az --version
这将生成类似如下的输出:
azure-cli 2.49.0core 2.49.0
telemetry 1.0.8
Dependencies:
msal 1.20.0
azure-mgmt-resource 22.0.0
Python location '/usr/local/bin/python'
Extensions directory '/root/.azure/cliextensions'
Python (Linux) 3.10.11 (main, May 11 2023, 23:59:31) [GCC 12.2.1 20220924]
Legal docs and information: aka.ms/AzureCliLegal
Your CLI is up-to-date.
好的——我们运行的版本是 2.49.0。接下来,我们需要登录我们的账户。执行此命令:
# az login
你将看到以下消息:
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code <code> to authenticate.
按照指示通过浏览器登录。一旦成功认证了 Azure 账户,你可以返回终端并且应该已成功登录,输出结果会显示如下:
[ {
"cloudName": "AzureCloud",
"id": "<id>",
"isDefault": true,
"name": "<account name>",
"state": "Enabled",
"tenantId": "<tenant-it>",
"user": {
"name": <your-email>,
"type": "user"
}
}
]
现在,我们已经准备好将容器镜像首先迁移到 Azure。
在 Azure 上创建容器注册表
首先,我们创建一个名为 animal-rg
的新资源组。在 Azure 中,资源组用于逻辑上将一组相关资源归为一类。为了获得最佳的云体验并保持低延迟,选择一个靠近您的数据中心位置非常重要。请按照以下步骤操作:
-
你可以使用以下命令列出所有区域:
# az account list-locations
输出应如下所示:
[ {
"displayName": "East Asia",
"id": "/subscriptions/186760.../locations/eastasia",
"latitude": "22.267",
"longitude": "114.188",
"name": "eastasia",
"subscriptionId": null
},
...
]
这将给你一长串所有可供选择的区域。使用名称——例如,eastasia
——来标识你选择的区域。在我的例子中,我将选择 westeurope
。请注意,并非所有列出的区域都有效用于资源组。
-
创建资源组的命令很简单;我们只需要为组指定名称和位置,如下所示:
# az group create --name animals-rg --location westeurope{ "id": "/subscriptions/186.../resourceGroups/animals-rg", "location": "westeurope", "managedBy": null, "name": "animals-rg", "properties": { "provisioningState": "Succeeded" }, "tags": null, "type": "Microsoft.Resources/resourceGroups"}
确保你的输出显示 "``provisioningState": "Succeeded"
。
注意
在生产环境中运行容器化应用时,我们希望确保能够从容器注册表中自由地下载相应的容器镜像。到目前为止,我们一直从 Docker Hub 下载镜像,但这通常是不可行的。出于安全原因,生产系统的服务器通常无法直接访问互联网,因此无法连接到 Docker Hub。让我们遵循这一最佳实践,并假设我们即将创建的 Kubernetes 集群也面临相同的限制。
那么,我们该怎么办呢?解决方案是使用一个接近我们集群并且处于相同安全上下文中的容器镜像注册表。在 Azure 中,我们可以创建一个 Azure 容器注册表(ACR)实例并在其中托管我们的镜像,接下来我们将执行以下操作:
-
让我们首先创建一个注册表,如下所示:
# az acr create --resource-group animals-rg \ --name <acr-name> --sku Basic
请注意 <acr-name>
必须是唯一的。在我的例子中,我选择了 gnsanimalsacr
这个名称。缩短后的输出如下所示:
Registration succeeded.{
"adminUserEnabled": false,
"creationDate": "2023-06-04T10:31:14.848776+00:00",
...
"id": "/subscriptions/186760ad...",
"location": "westeurope",
"loginServer": "gnsanimalsacr.azurecr.io",
"name": " gnsanimalsacr ",
...
"provisioningState": "Succeeded",
-
成功创建容器注册表后,我们需要使用以下命令登录该注册表:
# az acr login --name <acr-name>
对前述命令的响应应为:
Login Succeeded
一旦我们成功登录到 Azure 上的容器注册表,我们需要正确标记我们的容器,以便我们能够将其推送到 ACR。接下来将描述如何标记和推送镜像到 ACR。
将我们的镜像推送到 ACR
一旦成功登录到 ACR,我们可以标记我们的镜像,以便它们可以推送到注册表中。为此,我们需要知道 ACR 实例的 URL。它如下所示:
<acr-name>.azurecr.io
我们现在使用前面提到的 URL 来标记我们的镜像:
# docker image tag fundamentalsofdocker/ch11-db:2.0 \ <acr-name>.azurecr.io/db:2.0
# docker image tag fundamentalsofdocker/ch11-web:2.0 \
<acr-name>.azurecr.io/web:2.0
然后,我们可以将其推送到我们的 ACR 实例:
# docker image push <acr-name>.azurecr.io/db:2.0# docker image push <acr-name>.azurecr.io/web:2.0
为了确认我们的镜像确实位于 ACR 实例中,我们可以使用此命令:
# az acr repository list --name <acr-name> --output table
这应该会给你以下输出:
Result--------
Db
web
事实上,我们刚刚推送的两个镜像已列出。
到此,我们已准备好创建 Kubernetes 集群。
创建一个 Kubernetes 集群
我们将再次使用自定义的 Azure CLI,运行在 Docker 容器中来创建 Kubernetes 集群。我们需要确保集群能够访问我们刚刚创建的 ACR 实例,镜像就在其中。所以,创建名为 animals-cluster
的集群,并配置两个工作节点的命令如下所示:
# az aks create \ --resource-group animals-rg \
--name animals-cluster \
--node-count 2 \
--generate-ssh-keys \
--attach-acr <acr-name>
这个命令需要一些时间,但几分钟后,我们应该会收到一份 JSON 格式的输出,包含有关新创建集群的所有详细信息。
要访问集群,我们需要 kubectl
。我们可以通过以下命令轻松地在 Azure CLI 容器中安装它:
# az aks install-cli
安装了 kubectl
后,我们需要必要的凭证来使用该工具操作我们在 Azure 上的新 Kubernetes 集群。我们可以通过以下命令获取所需的凭证:
# az aks get-credentials --resource-group animals-rg \ --name animals-cluster
命令应返回以下内容:
Merged "animals-cluster" as current context in /root/.kube/config
在前面命令成功执行后,我们可以列出集群中的所有节点,如下所示:
# kubectl get nodes
这将为我们提供以下列表:
NAME STATUS ROLES AGE VERSIONaks-nodepool1-12528297-vmss000000 Ready agent 4m38s v1.25.68
aks-nodepool1-12528297-vmss000001 Ready agent 4m32s v1.25.68
正如预期的那样,我们有两个工作节点正在运行。这些节点上运行的 Kubernetes 版本是 v1.25.68
。
我们现在准备将应用程序部署到这个集群。在接下来的部分,我们将学习如何将应用程序部署到 Kubernetes。
将我们的应用程序部署到 Kubernetes 集群
为了部署应用程序,我们可以使用 kubectl
apply
命令:
# kubectl apply -f animals.yaml
上述命令的输出应该类似于此:
deployment.apps/web createdservice/web created
deployment.apps/db created
service/db created
现在,我们要测试应用程序。记住,我们为 Web 组件创建了一个类型为 LoadBalancer
的服务。该服务将应用程序暴露到互联网。
该过程可能需要一些时间,因为 AKS 需要为此服务分配一个公共 IP 地址,这只是其中的一项任务。我们可以通过以下命令来观察:
# kubectl get service web --watch
请注意,上述命令中的 --watch
参数。它允许我们监控命令的执行进度。最初,我们应该看到类似这样的输出:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb LoadBalancer 10.0.38.189 <pending> 3000:32127/TCP 5s
公共 IP 地址标记为 pending
。几分钟后,它应该会变成这样:
图 18.8 – Microsoft AKS 上动物应用的 LoadBalancer 服务
现在我们的应用已经准备好,可以通过 IP 地址 20.76.160.79
和端口号 3000
访问。
请注意,负载均衡器将内部端口 32127
映射到外部端口 3000
;这点我第一次并未注意到。
让我们来看看。在新的浏览器标签页中,访问 http://20.76.160.79:3000/pet
,你应该能够看到我们熟悉的应用:
图 18.9 – 我们的示例应用在 AKS 上运行
至此,我们已成功将分布式应用部署到 Azure 托管的 Kubernetes 上。我们无需担心安装或管理 Kubernetes,可以专注于应用本身。
请注意,你还可以通过 Azure 门户 portal.azure.com/
管理你的 Azure 资源组、容器注册表和集群。它的界面与此类似:
图 18.10 – 显示动物资源组的 Microsoft Azure 门户
请熟悉该门户,并尝试深入了解集群、节点和部署情况。
现在我们已经完成了应用的实验,不应忘记删除 Azure 上的所有资源,以避免产生不必要的费用。我们可以通过删除资源组来删除所有已创建的资源,操作如下:
# az group delete --name animal-rg --yes --no-wait
Azure 在容器工作负载方面有一些很有吸引力的服务,并且由于 Azure 主要提供开源的编排引擎,如 Kubernetes、Docker Swarm、DC/OS 和 Rancher,其锁定效应不像 AWS 那么明显。
从技术角度看,如果我们最初在 Azure 上运行容器化应用,后来决定迁移到其他云服务提供商,我们依然可以保持灵活性。成本应该是有限的。
注意
值得注意的是,当你删除资源组时,AKS 集群使用的Azure Active Directory(AAD)服务主体并不会被删除。
有关如何删除服务主体的详细信息,请参阅在线帮助页面。你可以在这里找到相关信息:learn.microsoft.com/en-us/powershell/module/azuread/remove-azureadserviceprincipal?view=azureadps-2.0
。
接下来是 Google 的 GKE 服务。
了解 GKE
Google 是 Kubernetes 的发明者,并且至今仍是其背后的推动力。因此,你可以合理预期,Google 会提供一个吸引人的托管 Kubernetes 服务。
现在让我们快速看一下。要继续,你需要有一个 Google Cloud 账户,或者在此处创建一个测试账户:console.cloud.google.com/freetrial
。请按照以下步骤操作:
-
在主菜单中,选择Kubernetes 引擎。第一次操作时,它会花费几分钟初始化 Kubernetes 引擎。
-
接下来,创建一个新项目并命名为
massai-mara
;这可能需要一些时间。 -
一旦准备好,我们可以通过点击弹出窗口中的创建集群来创建一个集群。
-
在
animals-cluster
上选择离你最近的区域。在作者的例子中,这是europe-west1
。然后点击下一步:网络。 -
保持所有设置为默认值,并点击下一步: 高级设置。
-
再次保持所有设置为默认值,然后点击下一步:审核 并创建。
-
审核你的集群设置,如果一切看起来正常,就点击创建集群,如下图所示:
图 18.11 – GKE 集群创建向导的审核和创建视图
这将再次花费一些时间来为我们配置集群。
- 集群创建完毕后,我们可以通过点击视图右上角的云端终端图标来打开 Cloud Shell。它应该是这样显示的:
图 18.12 – 第一个 Kubernetes 集群已准备好,并且 GKE 中打开了 Cloud Shell
-
现在我们可以通过以下命令将实验室的 GitHub 仓库克隆到这个环境中:
$ git clone https://github.com/PacktPublishing/The-Ultimate-Docker-Container-Book.git ~/src
-
切换到正确的文件夹,你将在其中找到示例解决方案:
$ cd ~/src/sample-solutions/ch18/gce
现在你应该能在当前文件夹中找到一个animals.yaml
文件,你可以使用它将animals
应用部署到我们的 Kubernetes 集群中。
-
通过运行以下命令查看文件内容:
$ less animals.yaml
它与我们在上一章中使用的相同文件几乎内容一致。两者的区别在于:
-
我们使用
LoadBalancer
类型的服务(而不是NodePort
)来公开web
组件。请注意,我们在 Azure AKS 上也做了相同的操作。 -
我们没有为 PostgreSQL 数据库使用卷,因为在 GKE 上正确配置
StatefulSet
比在 Minikube 或 Docker Desktop 这样的产品中更复杂。其结果是,如果db
Pod 崩溃,我们的animals
应用将不会持久化状态。如何在 GKE 上使用持久卷超出了本书的范围。
同时请注意,我们没有使用Google 容器注册表(GCR)来托管容器镜像,而是直接从 Docker Hub 拉取它们。这非常简单——就像我们在关于 AKS 的章节中学到的内容一样——在 Google Cloud 中创建这样的容器注册表非常容易。
-
在继续之前,我们需要设置
gcloud
和kubectl
凭证。以下是我们需要执行的代码:$ gcloud container clusters \ get-credentials animals-cluster --zone <zone>
请将<zone>
替换为你在第 5 步创建集群时选择的相同区域。
前面命令的响应应该是这样的:
Fetching cluster endpoint and auth data.kubeconfig entry generated for animals-cluster.
-
让我们通过运行以下命令查看为该集群创建了哪些节点:
$ kubectl get nodes
你应该会看到类似这样的内容:
图 18.13 – GCE 上的集群节点
我们可以看到在集群中创建了两个节点,并且部署的 Kubernetes 版本显然是v1.25.8
。
-
完成这些之后,是时候部署应用程序了,运行以下命令:
$ kubectl apply -f animals.yaml
输出应如下所示:
图 18.14 – 在 GKE 上部署应用程序
-
一旦对象创建完成,我们可以观察
LoadBalancer web
服务,直到它分配到一个公共 IP 地址,如下所示:$ kubectl get svc/web –watch
前面的命令输出如下:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb LoadBalancer 10.57.129.72 <pending> 3000: 32384/TCP 32s
web LoadBalancer 10\. 57.129.72 35.195.160.243 3000: 32384/TCP 39s
输出中的第二行显示了负载均衡器创建仍在待处理状态时的情况,而第三行则显示了最终状态。按Ctrl + C退出–watch
命令。显然,我们已经分配到了公共 IP 地址35.195.160.243
,端口是3000
。
-
然后,我们可以使用这个 IP 地址并导航到
http://<IP 地址>:3000/pet
,我们应该会看到熟悉的动物图片。 -
花点时间,使用你熟悉的各种
kubectl
命令来分析 GKE 集群中的情况。 -
同时,花点时间使用 GCE 的网页门户,深入查看你的集群详情。特别是,查看集群的可观测性标签。
-
一旦你完成了与应用程序的交互,删除 Google Cloud 控制台中的集群和项目,以避免不必要的费用。
-
你可以在 Cloud Shell 中使用
gcloud
命令行界面来删除集群,如下所示:$ gcloud container clusters delete animals-cluster
这会花费一点时间。或者,你也可以通过网页门户进行相同的操作。
-
接下来列出你所有的项目,如下所示:
$ gcloud projects list
-
接下来,你可以使用以下命令删除之前创建的项目:
$ gcloud projects delete <project-id>
在这里,你应该从之前的list
命令中获得正确的<project-id>
值。
我们在 GKE 上创建了一个托管的 Kubernetes 集群。然后,我们使用通过 GKE 门户提供的 Cloud Shell,首先克隆我们实验室的 GitHub 仓库,然后使用kubectl
工具将animals
应用程序部署到 Kubernetes 集群中。
在查看托管的 Kubernetes 解决方案时,GKE 是一个极具吸引力的选择。它让你轻松启动项目,并且由于 Google 是 Kubernetes 的主要推动者,我们可以放心,始终能利用 Kubernetes 的全部功能。
总结
本章首先介绍了如何在 Amazon EKS 上使用 Fargate 创建一个完全托管的 Kubernetes 集群,并在该集群上部署一个简单的应用程序。然后,你学习了如何在 Azure AKS 上创建托管的 Kubernetes 集群,并运行animals
应用程序,随后又进行了相同的操作来使用 Google 的托管 Kubernetes 解决方案——GKE。
你准备好解锁保持生产环境健康的秘密了吗?在下一章中,我们将深入探讨监控和排查在生产环境中运行的应用程序。我们将探索多种技术,用于对单个服务和整个分布式应用程序进行监控,尤其是它们在 Kubernetes 集群上运行时的情况。但这还不是全部——你还将学习如何基于关键指标创建警报。而当事情出现问题时,我们将指导你如何在不干扰集群或节点的情况下,排查运行中的应用程序。敬请期待,因为这一章将为你提供必要的工具,让你自信地在大规模环境中维护应用程序。
问题
为了评估你的知识,请回答以下问题:
-
列出几个你会选择托管 Kubernetes 服务(如 Amazon EKS、Microsoft 的 AKS 或 Google 的 GKE)来运行应用程序的原因。
-
列举使用托管 Kubernetes 解决方案(如 Amazon EKS、Azure AKS 或 Google GKE)时,考虑将容器镜像托管在相应云服务提供商的容器注册表中的两个原因。
答案
以下是本章问题的一些示例答案:
-
以下是考虑托管 Kubernetes 服务的一些原因:
-
你不想,或者没有资源来安装和管理 Kubernetes 集群。
-
你希望集中精力在为你的业务带来价值的事情上,而大多数情况下,这些事情是应该在 Kubernetes 上运行的应用程序,而不是 Kubernetes 本身。
-
你更倾向于选择按需付费的成本模型。
-
你的 Kubernetes 集群节点会自动修补和更新。
-
升级 Kubernetes 版本且不产生停机时间是简单且直接的。
-
-
将容器镜像托管在云服务提供商的容器注册表(例如 Microsoft Azure 上的 ACR)的两个主要原因如下:
-
镜像离你的 Kubernetes 集群地理位置较近,因此延迟和传输网络成本最低。
-
生产或类似生产的集群理想情况下应该与互联网隔离,因此 Kubernetes 集群节点无法直接访问 Docker Hub。
-