This commit is contained in:
huty 2024-02-20 17:15:27 +08:00 committed by huty
parent 6706e1a633
commit 34158042ad
1529 changed files with 177765 additions and 0 deletions

View File

@ -0,0 +1,5 @@
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 YDD
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,239 @@
# 一个月学会 Kubernetes
![header](./header.png)
哈喽,欢迎来到我的课程。我希望本课程可以给大家带来良好的学习体验。
在每一章中都有一个明确的重点,一个有用的话题,并且这些话题是相互关联的,让你有一个全面的了解,了解如何在实践中使用 Kubernetes。你需要大量的练习每天练习巩固每一章获得的知识形成肌肉记忆。
可以移步到 [GitHub Pages](https://yyong-brs.github.io/learn-kubernetes/) 页面进行阅读。
更多云原生技术,请关注公众号:云原生拓展
![公众号](./gongzh.png)
# 目录
- **第一部分** 快速了解 Kubernetes
- 第一章 [开始之前](./chapter1.md)
- 1.1 [了解 Kubernetes](./chapter1.md#11-了解-kubernetes)
- 1.2 [这本书适合你吗?](./chapter1.md#12-这本书适合你吗)
- 1.3 [创建你的实验环境](./chapter1.md#13-创建你的实验环境)
- 1.4 [立即见效](./chapter1.md#14-立即见效)
- 第二章 [Pods & Deployment 在 Kubernetes 中的应用](./chapter2.md)
- 2.1 [Kubernetes 如何运行并管理容器](./chapter2.md#21-kubernetes-如何运行并管理容器)
- 2.2 [通过控制器运行 Pods](./chapter2.md#22-通过控制器运行-pods)
- 2.3 [在清单文件中定义 Deployments](./chapter2.md#23-在清单文件中定义-deployments)
- 2.4 [应用在 Pods 中运行](./chapter2.md#24-应用在-pods-中运行)
- 2.5 [了解 Kubernetes 资源管理](./chapter2.md#25-了解-kubernetes-资源管理)
- 2.6 [实验室](./chapter2.md#26-实验室)
- 第三章 [通过 Service 网络连接 Pods](./chapter3.md)
- 3.1 [Kubernetes 如何路由网络流量](./chapter3.md#31-kubernetes-如何路由网络流量)
- 3.2 [在 Pods 间路由流量](./chapter3.md#32-在-pods-间路由流量)
- 3.3 [路由外部流量到 Pods](./chapter3.md#33-路由外部流量到-pods)
- 3.4 [将流量路由到 Kubernetes 外面](./chapter3.md#34-将流量路由到-kubernetes-外面)
- 3.5 [理解 Kubernetes Service 解析](./chapter3.md#35-理解-kubernetes-service-解析)
- 3.6 [实验室](./chapter3.md#36-实验室)
- 第四章 [通过 ConfigMaps 和 Secrets 配置应用程序](./chapter4.md)
- 4.1 [Kubernetes 如何为应用提供配置](./chapter4.md#41-kubernetes-如何为应用提供配置)
- 4.2 [在 ConfigMaps 中存储和使用配置文件](./chapter4.md#42-在-configmaps-中存储和使用配置文件)
- 4.3 [从 ConfigMaps 中查找配置数据](./chapter4.md#43-从-configmaps-中查找配置数据)
- 4.4 [使用 Secrets 配置敏感数据](./chapter4.md#44-使用-secrets-配置敏感数据)
- 4.5 [管理 Kubernetes 中的应用程序配置](./chapter4.md#45-管理-kubernetes-中的应用程序配置)
- 4.6 [实验室](./chapter4.md#46-实验室)
- 第五章 [通过 volumes,mounts,claims 存储数据](./chapter5.md)
- 5.1 [Kubernetes 如何构建容器文件系统](./chapter5.md#51-kubernetes-如何构建容器文件系统)
- 5.2 [在节点使用 volumes 及 mounts 存储数据](./chapter5.md#52-在节点使用-volumes-及-mounts-存储数据)
- 5.3 [使用 persistent volumes 及 claims 存储集群范围数据](./chapter5.md#53-使用-persistent-volumes-及-claims-存储集群范围数据)
- 5.4 [动态 volume provisioning 及 storage classes](./chapter5.md#54-动态-volume-provisioning-及-storage-classes)
- 5.5 [理解 Kubernetes 中存储的选择](./chapter5.md#55-理解-kubernetes-中存储的选择)
- 5.6 [实验室](./chapter5.md#56-实验室)
- 第六章 [通过 controllers 在多个 Pod 之间扩展应用](./chapter6.md)
- 6.1 [Kubernetes 如何大规模运行应用程序](./chapter6.md#61-kubernetes-如何大规模运行应用程序)
- 6.2 [使用 Deployments 和 ReplicaSets 来扩展负载](./chapter6.md#62-使用-deployments-和-replicasets-来扩展负载)
- 6.3 [使用 DaemonSets 实现高可用性](./chapter6.md#63-使用-daemonsets-实现高可用性)
- 6.4 [理解 Kubernetes 中的对象所有权](./chapter6.md#64-理解-kubernetes-中的对象所有权)
- 6.5 [实验室](./chapter6.md#65-实验室)
- **第二部分** 现实世界中的 Kubernetes
- 第七章 [使用多容器 Pods 扩展应用程序](./chapter7.md)
- 7.1 [Pod 中多个容器如何通信](./chapter7.md#71-pod-中多个容器如何通信)
- 7.2 [使用 init 容器设置应用程序](./chapter7.md#72-使用-init-容器设置应用程序)
- 7.3 [通过 adapter 容器以应用一致性](./chapter7.md#73-通过-adapter-容器以应用一致性)
- 7.4 [通过 ambassador 容器抽象连接](./chapter7.md#74-通过-ambassador-容器抽象连接)
- 7.5 [理解 Pod 环境](./chapter7.md#75-理解-pod-环境)
- 7.6 [实验室](./chapter7.md#76-实验室)
- 第八章 [使用 StatfulSets 和 Jobs 运行数据量大的应用](./chapter8.md)
- 8.1 [Kubernetes 如何用 StatefulSets 建模稳定性](./chapter8.md#81-kubernetes-如何用-statefulsets-建模稳定性)
- 8.2 [在 StatefulSets 中使用 init 容器引导 Pod](./chapter8.md#82-在-statefulsets-中使用-init-容器引导-pod)
- 8.3 [使用卷声明模板请求存储](./chapter8.md#83-使用卷声明模板请求存储)
- 8.4 [使用 Jobs 和 CronJobs 运行维护任务](./chapter8.md#84-使用-jobs-和-cronjobs-运行维护任务)
- 8.5 [为有状态应用程序选择平台](./chapter8.md#85-为有状态应用程序选择平台)
- 8.6 [实验室](./chapter8.md#86-实验室)
- 第九章 [通过 rollouts 和 rollbacks 管理应用发布](./chapter9.md)
- 9.1 [Kubernetes 如何管理 rollouts](./chapter9.md#91-kubernetes-如何管理-rollouts)
- 9.2 [使用 rollouts 和 rollbacks 更新 Deployments](./chapter9.md#92-使用-rollouts-和-rollbacks-更新-deployments)
- 9.3 [为 Deployments 配置滚动更新](./chapter9.md#93-为-deployments-配置滚动更新)
- 9.4 [DaemonSets 和 StatefulSets 中的滚动更新](./chapter9.md#94-daemonSets-和-statefulsets-中的滚动更新)
- 9.5 [理解发布策略](./chapter9.md#95-理解发布策略)
- 9.6 [实验室](./chapter9.md#96-实验室)
- 第十章 [通过 Helm 打包并管理应用](./chapter11.md)
- 10.1 [Helm 给 Kubernetes 带来了什么](./chapter11.md#101-helm-给-Kubernetes-带来了什么)
- 10.2 [使用 Helm 打包你自己的应用](./chapter11.md#102-使用-helm-打包你自己的应用)
- 10.3 [charts 中的模块依赖](./chapter11.md#103-charts-中的模块依赖)
- 10.4 [升级及回滚 Helm releases](./chapter11.md#104-升级及回滚-helm-releases)
- 10.5 [理解 Helm 定位](./chapter11.md#105-理解-helm-定位)
- 10.6 [实验室](./chapter11.md#106-实验室)
- 第十一章 [App 开发——开发人员工作流程及 CI/CD](./chapter11.md)
- 11.1 [Docker 开发人员工作流程](./chapter11.md#111-docker-开发人员工作流程)
- 11.2 [Kubernetes 开发人员工作流程](./chapter11.md#112-kubernetes-开发人员工作流程)
- 11.3 [使用上下文和名称空间隔离工作负载](./chapter11.md#113-使用上下文和名称空间隔离工作负载)
- 11.4 [在不考虑 Docker 的 Kubernetes 中持续交付](./chapter11.md#114-在不考虑-docker-的-kubernetes-中持续交付)
- 11.5 [评估 Kubernetes 上的开发人员工作流程](./chapter11.md#115-评估-kubernetes-上的开发人员工作流程)
- 11.6 [实验室](./chapter11.md#116-实验室)
- **第三部分** 为生产而准备
- 第十二章 [增强自我修复应用程序](./chapter12.md)
- 12.1 [使用 readiness 探测将流量路由到健康 Pods](./chapter12.md#121-使用-readiness-探测将流量路由到健康-pods)
- 12.2 [通过 liveness 探测重启不健康的 Pods](./chapter12.md#122-通过-liveness-探测重启不健康的-pods)
- 12.3 [使用 Helm 安全地部署升级](./chapter12.md#123-使用-helm-安全地部署升级)
- 12.4 [通过 resource limits 保护应用和节点](./chapter12.md#124-通过-resource-limits-保护应用和节点)
- 12.5 [了解自我修复应用的局限性](./chapter12.md#125-了解自我修复应用的局限性)
- 12.6 [实验室](./chapter12.md#126-实验室)
- 第十三章 [使用 Fluentd 和 Elasticsearch 集中化日志](./chapter13.md)
- 13.1 [Kubernetes 如何存储日志条目](./chapter13.md#131-kubernetes-如何存储日志条目)
- 13.2 [使用 Fluentd 收集节点日志](./chapter13.md#132-使用-fluentd-收集节点日志)
- 13.3 [向 Elasticsearch 发送日志](./chapter13.md#133-向-elasticsearch-发送日志)
- 13.4 [解析和过滤日志条目](./chapter13.md#134-解析和过滤日志条目)
- 13.5 [了解 Kubernetes 中的日志记录选项](./chapter13.md#135-了解-kubernetes-中的日志记录选项)
- 13.6 [实验室](./chapter13.md#136-实验室)
- 第十四章 [使用 Prometheus 监控应用程序和 Kubernetes](./chapter14.md)
- 14.1 [Prometheus 如何监控 Kubernetes 的工作负载](./chapter14.md#141-prometheus-如何监控-kubernetes-的工作负载)
- 14.2 [监视使用 Prometheus 客户端库构建的应用程序](./chapter14.md#142-监视使用-prometheus-客户端库构建的应用程序)
- 14.3 [通过 metrics exporters 来监控第三方应用](./chapter14.md#143-通过-metrics-exporters-来监控第三方应用)
- 14.4 [监控容器以及 kubernetes 对象](./chapter14.md#144-监控容器以及-kubernetes-对象)
- 14.5 [了解您在监控方面所做的投资](./chapter14.md#145-了解您在监控方面所做的投资)
- 14.6 [实验室](./chapter14.md#146-实验室)
- 第十五章 [使用 Ingress 管理流入流量](./chapter15.md)
- 15.1 [Kubernetes 如何使用 Ingress 路由流量](./chapter15.md#151-kubernetes-如何使用-ingress-路由流量)
- 15.2 [使用 Ingress rules 路由 Http 流量](./chapter15.md#152-使用-ingress-rules-路由-http-流量)
- 15.3 [比较 Ingress 控制器](./chapter15.md#153-比较-ingress-控制器)
- 15.4 [使用 Ingress 通过 HTTPS 保护您的应用程序](./chapter15.md#154-使用-ingress-通过-https-保护您的应用程序)
- 15.5 [理解 Ingress 及 Ingress 控制器](./chapter15.md#155-理解-ingress-及-ingress-控制器)
- 15.6 [实验室](./chapter15.md#156-实验室)
- 第十六章 [使用策略上下文和准入控制保护应用程序](./chapter16.md)
- 16.1 [使用网络策略(network policies)保护通信](./chapter16.md#161-使用网络策略(network-policies)保护通信)
- 16.2 [使用安全上下文(security contets)限制容器功能](./chapter16.md#162-使用安全上下文(security-contets)限制容器功能)
- 16.3 [使用 webhook 阻止和修改工作负载](./chapter16.md#163-使用-webhook-阻止和修改工作负载)
- 16.4 [使用 Open Policy Agent 控制准入](./chapter16.md#164-使用-open-policy-agent-控制准入)
- 16.5 [深入了解 Kubernetes 中的安全性](./chapter16.md#165-深入了解-kubernetes-中的安全性)
- 16.6 [实验室](./chapter16.md#166-实验室)

View File

@ -0,0 +1,5 @@
# Build settings
remote_theme: pages-themes/modernist@v0.2.0
plugins:
- jekyll-remote-theme

View File

@ -0,0 +1,206 @@
# 第一章 开始之前
Kubernetes 很强大。2014 年,在 GitHub 上它被作为开源项目发布,现如今在全球社区平均每周有 200 次变更提交拥有2500名贡献者。一年一度的 KubeCon 会议的与会者从 2016年的 1000 多人增加到现在的 12000 多人,现在已经成为美国、欧洲和亚洲举办的全球系列活动。所有主流的云服务商提供托管的 Kubernetes 服务,您可以在数据中心或者在你的笔记本电脑上运行 Kubernete最终它们都是等同的。
独立性和标准化是 Kubernetes 如此流行的主要原因。一旦您的应用程序在 Kubernetes 中良好运行,就可以部署它们到任何地方,这对迁移到云的组织都很有吸引力,因为
使它们能够在数据中心和其他云之间移动而无需重写代码。一旦你掌握了Kubernetes你就可以在项目和组织之间快速移动提高生产力。
但要达到这一点很难,因为 Kubernetes 很难。即使很简单应用程序也需要通过它部署为多个组件以轻松的就可以跨越数百行的自定义文件格式代码进行描述。Kubernetes 将诸如负载平衡、网络、存储和计算等基础设施级别的问题带入应用程序配置这可能是新概念具体取决于您的IT背景。此外Kubernetes 总是在扩展,它每季度发布新版本,通常会带来大量新功能。
但这是值得的。我花了很多年帮助人们学习 Kubernetes然后一种共同的模式出现了问题“为什么这么复杂” 变成 “你能做到吗?这太神奇了!” Kubernetes确实是一项令人惊叹的技术。你对它了解得越多你就会越喜欢它这本书会加速你
的 Kubernetes 精通之旅。
## 1.1 了解 Kubernetes
本书提供了 Kubernetes 的实际使用介绍。每个章节都提供了“现在就试试”的练习,您可以通过练习和实验室获得大量使用 Kubernetes 的经验。我们将在下一章开始实际工作,但我们需要一点
理论先行。让我们先了解一下 Kubernetes到底是什么以及它解决了什么问题。
Kubernetes 是一个运行容器的平台它负责启动容器化应用程序、滚动更新、Service 层面维护、扩展以满足需求、安全访问等。在 Kubernetes 中有两个核心概念,一个是 API用于定义你的应用另外一个是集群运行你的应用。集群是一组单独的服务器它们都配置了容器运行时如Docker然后使用Kubernetes 连接到单个逻辑单元中。图 1.1 显示了集群的高层级视图:
![图1.1](./images/Figure1.1.png)
<center>图1.1 Kubernetes 集群包含了一组服务器,它们加入到一个组中运行容器 </center>
集群管理员负责管理单独的服务器,它们在 Kubernetes 被称作节点node。你可以通过添加节点来扩展集群的容量也可以使节点脱机以维护或者升级 Kubernetes 集群。在像 Microsoft Azure Kubernetes SeviceAKS或 Amazon Elastic Kubernete ServiceEKS这样的托管服务中这些功能都封装在简单的 web 界面或命令行中。正常使用时您忘记了底层节点,将集群视为单个实体。
Kubernetes 集群用于运行你的应用程序。你通过 YAML 文件来定义应用,然后将这些文件发送给 Kubernetes API。Kubernetes 会查看你在 Yaml 文件中有什么要求,并将其与集群中已经运行的应用进行比较。它会进行任何必要的更改以达到所需的状态,这可能是更新配置、删除容器或创建新容器。为了高可用,容器将会被分发到集群中去,它们可以通过 Kubernetes 管理的虚拟网络进行通信。图 1.2 显示了部署的过程,但是没有看到节点,因为我们在这个层面上并不真正关心它们。
![图1.2](./images/Figure1.2.png)
<center>图1.2 当您将应用程序部署到 Kubernetes 集群时,通常可以忽略实际节点 </center>
定义应用的结构是你的工作,但是运行和管理的所有工作都交给了 Kubernetes 。如果某个集群中的节点断线了然后有一些容器在该节点上Kubernetes 发现了并开始在其它节点创建替换的容器。如果某个应用容器变成不健康状态Kubernetes 会去重启它。如果组件由于高负载而承受压力Kubernetes 可以启动额外的该组件的新容器来降低压力。如果你将你的工作通过 Docker image 以及 Kubernetes YAML 文件进行管理,你将得到可以以同样的方式在不同的集群上运行的自我修复应用程序。
Kubernetes 不仅仅管理容器这促使它成为一个功能完整的应用程序平台。Kubernetes 集群有一个分布式数据库您可以使用它来存储应用程序的配置文件和API密钥以及连接凭据等机密信息。Kubernetes 将这些信息无缝交付给您的容器
允许您在每个环境中使用相同的容器镜像并应用正确的配置。Kubernetes还提供存储因此您的应用程序可以在容器外维护数据为有状态应用程序提供高可用性。Kubernetes 还实现将网络流量发送到正确的容器进行处理。图1.3显示了其他资源类型:包括 Kubernetes的主要功能。
![图1.3](./images/Figure1.3.png)
<center>图1.3 Kubernetes 不仅仅管理容器,集群还管理其他资源 </center>
我还没有谈到容器中的应用程序是什么样子的;那是因为 Kubernetes 并不在乎。您可以在多个容器中运行通过云原生理念设计的跨多个微服务的应用。您可以运行作为一个整体构建在一个大容器中的旧版应用程序。它们可能是Linux应用程序或
Windows应用程序。您可以使用相同的API在YAML文件中定义所有类型的应用程序你可以在一个集群上运行它们。使用Kubernetes的乐趣在于它在所有应用之上上增加了一层一致性——老的 .Net和 Java 单体应用以及新的 Node.js和 Go 微服务都是以相同的方式被描述、部署和管理的。
这正是我们开始使用 Kubernetes 所需要的所有理论但在此之前我们再深入一点我想为我所讲的概念取一些恰当的名字。关于这些YAML文件被正确地称为应用程序清单manifests因为它们是一个应用程序的所有组件的列表。这些组件是Kubernetes 资源他们也有自己的名字。图1.4采用了图1.3中的概念并应用正确的Kubernetes资源名称。
![图1.4](./images/Figure1.4.png)
<center>图1.4 真实情况:这些是您需要掌握的最基本的 Kubernetes 资源 </center>
我告诉过你Kubernetes很难。:)但我们在接下来的几章中将一次覆盖所有这些资源对理解进行分层。当你完成第6章时该图将完全有意义您将在 YAML文件中定义这些资源并在自己的Kubernetes 中运行这些资源拥有很多经验。
## 1.2 这本书适合你吗?
本书的目标是快速跟踪您的 Kubernetes 学习, 让你有信心在 Kubernetes 中定义和运行自己的应用程序,并且让你了解上生产实践之路是怎样的。学习 Kubernetes 的最佳方法是练习,如果你遵循章节中的所有示例并在实验室中工作,
等你读完这本书那么您将对Kubernetes的所有最重要的部分有一个坚实的理解。
但 Kubernetes 是一个巨大的话题我不会涵盖所有内容。最大的差距在管理方面我不会深入讨论集群设置和管理因为它们在不同的基础设施中有所不同。如果你计划小跑进入云环境中的Kubernetes作为您的生产环境那么无论如何托管服务中都会处理这些问题。如果你想获得 Kubernetes 认证这本书是一个很好的开始但它不会让你一直受益。有两个主要的Kubernetes认证Certified Kubernetes Application Developer (CKAD) 以及 Certified Kubernetes Administrator (CKA)。这本书约占 CKAD 课程的 80%,约占 CKA 课程的50%。
此外,你还需要掌握合理数量的背景知识来有效地阅读本书。当我们在讲解 Kubernetes 的特性时,但我不会填补任何关于容器的空白。如果您不熟悉镜像、容器和注册表等概念,我建议从我的这本书《一个月学会 Docker》开始。你不需要在使用 Kubernetes 时使用 Docker但它是打包您的应用程序以便您可以在Kubernetes的容器中运行它们的工具。
如果您将自己归类为一个全新的或想提升 Kubernetes 知识那么这就是适合你的书。你的背景角色可能是开发、运维、架构、DevOps或站点可靠性工程SRE——Kubernetes涉及所有这些角色因此他们都受到欢迎并且你会学到很多东西。
## 1.3 创建你的实验环境
每个 Kubernetes 集群可以包含上百个节点,但是对于本书的练习来说,只需要单节点就够了。我们现在将会配置你的实验环境,为下一章做好准备。有数十种 kubernetes 平台可供使用,本书中的练习适用于任何经认证的 Kubernetes。我将会讲解如何在 Linux、Windows、Mac、AWS 以及 Azure 中创建你的实验环境,这些包括了主流的选项。我使用了 Kubernetes 1.18 版本,但是更早或更新的版本也是可以的。
本地运行 Kubernetes 最简单的选择是 Docker Desktop它是一个软件包为您提供 Docker 和 Kubernetes 以及所有命令行工具。它还可以很好地与您的计算机网络集成,并有一个方便的重置 Kubernetes 按钮必要时可以清除所有内容。Docker Desktop 在 Windows 10 和 macOS 上受支持,如果这对您不起作用,我也将介绍一些替代方案。
有一点您应该知道Kubernetes 本身的组件需要作为 Linux 容器运行。您不能在 Windows 中运行 Kubernetes尽管您可以在带有多节点的 Kubernetes 集群中的容器运行 Windows 应用程序),因此您需要一个 Linux虚拟机VM如果您在Windows上工作。Docker Desktop 启动该虚拟机并为您管理它。
对于 Windows 用户,最后一个注意事项是:请使用 PowerShell 来进行相关练习。PowerShell 支持许多 Linux 命令,如果您尝试使用经典的 Windows 命令终端,您将从一开始就遇到一些问题。
### 1.3.1 下载本书源码
本书的所有例子和练习的源码都存储在 GitHub 仓库中,同时还提供了一些样例解决方案。如果你真在使用 Git 并且已经安装 Git 客户端,你可以通过如下命令 Clone 仓库到你的电脑上:
`git clone https://github.com/yyong-brs/learn-kubernetes.git`
如果你不熟悉 Git 使用,你可以访问 https://github.com/yyong-brs/learn-kubernetes 点击 Code - Download ZIP 下载源文件。
根目录下有个 kiamol 文件夹包含了源码信息,然后它也包含了每一章的练习内容。
### 1.3.2 安装 Docker Desktop
Docker Desktop 运行在 Windows 10 或者 macOS Sierra (版本 10.12 或者更高版本)。浏览器访问 https://www.docker.com/products/docker-desktop 并选择稳定版本。下载之后运行它,接受所有的默认设置。在 Windows 上,这可能包括重新启动以添加新的 Windows 功能的操作。当 Docker Desktop 成功运行,您将在 Windows 任务栏或Mac菜单栏上的时钟附近看到 Docker 的鲸鱼图标。如果您是 Windows 上经验丰富的 Docker Desktop用户您需要确保您处于Linux 容器模式(对于新安装时这是默认模式)。
默认情况下Kubernetes 并未默认设置安装,因此你需要单机鲸鱼图标打开面板,然后点击“设置”,这将打开图 1.5 所示的窗口:从菜单中选择 Kubernetes ,然后选择 Enable Kubernetes。
![图1.5](./images/Figure1.5.png)
<center>图1.5 Docker Desktop 创建了一个 Linux 虚拟机去运行容器,同时可以运行 Kubernetes </center>
Docker Desktop 将会下载 Kubernetes 运行时的所有容器镜像——这将会花费一些时间,最终会启动所有服务。当你在“设置”屏幕底部看到两个绿色
的圆点Kubernetes集群已准备就绪。Docker Desktop 已安装您所需的所有功能,因此您可以跳到 1.3.7 节。
其他 Kubernetes 发行版可以在 Docker Desktop 上运行,但它们没有与 Docker Desktop 使用的网络设置完美集成练习时会出现问题。Docker Desktop 中的 Kubernetes 选项具有
这本书所需要的功能,绝对是最简单的选择。
### 1.3.3 安装 Docker 社区版本以及 K3s
如果您使用的是 Linux机器 或 Linux 虚拟机您可以有几个选项来运行单节点集群。Kind 和 minikube 很受欢迎但我更喜欢K3s,它包含最小的安装但具有练习所需的所有功能。k3s 是Kubernetes的缩写“K8s”上的一个参照。K3s对Kubernetes代码库进行了精简名称表明它的大小是K8s的一半。
K3s 与 Docker 兼容,因此首先,您应该安装 Docker 社区版。你可以在查看完整的安装步骤 https://rancher.com/docs/k3s/latest/en/quick-start/,这将使您快速启动:
```
# install Docker:
curl -fsSL https://get.docker.com | sh
# install K3s:
curl -sfL https://get.k3s.io | sh -s - --docker --disable=traefik --write-
kubeconfig-mode=644
```
如果您喜欢在 VM 中运行实验室环境,并且您熟悉使用 Vagrant 管理 VM您可以使用 Docker 和 K3s 的以下 Vagrant 设置,在本书的源存储库中可以找到:
```
# from the root of the Kiamol repo:
cd ch01/vagrant-k3s
# provision the machine:
vagrant up
# and connect:
vagrant ssh
```
K3s 安装了您需要的所有功能,因此您可以跳到 1.3.7 节。
### 1.3.4 安装 Kubernetes 命令行工具
您可以使用名为kubectl发音为“cube-cutle”的工具管理 Kubernetes。它连接到Kubernetes 集群并与 Kubernetes API 交互工作。Docker Desktop 和 K3s 都安装 kubectl但如果您使用下面描述的其他选项之一则需要自己安装。
完整的安装说明位于 https://kubernetes.io/docs/tasks/tools/install-kubectl/。您可以在 macOS 上使用 Homebrew在 Windows 上使用Chocolatey以及
对于Linux您可以下载二进制文件
```
# macOS:
brew install kubernetes-cli
# OR Windows:
choco install kubernetes-cli
# OR Linux:
curl -Lo ./kubectl https://storage.googleapis.com/kubernetes-
release/release/v1.18.8/bin/linux/amd64/kubectl
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectl
```
### 1.3.5 在 Azure 中运行一个单节点的 Kubernetes
您可以使用 AKS 在 Microsoft Azure 中运行托管 Kubernetes 集群。如果您想从多台计算机访问集群,或者具有 Azure 信用的MSDN订阅这可能是比较好的选择。您可以运行最小的单个节点集群这不会花费巨大的费用但要记住你没有办法停止集群您将支付 24*7 的费用,直到您移除它。
Azure 门户有一个很好的用户界面来创建AKS集群但它使用 az 命令要容易得多。您可以在查看最新文档https://docs.microsoft.com/en-us/azure/aks/kubenetes-walkthrough但您可以通过下载az命令行工具并运行一些命令如下所示
```
# log in to your Azure subscription:
az login
# create a resource group for the cluster:
az group create --name kiamol --location eastus
# create a single-code cluster with 2 CPU cores and 8GB RAM:
az aks create --resource-group kiamol --name kiamol-aks --node-count 1 --
node-vm-size Standard_DS2_v2 --kubernetes-version 1.18.8 --generate-ssh-keys
# download certificates to use the cluster with kubectl:
az aks get-credentials --resource-group kiamol --name kiamol-aks
```
最后一个命令从本地kubectl命令行下载连接到 Kubernetes API 的凭据。
### 1.3.6 在 AWS 中运行一个单节点的 Kubernetes
AWS 中的托管Kubernetes服务称为 Elastic Kubernetes ServiceEKS。您可以创建一个单节点EKS集群但需要注意的是您将在该节点运行的所有服务和资源的时间支付费用。
您可以使用 AWS 门户创建 EKS 集群,但推荐的方法是使用名为 eksctl 的专用工具。该工具的最新文档位于https://eksctl.io使用起来很简单。首先为您的操作系安装最新的工具如下
```
# install on macOS:
brew tap weaveworks/tap
brew install weaveworks/tap/eksctl
# OR on Windows:
choco install eksctl
# OR on Linux:
curl --silent --location
"https://github.com/weaveworks/eksctl/releases/download/latest/eksctl_$(uname
-s)_amd64.tar.gz" | tar xz -C /tmp
sudo mv /tmp/eksctl /usr/local/bin
```
假设您已经安装了AWS CLIeksctl将使用来自 CLI 的凭证如果没有请查看安装指南以验证eksctl。然后创建一个简单的单节点集群如下所示
```
# create a single node cluster with 2 CPU cores and 8GB RAM:
eksctl create cluster --name=kiamol --nodes=1 --node-type=t3.large
```
该工具设置从本地 kubectl 到EKS 集群的连接。
### 1.3.7 验证你的集群
现在你已经拥有了一个运行的 Kubernetes 集群,不论你如何安装的,它们都以同样的方式工作。运行下面的命令来检查集群运行状态:
`kubectl get nodes`
你应该会看到他 1.6 类似的输出。输出显示了集群中所有的节点列表,同时显示了一些基本信息比如状态以及 kubernetes 版本。你的集群信息跟我的可能会有所不通,但是一旦你看到节点列表并且是 ready 状态,那就说明你的集群已经正常运行。
![图1.6](./images/Figure1.6.png)
<center>图1.6 如果你可以运行 Kubectl 命令并且节点已就绪,那么你就可以继续往后了 </center>
## 1.4 立即见效
“立即见效” 是本书的核心原则,总结来说,就是聚焦于能力练习,跟随我在每一章中进行实操。
每一章都以一个简单的主体说明开始,随后跟随的就是“现在就试试”的练习,通过它你可以使用你自己的集群来练习。然后是一个包含更多细节的说明,以解决您可能因为深入学习遇到的一些问题。最后,有一个动手实验室供您自己尝试,请对你的新知识的掌握充满信心。
所有的主题都集中在现实世界中真正有用的任务上。你会在本章中学习如何立即有效地处理该主题,您将通过了解如何应用新技能来完成。让我们开始运行一些容器化应用程序!

View File

@ -0,0 +1,535 @@
# 第十章 通过 Helm 打包并管理应用
尽管 Kubernetes 规模庞大,但它本身并不能解决所有问题;一个庞大的生态系统填补了这些空白。其中一个差距就是应用的包装和分发,而 Helm 就是解决方案。您可以使用 Helm 将一组 Kubernetes YAML 文件分组到一个工件中,并在公共或私有存储库中共享该工件。任何可以访问存储库的人都可以通过一个 Helm 命令安装应用程序。该命令可能部署一整套相关的 Kubernetes 资源,包括 ConfigMaps、Deployments 和 Services您可以自定义配置作为安装的一部分。
人们使用 Helm 的方式各不相同。有些团队只使用 Helm 来安装和管理来自公共存储库的第三方应用程序。其他团队将 Helm 用于他们自己的应用程序,打包并发布到私有存储库。在本章中,您将学习如何做到这两点,并且您将带着自己的想法离开,了解 Helm 如何适合您的组织。你不需要学习 Helm 来有效地使用 Kubernetes但它被广泛使用所以你应该熟悉它。该项目由云原生计算基金会(CNCF)管理,与管理 kubernetes 的基金会相同,这是成熟度和寿命的可靠指标。
## 10.1 Helm 给 Kubernetes 带来了什么
Kubernetes 应用程序在设计时是在 YAML 文件的扩展中建模的在运行时使用一组标签进行管理。Kubernetes 中没有原生的“应用程序”概念,这显然是将一组相关资源组合在一起的,这是 Helm 解决的问题之一。它是一个命令行工具,用于与存储库服务器交互,以查找和下载应用程序包,并与 Kubernetes 集群一起安装和管理应用程序。
Helm 是另一个抽象层,这次是在应用程序级别。当您安装带有 Helm 的应用程序时,它会在 Kubernetes 集群中创建一组资源——它们是标准的 Kubernetes 资源。Helm 打包格式扩展了 Kubernetes YAML文件因此 Helm 包实际上只是一组 Kubernetes 清单和一些元数据。我们将首先使用 Helm 部署前面章节中的一个示例应用程序,但首先我们需要安装 Helm。
<b>现在就试试</b> helm 是一个跨平台的工具可以在Windows、macOS和Linux上运行。您可以在这里找到最新的安装说明:https://helm.sh/docs/intro/install。本练习假设您已经安装了 Homebrew 或 Chocolatey 这样的包管理器。如果没有你需要参考Helm网站的完整安装说明。
```
# 在 Windows, 使用 Chocolatey:
choco install -y kubernetes-helm
# 在 Mac, 使用 Homebrew:
brew install helm
# 在 Linux, 使用 Helm install script:
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-
helm-3 | bash
# 检查成功安装:
helm version
```
本练习中的安装步骤可能在您的系统上不起作用,在这种情况下,您需要在这里停下来,转向 Helm 安装文档。在安装 Helm 并看到 version 命令成功输出(如图10.1所示)之前,我们不能继续深入。
![图10.1](.\images\Figure10.1.png)
<center>图 10.1 有很多安装 Helm 的选项;使用包管理器是最简单的</center>
Helm 是一个客户端工具。以前版本的 Helm 需要在 Kubernetes 集群中部署服务器组件,但在 Helm 3 的主要更新中发生了变化。Helm CLI 使用与 kubectl 连接到 Kubernetes 集群相同的连接信息因此安装应用程序不需要任何额外配置。然而您需要配置一个包存储库。Helm 存储库类似于 Docker Hub 这样的容器镜像仓库,但服务器发布所有可用包的索引;Helm 缓存存储库索引的本地副本,您可以使用它来搜索包。
<b>现在就试试</b> 添加Helm存储库同步它并搜索一个应用程序。
```
# 添加一个仓库, 使用本地名称代替远程服务:
helm repo add kiamol https://kiamol.net
# 更新本地仓库缓存:
helm repo update
# 在仓库缓存中搜索应用:
helm search repo vweb --versions
```
Kiamol 存储库是一个公共服务器,在本练习中您可以看到这个名为 vweb 的包有两个版本。我的输出如图10.2所示。
![图10.2](.\images\Figure10.2.png)
<center>图 10.2 同步Kiamol Helm存储库的本地副本并搜索包</center>
你对 helm 有了一些了解但现在是时候介绍一些理论了这样我们就可以在进一步讨论之前使用正确的概念和名称。Helm 的应用程序包被称为 chart;可以在本地开发和部署 chart也可以将 chart 发布到存储库。当你安装一个 chart 时,这被称为发布;每个版本都有一个名称,您可以在集群中安装同一 chart 的多个实例作为独立的、命名的版本,被称作 release。
Charts 包含 Kubernetes YAML 清单,清单通常包含参数化值,因此用户可以使用不同的配置设置安装相同的 Chart——要运行的副本数量或应用程序日志级别可以是参数值。每个 Chart 还包含一组默认值可以使用命令行检查这些值。图10.3显示了 Helm Chart 的文件结构。
![图10.3](.\images\Figure10.3.png)
<center>图 10.3 Helm Chart 包含应用程序的所有 Kubernetes YAML加上一些元数据</center>
vweb charts 包包含了我们在第9章中用来演示更新和回滚的简单 web 应用程序。每个 Chart 都包含一个 Service 和 Deployment 的 spec以及一些参数化值和默认设置。您可以在安装 Chart 之前使用 Helm 命令行检查所有可用值,然后在安装版本时使用自定义值覆盖默认值。
<b>现在就试试</b> 检查 vweb Chart 版本 1 中可用的值,然后使用自定义值安装一个 release
```
# 查看 chart 中默认参数信息:
helm show values kiamol/vweb --version 1.0.0
# 安装 chart, 覆盖默认参数值:
helm install --set servicePort=8010 --set replicaCount=1 ch10-vweb
kiamol/vweb --version 1.0.0
# 检查你安装的 release:
helm ls
```
在本练习中,您可以看到 Chart 中 Service 端口和 Deployment 中的副本数量具有默认值。我的输出如图10.4所示。你使用 helm install 的 set 参数来指定你自己的值,当安装完成时,你有一个应用程序在 Kubernetes 中运行不使用kubectl也不直接使用 YAML 清单。
![图10.4](.\images\Figure10.4.png)
<center>图 10.4 使用helm安装应用程序-这将创建Kubernetes资源而不使用kubectl</center>
Helm 提供了一组用于使用存储库和 Chart 以及安装、更新和回滚版本的特性但它不适用于应用程序的持续管理。Helm 命令行并不是 kubectl 的替代品—您可以同时使用它们。现在已经安装了 release您可以以通常的方式使用 Kubernetes 资源,如果需要修改设置,还可以返回 Helm 操作。
<b>现在就试试</b> 使用 kubectl 检查 Helm 部署的资源然后返回Helm 扩容部署并检查应用程序是否正常工作
```
# 查看 Deployment:
kubectl get deploy -l app.kubernetes.io/instance=ch10-vweb --show-
labels
# 更新 release 增加副本数量:
helm upgrade --set servicePort=8010 --set replicaCount=3 ch10-vweb
kiamol/vweb --version 1.0.0
# 检查 ReplicaSet:
kubectl get rs -l app.kubernetes.io/instance=ch10-vweb
# 获取访问 url:
kubectl get svc ch10-vweb -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8010'
# 浏览器访问 url
```
让我们从这个练习中看看一些东西。首先,标签比标准的 “app” 和“version” 标签要冗长得多。这是因为这是一个公共存储库上的公共 Chart 所以我使用Kubernetes 配置最佳实践指南中推荐的标签名称——这是我的选择,而不是 Helm 的要求。其次Helm 升级命令再次指定了Service端口尽管我想修改的只是副本计数。这是因为 Helm 使用默认值除非您指定它们所以如果端口没有包含在升级命令中它将被更改为默认值。可以在图10.5中看到我的输出。
![图10.5](.\images\Figure10.5.png)
<center>图 10.5 你不使用 Helm 来管理应用程序,但你可以用它来更新配置</center>
这是 Helm 工作流的消费者端。您可以从 Helm 命令行搜索应用程序的存储库发现应用程序可用的配置值然后安装和升级应用程序。它是在Kubernetes中运行的应用程序的包管理。在下一节中您将学习如何打包和发布您自己的应用程序这是工作流的生产者方面。
## 10.2 使用 Helm 打包你自己的应用
Helm charts 是包含 Kubernetes 清单的文件夹或压缩档案。您可以使用应用程序清单创建自己的 Chart确定想要参数化的任何值并用模板化变量替换实际值。
清单 10.1 显示了模板化部署规范的开头部分,其中包括了使用 Helm 为资源名和标签值设置的值。
> 清单10.1 web-ping-deployment.yaml一个模板化的 Kubernetes manifest
```
apiVersion: apps/v1
kind: Deployment # 这些都是标准的 Kubernetes YAML.
metadata:
name: {{ .Release.Name }} # 使用 release 名称
labels:
kiamol: {{ .Values.kiamolChapter }} # 使用 变量 “kiamolChapter” 值
```
双大括号语法用于模板化值——从开头 {{ 到结束 }} 的所有内容都在安装时被替换Helm 将处理后的 YAML 发送到 Kubernetes。可以使用多个源作为输入来替换模板化的值。清单 10.1 中的代码片段使用 Release 对象获取发布的名称,使用 Values 对象获取名为 kiamolChapter 的参数值。Release 对象使用来自安装或升级命令的信息填充Values 对象使用 Chart 中的默认值和用户已覆盖的任何设置填充。模板还可以访问关于 chart 的静态细节和关于 Kubernetes 集群功能的运行时细节。
Helm 对 Chart 中的文件结构非常讲究。您可以使用 helm create 命令为新图表生成样板结构。顶层是一个文件夹,其名称必须与您想要使用的 Chart 名称相匹配,并且该文件夹必须至少具有以下三个项目:
- Chart.yaml 文件,指定 Chart 元数据,包括名称和版本
- values.yaml 文件,为参数设置默认值
- 存放 Kubernetes 清单模板的 templates 文件夹
清单 10.1 来自一个名为 web-ping-deployment 的文件。Yaml 在本章源代码的 web-ping/templates 文件夹中。web-ping 文件夹包含有效 chart 所需的所有文件Helm 可以验证 chart 内容并从 chart 文件夹中安装一个 release。
<b>现在就试试</b> 当你在开发 chart 时,你不需要将它们打包在 zip 档案中;您可以使用 chart 文件夹。
```
# 切换到本章源码目录:
cd ch10
# 校验 chart 内容:
helm lint web-ping
# 基于 chart 目录安装一个 release:
helm install wp1 web-ping/
# 检查已安装的 releases:
helm ls
```
lint 命令仅用于处理本地 chart但是 install 命令对于本地 chart 和 存储在存储库中的 chart 是相同的。本地 chart 可以是文件夹或压缩档案,在本练习中,您将看到从本地 chart 安装版本与从存储库安装版本的经验相同。图 10.6 中的输出显示我现在安装了两个 releases:一个来自vweb chart另一个来自web-ping chart。
![图10.6](.\images\Figure10.6.png)
<center>图 10.6 从本地文件夹安装和升级可以让您快速迭代 chart 开发</center>
web-ping 应用程序是一个基本的实用程序通过定期向域名发出HTTP请求来检查网站是否正常运行。现在你有一个Pod在运行它每 30 秒就会向我的博客发送请求。我的博客运行在Kubernetes上所以我相信它能够处理这个问题。应用程序使用环境变量来配置要使用的URL和调度间隔这些都在Helm的清单中模板化。清单10.2显示了带有模板化变量的Pod spec。
> 清单10.2 web-ping-deployment.yaml模板容器环境
```
spec:
containers:
- name: app
image: kiamol/ch10-web-ping
env:
- name: TARGET
value: {{ .Values.targetUrl }}
- name: INTERVAL
value: {{ .Values.pingIntervalMilliseconds | quote }}
```
Helm 有一组丰富的模板函数,您可以使用它来操作在 YAML 中设置的值。清单10.2 中的 quote 函数将提供的值包装在引号中,如果它还没有引号的话。您可以在模板中包含循环和分支逻辑,计算字符串和数字,甚至可以查询 Kubernetes API 以从其他对象中查找详细信息。我们不会讨论那么多细节,但重要的是要记住 Helm 可以让您生成复杂的模板,可以做几乎任何事情。
您需要仔细考虑 spec 中需要被模板化的部分。Helm 相对于标准清单部署的最大好处之一是,您可以从一个 chart 运行同一个应用程序的多个实例。kubectl 不能这样做,因为清单包含的资源名必须是唯一的。如果多次部署同一组 YAML, Kubernetes 只会更新相同的资源。如果你模板化了 spec 中所有独特的部分,比如资源名和标签选择器,那么你就可以用 Helm 运行同一个应用的多个副本。
<b>现在就试试</b> 部署 web-ping 应用程序的第二个版本,使用相同的 chart 文件夹但指定不同的URL来ping.
```
# 检查 chart 可用变量:
helm show values web-ping/
# 安装一个新的名为 wp2 的 release :
helm install --set targetUrl=kiamol.net wp2 web-ping/
# 等待然后检查日志:
kubectl logs -l app=web-ping --tail 1
```
在这个练习中您将看到我需要对我的博客做一些优化——它在500毫秒左右返回而Kiamol网站在100毫秒返回。更重要的是您可以看到应用程序正在运行的两个实例:两个deployment管理两组具有不同容器规格的pod。我的输出如图10.7所示。
![图10.7](.\images\Figure10.7.png)
<center>图 10.7 你不能用纯清单安装一个应用程序的多个实例但你可以用Helm</center>
现在应该清楚了,用于安装和管理应用程序的 Helm 工作流与 kubectl 工作流是不同的,但您还需要了解两者是不兼容的。不能通过在 chart 的模板文件夹中运行kubectl apply 来部署应用程序因为模板化的变量不是有效的YAML并且该命令将失败。如果您采用 Helm那么您需要在以下两种情况中做出选择:在每个环境中使用 Helm这可能会降低开发人员的工作流程;或者在其他环境中使用纯 Kubernetes 清单进行开发,而在其他环境中使用 Helm这意味着您将拥有YAML的多个副本。
请记住Helm 的作用不仅仅是安装它更是关于分发和发现。Helm 带来的额外摩擦是为了能够将复杂的应用程序简化为几个变量,并在存储库上共享它们。存储库实际上只是一个索引文件,其中包含可以存储在任何 Web 服务器上的一系列 chart 版本Kiamol 存储库使用 GitHub 页面,并且您可以在 https://kiamol.net/index.yaml 上查看其全部内容)。
您可以使用任何服务器技术来托管存储库,但在本节的其余部分中,我们将使用名为 ChartMuseum 的专用存储库服务器,这是一种流行的开源选项。您可以在自己的组织中运行 ChartMuseum 作为私有 Helm 存储库,而且它很容易设置,因为您可以使用 Helm Chart 安装它。
<b>现在就试试</b> ChartMuseum 的图表位于官方的 Helm 存储库中,通常被称为“稳定版”。添加该存储库后,您可以安装一个发行版在本地运行自己的存储库
```
# 添加官方 Helm 仓库:
helm repo add stable https://kubernetes-charts.storage.googleapis.com
# 安装 ChartMuseum— repo 参数指定了直接从远程仓库获取信息,所以你不需要更新本地缓存:
helm install --set service.type=LoadBalancer --set
service.externalPort=8008 --set env.open.DISABLE_API=false repo
stable/chartmuseum --version 2.13.0 --wait
# 获取访问 url:
kubectl get svc repo-chartmuseum -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8008'
# 将其添加到本地,名为 local:
helm repo add local $(kubectl get svc repo-chartmuseum -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8008')
```
现在您已经有三个存储库在 Helm 上注册了: Kiamol存储库、稳定的Kubernetes存储库(它是一组精心策划的 chart类似于Docker Hub中的官方镜像)以及您自己的本地存储库。您可以在图10.8中看到我的输出为了减少Helm安装命令的输出对其进行了删减。
![图10.8](.\images\Figure10.8.png)
<center>图 10.8 运行自己的Helm存储库就像从Helm repository 安装 chart一样简单</center>
在将 chart 发布到存储库之前,需要对 chart 进行打包,发布通常分为三个阶段:将chart 打包到zip归档文件中将归档文件上传到服务器并更新存储库索引以添加新 chart。ChartMuseum 为您完成了最后一步,因此您只需要打包并上传 chart以便自动更新存储库索引。
<b>现在就试试</b> 使用Helm为 chart 创建zip存档并使用curl将其上传到您的 ChartMuseum 存储库。检查存储库—您将看到您的 chart 已被索引
```
# 打包本地 chart:
helm package web-ping
# 在 win10 需要,删除 Powershell 别名以直接使用 curl:
Remove-Item Alias:curl -ErrorAction Ignore
# 上传 chart 压缩档到 ChartMuseum:
curl --data-binary "@web-ping-0.1.0.tgz" $(kubectl get svc repo-
chartmuseum -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8008/api/chart
s')
# 检查 ChartMuseum 已经更新索引:
curl $(kubectl get svc repo-chartmuseum -o jsonpath='http://{.status
.loadBalancer.ingress[0].*}:8008/index.yaml')
```
Helm 使用压缩档案使 chart 易于分发而且文件很小——它们包含Kubernetes清单、元数据和 values但不包含任何大的二进制文件。chart 中的Pod spec 指定了要使用的容器镜像,但镜像本身不是 chart 的一部分——它们是在安装版本时从 Docker Hub 或您自己的镜像注册表中提取的。在图10.9中可以看到,当您上传 chart 并添加新的 chart 细节时ChartMusem 生成存储库索引。
![图10.9](.\images\Figure10.9.png)
<center>图 10.9 您可以将ChartMuseum作为私有存储库来轻松地在团队之间共享 Chart</center>
您可以使用 ChartMuseum 或组织中的另一个存储库服务器来共享内部应用程序或将 chart 作为持续集成过程的一部分,然后在公共存储库上发布候选版本。您拥有的本地存储库只在您的实验室环境中运行,但它是使用 LoadBalancer Service发布的因此任何有网络访问权限的人都可以从中安装web-ping应用程序。
<b>现在就试试</b> 安装另一个版本的web-ping应用程序这次使用本地存储库中的 chart并提供一个 value 文件,而不是在安装命令中指定每个设置
```
# 更新仓库缓存:
helm repo update
# 验证 helm 可以发现你的 chart :
helm search repo web-ping
# 检查本地的 values 文件:
cat web-ping-values.yaml
# 从仓库安装并使用本地 values 文件:
helm install -f web-ping-values.yaml wp3 local/web-ping
# 查看 Pods:
kubectl get pod -l app=web-ping -o custom-
columns='NAME:.metadata.name,ENV:.spec.containers[0].env[*].value'
```
在本练习中,您看到了使用自定义设置安装 Helm release 的另一种方法—使用本地 values 文件。这是一个很好的实践因为您可以将不同环境的设置存储在不同的文件中并且可以降低在没有提供设置时更新恢复到默认值的风险。我的输出如图10.10所示。
![图10.10](.\images\Figure10.10.png)
<center>图 10.10 从本地存储库安装 chart 与从任何远程存储库安装相同</center>
在前面的练习中,您还看到可以在不指定版本的情况下从存储库安装 chart。这不是一个很好的做法因为它安装的是最新版本这是一个移动的目标。最好总是明确地说明 chart 版本。Helm要求您使用语义版本控制以便 chart 消费者了解他们即将升级的包是否是beta版本或者它是否有突破性的变化。
使用 chart 的功能不止我在这里要介绍的这些。它们可以包含测试,即 Kubernetes Job 规范,在安装后运行以验证部署;它们可以有钩子,在安装工作流程的特定点运行 Jobs它们可以被签名并附带来源的签名进行传输。在下一部分中我将介绍另一项您在编写模板中使用的重要功能即构建依赖于其他 chart 的 chart。
## 10.3 charts 中的模块依赖
Helm 允许你设计应用程序使其在不同的环境中工作这就产生了一个有趣的依赖关系问题。在某些环境中可能需要依赖项但在其他环境中则不需要。也许你有一个web 应用真的需要缓存反向代理来提高性能。在某些环境中你会想要将代理与应用一起部署而在其他环境中你已经有了一个共享的代理所以你只想部署web应用本身。Helm 通过条件依赖性来支持这些。
清单10.3显示了我们从第5章开始使用的 Pi web应用程序的 chart 清单。它有两个依赖项——一个来自Kiamol存储库另一个来自本地文件系统——它们是独立的chart。
> 清单10.3 chart.yaml一个包含可选依赖项的 chart
```
apiVersion: v2 # Helm 配置版本
name: pi # Chart name
version: 0.1.0 # Chart version
dependencies: # 其他依赖 chart
- name: vweb
version: 2.0.0
repository: https://kiamol.net # 其他仓库的依赖
condition: vweb.enabled # 按需安装
- name: proxy
version: 0.1.0
repository: file://../proxy # 本地目录的依赖
condition: proxy.enabled # 按需安装
```
在建模依赖项时,需要保持 chart 的灵活性。父 chart (在本例中是Pi应用程序)可能需要子chart(proxy和vweb chart),但子 chart 本身需要是独立的。您应该在子 chart 中模板化 Kubernetes清单以使其具有一般的用处。如果它只在一个应用程序中有用那么它应该是应用程序 chart 的一部分,而不是子 chart。
我的代理通常是有用的;它只是一个缓存反向代理,可以使用任何 HTTP 服务器作为内容源。该 chart 使用一个模板值作为服务器的名称来代理所以尽管它主要用于Pi应用程序但它可以用于代理任何 Kubernetes 服务。我们可以通过安装一个代理集群中现有应用程序的 release 来验证这一点。
<b>现在就试试</b> 单独安装 proxy chart使用它作为我们在本章前面安装的 vweb 应用程序的反向代理
```
# 从本地目录安装 release:
helm install --set upstreamToProxy=ch10-vweb:8010 vweb-proxy proxy/
# 从新的 proxy service 获取访问 url:
kubectl get svc vweb-proxy-proxy -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080'
# 浏览 url
```
练习中的 proxy chart 完全独立于Pi应用程序;它被用来代理我和Helm从Kiamol存储库部署的web应用程序。在图10.11中可以看到它可以作为任何HTTP服务器的缓存代理。
![图10.11](.\images\Figure10.11.png)
<center>图 10.11 代理子 chart 被构建成一个有用的chart它可以代理任何应用</center>
要将代理作为依赖项使用,需要将其添加到父 chart 中的依赖项列表中,这样它就成为子 chart。然后通过在设置名称前加上依赖项名称可以为父 chart 中的子chart 设置指定值——代理 chart 中的设置upstreamToProxy被引用为代理。在Pi图表中的upstreamToProxy。清单10.4显示了Pi应用程序的默认 values 文件,其中包括应用程序本身和代理依赖项的设置。
> 清单10.4 values.yamlPi chart 的默认设置
```
replicaCount: 2 # Pods 副本数
serviceType: LoadBalancer # Pi Service 类型
proxy: # 反向代理设置
enabled: false # 是否部署 proxy
upstreamToProxy: "{{ .Release.Name }}-web" # 代理的 server
servicePort: 8030 # proxy Service 的端口
replicaCount: 2 # proxy pod 副本数
```
这些值在没有代理的情况下部署应用程序本身使用Pi Pods的LoadBalancer Service。设置 proxy.enabled 被指定为Pi图中代理依赖关系的条件因此整个子 chart 将被跳过,除非安装设置覆盖默认值。完整的 values 文件还设置vweb.enabled值为false——该依赖项只是为了证明子 chart 可以从存储库中获得,因此默认情况下也不部署该 chart。
这里有一个额外的细节需要调用。Pi 应用程序的 Service 名称在 chart 中模板化,使用 release 名称。启用同一 chart 的多个安装是很重要的,但这会增加代理子 chart 的默认值的复杂性。代理服务器的名称需要与Pi服务名称相匹配因此 values 文件使用与 servie 名称相同的模板值,并将代理链接到同一版本中的服务。
在安装或打包 chart 之前chart 需要有它们的依赖项,可以使用 Helm 命令行来完成这一点。构建依赖项将把它们填充到 chart 的charts文件夹中方法是从存储库下载存档或将本地文件夹打包到存档中。
<b>现在就试试</b> 构建Pi chart 的依赖项,下载远程 chart打包本地 chart并将其添加到 chart 文件夹中
```
# 构建 dependencies:
helm dependency build pi
# 检查依赖已下载:
ls ./pi/charts
```
图10.12显示了版本控制对 Helm chart 如此重要的原因。chart 包使用 chart 元数据中的版本号进行版本控制。父chart 在指定的版本中与其依赖项一起打包。如果我更新 proxy chart 而不更新版本号我的Pi chart 将不同步因为Pi包中的 proxy chart 的版本0.1.0与最新版本0.1.0不同。你应该认为 Helm chart 是不可变的,并且总是通过发布一个新的包版本来发布变更。
![图10.12](.\images\Figure10.12.png)
<center>图 10.12 Helm 将依赖包捆绑到父 chart 中,并且它们作为一个包分发</center>
条件依赖的原则是你如何管理一个更复杂的应用程序比如第8章中的待办事项应用程序。Postgres数据库部署将是一个子 chart对于希望使用外部数据库的环境用户可以完全跳过该子 chart。或者您甚至可以有多个条件依赖项允许用户为开发环境部署简单的Postgres Deployment为测试环境使用高可用的StatefulSet并在生产环境中插入托管的Postgres服务。
Pi 应用程序比这更简单,我们可以选择是单独部署它还是通过代理部署它。这个 chart 使用了Pi Service类型的模板值但是如果没有部署代理可以将其设置为LoadBalancer如果部署了代理则可以将其设置为ClusterIP从而在模板中计算该值。
<b>现在就试试</b>部署启用代理子 chart 的Pi app。使用Helm 's dry-run特性检查默认部署然后使用自定义设置进行实际安装。
```
# 打印 helm 使用默认值部署的 yaml:
helm install pi1 ./pi --dry-run
# 使用自定义setting ,开启 proxy 安装:
helm install --set serviceType=ClusterIP --set proxy.enabled=true pi2
./pi
# 获取 proxied 应用 url:
kubectl get svc pi2-proxy -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8030'
# 访问 url
```
在本练习中您将看到dry-run 标志非常有用:它将值应用到模板并为它将要安装的资源写出所有YAML而不需要部署任何东西。然后在实际安装中设置几个标志将部署一个与主 chart 集成的附加 chart因此应用程序可以作为单个单元工作。我的圆周率计算如图10.13所示。
![图10.13](.\images\Figure10.13.png)
<center>图 10.13 通过覆盖默认设置,安装带有可选子 chart 的 chart</center>
在这一章中,我没有给 Helm 留出足够的空间,因为只有当你在 Helm 上下了很大的赌注并计划广泛使用它时,你才需要深入了解它的复杂性。如果那是你,你会发现 helm 有能力保护你。举个例子:您可以从ConfigMap模板的内容生成一个散列并将其用作部署模板中的标签因此每次配置更改时部署标签也会更改并且升级您的配置会触发 Pod rollout。
这很简洁但并不适用于所有人所以在下一节中我们将回到一个简单的演示应用程序看看Helm如何平滑升级和回滚过程。
## 10.4 升级及回滚 Helm releases
使用 Helm 升级应用并没有什么特别之处;它只是将更新后的 spec 发送给 Kubernetes, Kubernetes以通常的方式推出更改。如果您想配置 rollout 的细节,您仍然可以在 chart 中的YAML文件中使用我们在第9章中探索的设置进行配置。Helm 为升级带来的是对所有类型资源的一致方法,以及轻松回滚到以前版本的能力。
使用 Helm 的另一个好处是,可以通过在集群中部署额外的实例来安全地尝试新版本。本章开始时,我在我的集群中部署了 vweb 应用的1.0.0版本它仍然运行良好。2.0.0版本现已可用,但在升级运行中的应用程序之前,我可以使用 Helm 安装一个单独的版本并测试新功能。
<b>现在就试试</b> 检查原来的vweb版本仍然在那里然后安装一个版本2的版本并指定设置以保持应用程序私有
```
# 列出所有的 releases:
helm ls -q
# 检查新 chart 版本的 values:
helm show values kiamol/vweb --version 2.0.0
# 使用内部 service 类型部署新的 release:
helm install --set servicePort=8020 --set replicaCount=1 --set
serviceType=ClusterIP ch10-vweb-v2 kiamol/vweb --version 2.0.0
# 通过 port-forward 访问:
kubectl port-forward svc/ch10-vweb-v2 8020:8020
# 浏览 localhost:8020
```
本练习使用 chart 支持的参数来安装应用程序,但不使其公开可用,使用 ClusterIP 服务类型和端口转发因此应用程序只能由当前用户访问。原来的应用程序没有变化我有机会在目标集群中对新的Deployment进行烟雾测试。图10.14显示了新版本的运行情况。
![图10.14](.\images\Figure10.14.png)
<center>图 10.14 部署服务的 chart 通常允许您设置类型,因此您可以保持它们为私有</center>
现在我很高兴 2.0.0 版本很好,我可以使用 Helm 升级命令来升级我的实际版本。我想确保我部署的值与我在上一个版本中设置的值相同Helm具有显示当前值和在升级中重用自定义值的功能。
<b>现在就试试</b> 删除临时版本2版本并使用当前版本设置的相同值将版本1版本升级到版本2 chart
```
# 删除 test release:
helm uninstall ch10-vweb-v2
# 检查当前 v1 版本使用的 values:
helm get values ch10-vweb
# 使用相同的values 更新到 version 2 —将会失败:
helm upgrade --reuse-values --atomic ch10-vweb kiamol/vweb --version
2.0.0
```
哦亲爱的。这是一个特别棘手的问题,需要一些追踪才能理解。重用值标志告诉 Helm 在新版本上重用为当前版本设置的所有值但是2.0.0版本 chart 包括另一个值即服务的类型该值在当前版本中没有设置因为它不存在。最终结果是服务类型为空白在Kubernetes中默认为ClusterIP并且更新失败因为它与现有的服务 spec 冲突。您可以在图10.15的输出中看到这一点。
![图10.15](./images/Figure10.15.png)
<center>图 10.15 无效升级失败Helm可自动回退到上一版本</center>
这类问题正是 Helm 的抽象层真正有用的地方。在标准kubectl部署中也会遇到同样的问题但是如果一个资源更新失败则需要检查所有其他资源并手动回滚它们。helm 用原子 flag 自动做到了。它等待所有资源更新完成如果其中任何一个更新失败它将所有其他资源回滚到前一个状态。检查发布的历史记录您可以看到Helm已经自动回滚到版本1.0.0。
<b>现在就试试</b>回想第9章Kubernetes没有给你太多关于rollout历史的信息与Helm的细节相比
```
# 显示 vweb release 的历史信息:
helm history ch10-vweb
```
这个命令本身就是一个练习因为在标准的Kubernetes推出的历史中您无法获得大量的信息。图10.16显示了该版本的所有四个版本:第一次安装、一次成功升级、一次失败升级和一次自动回滚。
![图10.16](.\images\Figure10.16.png)
<center>图 10.16 发布历史清楚地将应用程序和 chart 版本链接到修订</center>
要修复失败的更新,我可以手动设置升级命令中的所有值,或者使用具有当前部署的相同设置的值文件。我没有那个值文件,但我可以将 get values 命令的输出保存到一个文件中并在升级中使用它这将为我提供所有以前的设置以及chart中任何新设置的默认值。
<b>现在就试试</b>再次升级到版本2这次将当前版本1的值保存到一个文件中并在升级命令中使用该文件
```
# 保存当前 release 到 yaml 文件:
helm get values ch10-vweb -o yaml > vweb-values.yaml
# 更新到 version 2 使用 values 文件并添加 atomic 参数:
helm upgrade -f vweb-values.yaml --atomic ch10-vweb kiamol/vweb
--version 2.0.0
# 检查 svc replicaset 配置:
kubectl get svc,rs -l app.kubernetes.io/instance=ch10-vweb
```
这次升级成功了所以原子回滚没有生效。升级实际上是由部署完成的部署会按照通常的方式扩大替换的ReplicaSet并缩小当前的ReplicaSet。图10.17显示了在上一个版本中设置的配置值被保留Service正在监听端口8010并且有三个pod正在运行。
![图10.17](.\images\Figure10.17.png)
<center>图 10.17 将发布设置导出到文件并再次使用,升级成功</center>
剩下的就是尝试回滚它在语法上类似于kubectl中的回滚但是 Helm 使查找想要使用的修订更加容易。您已经在图10.16中看到了有意义的发布历史您还可以使用Helm检查为特定修订设置的值。如果我想将web应用程序回滚到版本1.0.0但保留我在版本2中设置的值我可以先检查这些值。
<b>现在就试试</b>回滚到第二次修订即应用程序的1.0.0版本升级为使用三个副本
```
# 确认 revision 2 中使用的 values:
helm get values ch10-vweb --revision 2
# 回滚到 revision 2:
helm rollback ch10-vweb 2
# 检查最新的两个 revisions:
helm history ch10-vweb --max 2 -o yaml
```
您可以在图10.18中看到我的输出其中回滚成功历史记录显示最新的版本是6这实际上是回滚到版本2。
![图10.18](.\images\Figure10.18.png)
<center>图 10.18 Helm可以很容易地检查回滚到的内容</center>
这个示例的简单性有利于关注升级和回滚工作流并突出显示一些怪癖但它隐藏了Helm用于重大升级的强大功能。Helm release 是应用程序的抽象并且是不同的应用程序的版本可能以不同的方式建模。chart 可能在早期版本中使用ReplicationController然后更改为ReplicaSet然后更改为Deployment;只要面向用户的部分保持不变,内部工作就成为实现细节。
## 10.5 理解 Helm 定位
Helm 为 Kubernetes 增加了很多价值,但它是侵入性的——一旦你将清单模板化,就没有回头路了。团队中的每个人都必须切换到 Helm或者您必须承诺拥有多套清单:开发团队使用纯 Kubernetes其他环境使用 Helm。你真的不希望两组清单不同步但同样地Kubernetes本身也有很多值得学习的地方即使不添加Helm。
Helm 是否适合你很大程度上取决于你包装的应用程序类型和你的团队工作方式。如果你的应用由 50 多个微服务组成,那么开发团队可能只在整个应用的一个子集上工作,本机运行或使用 Docker 和 Docker Compose 运行而一个独立的团队拥有完整的Kubernetes部署。在这种环境下迁移到Helm将减少而不是增加摩擦将数百个YAML文件集中到可管理的 chart 中。
Helm 非常适合的其他几个指标包括完全自动化的持续部署过程——使用Helm可以更容易地构建这个过程——从具有自定义 values 文件的相同 chart 版本运行测试环境并将验证作业作为部署的一部分运行。当您发现自己需要为Kubernetes清单制作模板时(您迟早会这样做)helm为您提供了一种标准的方法这比编写和维护自己的工具更好。
以上就是本章 Helm 的全部内容,在进入实验室之前,是时候整理集群了。
<b>现在就试试</b>本章中的所有内容都是与Helm一起部署的所以我们可以使用Helm来卸载所有内容
```
# 卸载所有 releases :
helm uninstall $(helm ls -q)
```
## 10.6 实验室
这又回到了实验室的待办事项应用程序。您将使用一组Kubernetes清单并将它们打包到 Helm chart 中。别担心——它不是第8章中有StatefulSets和备份作业的完整应用;这是一个更简单的版本。目标如下:
- 使用实验室/待办事项列表文件夹中的清单作为起点(在YAML中有需要模板的提示)。
- 创建 Helm chart 结构。
- 模板化资源名和任何其他需要模板化的值,这样应用程序可以作为多个版本运行。
- 增加配置设置参数,以支持应用程序在不同环境下运行。
- 在使用默认值安装时chart 应作为测试配置运行。
- 使用 lab/dev-values.yaml 安装时chart 应该作为 Dev 配置运行。
如果你打算使用 Helm你真的应该为这个实验室腾出时间因为它包含了你在Helm中打包应用程序时需要执行的确切任务集。我的解决方案在GitHub上你可以在通常的地方检查: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch10/lab/README.md。
Happy Helming!

View File

@ -0,0 +1,438 @@
# 第十一章 App 开发——开发人员工作流程及 CI/CD
这是本书中有关实际应用 Kubernetes 的最后一章,重点是开发和交付在 Kubernetes 上运行的软件的实用性。无论您是开发人员还是正在与开发人员一起工作的操作人员容器的使用都会影响您的工作方式、使用的工具以及从进行代码更改到在开发和测试环境中看到其运行所需的时间和精力。在本章中我们将探讨Kubernetes如何影响内部循环——本地机器上的开发人员工作流程和外部循环——将更改推送到测试和生产的CI/CD工作流程中。
您在组织中如何使用Kubernetes将与您在本书中使用它的方式非常不同因为您将使用共享的资源例如群集和镜像仓库。随着我们在本章中探讨交付工作流程我们还将涵盖许多可能会在向现实世界转变时令您感到困惑的细节例如使用私有镜像仓库和维护共享群集上的隔离。本章的主要重点是帮助您了解在Docker为中心的工作流程和类似于运行在Kubernetes上的平台即服务PaaS之间做出选择。
## 11.1 Docker 开发人员工作流程
开发人员喜欢 Docker。在Stack Overflow的年度调查中它被评为“最想要的平台”第一名和“最受喜爱的”第二名两年。Docker使开发人员工作流程的一些部分变得非常容易但代价是Docker构件成为项目的核心这对内部循环产生了影响。如果您不熟悉使用容器构建应用程序请参阅我的另外一篇《在一个月的午餐时间内学习Docker》一书。
在本节中我们将介绍使用Docker和Kubernetes在每个环境中的开发人员工作流程并且开发人员拥有自己的专用群集。如果您想要跟随练习那么您需要安装Docker。如果您的实验室环境是Docker Desktop或K3s那么您就可以开始了。我们将首先看一下开发人员入职——加入新项目并尽快掌握工作。
现在本章提供了一个全新的演示应用程序——一个简单的公告板您可以在其中发布即将到来的活动详情。它是用Node.js编写的但您无需安装Node.js即可使用Docker工作流快速上手。
```
# 切换到本章的源代码文件夹:
cd ch11
# 构建应用程序:
docker-compose -f bulletin-board/docker-compose.yml build
# 运行应用程序:
docker-compose -f bulletin-board/docker-compose.yml up -d
# 检查正在运行的容器:
docker ps
# 浏览器打开http://localhost:8010/查看应用程序。
```
这是您可以作为新项目的开发人员开始的最简单的方式。您唯一需要安装的软件是Docker然后获取代码的副本就可以开始了。您可以在图11.1中看到我的输出。我的机器上没有安装Node.js无论您是否安装了它以及它的版本是什么您的结果将是相同的。
![图 11.1](images/Figure11.1.png)
<center>图 11.1 开发人员使用Docker和compose是轻而易举的事情——如果没有任何问题的话</center>
这背后的魔法有两个方面Dockerfile其中包含构建和打包Node.js组件的所有步骤以及Docker Compose文件其中指定了所有组件以及其Dockerfile的路径。此应用程序中只有一个组件但可能会有十几个组件——都使用不同的技术——工作流程都将是相同的。但这不是我们将在生产中运行应用程序的方式因此如果我们想使用相同的技术堆栈则可以切换到在Kubernetes中本地运行应用程序只使用Docker进行构建。
试一试,在源文件夹中提供了使用本地镜像运行应用程序的简单 Kubernetes 清单。删除 Compose 版本的应用程序,并将其部署到 Kubernetes 中。
```
# 停止 Compose 中的应用程序:
docker-compose -f bulletin-board/docker-compose.yml down
# 在 Kubernetes 中部署:
kubectl apply -f bulletin-board/kubernetes/
# 获取新的 URL:
kubectl get svc bulletin-board -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8011'
# 浏览
```
这个工作流程仍然非常简单尽管现在我们有三个容器构建工件要处理Dockerfile、Compose 文件和 Kubernetes 清单。我有自己的 Kubernetes 集群,可以像在生产环境中一样运行应用程序。我的输出在图 11.2 中显示,它展示了使用之前练习中使用 Docker Compose 构建的相同本地镜像运行的相同应用程序。
![图 11.2](images/Figure11.2.png)
<center>图 11.2 您可以使用 Compose 将 Docker 和 Kubernetes 混合使用以构建在 Pod 中运行的镜像</center>
Kubernetes 可以使用您创建或者使用 Docker 拉取的本地镜像,但是必须遵循一些规则,关于它是否使用本地镜像或从仓库中拉取它。如果镜像名称中没有显式标记(并且使用默认的 :latest 标记),那么 Kubernetes 将始终尝试首先拉取镜像。否则,如果节点上的镜像缓存中存在本地镜像,则 Kubernetes 将使用本地镜像。您可以通过指定镜像拉取策略来覆盖这些规则。清单 11.1 显示了公告板应用程序的 Pod spec其中包括显式策略。
> 清单 11.1 bb-deployment.yaml, 指定镜像拉取策略
```
spec: # 这是 Deployment 中的 Pod spec
containers:
- name: bulletin-board
image: kiamol/ch11-bulletin-board:dev
imagePullPolicy: IfNotPresent # 如果存在本地镜像,则优先使用本地镜像
```
这是在开发人员工作流程中可能会遇到的难题。Pod spec 可能被配置为首选仓库镜像,然后您可以无限制地重建自己的本地镜像,但是永远不会看到任何更改,因为 Kubernetes 将始终使用远程镜像。类似的复杂情况还存在于镜像版本周围,因为可以使用相同的名称和标记替换镜像版本。这在 Kubernetes 的期望状态方法中并不奏效,因为如果使用未更改的 Pod spec 部署更新,则不会发生任何事情,即使镜像内容已更改。
回到我们的演示应用程序。您在项目中的第一个任务是向事件列表添加一些更多的细节,这对您来说是一个容易的代码更改。测试更改更具有挑战性,因为您可以重复 Docker Compose 命令来重建镜像,但是如果您重复 kubectl 命令来部署更改,则会发现没有任何事情发生。如果您熟悉容器技术,可以进行一些调查以了解问题并删除 Pod 以强制替换,但是如果您不熟悉容器技术,则您的工作流程已经破损了。
试一试您实际上不需要更改代码—新文件中已经有了更改。只需替换代码文件并重建镜像然后删除Pod就可以看到在替换Pod中运行的新应用程序版本。
```
# 移除原始代码文件:
rm bulletin-board/src/backend/events.js
# 用更新的版本替换它:
cp bulletin-board/src/backend/events-update.js bulletin-board/src/backend/events.js
# 使用 Compose 重新构建镜像:
docker-compose -f bulletin-board/docker-compose.yml build
# 尝试使用 kubectl 重新部署:
kubectl apply -f bulletin-board/kubernetes/
# 删除现有的 Pod 以重新创建它:
kubectl delete pod -l app=bulletin-board
```
您可以在图 11.3 中看到我的输出。更新的应用程序在屏幕截图中运行,但是只有在手动删除 Pod 并由 Deployment 控制器重新创建它,使用最新的镜像版本时才能运行更新的应用程序。
![图 11.3](images/Figure11.3.png)
<center>图 11.3 Docker 镜像是可变的但重命名镜像不会触发Kubernetes中的更新</center>
如果您选择了基于 Docker 的工作流程,那么您可以使用 Docker Compose 快速构建和重复部署应用程序,而不必担心 Kubernetes 中的 Pod spec 和镜像缓存。但是,如果您选择将应用程序迁移到 Kubernetes 环境中,则必须考虑这些问题,并为此做好准备。在 Kubernetes 中部署应用程序需要更多的准备工作,但是它也提供了更大的弹性和可靠性。
另一种选择是将所有容器技术集中在一个团队中提供一个CI/CD管道开发团队可以插入该管道来部署他们的应用程序。管道负责包装容器镜像并将其部署到集群因此开发团队不需要将Docker和Kubernetes引入到自己的工作中。
## 11.2 Kubernetes 开发人员工作流程
在 Kubernetes 上运行的平台即服务 (PaaS) 体验对许多组织来说是一个有吸引力的选择。您可以为所有测试环境运行单个集群,该集群还托管 CI/CD 服务,以处理容器运行时的混乱细节。所有 Docker 构件都已从开发人员工作流程中删除,因此开发人员可直接在其计算机上运行 Node.js 和其他所需的所有组件,并且不在本地使用容器。
该方法将容器移至外部循环——当开发人员将更改推送到源代码时,会触发构建,该构建创建容器镜像,将其推送到仓库并将新版本部署到群集中的测试环境。您可以获得在容器平台上运行的所有好处,而无需容器对开发的摩擦。图 11.4 显示了使用一组技术选项的外部循环的外观。
![图 11.4](images/Figure11.4.png)
<center>图 11.4 在外循环中使用容器可以让开发人员专注于代码</center>
此方法的承诺是您可以在不影响开发人员工作流程或要求每个团队成员熟练掌握Docker和Compose的情况下在Kubernetes上运行应用程序。它可以在开发团队在小组件上工作且单独的团队将所有组件组装成工作系统的组织中运行良好因为只有组装团队需要容器技能。您还可以完全删除Docker这在您的集群使用不同的容器运行时非常有用。如果您要构建容器镜像而不使用Docker您需要将其替换为许多其他移动部件。您最终将获得更多的复杂性但它将集中在交付管道而不是项目中。
我们将在本章中通过示例来演示这一点但为了管理复杂性我们将分阶段进行从内部构建服务的视图开始。为了保持简单我们将运行自己的Git服务器以便我们可以从我们的实验室集群中推送更改并触发构建。
立即尝试Gogs是一个简单但功能强大的Git服务器它作为Docker Hub上的镜像发布。它是在您的组织中运行私有Git服务器或在在线服务离线时快速启动备份的好方法。在集群中运行Gogs以推送本书源代码的本地副本。
```
# 部署Git服务器:
kubectl apply -f infrastructure/gogs.yaml
# 等待它运行:
kubectl wait --for=condition=ContainersReady pod -l app=gogs
# 将本地Git服务器添加到书籍存储库—
# 这会从服务中获取URL以用作目标:
git remote add gogs $(kubectl get svc gogs -o jsonpath=
'http://{.status.loadBalancer.ingress[0].*}:3000/kiamol/kiamol.git')
# 将代码推送到您的服务器 - 使用身份验证
# 用户名kiamol和密码kiamol
git push gogs
# 查找服务器URL:
kubectl get svc gogs -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000'
# 浏览并使用相同的kiamol凭据登录
```
图11.5显示了我的输出。您不需要为此工作流程运行自己的Git服务器使用GitHub或任何其他源代码控制系统都可以使用相同的方式但是这样做可以获得易于重复的环境 - 本章的Gogs设置已经预配置了用户帐户因此您可以快速启动。
![图 11.5](images/Figure11.5.png)
<center>图 11.5 在Kubernetes中运行自己的Git服务器非常容易</center>
现在我们有了一个本地源代码控制服务器可以在其中插入其他组件。接下来是可以构建容器镜像的系统。为了使其可移植以便在任何集群上运行我们需要一些不需要Docker的东西因为群集可能使用不同的容器运行时。我们有几个选项但是最好的之一是BuildKit这是Docker团队的一个开源项目。BuildKit最初是Docker Engine内部的镜像构建组件的替代品它具有可插拔的架构因此您可以使用或不使用Dockerfiles构建映像。您可以将BuildKit作为服务器运行因此工具链中的其他组件可以使用它来构建映像。
试一试在集群中运行BuildKit作为服务器并确认它拥有在没有Docker的情况下构建容器镜像所需的所有工具。
```
# 部署BuildKit:
kubectl apply -f infrastructure/buildkitd.yaml
# 等待它开始:
kubectl wait --for=condition=ContainersReady pod -l app=buildkitd
# verify that Git and BuildKit are available:
kubectl exec deploy/buildkitd -- sh -c 'git version && buildctl
--version'
# check that Docker isnt installed—this command will fail:
kubectl exec deploy/buildkitd -- sh -c 'docker version'
```
您可以在图11.6中看到我的输出其中BuildKit Pod正在运行一个安装了BuildKit和Git客户端但没有Docker的镜像。重要的是要意识到BuildKit完全独立——它不连接到Kubernetes中的容器运行时以构建镜像所有这些都将在Pod内部完成。
![图 11.6](images/Figure11.6.png)
<center>图 11.6 BuildKit作为容器镜像构建服务运行无需Docker</center>
在我们能够看到完整的PaaS工作流程之前我们需要设置更多的组件但是现在我们已经有足够的内容来看到其中的构建部分是如何工作的。我们在这里针对无Docker的方法因此我们将忽略上一节中使用的Dockerfile并直接从源代码中构建应用程序到容器镜像中。如何做到这一点呢通过使用一个名为Buildpacks的CNCF项目这是由Heroku推出的一种技术用于支持其PaaS产品。
Buildpacks使用与多阶段Dockerfile相同的概念在容器内部运行构建工具来编译应用程序然后将编译的应用程序打包到另一个具有应用程序运行时的容器镜像之上。您可以使用一个名为Pack的工具对应用程序的源代码运行它。Pack会确定您使用的语言将其与Buildpack匹配然后将您的应用程序打包成一个映像——不需要Dockerfile。现在Pack仅能与Docker一起运行但我们不使用Docker因此可以使用另一种方法将Buildpacks与BuildKit集成。
现在我们将进入构建过程手动运行构建稍后在本章中自动化。连接到BuildKit Pod从本地Git服务器拉取书籍代码并使用Buildpacks而不是Dockerfile进行构建。
```
# 连接到 BuildKit Pod 中的会话:
kubectl exec -it deploy/buildkitd -- sh
# 从Gogs 克隆源码:
cd ~
git clone http://gogs:3000/kiamol/kiamol.git
# 切换到 app 目录:
cd kiamol/ch11/bulletin-board/
# 使用 BuidKit 构建应用:
buildctl build --frontend=gateway.v0 --opt source=kiamol/buildkit-buildpacks --local context=src --output
type=image,name=kiamol/ch11-bulletin-board:buildkit
# 构建完成退出会话
exit
```
这项任务需要运行一段时间但请注意来自BuildKit的输出您将看到正在发生的事情——首先它下载提供Buildpacks集成的组件然后运行并发现这是一个Node.js应用程序它将应用程序打包成压缩归档文件然后将归档文件导出到包含Node.js运行时的容器镜像中。图11.7显示了我的输出。
![图 11.7](images/Figure11.7.png)
<center>图 11.7 在没有Docker和Dockerfile的情况下构建容器镜像会增加很多复杂性</center>
您不能在BuildKit Pod上从该镜像运行容器因为它没有配置容器运行时但是BuildKit能够将镜像推送到仓库中构建和打包应用程序以在容器中运行而不需要Dockerfile或Docker是相当令人印象深刻的但代价是很大的。
最大的问题是构建过程的复杂性和所有组件的成熟度。 BuildKit是一个稳定的工具但它远不如标准的Docker构建引擎使用得广泛。 Buildpacks是一种有前途的方法但对Docker的依赖意味着它们在像云中的受管Kubernetes集群这样的无Docker环境中无法很好地工作。我们用来桥接它们的组件是由BuildKit项目的维护者Tõnis Tiigi编写的一种工具。它实际上只是一个将Buildpacks插入BuildKit中的概念验证工具它足以演示工作流程但不是您想依靠构建生产应用程序的工具。
有替代方案。 GitLab是将Git服务器与使用Buildpacks的构建流水线相结合的产品而Jenkins X是Kubernetes的本地构建服务器。它们本身就是复杂的产品您需要知道如果想从开发人员工作流程中删除Docker则会在构建过程中换取更多的复杂性。本章的最后您将能够决定结果是否值得。接下来我们将看看如何在Kubernetes中隔离工作负载以便单个集群可以运行您的交付流水线和所有测试环境。
## 11.3 使用上下文和名称空间隔离工作负载
在第3章中我介绍了Kubernetes命名空间并很快就进行了下一步。您需要了解它们才能理解Kubernetes为服务使用的完全限定DNS名称但在开始划分集群之前您不需要使用它们。命名空间是一种分组机制——每个Kubernetes对象都属于一个命名空间——您可以使用多个命名空间从一个真实的集群创建虚拟集群。
命名空间非常灵活,组织以不同的方式使用它们。您可以在生产集群中将它们用于将其划分为不同的产品,或将非生产集群划分为不同的环境——集成测试、系统测试和用户测试。您甚至可能有一个开发集群,每个开发人员都有自己的命名空间,以便他们不需要运行自己的集群。命名空间是一个边界,您可以在其中应用安全性和资源限制,以便部署,但我们将从简单的演练开始。
NOW 尝试一下Kubectl是命名空间感知的。您可以显式地创建一个命名空间然后使用命名空间标志部署和查询资源——这将创建一个简单的sleep部署。
```
# 创建命名空间:
kubectl create namespace kiamol-ch11-test
# 在命名空间中部署 sleep pod:
kubectl apply -f sleep.yaml --namespace kiamol-ch11-test
# 查询 sleep Pods—不会返回任何内容:
kubectl get pods -l app=sleep
# 现在指定命名空间查询:
kubectl get pods -l app=sleep -n kiamol-ch11-test
```
我的输出如图11.8所示其中可以看到命名空间是资源元数据的重要组成部分。您需要显式地指定命名空间来处理kubectl中的对象。我们在前10章中避免这样做的唯一原因是每个集群都有一个名为default的命名空间如果您没有指定就会使用它到目前为止我们已经在这里创建和使用了所有内容。
![图 11.8](images/Figure11.8.png)
<center>图 11.8 命名空间隔离工作负载—您可以使用它们来表示不同的环境</center>
命名空间中的对象是隔离的因此您可以在不同的命名空间中部署具有相同对象名称的相同应用程序。资源不能看到其他命名空间中的资源。Kubernetes的网络是扁平化的所以不同命名空间中的pod可以通过Services通信但是控制器只在自己的命名空间中查找pod。命名空间也是普通的Kubernetes资源。清单11.2显示了YAML中的命名空间 spec以及另一个使用新命名空间的 sleep 部署的元数据。
> 清单 11.2 sleep-uat.yaml, 创建和定位命名空间的清单
```
apiVersion: v1
kind: Namespace # 命名空间只需要一个名称的配置.
metadata:
name: kiamol-ch11-uat
---
apiVersion: apps/v1
kind: Deployment
metadata: # namespace 是 metadata 对象的一个属性,必须存在,否则 deployment 将失败
name: sleep
namespace: kiamol-ch11-uat
# 后续就是 pod 相关的 spec.
```
该YAML文件中的Deployment和Pod规范使用与上一练习中部署的对象相同的名称但由于控制器设置为使用不同的命名空间因此它创建的所有对象也将位于该命名空间中。当您部署这个清单时您将看到创建的新对象没有任何命名冲突。
现在试试吧从清单11.2中的YAML创建一个新的UAT命名空间和Deployment。控制器使用相同的名称您可以使用kubectl跨命名空间查看对象。删除命名空间会删除命名空间中的所有资源。
```
# 创建 namespace and Deployment:
kubectl apply -f sleep-uat.yaml
# 查看所有命名空间下的 sleep deployment:
kubectl get deploy -l app=sleep --all-namespaces
# 删除 UAT namespace:
kubectl delete namespace kiamol-ch11-uat
# 再次查看 Deployment:
kubectl get deploy -l app=sleep --all-namespaces
```
可以在图11.9中看到我的输出。最初的sleep Deployment没有在YAML文件中指定命名空间我们通过在kubectl命令中指定kiamol-ch11-test命名空间创建了它。第二个sleep 部署在YAML中指定了kiamol-ch11-uat命名空间因此它是在那里创建的不需要kubectl 命名空间标志。
![图 11.9](images/Figure11.9.png)
<center>图 11.9 命名空间是管理对象组的有用抽象</center>
在共享集群环境中您可能经常使用不同的命名空间——在您自己的开发命名空间中部署应用程序然后在测试命名空间中查看日志。使用kubectl标志在它们之间切换既耗时又容易出错而kubectl提供了一种更简单的上下文方式。上下文定义Kubernetes集群的连接细节并设置kubectl命令中使用的默认命名空间。您的实验室环境已经设置了上下文您可以修改它来切换命名空间。
现在试试吧显示您配置的上下文并更新当前上下文以将默认命名空间设置为test 命名空间。
```
# 查看所有contexts:
kubectl config get-contexts
# 更新默认的 命名空间上下文:
kubectl config set-context --current --namespace=kiamol-ch11-test
# 查看默认命名空间下的 Pods:
kubectl get pods
```
在图11.10中可以看到为上下文设置命名空间将为所有kubectl命令设置默认命名空间。任何没有指定命名空间的查询和任何YAML没有指定命名空间的创建命令现在都将使用test命名空间。您可以创建多个上下文所有上下文都使用相同的集群但不同的命名空间并使用kubectl use-context命令在它们之间切换。
![图 11.10](images/Figure11.10.png)
<center>图 11.10 上下文是在命名空间和集群之间切换的一种简单方法</center>
上下文的另一个重要用途是在集群之间切换。当您设置Docker Desktop或K3s时它们会为您的本地集群创建一个上下文—所有细节都保存在配置文件中该配置文件存储在主文件夹中的.kube目录中。管理Kubernetes服务通常具有向配置文件添加集群的功能因此您可以在本地计算机上使用远程集群。远程API服务器将使用TLS进行保护并且您的kubectl配置将使用客户端证书将您标识为用户。您可以通过查看配置来查看这些安全细节。
现在试试吧,重置上下文以使用默认命名空间,然后打印客户端配置的详细信息。
```
# 将命名空间设置为空白将重置默认值:
kubectl config set-context --current --namespace=
# 打印配置文件显示您的集群连接:
kubectl config view
```
图11.11显示了我的输出其中使用TLS证书(kubectl没有显示)对连接进行身份验证的Docker Desktop集群的本地连接。
![图 11.11](images/Figure11.11.png)
<center>图 11.11 上下文包含集群的连接细节,可以是本地的,也可以是远程的</center>
Kubectl 还可以使用令牌与 Kubernetes API 服务器进行身份验证Pods也提供了一个令牌可以用作Secret因此运行在Kubernetes中的应用程序可以连接到Kubernetes API来查询或部署对象。这距离我们接下来要去的地方还有很长的路要走:我们将在Pod中运行一个构建服务器当Git中的源代码发生变化时触发构建使用BuildKit构建镜像并将其部署到测试命名空间中的Kubernetes。
## 11.4 在不考虑 Docker 的 Kubernetes 中持续交付
实际上我们还没有完全做到这一点因为构建过程需要将镜像推到仓库中这样Kubernetes才能将其拉出以运行Pod容器。真正的集群有多个节点每个节点都需要能够访问镜像仓库。到目前为止这还很简单因为我们在Docker Hub上使用了公共镜像但在您自己的构建中您将首先推入私有存储库。Kubernetes支持通过在特殊类型的Secret对象中存储仓库凭据来提取私有镜像。
您需要在镜像仓库上设置一个帐户来跟随本节的内容—docker Hub就可以了或者您可以使用Azure容器仓库(ACR)或Amazon弹性容器仓库(ECR)在云中创建一个私有仓库。如果你在云中运行集群使用云的仓库来减少下载时间是有意义的但所有仓库都使用与Docker Hub相同的API所以它们是可互换的。
现在试试吧,创建Secret来存储仓库凭证。为了便于理解有一个脚本将凭据收集到局部变量中。别担心脚本不会把你的证书发邮件给我…
```
# 收集Windows上的详细信息:
. .\set-registry-variables.ps1
# 或者 Linux/Mac:
. ./set-registry-variables.sh
# 使用脚本中的细节创建Secret:
kubectl create secret docker-registry registry-creds --docker-server=$REGISTRY_SERVER --docker-username=$REGISTRY_USER --docker-password=$REGISTRY_PASSWORD
# 查看 Secret:
kubectl get secret registry-creds
```
我的输出如图11.12所示。我使用的是Docker Hub它允许您创建临时访问令牌您可以以与帐户密码相同的方式使用它。当我完成本章时我将撤销访问令牌——这是Hub中的一个很好的安全功能。
![图 11.12](images/Figure11.12.png)
<center>图 11.12 您的组织可能使用私有镜像仓库—您需要一个Secret来进行身份验证</center>
好了我们准备好了。我们有一个在BuildKit Pod中运行的无docker构建服务器一个本地Git服务器我们可以使用它快速遍历构建过程还有一个存储在集群中的仓库 Secret。我们可以使用自动化服务器来运行构建管道我们将使用Jenkins来实现这一点。Jenkins作为构建服务器有着悠久的历史它非常受欢迎但您不需要成为Jenkins专家来设置此构建因为我已经在自定义Docker Hub 镜像中配置了它。
本章的Jenkins 镜像安装了BuildKit和kubectl命令行Pod设置为在正确的位置显示凭据。在前面的练习中创建的仓库 Secret 被挂载在Pod容器中因此BuildKit在推送镜像时可以使用它对仓库进行身份验证。Kubectl被配置为使用Kubernetes在另一个Secret中提供的令牌连接到集群中的本地API服务器。部署Jenkins服务器并检查所有配置是否正确。
现在试试吧Jenkins使用容器镜像中的启动脚本从Kubernetes Secrets中获得所需的一切。首先部署Jenkins并确认它可以连接到Kubernetes。
```
# 部署 Jenkins:
kubectl apply -f infrastructure/jenkins.yaml
# 等待 pod 就绪:
kubectl wait --for=condition=ContainersReady pod -l app=jenkins
# 检查集群可连接:
kubectl exec deploy/jenkins -- sh -c 'kubectl version --short'
# 检查仓库 secret 已挂载:
kubectl exec deploy/jenkins -- sh -c 'ls -l /root/.docker'
```
在本练习中您将看到kubectl报告您自己的Kubernetes实验室集群的版本这将确认Jenkins Pod容器已正确设置为向Kubernetes进行身份验证因此它可以将应用程序部署到运行它的同一集群中。我的输出如图11.13所示。
![图 11.13](images/Figure11.13.png)
<center>图 11.13 Jenkins运行管道因此它需要Kubernetes和仓库的身份验证细节</center>
现在一切就绪Jenkins可以从Gogs Git服务器获取应用程序代码连接到BuildKit服务器使用Buildpacks构建容器镜像并将其推送到仓库并将最新的应用程序版本部署到测试命名空间。这项工作已经使用Jenkins管道进行了设置但是管道步骤只使用应用程序文件夹中的简单构建脚本。清单11.3显示了构建阶段,该阶段打包并推送镜像。
> 清单 11.3 build.sh, 使用BuildKit构建脚本
```
buildctl --addr tcp://buildkitd:1234 \ # The command runs on Jenkins,
build \ # but it uses the BuildKit server.
--frontend=gateway.v0 \
--opt source=kiamol/buildkit-buildpacks \ # Uses Buildpacks as input
--local context=src \
--output type=image,name=${REGISTRY_SERVER}/${REGISTRY_USER}/bulletin-board:
${BUILD_NUMBER}-kiamol,push=true # Pushes the output to the registry
```
该脚本是您在11.2节中运行的更简单的BuildKit命令的扩展当时您假装是构建服务器。buildctl命令对Buildpacks使用相同的集成组件因此这里没有Dockerfile。该命令在Jenkins Pod中运行因此它为BuildKit服务器指定了一个地址该服务器在名为buildkitd的服务后面的单独Pod中运行。这里也没有Docker。镜像名称中的变量都是由Jenkins设置的但它们是标准的环境变量因此在构建脚本中不依赖于Jenkins。
当管道的这一阶段完成时镜像将被构建并推送到仓库。下一阶段是部署更新后的应用程序该应用程序位于单独的脚本中如清单11.4所示。你不需要亲自操作这都在Jenkins的流程中。
> 清单 11.4 run.sh, 使用Helm的部署脚本
```
helm upgrade --install --atomic \ # Upgrades or installs the release
--set registryServer=${REGISTRY_SERVER}, \ # Sets the values for the
registryUser=${REGISTRY_USER}, \ # image tag, referencing
imageBuildNumber=${BUILD_NUMBER} \ # the new image version
--namespace kiamol-ch11-test \ # Deploys to the test namespace
bulletin-board \
helm/bulletin-board
```
deployment 使用Helm和一个 chart其中包含镜像名称部分的值。它们是从构建阶段使用的相同变量设置的这些变量是从Docker 仓库 Secret和Jenkins中的构建号编译的。在我的例子中第一个构建将一个名为sixeyed/bulletin-board:1-kiamol的镜像推送到Docker Hub并使用该镜像安装Helm发行版。要在集群中运行构建并推送到仓库您只需要登录到Jenkins并启用构建—管道本身已经设置好了。
现在试试吧Jenkins正在运行并配置但管道作业未启用。登录以启用作业您将看到管道执行应用程序部署到集群。
```
# 获取 Jenkins url:
kubectl get svc jenkins -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/job/kiamol'
# 用用户名kiamol和密码kiamol浏览和登录;
# 如果Jenkins还在设置你会看到等待屏幕
# 单击Kiamol作业的启用然后等待…
# 当管道完成时,检查部署:
kubectl get pods -n kiamol-ch11-test -l
app.kubernetes.io/name=bulletin-board -o=custom-
columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image
# 获取 test app url:
kubectl get svc -n kiamol-ch11-test bulletin-board -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8012'
# 浏览 url
```
构建应该很快因为它使用的是同一个BuildKit服务器该服务器已经为11.2节中的Buildpack构建缓存了镜像。构建完成后您可以浏览到Helm在test命名空间中部署的应用程序并看到应用程序正在运行——mine如图11.14所示。
![图 11.14](images/Figure11.14.png)
<center>图 11.14运行中的管道在没有Docker或Dockerfiles的情况下构建并部署到Kubernetes</center>
到目前为止一切顺利。我们扮演的是运维角色所以我们了解这个应用程序交付过程中的所有活动部分——我们将拥有Jenkinsfile中的管道和Helm Chart中的应用程序规格。其中有许多细微的细节如模板化的镜像名称和部署YAML中的镜像拉密但从开发人员的角度来看这些都是隐藏的。
开发人员的观点是您可以使用本地环境在应用程序上工作推送更改并看到它们在测试URL上运行而不用担心中间会发生什么。我们现在可以看到工作流了。您在前面进行了应用程序更改将事件描述添加到站点要部署该应用程序只需将更改推到本地Git服务器并等待Jenkins构建完成。
现在试试吧推送你的代码更改到你的Gogs服务器;Jenkins将在一分钟内看到更改并开始新的构建。这将向仓库推送一个新的镜像版本并更新Helm版本以使用该版本。
```
# 更改代码, 推送到 Git:
git add bulletin-board/src/backend/events.js
git commit -m 'Add event descriptions'
git push gogs
# 访问 Jenkins, 等待新的构建结束
# 检查 Pod 使用新的镜像版本:
kubectl get pods -n kiamol-ch11-test -l
app.kubernetes.io/name=bulletin-board -o=custom-
columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image
# 浏览 app
```
这是应用于Kubernetes的git推送PaaS工作流。这里我们处理的是一个简单的应用程序但对于具有许多组件的大型系统方法是相同的:共享命名空间可以是由许多不同团队推动的所有最新版本的部署目标。图11.15显示了Kubernetes中由代码推送触发的应用程序更新不需要开发人员使用Docker、Kubernetes或Helm。
![图 11.15](images/Figure11.15.png)
<center> 图 11.15 它是在你自己的Kubernetes集群上的PaaS——很多复杂的东西对开发人员来说是隐藏的</center>
当然PaaS方法和Docker方法并不相互排斥。如果你的集群运行在Docker上你可以利用一个更简单的基于Docker的应用程序构建过程但仍然支持其他应用程序的无Docker PaaS方法所有这些都在同一个集群中。每种方法都有优点和缺点最后我们将讨论如何在它们之间进行选择。
## 11.5 评估 Kubernetes 上的开发人员工作流程
在本章中我们研究了极端的开发人员工作流程从完全接受容器并希望将其置于每个环境的前沿和中心的团队到不希望在开发过程中添加任何仪式希望保持本地工作并将所有容器部分留给CI/CD管道的团队。在两者之间有很多地方很可能您将构建一种适合您的组织、应用程序架构和Kubernetes平台的方法。
这个决定既关乎技术,也关乎文化。您是希望每个团队都能提高容器知识的水平,还是希望将这些知识集中在服务团队中,而让开发人员团队专注于交付软件?虽然我希望看到每个人的办公桌上都有《一个月的午餐学会Docker》和《一个月的午餐学会Kubernetes》但熟练使用容器确实需要相当大的承诺。以下是我认为在项目中保留Docker和Kubernetes的主要优势:
- PaaS方法是复杂的和定制的——你将把许多不同的技术与不同的成熟度级别和支持结构组合在一起。
- Docker方法很灵活——你可以在Dockerfile中添加任何你需要的依赖和设置而PaaS方法更规范所以它们不适合每一个应用。
- PaaS技术没有你在微调Docker镜像时所获得的优化;Docker工作流的公告牌镜像是95mb而Buildpacks版本是1gb——这是一个更小的表面区域来保护。
- 致力于学习Docker和Kubernetes是值得的因为它们是可移植的技能——开发人员可以使用标准工具集轻松地在项目之间移动。
- 团队不必使用完整的容器堆栈;他们可以在不同的阶段选择退出——一些开发人员可能只使用Docker来运行容器而另一些开发人员可能使用Docker Compose或Kubernetes。
- 分布式知识有助于更好的协作文化—集中式服务团队可能会因为成为唯一能玩所有有趣技术的人而被怨恨。
最终,这是您的组织和团队的决定,需要考虑从当前工作流迁移到所需工作流的痛苦。在我自己的咨询工作中,我经常平衡开发和运营角色,而且我倾向于务实。当我积极开发时,我使用本地工具(我通常使用Visual Studio处理.net项目)但在我推动任何更改之前我在本地运行CI进程用Docker Compose构建容器镜像然后在本地Kubernetes集群中旋转所有内容。这并不适用于所有场景但我发现它在开发速度和我的更改在下一个环境中以同样的方式工作的信心之间取得了很好的平衡。
以上就是开发人员工作流的全部内容,因此我们可以在继续前进之前整理集群。让您的构建组件(Gogs、BuildKit和Jenkins)继续运行—您将在实验室中需要它们。
现在试试吧,移除布告板部署。
```
# 卸载 helm release:
helm -n kiamol-ch11-test uninstall bulletin-board
# 删除手动部署的 deployment:
kubectl delete all -l app=bulletin-board
```
## 11.6 实验室
这个实验室有点麻烦所以我要提前道歉——但是我想让您看到使用自定义工具集走PaaS之路是有危险的。本章的公告栏应用程序使用了Node运行时的一个非常旧的版本10.5.0版本在实验室需要更新到最新版本。有一个用于使用Node 10.6.0的实验室的新源代码文件夹您的工作是建立一个构建该版本的管道然后找出它失败的原因并修复它。下面有一些提示因为我们的目标不是让你学习Jenkins而是看看如何调试失败的管道:
- 开始从Jenkins主页创建一个新项目:选择复制一个现有作业的选项并复制kiamol作业;你喜欢怎么称呼新工作都行。
- 在Pipeline选项卡的新作业配置中将管道文件的路径更改为新的源代码文件夹:ch11/lab/bulletin-board/Jenkinsfile。
- 构建作业,并查看日志以找出失败的原因。
- 你需要在实验室源代码文件夹中进行更改并将其推送到Gogs以修复构建
我的样例解决方案在GitHub上有一些Jenkins设置的截图可以帮助你:https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch11/lab/README.md。

View File

@ -0,0 +1,568 @@
# 第十二章 增强自我修复应用程序
Kubernetes 在计算和网络层上对应用程序进行抽象建模。这些抽象允许Kubernetes控制网络流量和容器的生命周期因此如果应用程序的某些部分出现故障它可以采取纠正措施。如果您的规范中有足够的细节集群可以发现并修复临时问题并保持应用程序在线。这些是自我修复的应用程序可以在不需要人工指导的情况下渡过任何短暂的问题。在本章中你将学习如何在你自己的应用程序中建模使用容器探测来测试健康状况并施加资源限制这样应用程序就不会占用太多的计算量。
Kubernetes 的治疗能力是有限的你也会在本章学到这些。我们将主要研究如何在没有手动管理的情况下保持应用程序的运行但我们也将再次研究应用程序更新。更新是最有可能导致停机的原因我们将看看Helm的一些其他功能可以让你的应用在更新周期内保持健康。
## 12.1 使用 readiness 探测将流量路由到健康 Pods
Kubernetes 知道 Pod 容器是否正在运行但它不知道容器内的应用程序是否健康。每个应用程序都有自己对“健康”的定义——对HTTP请求的响应可能是200 OK——Kubernetes提供了一种使用容器探测来测试健康状况的通用机制。Docker 镜像可以配置健康检查但Kubernetes会忽略它们而是使用自己的探测。探针在Pod spec 中定义,它们按照固定的时间表执行,测试应用程序的某些方面,并返回一个指示器,以判断应用程序是否仍然健康。
如果探测响应显示容器不健康Kubernetes将采取行动而所采取的行动取决于探测的类型。Readiness 探测在网络级执行操作,管理侦听网络请求的组件的路由。
如果 Pod 容器不健康则将Pod从就绪状态中取出并从服务的活动Pod列表中删除。图12.1显示了如何查找具有多个副本的部署其中一个Pod不健康。
![图 12.1](images/Figure12.1.png)
<center>图12.1 服务的端点列表排除了尚未准备好接收流量的pod </center>
Readiness 就绪探测是管理临时负载问题的好方法。有些 pod 可能过载,对每个请求返回 503 状态代码。如果 readiness 就绪探测检查了200响应而这些pod返回503那么它们将从 Service 中删除并停止接收请求。Kubernetes在探测器失败后继续运行探测器所以如果过载的 Pod 在休息时有机会恢复探测器将再次成功Pod将重新加入 Service。
我们在本书中使用的随机数生成器有几个特性我们可以使用它来了解它是如何工作的。API可以在一种模式下运行即在特定数量的请求后失败并且它具有一个HTTP端点该端点返回它是正常状态还是处于失败状态。我们将在没有准备 readiness 探测的情况下运行它,以便了解问题。
现在试试吧,使用多个副本运行API看看当应用程序在没有任何容器探测进行测试的情况下失败时会发生什么。
```
# 进入章节目录:
cd ch12
# 部署 random-number API:
kubectl apply -f numbers/
# 等待就绪:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api
# 确认 Pod 注册作为 Service endpoints:
kubectl get endpoints numbers-api
# 将 api url 保存到文本文件:
kubectl get svc numbers-api -o jsonpath='http://{.status.loadBalancer
.ingress[0].*}:8013' > api-url.txt
# 访问 API—返回之后, 应用会显示不正常:
curl "$(cat api-url.txt)/rng"
# 检查 health 端点:
curl "$(cat api-url.txt)/healthz"; curl "$(cat api-url.txt)/healthz"
# 确认 service 使用的 Pods:
kubectl get endpoints numbers-api
```
从本练习中您将看到Service 将两个pod都保存在其端点列表中尽管其中一个不健康并且总是返回500错误响应。图12.2中的输出在请求之前和之后显示了端点列表中的两个IP地址这导致一个实例变得不健康。
![图 12.2](images/Figure12.2.png)
<center>图 12.2 应用程序容器可能不健康但Pod保持就绪状态</center>
这是因为 Kubernetes 不知道其中一个pod是不健康的。Pod容器中的应用程序仍在运行Kubernetes不知道有一个健康端点可以用来查看应用程序是否正常工作。你可以在Pod的容器 spec 中给它准备 readiness 探测的信息。清单12.1显示了API spec 的更新,其中包括健康检查。
> 清单 12.1 api-with-readiness.yaml, API 容器的 readiness 探测
```
spec: # 这是 Deployment 的 Pod spec
containers:
- image: kiamol/ch03-numbers-api
readinessProbe: # 探测在容器层面指定
httpGet:
path: /healthz # 这是一个 Get 路由
port: 80
periodSeconds: 5 # 探测每隔 5 秒钟触发
```
Kubernetes 支持不同类型的容器探测。这一个使用HTTP GET操作这是完美的web应用程序和api。探测器告诉Kubernetes每5秒测试一次/healthz端点;如果响应的HTTP状态码在200到399之间则探测成功;如果返回任何其他状态代码它将失败。随机数API在不健康时返回500代码因此我们可以看到准备就绪探测的工作。
现在试试吧,部署更新后的 spec 并验证包含失败应用程序的Pod已从 Service 中删除。
```
# 部署清单 12.1 更新的 spec:
kubectl apply -f numbers/update/api-with-readiness.yaml
# 等待替换的 Pods 就绪:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api,version=v2
# 检查 endpoints:
kubectl get endpoints numbers-api
# 触发一个应用容器变成不健康状态:
curl "$(cat api-url.txt)/rng"
# 等待 readiness 探测生效:
sleep 10
# 再次检查 endpoints:
kubectl get endpoints numbers-api
```
如图12.3中的输出所示readiness 探测检测到其中一个pod不健康因为对HTTP请求的响应返回500。Pod的IP地址从服务端点列表中删除因此它将不再接收任何流量。
![图 12.3](images/Figure12.3.png)
<center>图 12.3 就绪失败探测将pod移出就绪状态从而将它们从服务中移除</center>
这个应用程序也是一个很好的例子,说明准备 readiness 探测本身是多么危险。随机数API中的逻辑意味着一旦失败它将始终失败因此不健康的Pod将被排除在服务之外应用程序将以低于预期的容量运行。当探测失败时部署不会替换离开就绪状态的pod因此我们留下两个正在运行的pod但只有一个接收流量。如果另一个Pod也失败了情况会更糟。
现在试试吧,服务列表中只有一个Pod。你会发出一个请求Pod也会变得不健康所以两个Pod都会从服务中删除。
```
# 检查 Service endpoints:
kubectl get endpoints numbers-api
# 访问 API, 触发应用都变成不健康状态:
curl "$(cat api-url.txt)/rng"
# 等待 readiness 探测触发:
sleep 10
# 再次检查 endpoints:
kubectl get endpoints numbers-api
# 检查 Pod status:
kubectl get pods -l app=numbers-api
# 我们可以重置 API... 但是没有 Pods 可以接受请求,所以将会失败:
curl "$(cat api-url.txt)/reset"
```
现在我们有了一个解决方案——两个pod readiness 就绪探测都失败了Kubernetes已经从服务端点列表中删除了它们。这样服务就没有端点了所以应用程序处于离线状态如图12.4所示。现在的情况是任何试图使用API的客户端都会得到一个连接失败而不是一个HTTP错误状态代码对于试图使用特殊管理URL重置应用程序的管理员来说也是如此。
![图 12.4](images/Figure12.4.png)
<center>图 12.4 探头应该帮助应用程序,但它们可以从一个服务中删除所有 Pods</center>
如果您认为“这不是一个自我修复应用程序”,那么您完全正确,但请记住,应用程序无论如何都处于失败状态。没有 readiness 就绪探测,应用程序仍然不能工作,但有了 readiness 就绪探测,它就不会受到攻击请求,直到它恢复并能够处理它们。您需要了解应用程序的失败模式,以了解当探测失败时会发生什么,以及应用程序是否可能自行恢复。
随机数API再也不会恢复正常但我们可以通过重新启动Pod来修复失败状态。如果你在容器 spec 中包含另一个健康检查:一个 liveness 探测Kubernetes将为你做这件事。
## 12.2 通过 liveness 探测重启不健康的 Pods
Liveness 探针使用与 readiness 探针相同的健康检查机制——在Pod spec 中测试配置可能是相同的——但失败探针的操作是不同的。liveness 探测在计算级采取行动如果pod变得不健康将重新启动pod。重启是当Kubernetes用一个新的Pod容器替换Pod容器时;Pods 本身没有被替换;它继续在相同的节点上运行,但是使用了一个新的容器。
清单 12.2 显示了用于随机数API的 liveness 探测。这个探测使用相同的HTTP GET操作来运行探测但是它有一些额外的配置。
重新启动 Pod 比从 Service 中删除它更具侵入性,并且额外的设置有助于确保仅在我们真正需要它时才发生。
> 清单 12.2 api-with-readiness-and-liveness.yaml, 添加 liveness probe
```
livenessProbe:
httpGet: # HTTP GET 可以使用在 liveness 和 readiness 探测 - 他们使用相同的配置
path: /healthz
port: 80
periodSeconds: 10
initialDelaySeconds: 10 # 在第一次探测前等待 10 秒
failureThreshold: 2 # 在采取进一步行动前允许两次探测失败
```
这是对Pod spec 的更改因此应用更新将创建新的替换Pod开始时健康。这一次当Pod在应用程序失败后变得不健康时由于 readiness 就绪探测,它将从 service 中删除。由于 liveness 探针它将重新启动然后Pod将被添加回 Service 中。
现在试试吧,更新API并验证 liveness 状态检查和 readiness 状态检查相结合是否能保持应用程序正常运行。
```
# 部署清单 12.2 的更新:
kubectl apply -f numbers/update/api-with-readiness-and-liveness.yaml
# 等待新 pods 就绪:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api,version=v3
# 检查 Pod status:
kubectl get pods -l app=numbers-api -o wide
# 检查 Servivce endpoints:
kubectl get endpoints numbers-api # two
# 使其中一个 Pod 变成不健康:
curl "$(cat api-url.txt)/rng"
# 等待探测触发, 然后再次检查 pods:
sleep 20
kubectl get pods -l app=numbers-api
```
在本练习中,您将看到 liveness 探测的运行在应用程序失败时重新启动Pod。重新启动是一个新的Pod容器但是Pod环境是一样的——它有相同的IP地址如果容器在Pod中挂载了一个EmptyDir卷它就可以访问由前一个容器写入的文件。在图12.5中可以看到两个pod在重新启动后都在运行并准备就绪因此Kubernetes修复了故障并修复了应用程序。
![图 12.5](images/Figure12.5.png)
<center>图12.5 readiness 探测和 liveness 探测相结合有助于保持应用程序在线</center>
如果应用程序一直失败而没有健康的表现重新启动并不是一个永久性的解决方案因为Kubernetes不会无限期地重新启动一个失败的Pod。对于瞬态问题它工作得很好只要应用程序可以在替换容器中成功重新启动。探测对于在升级期间保持应用程序健康也很有用因为只有当新的pod进入就绪状态时才会进行铺开因此如果 readiness 就绪探测失败,将暂停铺开。
我们将通过待办事项列表应用程序来展示这一点,其中的 spec 包括对web应用程序Pod和数据库的 liveness和 readiness 检查。web探针使用我们已经看到的相同的HTTP GET操作但是数据库没有我们可以使用的HTTP端点。相反该 spec 使用了Kubernetes支持的其他类型的探测动作——TCP套接字动作用于检查端口是否打开并侦听传入的流量以及exec动作用于在容器内运行命令。清单12.3显示了探针的设置。
> 清单 12.3 todo-db.yaml, 使用 TCP 和 command 探针
```
spec:
containers:
- image: postgres:11.6-alpine
readinessProbe:
tcpSocket: # readiness 探测测试数据库监听端口
port: 5432
periodSeconds: 5
livenessProbe: # liveness 探测运行一个 Postgres 工具确认数据库在运行
exec:
command: ["pg_isready", "-h", "localhost"]
periodSeconds: 10
initialDelaySeconds: 10
```
当你部署这段代码时你会看到应用程序的工作方式和往常一样但现在它受到了保护防止了web和数据库组件中的瞬时故障。
现在试试吧,使用新的自修复 spec 运行待办事项列表应用程序。
```
# 部署 web and database:
kubectl apply -f todo-list/db/ -f todo-list/web/
# 等待应用 ready:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web
# 获取 service url:
kubectl get svc todo-web -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8081'
# 访问并添加一个数据项
```
这里没有什么新内容如图12.6中的输出所示。但是数据库探测意味着Postgres在数据库准备好之前不会收到任何流量如果Postgres服务器失败则数据库Pod将重新启动使用Pod中EmptyDir卷中的相同数据文件进行替换。
![图 12.6](images/Figure12.6.png)
<center>图12.6 探针正在触发并返回健康的响应,因此应用程序以通常的方式工作</center>
容器探测还可以在更新出错时保持应用程序运行。待办事项应用程序有一个新的数据库 spec它升级了Postgres的版本但它也覆盖了容器命令所以它会休眠而不是启动Postgres。这是一个典型的调试遗留错误:有人想用正确的配置启动Pod但没有运行应用程序这样他们就可以在容器中运行shell来检查环境但他们没有恢复他们的更改。如果Pod没有任何探测更新将成功并关闭应用程序。sleep命令保持Pod容器运行但没有数据库服务器供网站使用。探测会阻止这种情况发生并保持应用程序可用。
现在试试吧,部署坏的更新并验证新Pod中失败的探测阻止了原始Pod被删除。
```
# 部署更新:
kubectl apply -f todo-list/db/update/todo-db-bad-command.yaml
# 监控 Pod 状态变更:
kubectl get pods -l app=todo-db --watch
# 刷新应用确认应用还正常工作
# ctrl-c or cmd-c to exit the Kubectl watch
```
可以在图12.7中看到我的输出。创建了替换数据库Pod但它永远不会进入就绪状态因为就绪探测检查端口5342是否有进程在监听但没有。Pod也会不断重新启动因为 liveness 探测会运行一个命令来检查Postgres是否准备好接收客户端连接。虽然新的Pod不断出现故障但旧的Pod仍在运行应用程序也在继续工作。
![图 12.7](images/Figure12.7.png)
<center>图12.7 rollout等待新的pod准备就绪因此探针保护失败的更新</center>
如果你让这个应用程序再运行五分钟左右然后再次检查Pod状态你会看到新的Pod进入CrashLoopBackOff状态。这就是Kubernetes如何保护集群不浪费计算资源在不断失败的应用程序上:它在Pod重新启动之间增加了一个时间延迟并且这个延迟随着每次重新启动而增加。如果你在CrashLoopBackOff中看到Pod这通常意味着应用程序无法修复。
待办事项应用程序现在的情况与我们在第9章中首次看到的rollout失败相同。Deployment正在管理两个ReplicaSets它的目标是在新的ReplicaSets达到容量后将旧的ReplicaSets缩小到零。但是新的ReplicaSet
永远不会达到容量因为新Pod中的探针不断失效。部署就这样希望它最终能完成部署。Kubernetes没有自动回滚选项但Helm有您可以扩展Helm Chart 以支持健康升级。
## 12.3 使用 Helm 安全地部署升级
有 Helm 的话帮助就很大了。您已经在第10章学习了基本知识不需要深入了解模板函数和依赖管理就可以很好地利用Helm进行安全的应用程序升级。Helm支持原子安装和升级如果失败会自动回滚而且它还有一个部署生命周期可以在安装前后运行验证作业。
本章的源文件夹有多个用于待办事项应用程序的Helm Chart它们代表不同的版本(通常情况下每个版本都有一个单独的Helm Chart)。版本1 chart使用我们在第12.2节中使用的相同的 liveness和 readiness 检查来部署应用程序;唯一的区别是数据库使用PersistentVolumeClaim因此数据在升级之间被保留。我们将从清理之前的练习和安装Helm版本开始。
现在试试吧,使用相同的Pod spec 运行待办事项应用程序,但使用 Helm chart 进行部署。
```
# 删除章节已存在的应用:
kubectl delete all -l kiamol=ch12
# 安装 Helm release:
helm install --atomic todo-list todo-list/helm/v1/todo-list/
# 访问应用,添加新的数据项
```
该应用的 Version 1 现在正在Helm上运行除了 chart 在模板文件夹中包含一个名为NOTES.txt的文件外这里没有什么新内容该文件显示了安装后看到的有用文本。我的输出如图12.8所示。我没有附上应用程序的截图所以你只需要相信我的话我浏览了并添加了一个条目上面写着“完成第12章”。
![图12.8](images/Figure12.8.png)
<center>图12.8 使用Helm安装应用程序等待容器探测器正常运行</center>
Helm chart 的 Version 2 尝试了我们在第12.2节中看到的相同的数据库镜像升级完成了Postgres容器命令中的错误配置。当您使用 Helm 部署它时,同样的事情在底层发生:Kubernetes更新部署这添加了一个新的ReplicaSet并且ReplicaSet永远不会达到容量因为Pod readiness 探测失败。但是Helm会检查 rollout 的状态,如果在特定的时间内没有成功,它会自动回滚。
现在试试吧, 使用Helm升级待办事项应用程序发布。升级失败是因为 Pod spec 配置错误Helm 将其回滚。
```
# 列出当前 Pod 状态以及容器镜像信息:
kubectl get pods -l app=todo-list-db -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image
# 通过 helm 升级 release—将会失败:
helm upgrade --atomic --timeout 30s todo-list todo-list/helm/v2/todo-list/
# 再次列出 Pods:
kubectl get pods -l app=todo-list-db -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image
# 访问应用,刷新列表
```
如果在该练习中检查 Pod 列表几次就会看到发生了回滚如图12.9所示。起初只有一个运行Postgres 11.6的Pod然后加入了一个运行11.8的新Pod但那是一个容器探测失败的Pod。Pod在Helm超时时间内没有准备好因此升级回滚新的 Pod 被移除;它不会像kubectl更新那样不断重新启动并命中CrashLoopBackOff。
![图 12.9](images/Figure12.9.png)
<center>图 12.9 升级失败,因为新的 Pod 还没有准备好Helm 回滚</center>
在升级到版本 2 失败期间待办事项应用程序一直在线没有中断或减少容量。下一个版本通过删除Pod spec 中的坏容器命令修复了升级,它还为 Kubernetes Job添加了一个额外的模板您可以使用 Helm 作为部署测试运行。测试按需运行,而不是作为安装的一部分,因此它们非常适合烟雾测试—您可以运行这些自动化测试套件来确认成功的发行版正在正确运行。清单 12.4 显示了待办事项数据库的测试。
> 清单 12.4 todo-db-test-job.yaml, Kubernetes Job 作为 Helm 测试运行
```
apiVersion: batch/v1
kind: Job # 这个是标准的 Job spec
metadata:
# metadata 包括 name 和 labels
annotations:
"helm.sh/hook": test # 告诉Helm Job可以在发布的测试套件中运行
spec:
completions: 1
backoffLimit: 0 # 作业应该运行一次,而不是重试。
```
```
template:
spec: # 容器 spec 运行一个 sql 查询
containers:
- image: postgres:11.8-alpine
command: ["psql", "-c", "SELECT COUNT(*) FROM \"public\".\"ToDos\""]
```
我们在第 8 章中用到了 Jobshelm 很好地利用了他们。Job specs 包括期望运行多少次才能成功完成Helm 使用该期望来评估测试是否成功。版本 3 升级应该会成功,当它完成时,您可以运行测试 Job它将运行一条 SQL 语句来确认待办事项数据库是可访问的。
现在试试吧,升级到版本 3 chart修复Postgres更新。然后用 Helm 运行测试,并检查 Job Pod 的日志。
```
# 执行升级:
helm upgrade --atomic --timeout 30s todo-list todo-list/helm/v3/todo-list/
# 列出数据库 Pods 和镜像:
kubectl get pods -l app=todo-list-db -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image,IP:.status.podIPs[].ip
# 检查数据库 Service 端点:
kubectl get endpoints todo-list-db
# 现在使用 helm 运行 test job:
helm test todo-list
# 检查输出:
kubectl logs -l job-name=todo-list-db-test
```
我在图12.10中截取了我的输出但是细节都在那里——升级成功了但是没有作为升级命令一部分的测试。数据库现在使用升级版的Postgres当测试运行时Job连接到数据库并确认数据仍然存在。
![图 12.10](images/Figure12.10.png)
<center>图 12.10 使用Helm按需运行测试套件让您可以随时对应用进行冒烟测试</center>
Helm 为你管理 Jobs。它不会清理已完成的作业因此如果需要您可以检查Pod状态和日志但是当您重复测试命令时它会替换它们因此您可以随时重新运行测试套件。job还有另一个用途它有助于确保升级是安全的在升级之前运行它们这样您就可以检查当前版本是否处于可以升级的有效状态。
如果你的应用程序支持多个版本但只支持增量升级那么这个功能就特别有用所以版本1.1需要先升级到版本1.2然后才能升级到版本2。这样做的逻辑可能涉及到查询不同服务的API版本或数据库的模式版本Helm 可以在一个Job中运行该Job可以访问所有其他Kubernetes对象这些对象与应用程序Pods共享相同的ConfigMaps和Secrets。清单12.5显示了版本4中的待办事项Helm chart 的升级前测试。
> 清单 12.5 todo-db-check-job.yaml, 在 helm 升级前运行的 job
```
apiVersion: batch/v1
kind: Job # 标准的 job spec
metadata:
# metadata has name and labels
annotations:
"helm.sh/hook": pre-upgrade # 它在升级之前运行并告诉Helm创建的顺序
"helm.sh/hook-weight": "10"
spec:
template:
spec:
restartPolicy: Never
containers:
- image: postgres:11.8-alpine
# env includes secrets
command: ["/scripts/check-postgres-version.sh"]
volumeMounts:
- name: scripts
mountPath: "/scripts"
```
升级前检查有两个模板:一个是 Job spec另一个是 ConfigMap其中包含要在 Job 中运行的脚本。您可以使用注释来控制 Helm 生命周期中需要运行Job的位置并且此Job仅在升级时运行而不是作为新安装的一部分运行。权重注释确保在Job之前创建ConfigMap。生命周期和权重让您可以在Helm中建模复杂的验证步骤但是这个很简单——它升级数据库镜像但前提是该版本目前运行的是11.6版本。
现在试试吧从版本3升级到版本4无效因为版本3已经升级了Postgres版本。运行升级以验证它没有被部署。
```
# 运行升级到 version 4— 将会失败:
helm upgrade --atomic --timeout 30s todo-list todo-list/helm/v4/todo-list/
# 列出 Jobs:
kubectl get jobs --show-labels
# 输出 pre-upgrade job 的日志输出:
kubectl logs -l job-name=todo-list-db-check
# 确认 database pod 并没有变化:
kubectl get pods -l app=todo-list-db -o=custom-columns=NAME:.metadata.name,STATUS:.status.phase,IMAGE:.spec.containers[0].image
```
在本练习中您将看到Helm有效地阻止了升级因为升级前钩子运行并且Job失败。这些都记录在发行版的历史记录中这将显示最新的升级失败并且发行版已回滚到最后一个良好的修订。我的输出如图12.11所示,在整个更新过程中,应用程序仍然可用。
![图 12.11](images/Figure12.11.png)
<center>图12.11 Helm chart 中的升级前作业让您验证版本适合升级</center>
了解Helm在保持应用程序健康方面所带来的好处是有益的因为升级前验证和自动回滚也有助于保持应用程序升级的自我修复。Helm不是这样做的先决条件但如果您不使用Helm则应该考虑在部署管道中使用kubectl实现这些特性。在本章中我们还将介绍应用程序运行状况的另一部分——管理Pod容器可用的计算资源。
## 12.4 通过 resource limits 保护应用和节点
容器是应用程序进程的虚拟环境。Kubernetes构建了该环境并且您知道Kubernetes创建了容器文件系统并设置了网络。容器环境还包括内存和CPU这些也可以由Kubernetes管理但默认情况下它们不是。这意味着Pod容器可以访问它们正在运行的节点上的所有内存和CPU这是不好的原因有两个:应用程序可能会耗尽内存并崩溃,或者它们会耗尽节点的资源,从而使其他应用程序无法运行。
您可以在 Pod spec 中限制容器的可用资源,这些限制就像容器探针一样——如果没有它们,您真的不应该进入生产环境。有内存泄漏的应用程序可以很快地破坏您的集群,导致 CPU 峰值对入侵者来说是一个很好的、容易的攻击向量。在本节中,您将学习如何配置您的 Pods 来防止这种情况,我们将从一个内存需求很大的新应用程序开始。
现在试试吧,从上一个练习中清除并运行新的应用程序—除了分配内存和记录已分配的内存量外它什么也不做。这个Pod运行没有任何容器限制。
```
# 删除 helm release 释放资源:
helm uninstall todo-list
# 打印你的节点拥有多少内存可分配:
kubectl get nodes -o jsonpath='{.items[].status.allocatable.memory}'
# 部署内存分配应用程序:
kubectl apply -f memory-allocator/
# 等待几分钟,然后查看它分配了多少内存:
kubectl logs -l app=memory-allocator --tail 1
```
内存分配器应用程序每5秒就会获取大约 10mb 的内存并且它将一直持续下去直到用完实验室集群中的所有内存。您可以从图12.12中的输出中看到我的Docker Desktop节点可以访问大约25 GB的内存而在我截屏时allocator应用程序已经占用了近1.5 GB的内存。
只要应用程序在运行它就会继续分配内存所以我们需要在我的机器死机之前快速启动以免我失去对这一章的编辑。清单12.6显示了一个更新的Pod spec其中包括资源限制将应用程序的内存限制在50 MB。
![图 12.12](images/Figure12.12.png)
<center>图 12.12 不要在生产环境中运行这个应用程序——它会一直分配内存,直到拥有所有内存为止</center>
> 清单 12.6 memory-allocator-with-limit.yaml, 添加 memory limits 到容器
```
spec: # Deployment 中的 Pod spec
containers:
- image: kiamol/ch12-memory-allocator
resources:
limits: # resources limits 限制了容器的计算能力;这将 RAM 限制在50 MB。
memory: 50Mi
```
资源是在容器级别指定的但这是一个新的Pod spec所以当您部署更新时您将得到一个新的Pod。替换开始时没有分配内存然后每隔5秒重新分配10mb。然而现在它将达到50mb的限制Kubernetes将采取行动。
现在试试吧,使用清单12.6中定义的资源限制将更新部署到内存分配器应用程序。您应该看到Pod已重新启动但前提是您的Linux主机在没有启用交换内存的情况下运行。K3s没有这个设置(除非你使用Vagrant VM设置)所以你不会看到与Docker Desktop或云Kubernetes服务相同的结果。
```
# 应用更新:
kubectl apply -f memory-allocator/update/memory-allocator-with-limit.yaml
# 等待应用程序分配内存块:
sleep 20
# 打印应用日志:
kubectl logs -l app=memory-allocator --tail 1
# 查看 Pod 状态:
kubectl get pods -l app=memory-allocator --watch
```
在本练习中您将看到Kubernetes强制执行内存限制:当应用程序试图分配超过50mb的内存时容器将被替换您可以看到Pod进入OOMKilled状态。超过限制会导致Pod重新启动因此这与失败的 liveiness探测有相同的缺点——如果替换容器一直失败随着Kubernetes应用CrashLoopBackOff, Pod重新启动将花费越来越长的时间如图12.13所示。
![图 12.13](images/Figure12.13.png)
<center>图 12.13 内存限制是硬性限制——如果容器超过了这些限制它将被杀死Pod将重新启动</center>
应用资源约束的困难之处在于确定限制应该是什么。您需要考虑一些性能测试,以了解您的应用程序可以管理什么—要注意,如果某些应用程序平台看到大量可用内存,它们会占用超过所需的内存。您应该宽宏大量地发布初始版本,然后随着您从监视中获得更多反馈,逐渐降低限制。
您还可以以另一种方式应用资源限制——为命名空间指定最大配额。这种方法对于使用命名空间为不同团队或环境划分集群的共享集群特别有用;您可以强制限制命名空间可以使用的资源总量。清单12.7显示了ResourceQuota对象的 spec它将名为kiamol-ch12-memory 的命名空间中的可用内存总量限制在150 MB。
> 清单 12.7 02-memory-quota.yaml, 为命名空间设置内存配额
```
apiVersion: v1
kind: ResourceQuota # ResourceQuota 生效在指定的命名空间
metadata:
name: memory-quota
namespace: kiamol-ch12-memory
spec:
hard: # 配额包括 CPU 和内存
limits.memory: 150Mi
```
容器限制是反应性的,因此当内存限制超过时 pod 将重新启动。因为资源配额是主动的,所以如果 pod 指定的限制超过了配额中的可用容量则不会创建pod。如果有配额那么每个Pod spec 都需要包含一个资源部分这样Kubernetes就可以将规范需要的资源与命名空间中当前可用的资源进行比较。下面是内存分配器 spec 的更新版本其中Pod指定了一个大于配额的限制。
现在试试吧,在应用了资源配额的命名空间中部署内存分配器的新版本。
```
# 删除之前的应用:
kubectl delete deploy memory-allocator
# 部署 namespace, quota, 以及新的 Deployment:
kubectl apply -f memory-allocator/namespace-with-quota/
# 打印 ReplicaSet 的状态:
kubectl get replicaset -n kiamol-ch12-memory
# 查看 RepicaSet 的 events:
kubectl describe replicaset -n kiamol-ch12-memory
```
您将从 ReplicaSet 的输出中看到,它有 0 个pod而所需的总数是 1 个。它不能创建 Pod因为它会超过命名空间的配额如图12.14所示。控制器一直尝试创建Pod但它不会成功除非有足够的配额可用比如其他 Pod 终止但在这种情况下没有任何配额因此需要更新配额。Kubernetes还可以将CPU限制应用于容器和配额但它们的工作方式略有不同。具有CPU限制的容器以固定数量的处理能力运行并且它们可以使用任意数量的CPU—如果达到限制则不会替换它们。您可以将容器限制为CPU核心的一半它可以在100% CPU的情况下运行而节点上的所有其他内核保持空闲并可用于其他容器。计算Pi是一个计算密集型操作我们可以看到在书中之前使用过的Pi应用程序上应用CPU限制的影响。
![图 12.14](images/Figure12.14.png)
<center>图 12.14 具有硬限制的配额可以防止在 pod 超过配额时创建 pod</center>
现在试试吧,在有和没有CPU限制的情况下运行Pi应用程序并比较其性能。
```
# 显示节点可用的总CPU:
kubectl get nodes -o jsonpath='{.items[].status.allocatable.cpu}'
# 部署Pi没有任何CPU限制:
kubectl apply -f pi/
# 获取访问 url:
kubectl get svc pi-web -o jsonpath='http://{.status.loadBalancer
.ingress[0].*}:8012/?dp=50000'
# 浏览到URL并查看计算需要多长时间
# 现在更新Pod spec 与CPU限制:
kubectl apply -f pi/update/web-with-cpu-limit.yaml
# 刷新Pi应用程序看看计算需要多长时间
```
我的输出如图12.15所示。计时将有所不同这取决于您的节点上有多少CPU可用。我的应用有8个内核而且没有限制它能在3.4秒内连续计算圆周率到小数点后5万位。更新后应用程序容器被限制为一个核心的四分之一同样的计算需要14.4秒。
![图 12.15](images/Figure12.15.png)
<center>图 12.15 眯着眼看你会发现限制CPU对计算速度有影响</center>
Kubernetes 使用一个固定的单位来定义 CPU 限制其中一个代表一个单核。你可以使用倍数来让你的应用程序容器访问多个内核或者将单个内核划分为“毫核”其中一毫核是一个内核的千分之一。清单12.8显示了应用于上一练习中的Pi容器的CPU限制其中250毫核是一个核的四分之一。
> 清单 12.8 web-with-cpu-limit.yaml
```
spec:
containers:
- image: kiamol/ch05-pi
command: ["dotnet", "Pi.Web.dll", "-m", "web"]
resources:
limits:
cpu: 250m # 250 豪核限制容器占用 四分之一核
```
我一次只关注一种资源这样你就能清楚地看到影响但通常你应该包括CPU和内存的限制这样你的应用程序就不会激增和耗尽集群。资源 specs 还可以包括请求部分它说明容器预计使用多少CPU和内存。这有助于Kubernetes决定哪个节点应该运行Pod我们将在第18章讨论调度时详细介绍。
我们将用另一个练习来结束本章演示如何将CPU限制应用于名称空间的配额以及超过配额时的含义。Pi应用程序的新规范尝试在一个具有最大500毫核配额的命名空间中运行两个具有300毫核CPU限制的副本。
现在试试吧,在其自己的命名空间中运行更新后的Pi应用程序该名称空间应用了CPU配额。
```
# 删除存量 app:
kubectl delete deploy pi-web
# 部署 namespace, quota, 以及新的 app spec:
kubectl apply -f pi/namespace-with-quota/
# 输出 ReplicaSet status:
kubectl get replicaset -n kiamol-ch12-cpu
# 列出 Service 的 endpoints:
kubectl get endpoints pi-web -n kiamol-ch12-cpu
# 显示 RepicaSet 的 events:
kubectl describe replicaset -n kiamol-ch12-cpu
```
在本练习中,您可以看到配额应用于命名空间中的所有 pod。ReplicaSet 使用一个Pod而不是两个Pod运行因为第一个Pod分配了 300m CPU只剩下 200m 的配额—不足以让第二个Pod运行。图 12.16 显示了ReplicaSet事件中的失败原因。Pi应用程序仍在运行但容量不足因为没有足够的CPU可用。
![图 12.16](images/Figure12.16.png)
<center>图 12.16 在配额中强制执行硬 CPU 限制,以阻止对象超过总限制</center>
配额更多的是为了保护你的集群而不是应用本身,但它们是一种强制所有 Pod spec 都有指定限制的好方法。如果您没有使用命名空间划分集群,您仍然可以对默认命名空间应用具有较大 CPU 和内存限制的配额,以确保 Pod spec 包含它们自己的限制。
资源限制、容器探测和原子升级都有助于保持应用程序在正常故障条件下运行。这些应该在您的生产路线图上但您也需要意识到Kubernetes不能修复所有类型的故障。
## 12.5 了解自我修复应用的局限性
Kubernetes 将 Pod 分配给一个节点,这就是它将运行的节点。除非节点离线,否则 pod 不会被替换,因此我们在本章中看到的所有修复机制都是通过重新启动 pod 来工作的——替换应用程序容器。你需要确保你的应用程序可以容忍这种情况特别是在我们在第7章中介绍的多容器场景中因为 init 容器会再次执行,当 Pod 重新启动时sidecars 会被替换。
对于大多数暂时失败的场景Pod 重新启动是很好的,但重复失败将以 CrashLoopBackOff 状态结束这可能会使应用程序离线。Kubernetes 没有提供任何关于允许重启多少次或回退周期的配置选项,并且它不支持在不同的节点上用新 Pod 替换失败的 Pod。这些功能是需要的但在它们着陆之前你精心配置的自我修复应用程序仍然有可能使所有 Pods 处于回退状态,服务中没有端点。
这种边缘情况通常是由于配置错误的 spec 或应用程序的致命问题而出现的,这些问题需要 Kubernetes 本身无法处理的干预。对于典型的失败状态,容器探测和资源限制的组合对于保持应用程序自身平稳运行大有帮助。这就是自我修复应用程序的全部内容,所以我们可以整理集群,为实验室做准备。
现在试试吧,从本章中移除对象。
```
# 删除 namespaces:
kubectl delete ns -l kiamol=ch12
kubectl delete all -l kiamol=ch12
# 删除其余对象:
kubectl delete secret,configmap,pvc -l kiamol=ch12
```
## 12.6 实验室
在这个实验室里,我为你们准备了一个很好的能力规划练习。目标是将集群划分为三个环境来运行 Pi 应用程序:开发环境、测试环境和UAT环境。UAT应限制在节点总CPU的50%开发和测试各占25%。你的Pi部署应该设置限制以便在每个环境中至少可以运行四个副本然后你需要验证在UAT中可以扩展到多少。
- 首先在实验室文件夹中部署命名空间和服务。
- 然后计算出您的节点的 CPU 容量并部署资源配额来限制每个命名空间的CPU(您需要编写配额规格)。
- 更新 web 部署规范。yaml包含一个 CPU 限制,允许在每个命名空间中运行四个副本。
- 当一切都在运行时将UAT部署扩展到8个副本并尝试找出它们不能全部运行的原因。
这是一个很好的练习,可以帮助您理解如何共享 CPU 资源并练习如何使用多个命名空间。我的解决方案在GitHub上你可以查看: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch12/lab/README.md。

View File

@ -0,0 +1,431 @@
# 第十三章 使用 Fluentd 和 Elasticsearch 集中化日志
应用程序生成大量日志这些日志通常不是很有用。当您在集群中运行的多个pod上扩展应用程序时使用标准的Kubernetes工具很难管理这些日志。组织通常部署自己的日志框架该框架使用收集-转发模型来读取容器日志并将它们发送到中央存储,在那里可以对它们进行索引、筛选和搜索。在本章中,您将学习如何使用该领域最流行的技术:Fluentd和Elasticsearch。Fluentd是一个收集器组件它与Kubernetes有一些很好的集成;Elasticsearch是存储组件既可以作为集群中的Pods运行也可以作为外部服务运行。
在我们开始之前你应该注意几点。首先这个模型假设你的应用程序日志被写入容器的标准输出流这样Kubernetes就可以找到它们。我们在第7章中讨论了这个问题使用了直接写入标准输出或使用日志sidecar来中继日志的示例应用程序。其次Kubernetes的日志记录模型与Docker有很大的不同。电子书的附录D向您展示了如何在Docker中使用Fluentd但在Kubernetes中我们将采用不同的方法。
## 13.1 Kubernetes 如何存储日志条目
Kubernetes 有一种非常简单的日志管理方法:它从容器运行时收集日志条目并将它们作为文件存储在运行容器的节点上。如果您想执行更高级的操作那么您需要部署自己的日志管理系统幸运的是您有一个世界级的容器平台来运行它。日志系统的移动部分从节点收集日志将它们转发到集中存储并提供一个UI来搜索和过滤它们。图13.1显示了我们将在本章中使用的技术。
![图 13.1](images/Figure13.1.png)
<center>图13.1 登录Kubernetes使用Fluentd这样的采集器从节点读取日志文件</center>
节点使用包含命名空间、Pod和容器名称的文件名存储来自容器的日志条目。标准命名系统使日志采集器很容易向日志条目添加元数据以识别源而且由于采集器本身作为Pod运行因此它可以查询Kubernetes API服务器以获得更多详细信息。Fluentd 添加 Pod 标签和镜像标记作为额外的元数据,您可以使用它们过滤或搜索日志。
日志采集器的部署非常简单。我们将探索从节点上的原始日志文件开始看看我们正在处理什么。这一切的前提是将应用程序日志从容器中取出无论应用程序直接写入这些日志还是使用sidecar容器。首先将第7章中的timecheck应用部署到几个不同的配置中以生成一些日志。
现在试试吧在不同的命名空间中使用不同的设置运行timecheck应用程序然后检查日志看看如何在kubectl中本机使用它们。
```
# 切换到本章目录:
cd ch13
# 在开发和测试命名空间中部署时间检查应用程序:
kubectl apply -f timecheck/
# 等待开发命名空间启动:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch13-dev
# 检查日志:
kubectl logs -l app=timecheck --all-containers -n kiamol-ch13-dev --tail 1
```
您将从这个练习中看到在实际的集群环境中很难直接使用容器日志如图13.2中的输出所示。一次只能使用一个命名空间不能识别记录消息的Pod但可以仅根据日志条目的数量或时间段进行过滤。
![图 13.2](images/Figure13.2.png)
<center>图13.2 Kubectl非常适合快速检查日志但是在许多命名空间中有许多pod这就比较困难了</center>
Kubectl是读取日志的最简单选项但最终日志条目来自每个节点上的文件这意味着您可以使用其他选项来处理日志。本章的源代码包括一个简单的睡眠部署它将日志路径作为HostPath卷挂载到节点上即使您没有直接访问节点也可以使用它来查看日志文件。
现在试试吧为主机的日志目录运行一个带卷挂载的Pod并使用挂载浏览文件。
```
# 运行 Deployment:
kubectl apply -f sleep.yaml
# 连接到 Pod 中的容器会话:
kubectl exec -it deploy/sleep -- sh
# 进入到日志挂载目录:
cd /var/log/containers/
# 查看日志文件:
ls timecheck*kiamol-ch13*_logger*
# 查看 dev log 文件内容:
cat $(ls timecheck*kiamol-ch13-dev_logger*) | tail -n 1
# 退出会话:
exit
```
每个 Pod 容器都有一个用于日志输出的文件。timecheck应用程序使用一个名为logger的sidecar容器来从应用程序容器中中继日志您可以在图13.3中看到Kubernetes为日志文件使用的标准命名约定:pod-name_namespace_container-name-container-id.log。文件名有足够的数据来识别日志的来源文件的内容是容器运行时输出的原始JSON日志。
![图 13.3](images/Figure13.3.png)
<center>图13.3 对于一个现代平台Kubernetes有一个老派的日志存储方法</center>
Pod重新启动后日志文件仍会保留但大多数Kubernetes实现都包括在节点上运行的日志旋转系统(在Kubernetes之外)以防止日志占用所有磁盘空间。收集日志并将其转发到中央存储可以让您更长时间地保存它们并将日志存储隔离在一个地方—这也适用于来自Kubernetes核心组件的日志。Kubernetes的DNS服务器、API服务器和网络代理都以Pods的方式运行您可以像查看应用程序日志一样查看和收集它们的日志。
现在试试吧并不是每个Kubernetes节点都运行相同的核心组件但是您可以使用sleep Pod查看哪些通用组件正在您的节点上运行。
```
# 连接一个 Pod 容器:
kubectl exec -it deploy/sleep -- sh
# 进入到主机path 卷目录:
cd /var/log/containers/
# 网络代理服务运行在每个节点上:
cat $(ls kube-proxy*) | tail -n 1
# 如果你使用的是 core dns你将看到日志:
cat $(ls coredns*) | tail -n 1
# 如果你的节点运行 API server ,你将看到日志:
cat $(ls kube-apiserver*) | tail -n 1
# 退出会话:
exit
```
您可能会从该练习中得到不同的输出这取决于您的实验室集群是如何设置的。网络代理Pod运行在每个节点上所以你应该看到这些日志但如果你的集群使用CoreDNS(这是默认的DNS插件)你只会看到DNS日志如果你的节点正在运行API Server你只会看到API server 日志。我从Docker Desktop的输出如图13.4所示;如果你看到一些不同的东西你可以运行ls *.log来查看节点上所有的Pod日志文件。
![图 13.4](images/Figure13.4.png)
<center>图13.4收集和转发节点的日志也包括所有系统Pod日志</center>
现在您已经了解了Kubernetes是如何处理和存储容器日志的您可以看到集中式日志系统是如何使故障排除变得更加容易的。收集器在每个节点上运行从日志文件中抓取条目并转发它们。在本章的其余部分您将学习如何使用EFK堆栈:Elasticsearch、Fluentd和Kibana实现它。
## 13.2 使用 Fluentd 收集节点日志
Fluentd 是 CNCF 项目,有很好的基础,是一个成熟的、受欢迎的产品。存在其他日志收集组件,但 Fluentd 是一个很好的选择,因为它具有强大的处理管道来操作和过滤日志条目,以及可插入的体系结构,因此它可以将日志转发到不同的存储系统。它也有两种变体:完整的 Fluentd 快速高效有1000多个插件但我们将使用最小的替代品称为 Fluent Bit。
Fluent Bit 最初是作为 Fluentd 的轻量级版本开发的,用于 IoT 设备等嵌入式应用程序,但它具有在完整的 Kubernetes 集群中进行日志聚合所需的所有功能。每个节点都将运行一个日志采集器因此保持该组件的影响较小是有意义的而且Fluent Bit只占用几十兆的内存。Kubernetes 中的 Fluent Bit 架构很简单:DaemonSet 在每个节点上运行一个收集器Pod它使用 HostPath 卷挂载来访问日志文件就像在我们使用的睡眠示例中一样。Fluent Bit 支持不同的输出,所以我们将从简单的开始,只是登录到 Fluent Bit Pod 的控制台。
现在试试吧部署Fluent Bit配置为读取时间检查日志文件并将其写入Fluent Bit容器的标准输出流。
```
# 部署 DaemonSet and ConfigMap:
kubectl apply -f fluentbit/
# 等待 Pod 就緒:
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging
# 检查 Fluent Bit Pod 日志:
kubectl logs -l app=fluent-bit -n kiamol-ch13-logging --tail 2
```
我的输出如图 13.5所示,其中您可以看到来自 timecheck 容器的日志被显示在Fluent Bit容器中。创建日志条目的 pod 位于不同的命名空间中但是Fluent Bit从节点上的文件中读取它们。内容是原始JSON加上更精确的时间戳Fluent Bit将其添加到每个日志条目中。
![图 13.5](images/Figure13.5.png)
<center>图13.5 一个非常基本的 Fluent Bit配置仍然可以聚合来自多个pod的日志条目 </center>
在 Fluent Bit 的 DaemonSet spec 中您已经看到了所有内容。我使用单独的命名空间进行日志记录因为您通常希望它作为集群上运行的所有应用程序所使用的共享服务运行而命名空间是隔离所有对象的好方法。运行Fluent Bit pod很简单——复杂之处在于配置日志处理管道我们需要深入研究这一点以充分利用日志模型。图13.6显示了管道的各个阶段以及如何使用它们。
![图 13.6](images/Figure13.6.png)
<center>图 13.6 Fluent Bit的处理管道超级灵活每个阶段都使用插件模块</center>
我们目前正在运行一个简单的配置,有三个阶段:输入阶段读取日志文件解析器阶段分解JSON日志条目输出阶段将每个日志作为单独的行写入Fluent Bit容器中的标准输出流。JSON解析器是所有容器日志的标准并不是很有趣因此我们将重点关注清单13.1中的输入和输出配置。
> 清单 13.1 fluentbit-config.yaml, 一个简单的 Fluent Bit 管道
```
[INPUT]
Name tail # 从文件末尾读取
Tag kube.* # 为标记使用前缀
Path /var/log/containers/timecheck*.log
Parser docker # 解析JSON容器日志
Refresh_Interval 10 # 设置检查文件列表的频率
[OUTPUT]
Name stdout # 写入标准输出
Format json_lines # 将每个日志格式化为一行
Match kube.* # 写入带有kube标记前缀的日志
```
Fluent Bit 使用标记来标识日志条目的来源。标签在输入阶段添加可用于将日志路由到其他阶段。在此配置中日志文件名用作标记前缀为kube。匹配规则将所有带kube标记的条目路由到输出阶段因此每个日志都被打印出来但输入阶段只读取timcheck日志文件因此这些是您看到的唯一日志条目。
您并不是真的想过滤输入文件——这只是一种快速入门的方法不会让您被日志条目淹没。最好是读取所有输入然后根据标记路由日志这样就只存储感兴趣的条目。Fluent Bit内置了对Kubernetes的支持它有一个过滤器可以用元数据丰富日志条目以识别创建它的Pod。过滤器还可以配置为每个包含命名空间和Pod名称的日志构建自定义标记;使用它您可以更改管道以便只有来自test命名空间的日志被写入标准输出。
现在试试吧,更新Fluent Bit ConfigMap以使用Kubernetes过滤器重新启动DaemonSet以应用配置更改然后从timecheck应用程序中打印最新的日志以查看过滤器的功能。
```
# 更新数据管道配置文件:
kubectl apply -f fluentbit/update/fluentbit-config-match.yaml
# 重新启动DaemonSet使一个新的Pod获得更改后的配置:
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging
# 等待新的记录Pod:
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging
# 打印最后一个日志条目:
kubectl logs -l app=fluent-bit -n kiamol-ch13-logging --tail 1
```
您可以从图13.7中的输出中看到更多的数据来自Fluent bit—日志条目是相同的但添加了日志源的详细信息。Kubernetes过滤器从API server 获取所有数据,这为您在分析日志以跟踪问题时提供了真正需要的额外上下文。查看容器的镜像散列可以让您完全确定地检查软件版本。
![图13.7.png](images/Figure13.7.png)
<center>图13.7过滤器丰富了日志条目——单个日志消息现在有14个额外的元数据字段</center>
这个的Fluent Bit配置有点棘手。Kubernetes过滤器可以开箱即用地获取Pod的所有元数据但是为路由构建自定义标记需要一些精细的正则表达式。这都在前面练习中部署的ConfigMap中的配置文件中但我不打算重点讨论它因为我真的不喜欢正则表达式。这也没有必要——设置是完全通用的所以你可以将输入、过滤器和解析器配置插入到你自己的集群中它将适用于你的应用程序无需任何更改。
输出配置将有所不同因为这是您配置目标的方式。在插入日志存储和搜索组件之前我们将研究Fluent Bit的另一个特性——将日志条目路由到不同的输出。输入配置中的正则表达式为kube.namespace.container_name.pod_name格式的条目设置了一个自定义标记可以在匹配中使用该标记根据命名空间或pod名称对日志进行不同的路由。清单13.2显示了具有多个目的地的更新输出配置。
> 清单 13.2 fluentbit-config-match-multiple.yaml, 路由到多个输出
```
[OUTPUT]
Name stdout # 标准的out插件将
Format json_lines # 只打印命名空间为
Match kube.kiamol-ch13-test.* # test的日志条目。
[OUTPUT]
Name counter # 计数器打印来自
Match kube.kiamol-ch13-dev.* # dev名称空间的日志计数。
```
Fluent Bit支持许多输出插件从普通TCP到Postgres和云服务如Azure Log Analytics。到目前为止我们使用的是标准输出流它只是将日志条目中继到控制台。counter插件是一个简单的输出它只打印已经收集了多少日志条目。部署新配置时您将继续看到来自test命名空间的日志行还将看到来自dev命名空间的日志条目计数。
现在试试吧,更新配置以使用多个输出并从Fluent Bit Pod打印日志。
```
# 更新配置并重新启动Fluent Bit:
kubectl apply -f fluentbit/update/fluentbit-config-match-multiple.yaml
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging
# 打印最后两行日志:
kubectl logs -l app=fluent-bit -n kiamol-ch13-logging --tail 2
```
本练习中的计数器并不是特别有用但它可以向您展示管道早期部分的复杂可以使管道后面的路由更容易。图13.8显示了不同命名空间中的日志有不同的输出可以在输出阶段中使用匹配规则进行配置。应该很清楚如何在Kubernetes编写的简单日志文件之上插入复杂的日志系统。Fluent Bit中的数据管道允许您丰富日志条目并将它们路由到不同的输出。如果你想使用的输出不被Fluent Bit支持那么你可以切换到父项目Fluentd它有一个更大的插件集(包括MongoDB和AWS S3)——管道阶段和配置非常相似。我们将使用Elasticsearch进行存储它非常适合进行高性能搜索并且易于与Fluent Bit集成。
![图 13.8](images/Figure13.8.png)
<center>图13.8在Fluent Bit中不同的输出可以重塑数据-计数器只显示一个计数</center>
## 13.3 向 Elasticsearch 发送日志
Elasticsearch 是一个生产级开源数据库。它将数据项作为文档存储在称为索引的集合中。这是一种与关系数据库非常不同的存储模型因为它不支持索引中每个文档的固定模式——每个数据项可以有自己的一组字段。这很适合集中式日志记录其中来自不同系统的日志项将具有不同的字段。Elasticsearch作为一个单独的组件运行带有一个用于插入和查询数据的REST API。一个名为Kibana的配套产品提供了一个非常有用的Elasticsearch查询前端。您可以在与Fluent Bit相同的共享日志命名空间中运行Kubernetes中的两个组件。
现在试试吧部署Elasticsearch和kibana—日志系统的存储和前端组件。
```
# 创建Elasticsearch部署并等待Pod:
kubectl apply -f elasticsearch/
kubectl wait --for=condition=ContainersReady pod -l app=elasticsearch -n kiamol-ch13-logging
# 创建Kibana部署并等待它启动:
kubectl apply -f kibana/
kubectl wait --for=condition=ContainersReady pod -l app=kibana -n kiamol-ch13-logging
# 获取Kibana的URL:
kubectl get svc kibana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:5601' -n kiamol-ch13-logging
```
Elasticsearch 和 Kibana 的基本部署分别使用一个Pod如图13.9所示。日志很重要因此您需要在生产环境中为高可用性建模。Kibana 是一个无状态组件因此可以通过增加副本数量来提高可靠性。Elasticsearch 作为一个 StatefulSet 在多个使用持久存储的pod中工作得很好或者你可以在云中使用托管Elasticsearch服务。当您运行Kibana时您可以浏览到URL。我们将在下一个练习中使用它。
![图 13.9](images/Figure13.9.png)
<center>图 13.9运行Elasticsearch和服务使Kibana和Fluent Bit可以使用REST API</center>
Fluent Bit有一个Elasticsearch输出插件它使用Elasticsearch REST API为每个日志条目创建一个文档。该插件需要配置Elasticsearch服务器的域名您可以选择指定应该在其中创建文档的索引。这允许您使用多个输出阶段从不同索引中的不同命名空间隔离日志条目。清单13.3将日志条目与test命名空间中的Pods和Kubernetes系统Pods分开。
> 清单 13.3 fluentbit-config-elasticsearch.yaml, 将日志存储在Elasticsearch索引中
```
[OUTPUT]
Name es # 来自test 命名空间的日志
Match kube.kiamol-ch13-test.* # 被路由到Elasticsearch
Host elasticsearch # 并在“test”索引中作为文档创建。
Index test
[OUTPUT]
Name es # 系统日志创建在
Match kube.kube-system.* # 同一Elasticsearch服务器
Host elasticsearch # 的“sys”索引中。
```
如果有不匹配任何输出规则的日志项它们将被丢弃。部署这个更新后的配置时Kubernetes系统日志和test命名空间日志保存在Elasticsearch中但不保存来自dev命名空间的日志。
现在试试吧更新Fluent Bit配置以将日志发送到Elasticsearch然后连接到Kibana并在测试索引上设置搜索。
```
# 部署清单13.3中的更新配置
kubectl apply -f fluentbit/update/fluentbit-config-elasticsearch.yaml
# 更新Fluent Bit并等待它重新启动
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging
# 现在浏览到Kibana并设置搜索:
# - 单击左侧导航面板上的“发现”
# - 创建一个新的索引模式
# - 输入“test”作为索引模式
# - 在下一步中,选择@timestamp作为时间筛选字段
# - 单击创建索引模式
# - 再次单击左侧导航面板上的Discover查看日志
```
这个过程包含一些手动步骤因为Kibana不是一个可以自动化的好产品。图13.10中的输出显示了正在创建的索引模式。当您完成该练习时您将拥有一个强大、快速且易于使用的搜索引擎用于测试命名空间中的所有容器日志。Kibana中的Discover选项卡显示了随时间存储的文档的速率(即日志处理的速率),您可以向下钻取每个文档以查看日志详细信息。
![图 13.10](images/Figure13.10.png)
<center>图13.10 设置Fluent Bit将日志发送到Elasticsearch和Kibana以搜索测试索引</center>
Elasticsearch 和 Kibana 都是成熟的技术但如果你不熟悉它们现在是了解Kibana UI的好时机。您将在 Discover 页面的左侧看到一个字段列表您可以使用它来过滤日志。这些字段包含所有Kubernetes元数据因此您可以根据Pod名称、主机节点、容器镜像等进行过滤。您可以构建显示按应用程序划分的日志的标题统计信息的仪表板这对于显示错误日志的突然激增非常有用。您还可以在所有文档中搜索特定的值当用户从错误消息中提供ID时这是查找应用程序日志的好方法。
我不会在Kibana上花太多时间但是再做一个练习就会展示集中式日志记录系统是多么有用。我们将把一个新的应用程序部署到test命名空间中它的日志将自动由Fluent Bit提取并流到Elasticsearch而不需要对配置进行任何更改。当应用程序向用户显示错误时我们可以很容易地在Kibana中追踪到它。
现在试试吧部署我们以前使用过的随机数API(在第一次使用后崩溃的API)以及缓存响应并几乎修复问题的代理。尝试API当您得到一个错误时您可以在Kibana中搜索失败ID。
```
# 部署API和代理:
kubectl apply -f numbers/
# 等待应用程序启动:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api -n kiamol-ch13-test
# 通过代理获取使用API的URL:
kubectl get svc numbers-api-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/rng' -n kiamol-ch13-test
# 浏览到API等待30秒然后刷新直到出现错误
# browse到Kibana并在搜索栏中输入这个查询:
# kubernetes.labels.app:numbers-api AND log:<failure-ID-from-the-API>
```
我在图13.11中的输出很小,但您可以看到发生了什么:我从API获得了一个失败ID并将其粘贴到Kibana的搜索栏中该搜索栏返回一个匹配项。日志条目中包含了我需要调查Pod所需的所有信息。Kibana还有一个有用的选项可以在匹配前后显示文档我可以使用它来显示围绕失败日志的日志条目。
![图 13.11](images/Figure13.11.png)
<center>图 13.11 日志系统在操作中跟踪用户面对的错误消息中的失败</center>
可搜索的集中式日志记录消除了故障排除中的许多摩擦而且这些组件都是开源的因此您可以在每个环境中运行相同的日志记录堆栈。在开发和测试环境中使用与在生产环境中使用的相同的诊断工具可以帮助产品团队理解有用的日志级别并提高系统日志的质量。高质量的日志记录很重要但它在产品待办事项列表中很少排名靠前所以在一些应用中你会遇到一些用处不大的日志。Fluent Bit还有一些额外的特性可以提供帮助。
## 13.4 解析和过滤日志条目
理想的应用程序应该生成结构化的日志数据其中包含条目的严重程度字段和写入输出的类的名称以及事件类型的ID和事件的关键数据项。您可以在Fluent Bit管道中使用这些字段的值来过滤消息这些字段将在Elasticsearch中显示以便您可以构建更精确的查询。大多数系统不会产生这样的日志——它们只是发出文本——但如果文本使用已知的格式那么Fluent Bit可以在它通过管道时将其解析为字段。
随机数API就是一个简单的例子。日志条目是如下所示的文本行:<6>Microsoft.Hosting.Lifetime[0] 现在监听http://[::]:80.尖括号内的第一部分是消息的优先级然后是类的名称和方括号内的事件ID然后是日志的实际内容。每个日志条目的格式都是相同的因此Fluent Bit解析器可以将日志分解为单独的字段。为此必须使用正则表达式清单13.4展示了我所做的最大努力,它只提取优先级字段,而将其他所有内容留在消息字段中。
> 清单 13.4 fluentbit-config-parser.yaml, 应用程序日志的自定义解析器
```
[PARSER]
Name dotnet-syslog # 解析器的名称
Format regex # 用正则表达式解析
Regex ^\<(?<priority>[0-9]+)\>*(?<message>.*)$ # 令人反感的
```
部署此配置时Fluent Bit将有一个新的自定义解析器dotnet-syslog可供使用但它不会将其应用于任何日志。管道需要知道哪些日志条目应该使用自定义解析器而Fluent Bit允许您在Pods中使用注释进行设置。它们就像提示一样告诉管道将命名解析器应用于源自此Pod的任何日志。清单13.5显示了随机数API pod的解析器注释——就是这么简单。
> 清单 13.5 api-with-parser.yaml, 带有自定义Fluent Bit解析器的Pod规范
```
# 这是部署规范中的Pod模板.
template:
metadata: # 标签用于选择器
labels: # 和操作;
app: numbers-api # 注释通常用于集成标志。
annotations:
fluentbit.io/parser: dotnet-syslog # 为Pod日志使用解析器
```
解析器可以比我的自定义解析器有效得多Fluent Bit团队在他们的文档中有一些示例解析器包括一个用于Nginx的解析器。我使用Nginx作为随机数API的代理在下一个练习中我们将为每个带有注释的组件添加解析器并了解结构化日志如何在Kibana中实现更有针对性的搜索和过滤。
现在试试吧,更新Fluent Bit配置为随机数应用程序和Nginx代理添加解析器然后更新这些部署添加指定解析器的注释。试试这款应用在Kibana上查看日志。
```
# 更新管道配置:
kubectl apply -f fluentbit/update/fluentbit-config-parser.yaml
# 重启Fluent Bit:
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging
kubectl wait --for=condition=ContainersReady pod -l app=fluent-bit -n kiamol-ch13-logging
# 更新应用部署,添加解析器注释:
kubectl apply -f numbers/update/
# 等待API准备就绪:
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api -n kiamol-ch13-test
# 再次使用API并浏览到Kibana查看日志
```
在图13.12中可以看到Kibana可以对解析器中的提升字段进行筛选而不需要构建自己的查询。在我的屏幕截图中我过滤了来自一个Pod的日志其优先级值为4(这是一个警告级别)。当你自己运行这个程序时你会看到你还可以过滤API代理Pod。日志条目包括HTTP请求路径和响应代码字段都是从Nginx文本日志中解析出来的。
![图 13.12](images/Figure13.12.png)
<center>图 13.12日志中解析的字段被索引,因此过滤器和搜索更快更简单</center>
使用 Fluent Bit的集中式日志记录系统的最后一个好处是:数据处理管道独立于应用程序它是应用过滤的一个更好的地方。这个神话般的理想应用程序将能够动态地增加或减少日志级别而不需要重新启动应用程序。然而从第4章中你知道许多应用程序需要重新启动Pod来获取最新的配置更改。当你在对一个实时问题进行故障排除时这并不好因为这意味着如果你需要提高日志级别就需要重新启动受影响的应用程序。
Fluent Bit本身不支持实时配置重加载但是重新启动日志采集器Pods比重新启动应用程序Pods具有更小的侵入性并且Fluent Bit将从中断的地方开始因此您不会错过任何日志条目。使用这种方法您可以在应用程序中以更详细的级别记录日志并在Fluent Bit管道中进行筛选。清单13.6显示了一个过滤器它只在优先级字段的值为2、3或4时才包含来自随机数API的日志——它过滤掉了优先级较低的条目。
> 清单 13.6 fluentbit-config-grep.yaml, 根据字段值过滤日志
```
[FILTER]
Name grep # Grep是一个搜索过滤器。
Match kube.kiamol-ch13-test.api.numbers-api*
Regex priority [234] # 连我都能处理这个正则表达式
```
这里有更多的正则表达式争论但是您可以看出为什么将文本日志条目分割为管道可以访问的字段很重要。grep过滤器可以通过计算字段上的正则表达式来包含或排除日志。当您部署这个更新的配置时API可以愉快地在第6级写入日志条目但是它们会被Fluent Bit删除只有更重要的条目才会进入Elasticsearch。
现在试试吧部署更新后的配置以便只保存来自随机数API的高优先级日志。删除API Pod在Kibana中你将看不到任何启动日志条目但它们仍然存在于Pod日志中。
```
# 应用清单13.6中的grep筛选器:
kubectl apply -f fluentbit/update/fluentbit-config-grep.yaml
kubectl rollout restart ds/fluent-bit -n kiamol-ch13-logging
# 删除旧的API pod这样我们就得到了一组新的日志:
kubectl delete pods -n kiamol-ch13-test -l app=numbers-api
kubectl wait --for=condition=ContainersReady pod -l app=numbers-api -n kiamol-ch13-test
# 使用API并刷新直到看到失败
# 从 pod里打印日志:
kubectl logs -n kiamol-ch13-test -l app=numbers-api
# 现在浏览到Kibana并过滤显示API Pod日志
```
本练习向您展示了Fluent Bit如何有效地过滤日志只将您关心的日志项转发到目标输出。它还显示较低级别的日志记录并没有消失——使用kubectl可以查看原始容器日志。
只是后续的日志处理阻止了他们使用Elasticsearch。在实际的故障排除场景中您可以使用Kibana来识别导致问题的Pod然后使用kubectl进行深入分析如图13.13所示。
![图 13.13](images/Figure13.13.png)
<center>图 13.13在Fluent Bit中过滤日志条目可节省存储空间您可以轻松更改过滤器</center>
除了这些简单的管道还有很多关于Fluent Bit的内容:您可以修改日志内容、限制传入日志的速率,甚至可以运行由日志条目触发的自定义脚本。但是我们已经介绍了您可能需要的所有主要特性,我们将通过比较收集-转发日志模型和其他选项来结束本文。
## 13.5 了解 Kubernetes 中的日志记录选项
Kubernetes 期望您的应用程序日志将来自容器的标准输出流。它收集并存储来自这些流的所有内容这为我们在本章中介绍的日志模型提供了动力。这是一种通用而灵活的方法我们使用的技术堆栈是可靠的和高性能的但在整个过程中存在效率低下的问题。图13.14显示了将日志从容器中获取到可搜索存储器中的一些问题。
![图 13.14](images/Figure13.14.png)
<center>图 13.14目标是获取应用程序日志到Elasticsearch中但是需要很多步骤才能实现</center>
您可以使用更简单、移动部件更少的替代架构。您可以从应用程序代码直接将日志写入Elasticsearch或者您可以在每个应用程序Pod中运行一个sidecar从应用程序使用的任何日志sink中读取并将条目推入Elasticsearch。这将使您对存储的日志数据有更多的控制而无需借助正则表达式来解析文本字符串。这样做将您绑定到Elasticsearch(或您使用的任何存储系统),但如果该系统提供了您所需要的一切,那么这可能不是一个大问题。
对于您在Kubernetes上运行的第一个应用程序来说自定义日志框架可能很有吸引力但是当您将更多的工作负载转移到集群时它就会限制您。要求应用程序直接记录到Elasticsearch不适合写入操作系统日志的现有应用程序而且您很快就会发现您的日志sidecar不够灵活需要针对每个新应用程序进行调整。Fluentd/Fluent Bit模型的优势在于它是一种标准方法背后有一个社区;处理正则表达式比编写和维护自己的日志收集和转发代码要简单得多。
这就是应用程序日志的全部内容,因此我们可以清理集群,为实验室做好准备。
现在试试吧,删除本章的命名空间和剩余的Deployment。
```
kubectl delete ns -l kiamol=ch13
kubectl delete all -l kiamol=ch13
```
## 13.6 实验室
在本实验中您将扮演一个操作员的角色需要将一个新的应用程序部署到使用本章中的日志记录模型的集群中。您需要检查Fluent Bit配置以找到您应该为应用程序使用的命名空间然后部署我们之前在书中使用的简单版本网站。以下是这个实验室的部分:
- 首先在lab/logging文件夹中部署日志组件。
- 将应用程序从vweb文件夹部署到正确的命名空间以便收集日志并验证您可以在Kibana中看到日志。
- 您将看到日志是纯文本因此下一步是更新部署以使用正确的解析器。应用程序运行在Nginx上并且在Fluent Bit配置中已经为你设置了Nginx解析器。
- 当您在Kibana中确认新日志时您将看到几个状态代码为304的日志这告诉浏览器使用页面的缓存版本。这些日志并不有趣因此最后的任务是更新Fluent Bit配置以过滤掉它们。
这是一个非常现实的任务你需要在Kubernetes周围导航的所有基本技能来找到和更新所有的碎片。我的解决方案在GitHub上的常见地方你可以检查:https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch13/lab/README.md。

View File

@ -0,0 +1,476 @@
# 第十四章 使用 Prometheus 监控应用程序和 Kubernetes
监控是日志的伙伴:监视系统告诉您某些地方出了问题然后您可以深入日志以找出详细信息。与日志记录一样您希望有一个集中的系统来收集和可视化关于所有应用程序组件的指标。Kubernetes中已建立的监控方法使用另一个CNCF项目:Prometheus这是一个收集和存储指标的服务器应用程序。在本章中您将学习如何在Kubernetes中部署一个共享监控系统使用 dashboard 面板显示单个应用程序和整个集群的健康状况。
Prometheus 在许多平台上运行但它特别适合Kubernetes。您可以在一个Pod中运行Prometheus该Pod可以访问Kubernetes API服务器然后Prometheus查询API以找到它需要监视的所有目标。
当你部署新的应用程序时你不需要做任何设置更改——prometheus会自动发现它们并开始收集指标。Kubernetes的应用程序也特别适合Prometheus。在本章中你将看到如何很好地利用sidecar模式因此每个应用程序都可以为Prometheus提供一些指标即使应用程序本身还没有准备好。
## 14.1 Prometheus 如何监控 Kubernetes 的工作负载
Prometheus 中的度量完全是通用的:您想要监视的每个组件都有一个HTTP端点该端点返回对该组件重要的所有值。web服务器包含它所服务的请求数量的指标Kubernetes节点包含可用内存数量的指标。Prometheus并不关心度量标准中的内容;它只存储组件返回的所有内容。对普罗米修斯来说,重要的是它需要收集的目标列表。
图14.1显示了如何使用Prometheus的内置服务发现在Kubernetes中工作。
![图14.1](./images/Figure14.1.png)
<center>图14.1普罗米修斯使用拉模型收集指标,自动找到目标</center>
本章的重点是让 Prometheus 与 Kubernetes 很好地合作为您提供一个动态监控系统当您的集群扩展到更多的节点运行更多的应用程序时该系统仍能保持工作。我不会详细介绍如何在应用程序中添加监控或者应该记录哪些指标电子书的附录B是《Learn Docker in a Month of lunch》中的“添加可观察性与容器化监控”一章它将为您提供额外的细节。
我们将从启动普罗米修斯开始。Prometheus服务器是一个单独的组件负责服务发现、指标收集和存储它有一个基本的web UI您可以使用它来检查系统的状态并运行简单的查询。
现在试试吧将Prometheus部署在专用的监视命名空间中配置为在 test 命名空间中查找应用程序(test 命名空间还不存在)。
```
# 切换到章节目录:
cd ch14
# 创建 Prometheus Deployment and ConfigMap:
kubectl apply -f prometheus/
# 等待 Prometheus to start:
kubectl wait --for=condition=ContainersReady pod -l app=prometheus -n kiamol-ch14-monitoring
# 获取 web UI 地址:
kubectl get svc prometheus -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:9090' -n kiamol-ch14-monitoring
# 访问 UI, 查看 /targets page
```
Prometheus 称 metrics 收集为 scraping。当您访问 Prometheus UI 时,您将看到没有抓取目标,尽管有一个名为 testpods 的类别它列出了零目标。图14.2显示了我的输出。test-pods 的名称来自您在ConfigMap中部署的Prometheus配置Pod从中读取该配置。
![图14.2](./images/Figure14.2.png)
<center>图14.2 目前还没有目标但Prometheus将继续检查Kubernetes API以寻找新的pod </center>
配置 Prometheus 以在 Kubernetes 中寻找目标是相当简单的尽管术语一开始令人困惑。Prometheus 使用作业来定义一组相关的目标,这些目标可以是应用程序的多个组件。抓取配置可以简单到一个静态域名列表(Prometheus轮询该列表以获取指标)也可以使用动态服务发现。清单14.1 向 Prometheus 展示了test-pods作业配置的开头它使用Kubernetes API进行服务发现。
> 清单 14.1 prometheus-config.yaml, 使用Kubernetes scrape 配置
```
scrape_configs: # 这是 configmap 中的 yaml.
- job_name: 'test-pods' # 用于 test apps
kubernetes_sd_configs: # 从 kubernetes API 查找目标
- role: pod # 搜寻 Pods
relabel_configs: # 应用这些过滤规则
- source_labels:
- __meta_kubernetes_namespace
action: keep # 只包含本章测试命名空间的 pods
regex: kiamol-ch14-test
```
需要解释的是 relabel_configs 部分。Prometheus使用标签存储度量标签是标识源系统和其他相关信息的键值对。您将在查询中使用标签来选择或聚合指标还可以在将指标存储到Prometheus之前使用它们来过滤或修改指标。这是重新标签从概念上讲它类似于Fluent bit中的数据管道——您有机会丢弃不想要的数据并重新塑造您想要的数据。正则表达式在 Prometheus 中也出现了不必要的复杂问题,但很少需要进行更改。你在重新标签阶段设置的管道应该足够通用,适用于所有应用程序。配置文件中的全管道应用如下规则:
- 只包含命名空间 kiamol-ch14-test 中的Pods。
- 使用 Pod 名称作为Prometheus实例标签的值。
- 使用 Pod 元数据中的app标签作为Prometheus作业标签的值。
- 在 Pod 元数据中使用可选注解配置抓取目标。
这种方法是由约定驱动的——只要您的应用程序被建模以适应规则它们就会自动被选为监视目标。Prometheus使用规则来查找匹配的Pods对于每个目标它通过向/metrics路径发出HTTP GET请求来收集指标。Prometheus需要知道使用哪个网络端口因此Pod规范需要显式地包括容器端口。这是一个很好的实践因为它有助于记录应用程序的设置。让我们将一个简单的应用程序部署到test命名空间看看Prometheus用它做了什么。
现在试试吧,将时间检查应用程序部署到测试命名空间。该规格匹配所有的普罗米修斯 scrape 规则所以新的Pod应该被找到并添加为 scrape目标。
```
# 创建 test namespace 以及 timecheck Deployment:
kubectl apply -f timecheck/
# 等待 app 启动:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch14-test
# 刷新Prometheus界面中的 target 列表,并确认
# timecheck Pod然后浏览到/graph页面选择
# timecheck_total然后单击“执行”
```
我的输出如图14.3所示其中我打开了两个浏览器窗口以便您可以看到部署应用程序时发生了什么。普罗米修斯看到时间检查Pod被创建它符合重新标记阶段的所有规则所以它被添加为目标。普罗米修斯配置设置为每30秒检查一次目标。时间检查应用程序有一个/metrics端点它返回它写了多少时间检查日志的计数。当我在Prometheus中查询该指标时应用程序已经写入了22个日志条目。
![图14.3](./images/Figure14.3.png)
<center>图14.3 将应用程序部署到测试命名空间- prometheus找到它并开始收集指标. </center>
这里您应该认识到两个重要的事情:应用程序本身需要提供度量因为Prometheus只是一个收集器而那些度量代表应用程序实例的活动。时间检查应用程序不是一个web应用程序——它只是一个后台进程——所以没有服务将流量导向它。普罗米修斯在查询Kubernetes API时获得Pod的IP地址并直接向Pod发出HTTP请求。您也可以配置Prometheus来查询Services但是这样您就会得到一个目标它是跨多个Pod的负载均衡器并且您希望Prometheus独立地抓取每个Pod。
你可以使用Prometheus中的指标来增强仪表板显示应用程序的整体健康状况你可以汇总所有pod来获得标题值。您还需要能够向下钻取以查看pod之间是否有差异。这将帮助您确定某些实例是否执行不良并将反馈到您的运行状况检查中。我们可以放大时间检查应用程序看看在单个Pod级别收集的重要性。
现在试试吧添加另一个副本到时间检查应用程序。这是一个新的Pod符合普罗米修斯规则所以它将被发现并添加为另一个 scrape 目标。
```
# 缩放部署以添加另一个Pod:
kubectl scale deploy/timecheck --replicas 2 -n kiamol-ch14-test
# 等待新的 pod 启动:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck -n kiamol-ch14-test
# 回到普罗米修斯,检查 target 列表,在 graph 页面,
# 执行timecheck_total和dotnet_total_memory_bytes的查询
```
在这个练习中,你会看到普罗米修斯发现了新的 pod并开始采集它。两个Pod记录相同的指标Pod名称被设置为每个指标上的标签。对timecheck_total指标的查询现在返回两个结果——每个Pod一个结果——在图14.4中可以看到一个Pod比另一个Pod完成了更多的工作。
![图14.4](./images/Figure14.4.png)
<center>图14.4 每个实例都记录自己的指标因此您需要从每个Pod收集数据。 </center>
时间检查计数器是在应用程序代码中显式捕获的度量。大多数语言都有一个Prometheus客户端库您可以将其插入到您的构建中。这些库允许您像这样捕获特定于应用程序的细节它们还收集有关应用程序运行时的一般信息。这是一个.net应用程序Prometheus客户端库记录运行时细节比如正在使用的内存和CPU数量以及正在运行的线程数量。在下一节中我们将运行一个分布式应用程序其中每个组件都公开Prometheus指标我们将看到当应用程序仪表板包括运行时性能和应用程序详细信息时它是多么有用。
## 14.2 监视使用 Prometheus 客户端库构建的应用程序
电子书的附录B介绍了如何向一个应用程序添加指标该应用程序显示了来自NASA“每日天文照片”(APOD)服务的图片。该应用程序的组件在Java、Go和Node.js中它们都使用Prometheus客户端库来公开运行时和应用程序指标。本章包括部署到test 命名空间的应用程序的Kubernetes清单因此所有应用程序Pods将被Prometheus发现。
现在试试吧将APOD应用程序部署到测试命名空间并确认应用程序的三个组件被添加为Prometheus目标。
```
# 部署 app:
kubectl apply -f apod/
# 等待启动成功:
kubectl wait --for=condition=ContainersReady pod -l app=apod-api -n kiamol-ch14-test
# 获取 app URL:
kubectl get svc apod-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8014' -n kiamol-ch14-test
# 访问 app, 然后刷新 Prometheus targets 界面
```
你可以在图 14.5 中看到我的输出其中有一个非常令人愉快的图像叫做林德斯暗星云1251。应用程序如预期的那样运行普罗米修斯已经发现了所有新的pod。在部署应用程序的30秒内你应该会看到所有新目标的状态都是正常的这意味着普罗米修斯已经成功地抓取了它们。
![图14.5](./images/Figure14.5.png)
<center>图14.5 APOD组件都有服务但它们仍然在Pod级别被抓取</center>
在这个练习中我还有两件重要的事情要指出。首先Pod规范都包括一个容器端口它说明应用程序容器正在监听端口80这就是Prometheus找到要抓取的目标的方法。web UI的Service实际上监听端口8014但是Prometheus直接访问Pod端口。其次API目标没有使用标准/度量路径因为Java客户机库使用不同的路径。我在Pod规范中使用了注释来说明正确的路径。
基于约定的发现很棒因为它消除了大量重复的配置和潜在的错误但并不是每个应用都符合约定。我们在普罗米修斯中使用的重标签管道为我们提供了一个很好的平衡。默认值适用于任何符合约定的应用程序但任何不符合约定的应用程序都可以用注释覆盖默认值。清单14.2显示了如何配置覆盖以设置度量的路径。
> 清单 14.2 prometheus-config.yaml, 使用 annotations 覆盖默认值
```
- source_labels: # 这个是 test-pods job 中的 relabel 配置.
- __meta_kubernetes_pod_annotationpresent_prometheus_io_path
- __meta_kubernetes_pod_annotation_prometheus_io_path
regex: true;(.*) # 如果 pod 具有一个名为 prometheus.io/path 的 annotation
target_label: __metrics_path__ #从 annotation 中获取设置 target path
```
这远没有看起来那么复杂。规则是这样的:如果 pod 有一个叫 prometheus.io/path 注释然后就使用该注释的值作为度量路径。Prometheus使用标签来完成这一切因此每个Pod注释都成为一个名称为meta_kubernetes_pod_annotation_<annotation-name>的标签并且有一个附带的标签名为meta_kubernetes_pod_annotationpresent_<annotation-name>您可以使用它来检查注释是否存在。任何使用自定义指标路径的应用程序都需要添加注释。清单14.3显示了APOD API。
> Listing 14.3 api.yaml, path annotation
```
template: # 这是 deployment 的 pod spec 配置
metadata:
labels:
app: apod-api # 在 prometheus 中用作 job 标签
annotations:
prometheus.io/path: "/actuator/prometheus" # 设置 metrics path
```
复杂性集中在 Prometheus 配置中应用程序清单非常容易指定覆盖。当您多使用一些重新标记规则时它们就不那么复杂了并且通常遵循完全相同的模式。完整的Prometheus配置包括类似的规则应用程序可以覆盖指标端口并选择完全退出抓取。
当你在读这篇文章的时候普罗米修斯一直在忙着抓取时间支票和APOD应用程序。看看Prometheus UI的Graph页面上的指标可以看到大约有200个指标正在被收集。UI非常适合运行查询并快速查看结果但你不能用它来构建一个仪表板在一个屏幕上显示应用程序的所有关键指标。为此您可以使用Grafana它是容器生态系统中的另一个开源项目由Prometheus团队推荐。
现在试试吧使用ConfigMaps部署Grafana它建立了与Prometheus的连接并包括APOD应用程序的仪表板。
```
# 在监控命名空间中部署Grafana:
kubectl apply -f grafana/
# 等待启动:
kubectl wait --for=condition=ContainersReady pod -l app=grafana -n
kiamol-ch14-monitoring
# 获取 dashboard url:
kubectl get svc grafana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000/d/kb5nhJAZk' -n kiamol-ch14-monitoring
# 浏览到URL;使用用户名kiamol和密码kiamol登录
```
图14.6所示的仪表板很小但它让您了解了如何将原始指标转换为系统活动的信息视图。仪表板由Prometheus查询驱动Grafana在后台运行该查询。每个组件都有一行其中包括运行时指标(处理器和内存使用)和应用程序指标(http请求和缓存使用)的混合。
![图14.6](./images/Figure14.6.png)
<center>图14.6应用程序仪表板提供了对性能的快速洞察。这些图表都是由Prometheus metrics提供的</center>
像这样的仪表板将是贯穿整个组织的共同努力。支持团队将设置他们需要看到的需求应用程序开发和运营团队确保应用程序捕获数据并在仪表板上显示它。就像我们在第13章中看到的日志系统一样这是一个由轻量级开源组件构建的解决方案因此开发人员可以在他们的笔记本电脑上运行与在生产环境中运行的相同的监控系统。这有助于在开发和测试中进行性能测试和调试。
使用Prometheus转移到集中监视将需要开发工作但是它可以是一个增量过程您从基本的度量开始并随着团队开始提出更多的需求而添加它们。我在本章的待办事项列表应用程序中添加了对普罗米修斯的支持这大约花了十几行代码。在Grafana中有一个简单的应用程序仪表板所以当你部署应用程序时你将能够看到一个仪表板的起点它将在未来的版本中得到改进。
现在试试吧,运行启用指标的待办事项列表应用程序并使用该应用程序生成一些指标。在Grafana中已经有一个仪表盘来可视化指标。
```
# 部署 app:
kubectl apply -f todo-list/
# 等待启动:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web -n
kiamol-ch14-test
# 访问应用,插入待办项
# 然后在 windows 中运行脚本:
.\loadgen.ps1
# 或者 macOS/Linux:
chmod +x ./loadgen.sh && ./loadgen.sh
# 获取新的 dashboard 地址:
kubectl get svc grafana -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000/d/Eh0VF3iGz' -n kiamol-ch14-monitoring
# 浏览 dashboard
```
仪表板上没有太多东西但它比没有仪表板的信息要多得多。它告诉你应用程序在容器内使用了多少CPU和内存创建任务的速率以及HTTP请求的平均响应时间。您可以在图14.7中看到我的输出,其中我添加了一些任务,并使用负载生成脚本发送了一些流量。
![图14.7](./images/Figure14.7.png)
<center>图14.7 一个由 Prometheus 客户端库和几行代码驱动的简单仪表板 </center>
所有这些指标都来自待办事项应用程序Pod。在这个版本中应用程序还有另外两个组件:一个用于存储的Postgres数据库和一个Nginx代理。这两个组件都没有对Prometheus的本地支持因此它们被排除在目标列表之外。否则普罗米修斯将继续尝试着获取度量标准并失败。对应用程序进行建模的人员的工作是了解一个组件不公开指标并指定应该排除它。清单14.4显示了使用一个简单注释完成的操作。
> 清单 14.4 proxy.yaml, 一个 pod spec 排除 监控
```
template: # 这是 deployment pod spec 配置
metadata:
labels:
app: todo-proxy
annotations: # 排除 target
prometheus.io/scrape: "false"
```
组件不需要对Prometheus提供本地支持并提供自己的度量端点以包含在监视系统中。Prometheus有自己的生态系统——除了可以用来向自己的应用程序添加指标的客户端库之外一整套 exporter 可以为第三方应用程序提取和发布指标。我们可以使用 exporter 为代理和数据库组件添加缺少的指标。
## 14.3 通过 metrics exporters 来监控第三方应用
大多数应用程序都以某种方式记录指标但较老的应用程序不会以Prometheus格式收集和公开它们。Exporters 是独立的应用程序了解目标应用程序如何进行监视并可以将这些指标转换为Prometheus格式。Kubernetes提供了完美的方式来运行一个exporter 与应用程序的每个实例使用 sidecar 容器。这就是我们在第7章中介绍的适配器模式。
Nginx和Postgres都有可用的 exporters 我们可以作为sidecars来运行以改善待办应用程序的监控仪表板。Nginx exporter 从Nginx服务器上的状态页面读取数据并将数据转换为Prometheus格式。记住Pod中的所有容器都共享网络命名空间因此exporter 容器可以在本地主机地址访问Nginx容器。exporter 为自定义端口上的指标提供了自己的HTTP端点因此完整的Pod spec 包括sidecar容器和指定指标端口的注释。清单14.5显示了关键部分。
> 清单 14.5 proxy-with-exporter.yaml, 添加 metrics exporter 容器
```
template: # Deployment 中的 pod spec
metadata:
labels:
app: todo-proxy
annotations:
prometheus.io/port: "9113" # 指定 metrics port
spec:
containers:
- name: nginx
# ... nginx spec 没有变化
- name: exporter # exporter 作为 sidecar 运行.
image: nginx/nginx-prometheus-exporter:0.8.0
ports:
- name: metrics
containerPort: 9113 # 指定 metrics port
args: # 从 ngins load metrics
- -nginx.scrape-uri=http://localhost/stub_status
```
排除 scrape 已经被移除所以当你部署这个更新普罗米修斯将scrape 端口9113上的Nginx Pod在那里exporter 正在监听。所有的Nginx指标将由Prometheus存储Grafana仪表板可以更新为代理添加一行。在本章中我们不打算讨论Prometheus查询语言(PromQL)或构建Grafana仪表板——仪表板可以从JSON文件导入并且有一个更新的仪表板可以部署。
现在试试吧,更新 proxy deployment 以添加 sidecar并将更新后的仪表板加载到Grafana ConfigMap中。
```
# 添加 proxy sidecar:
kubectl apply -f todo-list/update/proxy-with-exporter.yaml
# 等待启动:
kubectl wait --for=condition=ContainersReady pod -l app=todo-proxy -n
kiamol-ch14-test
# 输出 exporter 日志:
kubectl logs -l app=todo-proxy -n kiamol-ch14-test -c exporter
# 更新 app dashboard:
kubectl apply -f grafana/update/grafana-dashboard-todo-list-v2.yaml
# 重启 Grafana load 新 dashboard:
kubectl rollout restart deploy grafana -n kiamol-ch14-monitoring
# 刷新 dashboard
```
Nginx exporter 没有提供大量的信息但基本的细节都在那里。你可以在图14.8中看到我们得到了HTTP请求的数量以及Nginx如何处理连接请求的底层分解。即使使用这个简单的仪表板你也可以看到Nginx正在处理的流量和web应用程序正在处理的流量之间的相关性这表明代理没有缓存响应而是对每个请求都调用web应用程序。
![图14.8](./images/Figure14.8.png)
<center>图14.8使用导出器收集代理指标为仪表板添加了另一层细节. </center>
如果能从Nginx得到更多的信息就好了——比如响应中HTTP状态码的分解——但是 exporter 只能从源系统中转可用的信息这对Nginx来说并不多。其他 exporter 提供更多细节但您需要集中您的仪表板以便显示关键指标。超过12个左右的可视化和仪表板变得势不可挡而且如果它不能一眼传达有用的信息那么它就没有做得很好。
还有一个组件要添加到待办事项列表仪表板中:Postgres数据库。Postgres将各种有用的信息存储在数据库中的表和函数中exporter 运行查询来支持其metrics端点。Postgres exporter 的设置遵循我们在Nginx中看到的相同模式。在这种情况下sidecar被配置为访问本地主机上的Postgres使用与Postgres容器用于admin密码相同的Kubernetes Secret。我们将对应用程序仪表板进行最后的更新以显示来自 exporter 的关键数据库指标。
现在试试,更新数据库部署规范添加Postgres exporter 作为 sidecar 容器。然后用更新待办事项列表仪表板以显示数据库性能。
```
# 添加 Postgres exporter sidecar:
kubectl apply -f todo-list/update/db-with-exporter.yaml
# 等待 pod 启动:
kubectl wait --for=condition=ContainersReady pod -l app=todo-db -n
kiamol-ch14-test
# 输出 exporter 日志:
kubectl logs -l app=todo-db -n kiamol-ch14-test -c exporter
# 更新 dashboard 重启 Grafana:
kubectl apply -f grafana/update/grafana-dashboard-todo-list-v3.yaml
kubectl rollout restart deploy grafana -n kiamol-ch14-monitoring
```
在图14.9中,我缩小并向下滚动,这样您就可以看到新的可视化效果,但是在全屏模式下,整个仪表板都是赏心悦目的。一个页面显示了代理的流量,应用程序的工作强度,用户实际在做什么,以及数据库内部发生了什么。您可以在自己的应用程序中使用客户端库和 exporter 获得相同级别的细节,而这只需要几天的努力。
![图14.9](./images/Figure14.9.png)
<center> 图 14.9 数据库 exporter 记录有关数据活动的度量,这将向仪表板添加详细信息. </center>
exporter 在那里为没有普罗米修斯支持的应用程序添加指标。如果您的目标是将一组现有的应用程序转移到Kubernetes上那么您可能没有一个奢侈的开发团队来添加自定义指标。对于这些应用程序您可以使用Prometheus blackbox exporter这是一种极端的方法即有监控总比没有好。
blackbox exporter 可以在sidecar中运行向应用程序容器发出TCP或HTTP请求并提供一个基本的度量端点来说明应用程序是否启动。这种方法类似于在Pod规范中添加容器探测除了 blackbox exporter 仅供参考。如果应用程序不适合Kubernetes 的自我修复机制你可以运行一个仪表板来显示应用程序的状态比如我们在本书中使用的随机数API。
现在试试吧,使用 blackbox exporter 和最简单的Grafana仪表板部署随机数API。您可以通过重复使用API来破坏它然后重置它使其重新工作仪表板跟踪状态。
```
# 部署 API 程序到测试命名空间:
kubectl apply -f numbers/
# 添加新的 dashboard 到 grafana:
kubectl apply -f grafana/update/numbers-api/
# 获得 API URL:
kubectl get svc numbers-api -o jsonpath='#app - http://{.status.loadBalancer.ingress[0].*}:8016/rng' -n kiamol-ch14-test
# 通过访问/rng URL来使用API
# 它将在三次调用后中断;
# 然后访问 /reset 来修复它
# 获取仪表盘的URL并在Grafana中加载它:
kubectl get svc grafana -o jsonpath='# dashboard - http://{.status
.loadBalancer.ingress[0].*}:3000/d/Tb6isdMMk' -n kiamol-ch14-
monitoring
```
随机数API不支持Prometheus但是运行 blackbox exporter 作为sidecar容器可以基本了解应用程序状态。图14.10显示了一个大部分为空的仪表板,但这两个可视化显示了应用程序是否健康,以及应用程序在不健康和被重置之间切换时状态的历史趋势。
![图14.10](./images/Figure14.10.png)
<center>图14.10 即使是一个简单的仪表盘也是有用的。这显示了API的当前和历史状态。 </center>
随机数API的Pod规范遵循与待办应用中的Nginx和Postgres类似的模式: blackbox exporter 被配置为一个额外的容器并指定暴露指标的端口。Pod 注释自定义度量URL的路径因此当Prometheus从sidecar中抓取度量时它调用 blackbox exporter 该导出器检查API是否响应HTTP请求。
现在我们有三个不同的应用程序的仪表板,它们有不同的细节级别,因为应用程序组件与它们收集的数据不一致。但是所有的组件都有一个共同点:它们都在 kubernetes 容器中运行.在下一节中您将学习如何通过配置Prometheus从集群本身收集平台指标。
## 14.4 监控容器以及 kubernetes 对象
Prometheus与Kubernetes集成用于服务发现但它不从API收集任何指标。你可以从另外两个组件获得关于Kubernetes对象和容器活动的指标:cAdvisor(谷歌开源项目)和kube-state-metrics (Kubernetes组织的一部分)。两者都作为集群中的容器运行但它们从不同的来源收集数据。cAdvisor从容器运行时收集度量因此它作为一个DaemonSet运行每个节点上都有一个Pod以报告该节点的容器。kube-state-metrics查询Kubernetes API因此它可以作为 Deployment 运行,在任何节点上都有一个副本。
现在试试吧,为 cAdvisor 和 kube-state-metrics 部署度量收集器并更新Prometheus配置以将它们包括为抓取目标。
```
# 部署 cAdvisor 和 kube-state-metrics:
kubectl apply -f kube/
# 等待 cAdvisor 启动:
kubectl wait --for=condition=ContainersReady pod -l app=cadvisor -n kube-system
# 更新 Prometheus config:
kubectl apply -f prometheus/update/prometheus-config-kube.yaml
# 等待 ConfigMap 更新生效:
sleep 30
# 使用 HTTP POST 请求重新加载 Prometheus 配置:
curl -X POST $(kubectl get svc prometheus -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:9090/-/reload'
-n kiamol-ch14-monitoring)
# 访问 Prometheus UI— 在 Graph 页面你将看到包含 容器以及 kubernetes 对象的 metrics 信息
```
在本练习中,您将看到 Prometheus 正在收集数以千计的新度量。原始数据包括每个容器使用的计算资源和每个Pod的状态。我的输出如图14.11所示。当您运行这个练习时您可以检查Prometheus UI中的Targets页面以确认新的目标正在被抓取。Prometheus不会自动重新加载配置因此在练习中会有一个延迟让Kubernetes有时间传播ConfigMap更新并且curl命令强制Prometheus重新加载配置。
![图14.11](./images/Figure14.11.png)
<center>图 14.11 新指标显示集群和容器级别的活动. </center>
刚刚部署的更新后的Prometheus配置包括两个新的作业定义如清单14.6所示。kube-state-metrics使用服务的完整DNS名称指定为静态目标。一个Pod收集所有的指标所以这里不存在负载平衡问题。cAdvisor使用Kubernetes服务发现来查找DaemonSet中的每个Pod这将为多节点集群中的每个节点提供一个目标。
> 清单 14.6 prometheus-config-kube.yaml, Prometheus 新的采集 target
```
- job_name: 'kube-state-metrics' # Kubernetes metrics 使用静态的 DNS 配置
static_configs:
- targets:
- kube-state-metrics.kube-system.svc.cluster.local:8080
- kube-state-metrics.kube-system.svc.cluster.local:8081
- job_name: 'cadvisor' # Container metrics 使用 kubernetes 服务发现
kubernetes_sd_configs: # 来找到所有的 DaemonSet Pods,加上命名空间以及标签参数
- role: pod
relabel_configs:
- source_labels:
- __meta_kubernetes_namespace
- __meta_kubernetes_pod_labelpresent_app
- __meta_kubernetes_pod_label_app
action: keep
regex: kube-system;true;cadvisor
```
现在我们遇到了与随机数仪表板相反的问题:新指标中有太多信息,所以平台仪表板需要高度选择性,如果它想要有用的话。我准备了一个示例仪表板,这是一个很好的开始。它包括集群的当前资源使用情况和所有可用资源数量,以及按命名空间划分的一些高级分解和节点运行状况的警告指示器。
现在试试吧为关键集群指标部署一个仪表板并对Grafana进行更新以便它加载新的仪表板。
```
# 创建仪表板ConfigMap并更新Grafana:
kubectl apply -f grafana/update/kube/
# 等待 grafana 加载:
kubectl wait --for=condition=ContainersReady pod -l app=grafana -n
kiamol-ch14-monitoring
# 获取新的 dashboard url:
kubectl get svc grafana -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:3000/d/oWe9aYxmk' -n kiamol-ch14-monitoring
# 浏览 dashboard
```
这是另一个用于大屏幕的仪表板,因此图 14.12 中的屏幕截图并不能准确地显示它。当您运行这个练习时您可以更仔细地检查它。上面一行显示内存使用情况中间一行显示CPU使用情况下面一行显示Pod容器的状态。
![图14.12](./images/Figure14.12.png)
<center>图14.12 另一个小截图—在您自己的集群中运行练习以查看完整的大小. </center>
像这样的平台 dashboard 级别相当低——它实际上只是显示您的集群是否接近饱和点。支持这个仪表板的查询将更有用可以作为警报在资源使用失控时发出警告。Kubernetes 的压力指示器在那里很有用。内存压力和进程压力值显示在仪表板中还有一个磁盘压力指示器。这些值很重要因为如果节点受到计算压力它可以终止Pod容器。这些都是值得注意的良好指标因为如果您达到了这个阶段您可能需要呼叫某人来看护集群恢复正常。
平台指标还有另一个用途:在应用程序本身无法提供足够详细指标的情况下为应用程序仪表板添加细节。平台仪表板显示整个集群中聚合的计算资源使用情况但cAdvisor在容器级别上收集它。kube-state-metrics也是如此您可以过滤特定工作负载的指标以便向应用程序仪表板添加平台信息。我们将在本章中进行最后一次仪表板更新将平台的详细信息添加到随机数应用程序中。
现在试试吧,为随机数API更新仪表板以添加来自平台的指标。这只是一个Grafana更新;应用程序本身和普罗米修斯都没有变化。
```
# 更新 dashboard:
kubectl apply -f grafana/update/grafana-dashboard-numbers-api-v2.yaml
# 重新启动Grafana让它重新加载仪表板:
kubectl rollout restart deploy grafana -n kiamol-ch14-monitoring
# 等待 pod 启动:
kubectl wait --for=condition=ContainersReady pod -l app=grafana -n
kiamol-ch14-monitoring
# 浏览随机数API仪表板
```
如图14.13所示仪表板仍然是基础信息但至少我们现在有了一些细节可以帮助关联任何问题。如果HTTP状态代码显示为503我们可以快速查看CPU是否也处于峰值状态。如果Pod标签包含一个应用程序版本(这是应该的),我们可以确定哪个版本的应用程序遇到了问题。
![图14.13](./images/Figure14.13.png)
<center>图14.13 使用容器和Pod指标增强基本运行状况统计可以增加相关性. </center>
关于监控还有很多我不会在这里介绍的内容但是现在您已经对Kubernetes和Prometheus如何协同工作有了一个坚实的基础。您缺少的主要部分是在服务器级收集指标和配置警报。服务器指标提供磁盘和网络使用情况等数据。您可以通过直接在节点上运行 exporter 来收集它们(对于Linux服务器使用Node export对于Windows服务器使用Windows export)并使用服务发现将节点添加为抓取目标。Prometheus 有一个复杂的警报系统,它使用 PromQL 查询定义警报规则。您可以配置警报以便当规则被触发时Prometheus 将发送电子邮件、创建Slack消息或通过PagerDuty发送通知。
我们将通过查看《Kubernetes》中普罗米修斯的完整架构来结束本章并深入研究哪些部分需要定制工作以及需要在哪里努力。
## 14.5 了解您在监控方面所做的投资
当你走出核心 Kubernetes进入生态系统时你需要了解你所依赖的项目是否在五年后或一年后或在你所写的章节付印时仍然存在。在这本书中我一直很小心地只包括那些开源的、被大量使用的、具有既定历史和治理模型的生态系统组件。图14.14中的监视体系结构使用了全部满足这些标准的组件。
![图14.14](./images/Figure14.14.png)
<center>图14.14 监控不是免费的——它需要开发并依赖于开源项目. </center>
我提出这一点是因为迁移到普罗米修斯将涉及开发工作。您需要为您的应用程序记录有趣的指标以使您的仪表板真正有用。您应该对投资有信心因为Prometheus是监视容器应用程序最流行的工具并且该项目是cncf继Kubernetes之后第二个毕业的项目。目前正在进行的工作是将Prometheus度量格式引入一个开放标准(称为OpenMetrics)这样其他工具将能够读取以Prometheus格式公开的应用程序度量。
你在这些指标中包含什么将取决于你的应用程序的性质,但是一个好的通用方法是遵循谷歌网站可靠性工程实践的指导方针。在应用指标中添加四个黄金信号通常非常简单:延迟、流量、错误和饱和度。(电子书的附录B介绍了这些在《普罗米修斯》中的样子。)但真正的价值来自于从用户体验的角度考虑应用程序性能。一个显示数据库中磁盘使用率很高的图表并不能告诉你太多东西,但是如果你能看到有很大比例的用户因为你网站的结帐页面加载时间太长而没有完成购买,这是值得了解的。
监控到此结束,我们可以清理集群为实验室做准备了。
现在试试吧,删除本章的命名空间以及system命名空间下创建的对象。
```
kubectl delete ns -l kiamol=ch14
kubectl delete all -n kube-system -l kiamol=ch14
```
## 14.6 实验室
本章的另一个调查实验室。在lab文件夹中有一组清单用于稍微简单一点的Prometheus部署和Elasticsearch的基本部署。我们的目标是运行Elasticsearch并将度量流入到Prometheus中。以下是细节:
- Elasticsearch不提供自己的指标所以你需要找到一个组件来为你做这件事。
- Prometheus 配置将告诉您需要为Elasticsearch使用哪个命名空间以及您需要为度量路径使用注释。
- 你应该在你的Elasticsearch Pod spec 中包含一个版本标签,这样 Prometheus 就会把它添加到度量标签中。
您需要搜索Prometheus的文档才能开始它应该会为您指明方向。我的解决方案在GitHub上你可以在通常的地方检查: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch14/lab/README.md。

View File

@ -0,0 +1,523 @@
# 第十五章 使用 Ingress 管理流入流量
Services 将网络流量引入 Kubernetes您可以拥有多个具有不同公共 IP 地址的 LoadBalancer 服务,以使您的 Web 应用程序可供全世界使用。这样做会给管理带来麻烦,因为这意味着为每个应用程序分配一个新的 IP 地址,并将地址映射到您的 DNS 提供商的应用程序。将流量转移到正确的应用程序是一个路由问题,但您可以使用 Ingress 在 Kubernetes 内部管理它。 Ingress 使用一组规则将域名和请求路径映射到应用程序,因此您可以为整个集群使用一个 IP 地址并在内部路由所有流量。
域名路由是一个老问题,通常已经用反向代理解决了,而 Kubernetes 对 Ingress 使用了可插拔的架构。您将路由规则定义为标准资源,并部署您选择的反向代理来接收流量并根据规则执行操作。所有主要的反向代理都有 Kubernetes 支持,以及一种新的容器感知反向代理。它们都有不同的功能和工作模型,在本章中,您将学习如何使用 Ingress 在集群中托管多个应用程序其中两个应用程序是最流行的Nginx 和 Traefik。
## 15.1 Kubernetes 如何使用 Ingress 路由流量
我们已经在本书中多次使用 Nginx 作为反向代理(据我统计 17 次),但我们总是一次将它用于一个应用程序。我们在第 6 章中使用了一个反向代理来缓存来自 Pi 应用程序的响应,在第 13 章中使用了另一个用于缓存随机数 API 的响应。Ingress 将反向代理移至中心角色,将其作为称为 ingress 控制器的组件运行,但方法是相同:代理从 LoadBalancer 服务接收外部流量,并使用 ClusterIP 服务从应用程序中获取内容。图 15.1 显示了架构。
![图15.1](./images/Figure15.1.png)
<center>图 15.1 Ingress 控制器是集群的入口点,根据 Ingress 规则路由流量。</center>
这张图中最重要的是 ingress 控制器,它是可插入的反向代理——它可能是 Nginx、HAProxy、Contour 和 Traefik 等十几个选项之一。 Ingress 对象以通用方式存储路由规则,控制器将这些规则提供给代理。代理具有不同的功能集,并且 Ingress 规范不会尝试对每个可能的选项进行建模,因此控制器使用注释添加对这些功能的支持。您将在本章中了解到,路由和 HTTPS 支持的核心功能使用起来很简单,但复杂性在于 ingress 控制器部署及其附加功能。
我们将从运行第 2 章中的基本 Hello, World Web 应用程序开始,将其作为具有 ClusterIP 服务的内部组件,并使用 Nginx ingress 控制器来路由流量。
立即尝试,运行 Hello, World 应用程序,并确认它只能在集群内部或外部使用 kubectl 中的端口转发访问。
```
# 进入本章目录:
cd ch15
# 部署 web app:
kubectl apply -f hello-kiamol/
# 确认服务是集群内部的:
kubectl get svc hello-kiamol
# 启动到应用程序的端口转发:
kubectl port-forward svc/hello-kiamol 8015:80
# 访问 http://localhost:8015
# 然后按Ctrl-C/Cmd-C退出端口转发
```
该应用程序的部署或服务规范中没有任何新内容——没有特殊标签或注释,没有您尚未使用过的新字段。您可以在图 15.2 中看到该服务没有外部 IP 地址,只有在运行端口转发时我才能访问该应用程序。
![图15.2](./images/Figure15.2.png)
<center>ClusterIP 服务使应用程序在内部可用——它可以通过 Ingress 公开。</center>
要使用 Ingress 规则使应用程序可用,我们需要一个 Ingress 控制器。控制器管理其他对象。你知道 Deployments 管理 ReplicaSets 和ReplicaSets 管理 Pod。Ingress 控制器略有不同;它们在标准 Pod 中运行并监控 Ingress 对象。当他们看到任何变化时,他们会更新代理中的规则,我们将从 Nginx ingress 控制器开始,它是更广泛的 Kubernetes 项目的一部分。控制器有一个生产就绪的 Helm chart但我使用的部署要简单得多。即便如此清单中仍有一些我们尚未涵盖的安全组件但我现在不会详细介绍它们。 如果您想调查YAML 中有注释。)
立即尝试,部署 Nginx ingress 控制器。这使用服务中的标准 HTTP 和 HTTPS 端口,因此您的计算机上需要有可用的端口 80 和 443。
```
# 为Nginx Ingress 控制器创建部署和服务:
kubectl apply -f ingress-nginx/
# 确认该服务是公开可用的:
kubectl get svc -n kiamol-ingress-nginx
# 获取代理的URL:
kubectl get svc ingress-nginx-controller -o jsonpath='http://{.status.loadBalancer.ingress[0].*}' -n kiamol-ingress-nginx
# 访问 URL—你将发现 error
```
当你运行这个练习时,你会在浏览时看到一个 404 错误页面。这证明服务正在接收流量并将其定向到 ingress 控制器,但是还没有任何路由规则,因此 Nginx 没有内容可显示,它返回默认的未找到页面。我的输出如图 15.3 所示,您可以在其中看到服务正在使用标准 HTTP 端口。
![图15.3](./images/Figure15.3.png)
<center>图 15.3 控制器接收传入流量,但它们需要路由规则来知道如何处理它 </center>
现在我们有一个正在运行的应用程序和一个 Ingress 控制器,我们只需要部署一个带有路由规则的 ingress 对象来告诉控制器每个传入请求使用哪个应用程序服务。清单 15.1 显示了 Ingress 对象的最简单规则,它将每个进入集群的请求路由到 Hello, World 应用程序。
> 清单 15.1 localhost.yamlHello, World 应用程序的路由规则
```
apiVersion: networking.k8s.io/v1beta1 # Beta API版本意味着规范不是最终的可能会改变。
kind: Ingress
metadata:
name: hello-kiamol
spec:
rules:
- http: # Ingress 仅用于HTTP/S流量
paths:
- path: / # 将每个传入请求映射到hello-kiamol服务
backend:
serviceName: hello-kiamol
servicePort: 80
```
Ingress 控制器正在监视新的和更改的 ingress 对象,因此当您部署任何对象时,它会将规则添加到 Nginx 配置中。在 Nginx 术语中,它将设置一个代理服务器,其中 hello-kiamol 服务是上游(内容的来源),它将为根路径的传入请求提供该内容。
立即尝试,创建通过 ingress 控制器发布 Hello, World 应用程序的入口规则。
```
# 部署 rule:
kubectl apply -f hello-kiamol/ingress/localhost.yaml
# 确认Ingress对象已经创建:
kubectl get ingress
# 从前面的练习中刷新浏览器
```
好吧,这很简单——在 Ingress 对象中为应用程序映射到后端服务的路径,控制器负责处理其他所有事情。我在图 15.4 中的输出显示了本地主机地址,它之前返回了 404 错误,现在返回了 Hello, World 应用程序的所有荣耀。
![图15.4](./images/Figure15.4.png)
<center>图 15.4 Ingress 对象规则将 Ingress 控制器链接到应用服务。</center>
Ingress 通常是集群中的集中服务,例如日志记录和监控。管理团队可能会部署和管理 Ingress 控制器,而每个产品团队都拥有将流量路由到其应用程序的 Ingress 对象。这个过程可能会产生冲突—— ingress 规则不必是唯一的,一个团队的更新最终可能会将另一个团队的所有流量重定向到其他应用程序。这种情况不会发生,因为这些应用程序将托管在不同的域中,并且 Ingress 规则将包含一个域名来限制它们的范围。
## 15.2 使用 Ingress rules 路由 Http 流量
Ingress 仅适用于 Web 流量HTTP 和 HTTPS 请求),因为它需要使用请求中指定的路由将其与后端服务相匹配。 HTTP 请求中的路由包含两部分:主机和路径。主机是域名,如 www.manning.com路径是资源的位置如每日交易页面的 /dotd。清单 15.2 显示了对使用特定主机名的 Hello, World Ingress 对象的更新。现在,仅当传入请求是针对主机 hello.kiamol.local 时,路由规则才适用。
> 清单 15.2 hello.kiamol.local.yaml为 Ingress 规则指定主机域
```
spec:
rules:
- host: hello.kiamol.local # 将规则的范围限制到特定的域
http:
paths:
- path: / # 该域中的所有路径都将从同一个服务中获取。
backend:
serviceName: hello-kiamol
servicePort: 80
```
当您部署此代码时,您将无法访问应用程序,因为域名 hello.kiamol.local 不存在。 Web 请求通常从公共 DNS 服务器查找域名的 IP 地址,但所有计算机在主机文件中也有自己的本地列表。在下一个练习中,您将部署更新的 Ingress 对象并在本地主机文件中注册域名——为此您需要在终端会话中具有管理员访问权限。
立即尝试 编辑主机文件受到限制。您需要为 Windows 中的终端会话使用“以管理员身份运行”选项,并使用 Set-ExecutionPolicy 命令启用脚本。准备好在 Linux 或 macOS 中输入您的管理员 (sudo) 密码。
```
# 添加域到主机- Windows上:
./add-to-hosts.ps1 hello.kiamol.local ingress-nginx
# 或者 Linux/macOS:
chmod +x add-to-hosts.sh && ./add-to-hosts.sh hello.kiamol.local
ingress-nginx
# 更新Ingress对象添加主机名:
kubectl apply -f hello-kiamol/ingress/hello.kiamol.local.yaml
# 确认更新:
kubectl get ingress
# 访问 http://hello.kiamol.local
```
在本练习中,现有的 Ingress 对象已更新,因此仍然只有一个路由规则供 ingress 控制器映射。现在该规则仅限于显式域名。您可以在图 15.5 中看到对 hello.kiamol.local 的请求返回了应用程序,我还浏览了位于 localhost 的 ingress 控制器,它返回了 404 错误,因为没有 localhost 域的规则。
![图15.5](./images/Figure15.5.png)
<center>图 15.5 您可以使用 Ingress 规则通过域名发布应用程序,并通过编辑您的主机文件在本地使用它们</center>
路由是一个基础架构级别的问题,但与我们在本书的这一部分看到的其他共享服务一样,它在轻量级容器中运行,因此您可以在开发、测试和生产环境中使用完全相同的设置。这使您可以使用友好的域名在非生产集群中运行多个应用程序,而不必使用不同的端口—— ingress 控制器的服务为每个应用程序使用标准的 HTTP 端口。
如果你想在你的实验室环境中运行具有不同域的多个应用程序,你需要摆弄你的主机文件。通常,所有域都将解析为 127.0.0.1,这是您机器的本地地址。组织可能在测试环境中运行自己的 DNS 服务器,因此任何人都可以从公司网络访问 hello.kiamol.test它将解析为在数据中心运行的测试集群的 IP 地址。然后在生产中DNS 解析来自公共 DNS 服务,因此 hello.kiamol.net 解析为在云中运行的 Kubernetes 集群。
您可以在 Ingress 规则中组合主机名和路径,为您的应用程序提供一组一致的地址,尽管您可以在后端使用不同的组件。您可能有一个 REST API 和一个在单独的 Pod 中运行的网站,您可以使用 Ingress 规则使 API 在子域 (api.rng.com) 上可用或作为主域 (rng.com/api) 上的路径.清单 15.3 显示了第 9 章中简单版本化 Web 应用程序的 Ingress 规则,其中应用程序的两个版本都可以从一个域获得。
> 清单 15.3 vweb/ingress.yaml带有主机名和路径的入口规则
```
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: vweb # 在Nginx中配置一个特定的特性
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: vweb.kiamol.local # 所有规则适用于此域。
http:
paths:
- path: / # 对根路径的请求是由版本2应用程序代理的。
backend:
serviceName: vweb-v2
servicePort: 80
- path: /v1 # 对/v1路径的请求是从版本1应用程序代理的。
backend:
serviceName: vweb-v1
servicePort: 80
```
建模路径增加了复杂性,因为您呈现的是虚假 URL需要对其进行修改以匹配服务中的真实 URL。在这种情况下ingress 控制器将响应 http://vweb.kiamol.local/v1 的请求并从 vweb-v1 服务获取内容。但是应用程序在 /v1 中没有任何内容,因此代理需要重写传入的 URL——清单 15.3 中的注释就是这样做的。这是一个忽略请求中的路径并始终使用后端中的根路径的基本示例。您不能使用 Ingress 规范来表达 URL 重写,因此它需要来自 ingress 控制器的自定义支持。更现实的重写规则将使用正则表达式将请求的路径映射到目标路径。
我们将部署这个简单版本以避免任何正则表达式,并查看 ingress 控制器如何使用路由规则来识别后端服务并修改请求路径。
立即尝试,使用新的 Ingress 规则部署一个新应用程序,并向您的主机文件添加一个新域,以查看 ingress控制器为来自同一域的多个应用程序提供服务。
```
# 在Windows上添加新的域名:
./add-to-hosts.ps1 vweb.kiamol.local ingress-nginx
# 或者在 Linux/macOS:
./add-to-hosts.sh vweb.kiamol.local ingress-nginx
# 部署 app, Service, and Ingress:
kubectl apply -f vweb/
# 确认 Ingress domain:
kubectl get ingress
# 访问http://vweb.kiamol.local
# 以及 http://vweb.kiamol.local/v1
```
在图 15.6 中,您可以看到两个单独的应用程序在同一个域名下可用,使用请求路径在不同组件之间进行路由,这些组件是本练习中应用程序的不同版本。
![图15.6](./images/Figure15.6.png)
<center>图 15.6 主机名和路径上的 Ingress 路由显示了同一域名上的多个应用程序. </center>
映射路由规则是将新应用发布到 ingress 控制器中最复杂的部分,但它确实给了你很多控制权。 Ingress 规则是您应用程序的公开面孔,您可以使用它们来组合多个组件——或限制对功能的访问。在本书的这一部分,我们已经看到,如果应用程序具有用于容器探测的健康端点和用于 Prometheus 抓取的指标端点,那么它们在 Kubernetes 中运行得更好,但这些应用程序不应该公开可用。你可以使用 Ingress 来控制它,使用精确的路径映射,所以只有明确的路径列出的在集群外可用。清单 15.4 显示了待办事项列表应用程序的示例。它被删减了,因为这种方法的缺点是您需要指定要发布的每个路径,因此任何未指定的路径都会被阻止。
> 清单 15.4 ingress-exact.yaml使用精确路径匹配来限制访问
```
rules:
- host: todo.kiamol.local
http:
paths:
- pathType: Exact # 精确匹配意味着只匹配/new路径—对于/list和根路径还有其他规则。
path: /new
backend:
serviceName: todo-web
servicePort: 80
- pathType: Prefix # 前缀匹配意味着任何以/static开头的路径都将被映射包括/static/app.css这样的子路径。
path: /static
backend:
serviceName: todo-web
servicePort: 80
```
待办事项列表应用程序有几个不应该在集群外部可用的路径以及 /metrics有一个 /config 端点列出了所有应用程序配置和一个诊断页面。这些路径都没有包含在新的 Ingress 规范中,我们可以看到在应用规则时它们被有效地阻止了。 PathType 字段是后来添加到 Ingress 规范中的,因此您的 Kubernetes 集群至少需要运行 1.18 版本;否则,您将在本练习中出错。
立即尝试,使用允许所有访问的 Ingress 规范部署待办事项列表应用程序,然后使用精确路径匹配更新它,并确认敏感路径不再可用。
```
# 在Windows上为应用程序添加一个新域:
./add-to-hosts.ps1 todo.kiamol.local ingress-nginx
# 或者在 Linux/macOS:
./add-to-hosts.sh todo.kiamol.local ingress-nginx
# 使用允许所有路径的Ingress对象部署应用程序:
kubectl apply -f todo-list/
# 访问 http://todo.kiamol.local/metrics
# 用精确的路径更新Ingress:
kubectl apply -f todo-list/update/ingress-exact.yaml
# 再次浏览-应用程序工作,但指标和诊断阻塞
```
当您运行此练习时,您会看到部署更新的 Ingress 规则时所有敏感路径都被阻止。我的输出如图 15.7 所示。这不是一个完美的解决方案,但您可以扩展 ingress 控制器以显示友好的 404 错误页面,而不是 Nginx 默认值。 Docker 有一个很好的例子:试试 https://www.docker.com/not-real-url 。)应用程序仍然显示诊断页面的菜单,因为它不是删除页面的应用程序设置;它发生在这个过程的早期。
![图15.7](./images/Figure15.7.png)
<center>图 15.7 Ingress 规则中的精确路径匹配可用于阻止对功能的访问。 </center>
Ingress 规则和 ingress 控制器之间的分离使得比较不同的代理实现变得容易并查看哪个为您提供了您满意的功能和可用性组合。但它带有警告因为没有严格的Ingress 控制器规范,并且并非每个控制器都以相同的方式实现入口规则。一些控制器会忽略 PathType 字段因此如果您依靠它来构建具有确切路径的访问列表如果您切换到不同的Ingress 控制器,您可能会发现您的站点变成了访问所有区域。
Kubernetes 确实允许您运行多个Ingress 控制器,并且在复杂的环境中,您可以这样做为不同的应用程序提供不同的功能集。
## 15.3 比较 Ingress 控制器
Ingress 控制器分为两类:反向代理,已经存在了很长时间,工作在网络级别,使用主机名获取内容;和现代代理,它们是平台感知的并且可以与其他服务集成(云控制器可以提供外部负载平衡器)。在它们之间进行选择取决于功能集和您自己的技术偏好。如果您与 Nginx 或 HAProxy 建立了关系,则可以在 Kubernetes 中继续这种关系。或者,如果您与 Nginx 或 HAProxy 建立了良好的关系,您可能会很乐意尝试更轻量级、更现代的选择。
您的Ingress 控制器成为集群中所有应用程序的单一公共入口点,因此它是集中常见问题的好地方。所有控制器都支持 SSL 终止,因此代理提供了安全层,并且您可以为所有应用程序获取 HTTPS。大多数控制器都支持 Web 应用程序防火墙,因此您可以在代理层提供针对 SQL 注入和其他常见攻击的保护。一些控制器具有特殊的能力——我们已经使用 Nginx 作为缓存代理,您也可以将它用于入口级别的缓存。
立即尝试,使用 Ingress 部署 Pi 应用程序,然后更新 Ingress 对象,以便 Pi 应用程序使用Ingress 控制器中的 Nginx 缓存。
```
# 将Pi应用程序域添加到hosts文件- windows:
./add-to-hosts.ps1 pi.kiamol.local ingress-nginx
# 或者 Linux/macOS:
./add-to-hosts.sh pi.kiamol.local ingress-nginx
# 部署应用程序和一个简单的Ingress:
kubectl apply -f pi/
# 访问 http://pi.kiamol.local?dp=30000
# 刷新并确认页面加载需要相同的时间将更新部署到Ingress以使用缓存:
kubectl apply -f pi/update/ingress-with-cache.yaml
# 再次浏览30K Pi计算-第一次加载需要几秒钟,但现在刷新会很快
```
您将在本练习中看到Ingress 控制器是集群中的一个强大组件。您只需指定新的 Ingress 规则即可将缓存添加到您的应用程序中——无需更新应用程序本身,也无需管理新组件。唯一的要求是来自您的应用程序的 HTTP 响应包含正确的缓存标头,无论如何它们都应该包含这些标头。图 15.8 显示了我的输出,其中 Pi 计算耗时 1.2 秒但响应来自Ingress 控制器的缓存,因此页面几乎立即加载。
![图15.8](./images/Figure15.8.png)
<center>图 15.8 如果你的Ingress 控制器支持响应缓存,那很容易提升性能. </center>
并非每个Ingress 控制器都提供响应缓存,因此这不是入口规范的特定部分。任何自定义配置都会应用注释,控制器会拾取这些注释。清单 15.5 显示了您在上一个练习中应用的更新缓存设置的元数据。如果您熟悉 Nginx您会认出这些是您通常在配置文件中设置的代理缓存设置。
> 清单 15.5 ingress-with-cache.yaml在Ingress 控制器中使用 Nginx 缓存
```
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: # The ingress controller looks in annotations for
name: pi # custom configuration—this adds proxy caching.
annotations:
nginx.ingress.kubernetes.io/proxy-buffering: "on"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_cache static-cache;
proxy_cache_valid 10m;
```
Ingress 对象中的配置适用于它的所有规则,但如果您的应用程序的不同部分需要不同的功能,您可以有多个 Ingress 规则。待办事项列表应用程序也是如此它需要来自Ingress 控制器的更多帮助才能大规模正常工作。如果一个服务有很多 PodIngress 控制器使用负载平衡,但是待办事项应用程序有一些跨站点伪造保护,如果创建新项目的请求被发送到与最初呈现的应用程序容器不同的应用程序容器,它就会中断新项目页面。许多应用程序都有这样的限制,代理使用粘性会话来解决。
粘性会话是Ingress 控制器将请求从同一最终用户发送到同一容器的一种机制,这通常是组件不是无状态的旧应用程序的要求。这是要尽可能避免的事情,因为它限制了集群负载平衡的潜力,所以在待办事项列表应用程序中,我们希望将其限制在一页上。图 15.9 显示了我们将应用的 Ingress 规则,以获得应用程序不同部分的不同功能。
![图15.9](./images/Figure15.9.png)
<center>图 15.9 一个域可以映射到多个 Ingress 规则,使用不同的代理功能</center>
我们现在可以扩展待办事项应用程序以了解问题,然后应用更新的 Ingress 规则来修复它。
立即尝试,扩大待办事项应用程序以确认它在没有粘性会话的情况下中断,然后部署图 15.9 中更新的 Ingress 规则并再次确认一切正常。
```
: 放大-控制器之间的负载平衡
kubectl scale deploy/todo-web --replicas 3
# 等待新的 pod 启动:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web
# 访问 http://todo.kiamol.local/new, 然后添加一个项目这将失败并显示一个400错误页面
# 打印应用程序日志查看问题:
kubectl logs -l app=todo-web --tail 1 --since 60s
# 更新入口以添加粘滞会话:
kubectl apply -f todo-list/update/ingress-sticky.yaml
# 再次浏览,并添加一个新项目——这一次可以正常工作
```
你可以在图 15.10 中看到我的输出但除非你自己运行这个练习否则你必须相信我的话哪个是“之前”哪个是“之后”的截图。扩展应用程序副本意味着来自Ingress 控制器的请求是负载平衡的,这会触发防伪错误。应用粘性会话会停止新项目路径上的负载平衡,因此用户的请求总是被路由到同一个 Pod并且伪造检查通过。
![图15.10](./images/Figure15.10.png)
<center>图 15.10 代理功能可以解决问题并提高性能. </center>
待办事项应用程序的 Ingress 资源使用主机、路径和注释的组合来设置要应用的所有规则和功能。在幕后,控制器的工作是将这些规则转换为代理配置,在 Nginx 的情况下意味着编写配置文件。控制器进行了大量优化以最大限度地减少文件写入和配置重新加载的次数但结果是Nginx 配置文件非常复杂。如果你选择 Nginx Ingress 控制器是因为你有 Nginx 经验并且你会很舒服地调试配置文件,你会遇到一个不愉快的惊喜。
立即尝试Nginx 配置位于Ingress 控制器 Pod 中的一个文件中。在 Pod 中运行一个命令来检查文件的大小。
```
# 运行wc命令查看文件中有多少行:
kubectl exec -n kiamol-ingress-nginx deploy/ingress-nginx-controller -- sh -c 'wc -l /etc/nginx/nginx.conf'
```
图 15.11 显示我的 Nginx 配置文件中有 1,700 多行。如果你运行 cat 而不是 wc你会发现内容很奇怪即使你很熟悉Nginx。 (控制器使用 Lua 脚本,因此它可以在不重新加载配置的情况下更新端点。)
![图15.11](./images/Figure15.11.png)
<center>图 15.11 生成的 Nginx 配置文件不是很人性化. </center>
Ingress 控制器拥有这种复杂性但它是您解决方案的关键部分您需要对如何对代理进行故障排除和调试感到满意。这时您可能想要考虑一个替代的Ingress 控制器,它是平台感知的并且不从复杂的配置文件运行。我们将在本章中介绍 Traefik——它是一个开源代理自 2015 年推出以来越来越受欢迎。Traefik 了解容器,它从平台 API 构建路由列表,原生支持 Docker 和 Kubernetes所以它没有要维护的配置文件。
Kubernetes 支持在单个集群中运行多个 Ingress 控制器。它们将作为 LoadBalancer 服务公开因此在生产中不同的Ingress 控制器可能有不同的 IP 地址,并且您需要在 DNS 配置中将域映射到入口。在我们的实验室环境中我们将重新使用不同的端口。我们将从使用Ingress 控制器服务的自定义端口部署 Traefik 开始。
立即尝试,将 Traefik 部署为集群中的附加Ingress 控制器。
```
# 创建Traefik部署、服务和安全资源:
kubectl apply -f ingress-traefik/
# 获取运行在 ingress 控制器中的Traefik UI的URL:
kubectl get svc ingress-traefik-controller -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080' -n
kiamol-ingress-traefik
# 浏览到管理UI查看Traefik映射的路由
```
你会在那个练习中看到 Traefik 有一个管理 UI。它向您显示代理正在使用的路由规则并且当流量通过时它可以收集并显示性能指标。它比 Nginx 配置文件更容易使用。图 15.12 显示了两个路由器,它们是 Traefik 管理的传入路由。如果您浏览仪表板,您会看到那些不是 Ingress 路由;它们是 Traefik 自己的仪表板的内部路由——Traefik 没有选择集群中任何现有的 Ingress 规则。
![图15.12](./images/Figure15.12.png)
<center>图 15.12 Traefik 是一个容器原生代理,它从平台构建路由规则并有一个 UI 来显示它们. </center>
为什么 Traefik 没有为待办事项列表或 Pi 应用程序构建一套路由规则?如果我们以不同的方式配置它,那么所有现有路由都可以通过 Traefik 服务使用但这不是您使用多个Ingress 控制器的方式,因为它们最终会争夺传入请求。您运行多个控制器以提供不同的代理功能,并且您需要应用程序选择使用哪一个。您可以使用入口类来做到这一点,入口类是与存储类类似的概念。 Traefik 已经部署了一个命名 ingress 类,只有请求该类的入口对象才会通过 Traefik 进行路由。
ingress 类并不是Ingress 控制器之间的唯一区别,您可能需要为不同的代理完全不同地建模路由。图 15.13 显示了如何在 Traefik 中配置待办事项应用程序。 Traefik 中没有响应缓存,因此我们不会获取静态资源的缓存,并且粘性会话是在服务级别配置的,因此我们需要为新项目路由提供额外的服务。
![图15.13](./images/Figure15.13.png)
<center>图 15.13 Ingress 控制器的工作方式不同,您的路由模型将需要相应地更改。 </center>
该模型与图 15.9 中的 Nginx 路由明显不同因此如果您确实计划运行多个Ingress 控制器,您需要意识到配置错误的高风险,因为团队会混淆不同的功能和方法。 Traefik 使用 Ingress 资源上的注解来配置路由规则。清单 15.6 显示了新项目路径的规范,它选择 Traefik 作为 ingress类并使用注释进行精确路径匹配因为 Traefik 不支持 PathType 字段。
> 清单 15.6 ingress-traefik.yaml选择带有 Traefik 注解的 ingress 类
```
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata: # 注释选择Traefik ingress 类并应用精确的路径匹配。
name: todo2-new
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.pathmatcher: Path
spec:
rules:
- host: todo2.kiamol.local # 使用不同的主机所以应用程序通过Nginx保持可用
http:
paths:
- path: /new
backend:
serviceName: todo-web-sticky # 使用为Traefik配置了会话保持的服务
servicePort: 80
```
我们将使用不同的主机名部署一组新的 Ingress 规则,因此我们可以通过 Nginx 或 Traefik 将流量路由到同一组待办事项列表 Pod。
立即尝试,使用图 15.13 中建模的入口路由,通过 Traefik Ingress 控制器发布待办事项应用程序。
```
# 在Windows上为应用程序添加一个新域 :
./add-to-hosts.ps1 todo2.kiamol.local ingress-traefik
# 或则在 Linux/macOS:
./add-to-hosts.sh todo2.kiamol.local ingress-traefik
# 应用新的入口规则和粘性服务:
kubectl apply -f todo-list/update/ingress-traefik.yaml
# 刷新Traefik管理员界面确认新路由
# 访问 http://todo2.kiamol.local:8015
```
Traefik 监视来自 Kubernetes API 服务器的事件并自动刷新其路由列表。当您部署新的 Ingress 对象时,您会在 Traefik 仪表板中看到显示为路由器的路径,链接到后端服务。图 15.14 显示了路由列表的一部分,以及通过新 URL 可用的待办事项应用程序。
![图15.14](./images/Figure15.14.png)
<center>图 15.14 Ingress 控制器通过不同的配置模型实现相同的目标。 </center>
如果您正在评估Ingress 控制器,您应该查看应用程序路径建模的难易程度,以及故障排除方法和代理的性能。专用环境中的双运行控制器对此有所帮助,因为您可以隔离其他因素并使用相同的应用程序组件进行比较。更现实的应用程序将具有更复杂的 Ingress 规则并且您需要熟悉控制器如何实现速率限制、URL 重写和客户端 IP 访问列表等功能。
Ingress 的另一个主要功能是通过 HTTPS 发布应用程序而无需在应用程序中配置证书和安全设置。这是Ingress 控制器之间一致的一个区域,在下一节中,我们将在 Traefik 和 Nginx 中看到它。
## 15.4 使用 Ingress 通过 HTTPS 保护您的应用程序
您的 Web 应用程序应通过 HTTPS 发布,但加密需要服务器证书,而证书是敏感数据项。使 HTTPS 成为入口问题是一种很好的做法,因为它集中了证书管理。 Ingress资源可以在Kubernetes Secret中指定一个TLS证书TLS是Transport Layer SecurityHTTPS的加密机制。将 TLS 远离应用程序团队意味着您可以使用标准方法来配置、保护和更新证书,并且您不必花时间解释为什么将证书打包在容器映像中是个坏主意。
所有Ingress 控制器都支持从 Secret 加载 TLS 证书,但 Traefik 使它更容易。如果您想在开发和测试环境中使用 HTTPS 而无需提供任何 SecretsTraefik 可以在运行时生成自己的自签名证书。您可以在 Ingress 规则中使用注释对其进行配置,以启用 TLS 和默认证书解析器。
立即尝试,使用 Traefik 生成的证书是通过 HTTPS 测试您的应用程序的快速方法。它在 Ingress 对象中启用了更多注释。
```
# 更新Ingress以使用Traefik自己的证书:
kubectl apply -f todo-list/update/ingress-traefik-certResolver.yaml
# 访问 https://todo2.kiamol.local:9443
# 您将在浏览器中看到一个警告
```
浏览器不喜欢自签名证书,因为任何人都可以创建它们——没有可验证的授权链。当您第一次浏览该网站时,您会看到一个很大的警告,告诉您它不安全,但您可以继续,待办事项列表应用程序将加载。如图 15.15 所示,该站点使用 HTTPS 加密,但带有警告,因此您知道它并不真正安全。
![图15.15](./images/Figure15.15.png)
<center>图 15.15 并非所有 HTTPS 都是安全的——自签名证书适用于开发和测试环境。 </center>
您的组织可能对证书有自己的想法。如果您能够拥有供应过程,您可以拥有一个完全自动化的系统,您的集群在其中从证书颁发机构 (CA) 获取短期证书,安装它们,并在需要时更新它们。 Let's Encrypt 是一个不错的选择:它通过一个易于自动化的过程颁发免费证书。 Traefik 与 Let's Encrypt 原生集成对于其他Ingress 控制器,您可以使用开源证书管理器工具 ( https://cert-manager.io ),这是一个 CNCF 项目。
不过,并不是每个人都准备好进行自动配置过程。一些颁发者需要人工下载证书文件,或者您的组织可能会从其自己的证书颁发机构为非生产域创建证书文件。然后,您需要将 TLS 证书和密钥文件部署为集群中的 Secret。这种情况很常见因此我们将在下一个练习中逐步完成生成我们自己的证书。
立即尝试,运行生成自定义 TLS 证书的 Pod并连接到 Pod 以将证书文件部署为 Secret。 Pod 规范配置为连接到它运行的 Kubernetes API 服务器。
```
# 运行pod——这将在启动时生成证书:
kubectl apply -f ./cert-generator.yaml
# 连接到 pod:
kubectl exec -it deploy/cert-generator -- sh
# 在Pod中确认证书文件已经创建:
ls
# 重命名证书文件——kubernetes需要特定的名称:
mv server-cert.pem tls.crt
mv server-key.pem tls.key
# 从证书文件中创建并标记一个Secret :
kubectl create secret tls kiamol-cert --key=tls.key --cert=tls.crt
kubectl label secret/kiamol-cert kiamol=ch15
# 退出 Pod:
exit
# 回到主机上确认Secret在那里:
kubectl get secret kiamol-cert --show-labels
```
该练习模拟了某人将 TLS 证书作为一对 PEM 文件提供给您的情况,您需要将其重命名并用作在 Kubernetes 中创建 TLS Secret 的输入。证书生成全部使用名为 OpenSSL 的工具完成,在 Pod 中运行它的唯一原因是打包该工具和脚本以使其易于使用。图 15.16 显示了我的输出,其中在集群中创建了一个可供 Ingress 对象使用的 Secret。
![图15.16](./images/Figure15.16.png)
<center>图 15.16 如果您从证书颁发者那里获得 PEM 文件,您可以将它们创建为 TLS Secret。 </center>
HTTPS 支持通过Ingress 控制器很简单。您将 TLS 部分添加到 Ingress 规范并声明要使用的 Secret 的名称——仅此而已。清单 15.7 显示了对 Traefik ingress 的更新,它将新证书应用于 todo2.kiamol.local host 。
> 清单 15.7 ingress-traefik-https.yaml使用标准的 Ingress HTTPS 特性
```
spec:
rules:
- host: todo2.kiamol.local
http:
paths:
- path: /new
backend:
serviceName: todo-web-sticky
servicePort: 80
tls: # TLS部分使用这个Secret中的证书开启HTTPS。
- secretName: kiamol-cert
```
带有 Secret 名称的 TLS 字段就是您所需要的它可以跨所有Ingress 控制器移植。当您部署更新的 Ingress 规则时,该站点将使用您的自定义证书通过 HTTPS 提供服务。你仍然会从浏览器中收到安全警告,因为证书颁发机构不受信任,但如果你的组织有自己的 CA那么你的机器将信任它并且组织的证书将有效。
立即尝试,更新待办事项列表 ingress 对象以使用 Traefik Ingress 控制器和您自己的 TLS 证书发布 HTTPS。
```
# 应用 Ingress 更新:
kubectl apply -f todo-list/update/ingress-traefik-https.yaml
# 访问 https://todo2.kiamol.local:9443
# 仍然有一个警告但这一次是因为KIAMOL CA不可信
```
你可以在图 15.17 中看到我的输出。我在一个屏幕上打开了证书详细信息以确认这是我自己的“kiamol”证书。我接受了第二个屏幕中的警告待办事项列表流量现在已使用自定义证书加密。这生成证书的脚本将它设置为我们在本章中使用的所有 kiamol.local 域,因此证书对地址有效,但它不是来自受信任的颁发者。
![图15.17](./images/Figure15.17.png)
<center>图 15.17 Ingress 控制器可以应用来自 Kubernetes Secrets 的 TLS 证书。如果证书来自受信任的颁发者,则该站点将是安全的 </center>
我们将切换回 Nginx 进行最后的练习——使用与 Nginx Ingress 控制器相同的证书,只是为了表明过程是相同的。更新后的 Ingress 规范使用与之前 Nginx 部署相同的规则,但现在他们添加了与清单 15.7 具有相同 Secret 名称的 TLS 字段。
立即尝试,更新 Nginx 的待办事项 ingress 规则,以便通过标准端口 443 使用 HTTPS 访问应用程序Nginx Ingress 控制器正在使用该端口。
```
# 更新 Ingress 资源:
kubectl apply -f todo-list/update/ingress-https.yaml
# 访问 https://todo.kiamol.local
# 接受警告以查看站点确认HTTP请求被重定向到HTTPS:
curl http://todo.kiamol.local
```
当我运行该练习并将 Kiamol CA 添加到浏览器中我的可信发行者列表时,我作弊了。您可以在图 15.18 中看到该站点显示为安全的没有任何警告这是您在组织自己的证书中看到的。你也可以看到Ingress 控制器将 HTTP 请求重定向到 HTTPS——curl 命令中的 308 重定向响应由 Nginx 处理。
![图15.18](./images/Figure15.18.png)
<center>图 15.18 TLS 入口配置与 Nginx Ingress 控制器的工作方式相同。</center>
Ingress 的 HTTPS 部分可靠且易于使用很高兴以高调进入本章末尾。但是使用Ingress 控制器具有很多复杂性,在某些情况下,您将花费更多时间来制定入口规则,而不是为应用程序的部署建模。
## 15.5 理解 Ingress 及 Ingress 控制器
您几乎肯定会在集群中运行Ingress 控制器,因为它集中了域名路由并将 TLS 证书管理从应用程序中移开。 Kubernetes 模型使用通用的 Ingress 规范和非常灵活的可插拔实现,但用户体验并不直接。 Ingress 规范仅记录最基本的路由详细信息,要使用代理的更多高级功能,您需要添加配置块作为注释。
这些注释不可移植并且没有针对Ingress 控制器必须支持的功能的接口规范。如果你想从 Nginx 迁移到 Traefik 或 HAProxy 或 Contour在我写这章的那天一个开源项目被 CNCF 接受),将会有一个迁移项目,你可能会发现你需要的功能并不是全部可用的。 Kubernetes 社区意识到了 Ingress 的局限性,并正在研究称为服务 API 的长期替代品,但截至 2021 年,它仍处于早期阶段。
这并不是说应该避免使用 Ingress——它是目前最好的选择而且它可能会成为未来很多年的生产选择。评估不同的Ingress 控制器然后选择一个选项是值得的。 Kubernetes 支持多个Ingress 控制器,但如果您使用不同的实现并且必须管理具有通过难以理解的注释调用的不兼容功能集的入口规则集,那么麻烦就会真正开始。在本章中,我们研究了 Nginx 和 Traefik它们都是不错的选择但还有很多其他选择包括以支持合同为后盾的商业选择。
我们现在已经完成了 Ingress所以我们可以整理集群为实验室做好准备。
立即尝试,清除 Ingress 命名空间和应用程序资源。
```
kubectl delete ns,all,secret,ingress -l kiamol=ch15
```
## 15.6 实验室
这是一个不错的实验,您可以按照第 13 章和第 14 章中的模式进行操作。您的工作是为 Astronomy Picture of the Day 应用程序构建入口规则。简单的...
- 首先在 lab/ingress-nginx 文件夹中部署Ingress 控制器。
- Ingress 控制器仅限于在一个命名空间中查找入口对象,因此您需要找出是哪一个并将 lab/apod/ 文件夹部署到该命名空间。
- Web 应用程序应发布在 www.apod.localAPI 应发布在 api.apod.local。
- 我们要防止分布式拒绝服务攻击因此您应该在Ingress 控制器中使用限速功能来防止来自同一 IP 地址的过多请求。
- Ingress 控制器使用自定义类名,因此您也需要找到它。
这部分是关于深入研究Ingress 控制器配置,部分是关于控制器的文档——请注意有两个 Nginx Ingress 控制器。我们在本章中使用了 Kubernetes 项目中的一个,但 Nginx 项目发布了一个替代方案。我的解决方案已准备好供您检查: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch15/lab/README.md 。

View File

@ -0,0 +1,591 @@
# 第十六章 使用策略上下文和准入控制保护应用程序
容器是围绕应用程序进程的轻量级包装器。它们启动迅速并且几乎不会增加应用程序的开销,因为它们使用运行它们的机器的操作系统内核。这使它们非常高效,但代价是强隔离——容器可能会受到损害,而受到损害的容器可能会提供对服务器及其上运行的所有其他容器的不受限制的访问。 Kubernetes 有许多功能可以保护您的应用程序,但默认情况下都没有启用。在本章中,您将学习如何使用 Kubernetes 中的安全控制以及如何设置您的集群,以便您的所有工作负载都需要这些控制。
保护 Kubernetes 中的应用程序就是限制容器可以执行的操作,因此如果攻击者利用应用程序漏洞在容器中运行命令,他们将无法越过该容器。我们可以通过限制对其他容器和 Kubernetes API 的网络访问、限制主机文件系统的挂载以及限制容器可以使用的操作系统功能来做到这一点。我们将介绍基本方法,但安全空间很大且不断发展。本章比其他章节更长——您将学到很多东西,但这只是您通往安全 Kubernetes 环境之旅的开始。
## 16.1 使用网络策略(network policies)保护通信
限制网络访问是保护应用程序的最简单方法之一。 Kubernetes 有一个扁平的网络模型,其中每个 Pod 都可以通过其 IP 地址访问每个其他 Pod并且服务可以在整个集群中访问。 Pi web 应用程序没有理由访问待办事项列表数据库,或者 Hello, World web 应用程序为什么应该使用 Kubernetes API但默认情况下它们可以。您在第 15 章中学习了如何使用 Ingress 资源来控制对 HTTP 路由的访问但这仅适用于进入集群的外部流量。您还需要控制集群内的访问为此Kubernetes 提供了网络策略(Network policy)。
网络策略像防火墙规则一样工作,在端口级别阻止进出 Pod 的流量。规则很灵活,使用标签选择器来识别对象。您可以部署一揽子拒绝所有策略来阻止所有 Pod 的传出流量,或者您可以部署一个策略来限制传入流量到 Pod 的指标端口,以便只能从监控命名空间中的 Pod 访问它。图 16.1 显示了它在集群中的样子。
![图16.1](./images/Figure16.1.png)
<center>图 16.1 网络策略规则是细粒度的——您可以应用集群范围内的默认设置和 Pod 覆盖。 </center>
NetworkPolicy 对象是独立的资源,这意味着它们可以由安全团队在应用程序外部建模,也可以由产品团队构建。或者,当然,每个团队都可能认为另一个团队已经涵盖了它,并且应用程序在没有任何策略的情况下投入生产,这是一个问题。我们将部署一个没有策略的应用程序,并查看它存在的问题。
现在就试试,部署 Astronomy Picture of the Day (APOD) 应用程序,并确认任何 Pod 都可以访问该应用程序组件。
```
# 进入本章节目录:
cd ch16
# 部署 APOD app:
kubectl apply -f apod/
# 等待运行:
kubectl wait --for=condition=ContainersReady pod -l app=apod-web
# 通过 8016端口访问 service ,查看今天的图片
# 现在运行一个 sleep Pod:
kubectl apply -f sleep.yaml
# 确认 sleep pod 可以使用 API:
kubectl exec deploy/sleep -- curl -s http://apod-api/image
# 查看 access log 的 metrics 信息:
kubectl exec deploy/sleep -- sh -c 'curl -s http://apod-log/metrics |
head -n 2'
```
你可以清楚地看到这个练习中的问题——整个集群是完全开放的,所以你可以从 sleep Pod 访问 APOD API 和来自访问日志组件的指标图 ,16.2 显示了我的输出。让我们明确一点,没有什么特别的关于 sleep pod这只是演示问题的一种简单方法。集群中的任何容器都可以做同样的事情。
![图16.2](./images/Figure16.2.png)
<center>图 16.2 Kubernetes 扁平网络模型的缺点是每个 Pod 都是可访问的。</center>
Pod 应该是隔离的,以便它们只从需要访问它们的组件接收流量,并且它们只将流量发送到它们需要访问的组件。网络策略使用限制传入流量的入口规则(不要将它们与入口资源混淆)和限制传出流量的出口规则建模。在 APOD 应用程序中,唯一应该有权访问 API 的组件是 Web 应用程序。清单 16.1 将其显示为 NetworkPolicy 对象中的入口规则。
> 清单 16.1 networkpolicy-api.yaml通过标签限制对 Pod 的访问
```
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: apod-api
spec:
podSelector: # 规则生效的 POD
matchLabels:
app: apod-api
ingress: # 规则默认是拒绝策略, 所以这个规则 this rule
from: # 拒绝所有的流入流量 除了
- podSelector: # 来自匹配标签 apod-web 的 pod 流量
matchLabels:
app: apod-web
ports: # 限定端口
- port: api # 这个端口名字是 API pod 中 spec 中配置的
```
NetworkPolicy 规范相当简单,并且可以在应用程序之前部署规则,因此一旦 Pod 启动它就是安全的。入口和出口规则遵循相同的模式,并且都可以使用命名空间选择器和 Pod 选择器。您可以创建全局规则,然后在应用程序级别使用更细粒度的规则覆盖它们。
网络策略的一个大问题——当你部署规则时,它们可能不会做任何事情。就像 Ingress 对象需要一个入口控制器来对它们进行操作一样NetworkPolicy 对象依赖于集群中的网络实现来强制执行它们。当您在下一个练习中部署此策略时,您可能会失望地发现 APOD API 仍不限于 Web 应用程序。
现在就试试, 应用网络策略,看看您的集群是否真正执行了它。
```
# 创建 policy:
kubectl apply -f apod/update/networkpolicy-api.yaml
# 确认已创建:
kubectl get networkpolicy
# 尝试从 sleep pod 访问 API—这是不被策略所允许的:
kubectl exec deploy/sleep -- curl http://apod-api/image
```
您可以在图 16.3 中看到 sleep Pod 可以访问 API——限制进入 Web Pod 的 NetworkPolicy 被完全忽略。我在 Docker Desktop 上运行它,但你会在 K3s、AKS 或 EKS 中使用默认设置获得相同的结果。
![图16.3](./images/Figure16.3.png)
<center>图 16.3 Kubernetes 集群中的网络设置可能不会强制执行网络策略 </center>
Kubernetes 中的网络层是可插入的,并非每个网络插件都支持 NetworkPolicy 实施。标准集群部署中的简单网络不受支持,因此您会陷入这种棘手的情况,您可以部署所有 NetworkPolicy 对象,但除非您对其进行测试,否则您不知道它们是否被强制执行。云平台在这里有不同级别的支持。您可以在创建 AKS 群集时指定网络策略选项;使用 EKS您需要在创建集群后手动安装不同的网络插件。
这对你进行这些练习(以及我编写它们)来说非常令人沮丧,但它会给组织带来更危险的脱节在生产中使用 Kubernetes。您应该在构建周期的早期采用安全控制以便在您的开发和测试环境中应用 NetworkPolicy 规则使用接近生产的配置运行应用程序的环境。错误配置的网络策略很容易破坏您的应用程序,但如果您的非生产环境不执行策略,您将不会知道这一点。
如果您想了解 NetworkPolicy 的实际应用,下一个练习将使用 Kind 和 Calico 创建一个自定义集群Calico 是一个实施策略的开源网络插件。为此,您需要安装 Docker 和 Kind 命令行。请注意:此练习会更改 Docker 的 Linux 配置,并会使您的原始集群无法使用。 Docker Desktop 用户可以使用 Reset Kubernetes 按钮修复所有问题Kind 用户可以用新集群替换旧集群,但其他设置可能就没那么幸运了。跳过这些练习并通读我的输出就可以了;我们将在下一节中切换回您的普通集群。
现在就试试,使用 Kind 创建一个新集群,并部署一个自定义网络插件。
```
# install the Kind command line using instructions at
# https://kind.sigs.k8s.io/docs/user/quick-start/
# create a new cluster with a custom Kind configuration:
# 按照说明安装 kind 命令行https://kind.sigs.k8s.io/docs/user/quick-start/ 使用自定义类配置创建新群集
kind create cluster --image kindest/node:v1.18.4 --name kiamol-ch16
--config kind/kind-calico.yaml
# 安装 Calico 网络 plugin:
kubectl apply -f kind/calico.yaml
# 等待 Calico 启动:
kubectl wait --for=condition=ContainersReady pod -l k8s-app=calico-
node -n kube-system
# 确认集群已 ready:
kubectl get nodes
```
我在图 16.4 中的输出是缩写的;您会看到在 Calico 部署中创建了更多对象。最后,我有一个执行网络策略的新集群。不幸的是,要知道您的集群是否使用执行策略的网络插件的唯一方法是使用您知道执行策略的网络插件设置您的集群。
![图16.4](./images/Figure16.4.png)
<center>图 16.4 安装 Calico 为您提供了一个具有网络策略支持的集群——以您的其他集群为代价。</center>
现在我们可以再试一次。这个集群是全新的没有任何运行应用但是当然Kubernetes 清单是可移植的,所以我们可以快速再次部署 APOD 应用程序并进行试用。 Kind 支持运行具有不同配置的不同 Kubernetes 版本的多个集群,因此它是测试环境的绝佳选择,但它不像 Docker Desktop 或 K3s 那样对开发人员友好)。
现在就试试,重复 APOD 和sleep 部署,并确认网络策略阻止未经授权的流量。
```
# 部署 APOD app 到新集群:
kubectl apply -f apod/
# 等待启动:
kubectl wait --for=condition=ContainersReady pod -l app=apod-web
# 部署 sleep Pod:
kubectl apply -f sleep.yaml
# 确认 sleep Pod 可以访问 APOD API:
kubectl exec deploy/sleep -- curl -s http://apod-api/image
# 创建 network policy:
kubectl apply -f apod/update/networkpolicy-api.yaml
```
```
# 确认 sleep Pod 无法访问 API:
kubectl exec deploy/sleep -- curl -s http://apod-api/image
# 确认 APOD web app 仍然可以:
kubectl exec deploy/apod-web -- wget -O- -q http://apod-api/image
```
图 16.5 显示了我们第一次的预期:只有 APOD 网络应用程序可以访问 API并且 sleep 应用程序在尝试连接时超时,因为网络插件阻止了流量。
![图16.5](./images/Figure16.5.png)
<center>图 16.5 Calico 强制执行策略,因此只允许从 Web Pod 到 API Pod 的流量。</center>
网络策略是 Kubernetes 中重要的安全控制,它们对习惯于防火墙和隔离网络的基础设施团队很有吸引力。但是,如果您确实选择采用政策,则需要了解政策在哪些方面适合您的开发人员工作流程。如果工程师在没有强制执行的情况下运行他们自己的集群,而您只是在管道的后期应用策略,那么您的环境具有非常不同的配置,并且某些东西会被破坏。
我在这里只介绍了 NetworkPolicy API 的基本细节,因为集群配置比策略资源更复杂。如果您想进一步探索,有一个很棒的 GitHub 存储库,其中包含由 Google 工程师 Ahmet Alp Balkan 发布的网络策略配方: https://github.com/ahmetb/kubernetes-network-policy-recipes 。
现在让我们清理新集群,看看你的旧集群是否仍然有效。
现在就试试,删除 Calico 集群,并查看旧集群是否仍可访问。
```
# 删除新集群:
kind delete cluster --name kiamol-ch16
# 查看你的 Kubernetes contexts:
kubectl config get-contexts
# 切换到之前的集群:
kubectl config set-context <your_old_cluster_name>
# 查看:
kubectl get nodes
```
由于 Calico 所做的网络更改,您之前的集群可能无法再访问,即使 Calico 现在没有运行。图 16.6 显示我即将点击 Docker Desktop 中的 Reset Kubernetes 按钮;如果您使用的是 Kind则需要删除并重新创建您的原始集群如果您使用的是其他集群并且它不起作用。 . .我确实警告过你。
![图16.6](./images/Figure16.6.png)
<center>图 16.6 在容器中运行的 Calico 能够重新配置我的网络并破坏一切。</center>
现在我们都恢复正常了(希望如此),我们可以继续保护容器本身,这样应用程序就没有像重新配置网络堆栈这样的特权了。
## 16.2 使用安全上下文(security contets)限制容器功能
容器安全性实际上是关于 Linux 安全性和容器用户的访问模型Windows Server 容器具有不同的用户模型,但不存在相同的问题)。 Linux 容器通常以 root 超级管理员帐户运行,除非您明确配置用户,否则容器内的 root 也是主机上的 root。如果攻击者可以突破以 root 身份运行的容器,他们现在就可以控制您的服务器。这是所有容器运行时的问题,但 Kubernetes 本身又增加了一些问题。
在下一个练习中,您将使用基本部署配置运行 Pi Web 应用程序。该容器镜像打包在 Microsoft 的官方 .NET Core 应用程序运行时镜像之上。 Pod 规范并非故意不安全,但您会发现默认设置并不令人鼓舞。
现在就试试,运行一个简单的应用程序,并检查默认的安全情况。
```
# 部署 app:
kubectl apply -f pi/
# 等待容器启动:
kubectl wait --for=condition=ContainersReady pod -l app=pi-web
# 输出 pod 容器的用户名:
kubectl exec deploy/pi-web -- whoami
# 尝试访问 Kubernetes API server:
kubectl exec deploy/pi-web -- sh -c 'curl -k -s
https://kubernetes.default | grep message'
# 输出 API access token:
kubectl exec deploy/pi-web -- cat /run/secrets/kubernetes.io/
serviceaccount/token
```
行为很可怕:应用程序以 root 身份运行,它可以访问 Kubernetes API 服务器,它甚至设置了一个令牌,以便它可以通过 Kubernetes 进行身份验证。图 16.7 展示了这一切。以 root 身份运行会放大攻击者可以在应用程序代码或运行时中找到的任何漏洞。访问 Kubernetes API 意味着攻击者甚至不需要突破容器——他们可以使用令牌查询 API 并做一些有趣的事情,比如获取 Secrets 的内容(取决于 Pod 的访问权限) ,您将在第 17 章中了解)。
![图16.7](./images/Figure16.7.png)
<center>图 16.7 如果您听说过“默认安全”这句话,那么它并不是关于 Kubernetes 的</center>
Kubernetes 在 Pod 和容器级别提供多种安全控制,但默认情况下未启用它们,因为它们可能会破坏您的应用程序。您可以以不同的用户身份运行容器,但某些应用程序只有在以 root 用户身份运行时才能运行。您可以删除 Linux 功能以限制容器可以执行的操作,但某些应用程序功能可能会失败。这就是自动化测试的用武之地,因为您可以越来越多地加强应用程序的安全性,在每个阶段运行测试以确认一切仍然有效。
您将使用的主要控件是 SecurityContext 字段,它在 Pod 和容器级别应用安全性。清单 16.2 显示了一个明确设置用户和 Linux 组(用户集合)的 Pod SecurityContext因此 Pod 中的所有容器都以未知用户而不是 root 身份运行。
> 清单 16.2 deployment-podsecuritycontext.yaml作为特定用户运行
```
spec: # 这是 deployment 的spec
securityContext: # 此控制生效到所有POd 的容器
runAsUser: 65534 # 以 “unknown” 用户运行
runAsGroup: 3000 # 以不存在的 group 运行
```
这很简单,但是离开 root 会产生影响,并且 Pi spec 需要做更多的改变。该应用程序在容器内侦听端口 80而 Linux 需要提升权限才能侦听该端口。 Root有权限新用户没有权限所以应用会启动失败。它需要在环境变量中进行一些额外的配置以将应用程序设置为侦听端口 5001这对新用户有效。这是您需要为每个应用程序或每个类别的应用程序推出的那种细节只有当应用程序停止工作时您才会找到这些要求。
现在就试试,部署受保护的 Pod 规范。这使用非 root 用户和不受限制的端口,但服务中的端口映射向消费者隐藏了该详细信息。
```
# 添加非 root SecurityContext:
kubectl apply -f pi/update/deployment-podsecuritycontext.yaml
# 等待新 Pod 启动:
kubectl wait --for=condition=ContainersReady pod -l app=pi-web
# 确认用户:
kubectl exec deploy/pi-web -- whoami
# 查看 API token 文件:
kubectl exec deploy/pi-web -- ls -l /run/secrets/kubernetes.io/
serviceaccount/token
# 输出 access token
kubectl exec deploy/pi-web -- cat /run/secrets/kubernetes.io/
serviceaccount/token
```
以非 root 用户身份运行解决了应用程序漏洞升级为完全服务器接管的风险,但如图 16.8 所示,它并没有解决所有问题。 Kubernetes API 令牌的安装权限允许任何帐户读取它,因此攻击者仍然可以在此设置中使用该 API。他们可以用 API 做什么取决于你的集群是如何配置的——在 Kubernetes 的早期版本中,他们可以做任何事情。访问 API 服务器的身份与 Linux 用户不同,它可能在集群中具有管理员权限,即使容器进程以最低权限用户身份运行。
![图16.8](./images/Figure16.8.png)
<center>图 16.8 您需要一种深入的 Kubernetes 安全方法;一种设置是不够的。</center>
Pod spec 中的一个选项阻止 Kubernetes 安装访问令牌,你应该为每个实际上不需要使用 Kubernetes API 的应用程序包含访问令牌——这几乎是所有的东西,除了像入口控制器这样的工作负载,它需要找到服务端点。这是一个安全的设置选项,但下一级运行时控制将需要更多的测试和评估。容器 spec 中的 SecurityContext 字段允许比 Pod 级别更细粒度的控制。清单 16.3 显示了一组适用于 Pi 应用程序的选项
> 清单 16.3 deployment-no-serviceaccount-token.yaml更严格的安全策略
```
spec:
automountServiceAccountToken: false # 卸掉 API token
securityContext: # 生效到所有 containers
runAsUser: 65534
runAsGroup: 3000
containers:
- image: kiamol/ch05-pi
# …
securityContext: # 仅针对当前容器
allowPrivilegeEscalation: false # 上下文设置阻止进程
capabilities: # 升级到更高的权限,
drop: # 并删除所有附加功能。
- all
```
capabilities 字段允许您显式添加和删除 Linux 内核功能。此应用程序可以在所有功能被删除的情况下正常运行,但其他应用程序将需要重新添加一些功能。此应用程序不支持的一项功能是 readOnlyRootFilesystem 选项。如果您的应用程序可以使用只读文件系统,那么这是一个强大的功能,因为这意味着攻击者无法写入文件,因此他们无法下载恶意脚本或二进制文件。你能走多远取决于你的组织的安全配置文件。您可以要求所有应用程序都需要以非 root 用户身份运行,放弃所有功能并使用只读文件系统,但这可能意味着您需要重写大部分应用程序。
一种务实的方法是在容器级别尽可能严密地保护现有应用程序,并确保围绕其余策略和流程提供深入的安全保护。 Pi 应用程序的最终规范并不完全安全,但它是对默认设置的重大改进——应用程序仍然可以运行。
现在就试试,使用最终的安全配置更新 Pi 应用程序。
```
# 更新清单 16.3 中的 pod spec:
kubectl apply -f pi/update/deployment-no-serviceaccount-token.yaml
# 确认 API token 不存在了:
kubectl exec deploy/pi-web -- cat /run/secrets/kubernetes.io/
serviceaccount/token
# 确认 API Server 任然可以访问:
kubectl exec deploy/pi-web -- sh -c 'curl -k -s
https://kubernetes.default | grep message'
# 获得 url 检查 app 任然正常工作:
kubectl get svc pi-web -o jsonpath='http://{.status.loadBalancer
.ingress[0].*}:8031'
```
如图 16.9 所示,应用程序仍然可以访问 Kubernetes API 服务器,但它没有访问令牌,因此攻击者需要做更多的工作才能发送有效的 API 请求。应用 NetworkPolicy 拒绝进入 API 服务器将完全删除该选项。
![图16.9](./images/Figure16.9.png)
<center>图 16.9 受保护的应用程序对用户来说是一样的,但对攻击者来说乐趣却少得多。</center>
您需要投资以增加应用程序的安全性,但如果您的应用程序平台范围相当小,则可以构建通用配置文件:您可能会发现所有 .NET 应用程序都可以非根用户运行但需要可写文件系统,并且您所有的 Go 应用程序都可以使用只读文件系统运行,但需要添加一些 Linux 功能。那么,挑战在于确保您的配置文件实际得到应用,而 Kubernetes 有一个很好的功能准入控制admission control
## 16.3 使用 webhook 阻止和修改工作负载
您在 Kubernetes 中创建的每个对象都会经过一个过程来检查集群是否可以运行该对象。这个过程就是准入控制,我们在第 12 章中看到了一个准入控制器在工作,它试图部署一个 Pod 规范,该规范请求的资源多于命名空间可用的资源。 ResourceQuota 准入控制器是一个内置控制器,它会在工作负载超过配额时停止运行,并且 Kubernetes 有一个插件系统,因此您可以添加自己的准入控制规则。
其他两个控制器增加了这种可扩展性ValidatingAdmissionWebhook它像 ResourceQuota 一样工作以允许或阻止对象创建,以及 MutatingAdmissionWebhook它实际上可以编辑对象规范因此创建的对象与请求不同。两个控制器的工作方式相同您创建一个配置对象指定要控制的对象生命周期和应用规则的 Web 服务器的 URL。图 16.10 显示了这些片段是如何组合在一起的。
![图16.10](./images/Figure16.10.png)
<center>图 16.10 Admission webhooks 允许您在创建对象时应用自己的规则。</center>
Admission webhooks 非常强大,因为 Kubernetes 调用您自己的代码,这些代码可以以您喜欢的任何语言运行,并且可以应用您需要的任何规则。在本节中,我们将应用一些我用 Node.js 编写的 webhook。您不需要编辑任何代码但您可以在清单 16.4 中看到代码并不是特别复杂。
> 清单 16.4 validate.js用于验证 webhook 的自定义逻辑
```
# 传入的请求具有对象规范——这将检查服务令牌挂载属性是否设置为false;
# 如果不是,响应将停止创建对象:
if (object.spec.hasOwnProperty("automountServiceAccountToken")) {
admissionResponse.allowed =
(object.spec.automountServiceAccountToken == false);
}
```
Webhook 服务器可以在任何地方运行——集群内部或外部——但它们必须在 HTTPS 上提供服务。如果您想在集群内运行由您自己的证书颁发机构 (CA) 签名的 webhook唯一的麻烦就来了因为 webhook 配置需要一种信任 CA 的方法。这是一个常见的场景,所以我们将在接下来的练习中逐步介绍这种复杂性。
现在就试试,首先创建证书并部署 webhook 服务器以使用该证书。
```
# 运行 Pod 生成 certificate:
kubectl apply -f ./cert-generator.yaml
# 当容器 ready, certificate 就生成了:
kubectl wait --for=condition=ContainersReady pod -l app=cert-generator
# POD 已经将 cert 部署作为 TLS Secret:
kubectl get secret -l kiamol=ch16
# 部署 webhook server, 使用 TLS Secret:
kubectl apply -f admission-webhook/
# 输出 CA certificate:
kubectl exec -it deploy/cert-generator -- cat ca.base64
```
该练习中的最后一个命令将用 Base64 编码的文本填充您的屏幕,您将在下一个练习中需要它(不过不要担心写下来;我们将自动执行所有步骤)。您现在已经运行了 webhook 服务器,它由自定义 CA 颁发的 TLS 证书保护。我的输出如图 16.11 所示。
![图16.11](./images/Figure16.11.png)
<center>图 16.11 Webhooks 具有潜在危险,因此需要使用 HTTPS 对其进行保护。</center>
Node.js 应用程序正在运行并具有两个端点:一个验证 webhook它检查所有 Pod 规范是否将 automountServiceAccountToken 字段设置为 false以及一个变异 webhook它应用设置了 runAsNonRoot 标志的容器 SecurityContext。这两个策略旨在协同工作以确保所有应用程序的基本安全级别。清单 16.5 显示了 ValidatingWebhookConfiguration 对象的规范。
> 清单 16.5 validatingWebhookConfiguration.yaml应用 webhook
```
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: servicetokenpolicy
webhooks:
- name: servicetokenpolicy.kiamol.net
rules: # 这些是
- operations: [ "CREATE", "UPDATE" ] # 调用webhook-all pod
apiGroups: [""] # 的对象类型和操作。
apiVersions: ["v1"]
resources: ["pods"]
clientConfig:
service:
name: admission-webhook # 要调用的webhook服务
namespace: default
path: "/validate" # webhook URL
caBundle: {{ .Values.caBundle }} # CA certificate
```
Webhook 配置灵活可以设置操作的类型和webhook操作的对象类型。您可以为同一个对象配置多个 webhooks——验证 webhooks 都是并行调用的,其中任何一个都可以阻止操作。这个 YAML 文件是我为这个配置对象使用的 Helm chart 的一部分,作为注入 CA 证书的简单方法。更高级的 Helm chart 将包括生​​成证书和部署 webhook 服务器以及配置的作业——但是你不会看到它们是如何组合在一起的。
现在就试试,部署 webhook 配置,将 CA 证书作为值从生成器 Pod 传递到本地 Helm chart。然后尝试部署一个应用程序但该策略未通过该策略。
```
# 安装 configuration object:
helm install validating-webhook admission-webhook/helm/validating-
webhook/ --set caBundle=$(kubectl exec -it deploy/cert-generator
-- cat ca.base64)
# 确认已创建:
kubectl get validatingwebhookconfiguration
# 部署一个app:
kubectl apply -f vweb/v1.yaml
# 检查 webhook logs:
kubectl logs -l app=admission-webhook --tail 3
# 显示 app ReplicaSet status :
kubectl get rs -l app=vweb-v1
# 查看详情:
kubectl describe rs -l app=vweb-v1
```
在本练习中,您可以看到验证 webhook 的优势和局限性。 webhook 在 Pod 级别运行,如果 Pod 与服务令牌规则不匹配,它会停止创建 Pod。但尝试创建 Pod 的是 ReplicaSet 和 Deployment它们不会被准入控制器阻止因此您必须更深入地挖掘才能找到应用程序未运行的原因。我的输出如图 16.12 所示,其中 describe 命令被删减以仅显示错误行。
![图16.12](./images/Figure16.12.png)
<center>图 16.2 验证 webhook 可以阻止对象的创建,无论它是由用户还是控制器发起的。</center>
您需要仔细考虑您希望 webhook 执行的对象和操作。这种验证可以在 Deployment 级别进行,这将提供更好的用户体验,但它会错过直接创建的 Pod 或由其他类型的控制器创建的 Pod。在 webhook 响应中返回一条明确的消息也很重要,这样用户就知道如何解决问题。 ReplicaSet 将继续尝试创建 Pod 并失败(在我写这篇文章时它在我的集群上尝试了 18 次),但失败消息告诉我该怎么做,而且这个很容易修复。
admission webhooks 的问题之一是它们在可发现性方面的得分非常低。您可以使用 kubectl 检查是否配置了任何验证 webhook但这并没有告诉您任何关于实际规则的信息因此您需要在集群外部记录这些内容。变异的 webhook 使情况变得更加混乱,因为如果它们按预期工作,它们会为用户提供与他们试图创建的对象不同的对象。在下一个练习中,您将看到一个善意的变异 webhook 可以破坏应用程序。
现在就试试,使用相同的 webhook 服务器但不同的 URL 路径配置可变 webhook。此 webhook 将安全设置添加到 Pod 规范。部署另一个应用程序,您会看到来自 webhook 的更改使应用程序停止运行。
```
# 部署 webhook configuration:
helm install mutating-webhook admission-webhook/helm/mutating-webhook/
--set caBundle=$(kubectl exec -it deploy/cert-generator -- cat
ca.base64)
# 确认已创建:
kubectl get mutatingwebhookconfiguration
# 部署一个新的 web app:
kubectl apply -f vweb/v2.yaml
# 输出 webhook server logs:
kubectl logs -l app=admission-webhook --tail 5
# 查看 ReplicaSet 状态:
kubectl get rs -l app=vweb-v2
# 查看详情:
kubectl describe pod -l app=vweb-v2
```
哦亲爱的。可变 webhook 将 SecurityContext 添加到 Pod 规范,其中 runAsNonRoot 字段设置为 true。该标志告诉 Kubernetes 不要在 Pod 中运行任何容器,如果它们被配置为以 root 运行——这个应用程序就是这样,因为它基于官方 Nginx 镜像,它确实使用 root。正如你在图 16.13 中看到的,描述 Pod 告诉你问题是什么,但它并没有说明规范已经改变。当用户再次检查他们的 YAML 并发现没有 runAsNonRoot 字段时,他们会非常困惑。
![图16.13](./images/Figure16.13.png)
<center>图 16.13 变异的 webhooks 会导致应用程序故障,这很难调试。</center>
变异 webhook 中的逻辑完全取决于您——您可能会不小心更改对象以设置它们永远不会部署的无效规范。为您的 webhook 配置设置一个限制性更强的对象选择器是个好主意。清单 16.5 适用于每个 Pod但您可以添加命名空间和标签选择器来缩小范围。这个 webhook 是用合理的规则构建的,如果 Pod 规范已经包含一个 runAsNonRoot 值webhook 就不会管它,所以应用程序可以被建模为明确需要 root 用户。
Admission Controller webhooks 是一个有用的工具,可以让你做一些很酷的事情。您可以使用可变 webhook 将 sidecar 容器添加到 Pod这样您就可以使用标签来识别所有写入日志文件的应用程序并让 webhook 自动将日志记录 sidecar 添加到这些 Pod。 Webhooks 可能很危险,您可以通过配置对象中的良好测试和选择性规则来缓解这种情况,但它们将始终不可见,因为逻辑隐藏在 webhook 服务器内部。
在下一节中,我们将研究另一种方法,该方法在底层使用验证 webhook但将它们包装在管理层中。 Open Policy Agent (OPA) 允许您在 Kubernetes 对象中定义规则,这些对象在集群中是可发现的并且不需要自定义代码。
## 16.4 使用 Open Policy Agent 控制准入
OPA 是编写和实施策略的统一方法。目标是提供一种标准语言来描述各种策略和集成,以在不同平台上应用策略。你可以描述数据访问策略并将它们部署在 SQL 数据库中,你可以描述 Kubernetes 对象的准入控制策略。 OPA 是另一个 CNCF 项目,它为使用 OPA Gatekeeper 的自定义验证 webhooks 提供了更简洁的替代方案。
OPA Gatekeeper 具有三个部分:您在集群中部署 Gatekeeper 组件,其中包括一个 webhook 服务器和一个通用的 ValidatingWebhookConfiguration然后创建一个约束模板它描述了准入控制策略然后根据模板创建特定约束。这是一种灵活的方法您可以为“所有 Pod 必须具有预期的标签”策略构建一个模板,然后部署一个约束来说明在哪个命名空间中需要哪些标签。
我们将首先删除我们添加的自定义 webhook 并部署 OPA Gatekeeper准备应用一些准入策略。
现在就试试,卸载 webhook 组件,并部署 Gatekeeper。
```
# 卸载:
helm uninstall mutating-webhook
helm uninstall validating-webhook
# 删除 Node.js webhook server:
kubectl delete -f admission-webhook/
# 部署 Gatekeeper:
kubectl apply -f opa/
```
我在图 16.14 中简化了我的输出——当您运行该练习时,您会看到 OPA Gatekeeper 部署安装了更多的对象,包括我们尚未遇到的称为 CustomResourceDefinitions (CRD) 的对象。当我们着眼于扩展 Kubernetes 时,我们将在第 20 章中更详细地介绍这些内容,但就目前而言,知道 CRD 可以让您定义 Kubernetes 为您存储和管理的新对象类型就足够了。
![图16.14](./images/Figure16.14.png)
<center>图 16.14 OPA Gatekeeper 负责处理运行 webhook 服务器的所有棘手部分。</center>
Gatekeeper 使用 CRD因此您可以创建模板和约束作为普通 Kubernetes 对象,在 YAML 中定义并使用 kubectl 部署。该模板包含使用称为 Rego发音为“ray-go”的语言的通用策略定义。它是一种表达性语言可让您评估某些输入对象的属性以检查它们是否满足您的要求。学习是另一回事但 Rego 有一些很大的优势:策略相当容易阅读,并且它们存在于您的 YAML 文件中,因此它们不会隐藏在自定义 webhook 的代码中;并且有很多示例 Rego 策略来执行我们在本章中看到的那种规则。清单 16.6 显示了要求对象具有标签的 Rego 策略。
> 清单 16.6 requiredLabels-template.yaml一个基本的 Rego 策略
```
# 这将获取对象上的所有标签以及约束中所需的所有标签;
# 如果缺少必需的标签,就会妨碍对象的创建.
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("you must provide labels: %v", [missing])
}
```
您使用 Gatekeeper 部署该策略作为约束模板,然后部署强制执行该模板的约束对象。在这种情况下,名为 RequiredLabels 的模板使用参数来定义所需的标签。清单 16.7 显示了所有 Pod 具有应用和版本标签的特定约束。
> 清单 16.7 requiredLabels.yaml来自 Gatekeeper 模板的约束
```
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: RequiredLabels # API和Kind将其识别为RequiredLabels模板中的Gatekeeper约束。
metadata:
name: requiredlabels-app
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"] # 该约束适用于所有pod
parameters:
labels: ["app", "version"] # 需要设置两个标签
```
这更容易阅读,并且您可以从同一个模板部署许多约束。 OPA 方法允许您构建一个标准策略库,用户可以在他们的应用程序规范中应用该库,而无需深入研究 Rego。在下一个练习中您将部署清单 16.7 中的约束和另一个要求所有 Deployments、Services 和 ConfigMaps 都具有 kiamol 标签的约束。然后,您将尝试部署一个不符合所有这些政策的待办事项应用程序版本。
现在就试试,使用 Gatekeeper 部署所需的标签策略,并查看它们的应用方式。
```
# 首先创建约束模板:
kubectl apply -f opa/templates/requiredLabels-template.yaml
# 然后创建约束:
kubectl apply -f opa/constraints/requiredLabels.yaml
# 待办事项列表规范不符合策略:
kubectl apply -f todo-list/
# 确认应用程序没有部署:
kubectl get all -l app=todo-web
```
您可以在图 16.15 中看到这种用户体验很干净——我们尝试创建的对象没有所需的标签,因此它们被阻止了,我们在 kubectl 的输出中看到了来自 Rego 策略的消息。
![图16.15](./images/Figure16.15.png)
<center>图 16.15 部署失败显示从 Rego 策略返回的明确错误消息。</center>
Gatekeeper 使用验证 webhook 评估约束,当您创建的对象出现故障时,这一点非常明显。当控制器创建的对象验证失败时不太清楚,因为控制器本身可能没问题。我们在 16.3 节中看到,由于 Gatekeeper 使用相同的验证机制,因此存在相同的问题。您会看到,如果您更新待办事项应用程序,那么 Deployment 会满足标签要求,但 Pod 规范不会。
现在就试试,部署更新的待办事项列表规范,其中包含除 Pod 之外的所有对象的正确标签。
```
# 部署更新的清单:
kubectl apply -f todo-list/update/web-with-kiamol-labels.yaml
# 显示ReplicaSet的状态:
kubectl get rs -l app=todo-web
```
```
# 输出 detail:
kubectl describe rs -l app=todo-web
# 删除待办事项应用程序,为下一个练习做准备:
kubectl delete -f todo-list/update/web-with-kiamol-labels.yaml
```
在本练习中,您会发现准入策略有效,但只有当您深入了解失败的 ReplicaSet 的描述时,您才会发现问题,如图 16.16 所示。那不是很好的用户体验。您可以使用更复杂的策略来解决此问题,该策略适用于 Deployment 级别并检查 Pod 模板中的标签——这可以通过约束模板的 Rego 中的扩展逻辑来完成。
![图16.16](./images/Figure16.16.png)
<center>图 16.16 OPA Gatekeeper 实现了更好的流程,但它仍然是验证 webhook 的包装器。 </center>
我们将以下面一组涵盖更多生产最佳实践的准入政策来结束本节,所有这些都有助于提高您的应用程序的安全性:
- 所有 Pod 都必须定义容器探测器。这是为了保持您的应用程序健康,但失败的健康检查也可能表明来自攻击的意外活动。
- Pod 只能从已批准的镜像存储库运行容器。将容器限制在一组具有安全生产镜像的“黄金”存储库中,可确保无法部署恶意负载。
- 所有容器都必须设置内存和 CPU 限制。这可以防止受损容器最大化节点的计算资源并使所有其他 Pod 挨饿。
这些通用政策几乎适用于每个组织。您可以向它们添加约束,要求每个应用程序的网络策略和每个 Pod 的安全上下文。正如您在本章中了解到的,并非所有规则都是通用的,因此您可能需要选择如何应用这些约束。在下一个练习中,您将把生产约束集应用到单个命名空间。
现在就试试,部署一组新的约束和一个待办应用程序版本,其中 Pod 规范不符合大多数策略。
```
# 为生产约束创建模板:
kubectl apply -f opa/templates/production/
# 创建约束:
kubectl apply -f opa/constraints/production/
# 部署新的待办事项规范:
kubectl apply -f todo-list/production/
# 确认pod没有创建:
kubectl get rs -n kiamol-ch16 -l app=todo-web
#显示错误细节:
kubectl describe rs -n kiamol-ch16 -l app=todo-web
```
图 16.17 显示 Pod 规范不符合所有规则,除了一个——我的镜像存储库策略允许来自 kiamol 组织中的 Docker Hub 的任何镜像,因此待办事项应用程序镜像是有效的。但是没有版本标签,没有健康探测,也没有资源限制,这个规范不适合生产。
![图16.17](./images/Figure16.17.png)
<center>图 16.17 计算所有约束,您可以在 Rego 输出中看到完整的错误列表。 </center>
只是为了证明这些政策是可以实现的,并且 OPA Gatekeeper 实际上会让待办事项应用程序运行,您可以应用满足所有生产规则的更新规范。如果比较生产文件夹和更新文件夹中的 YAML 文件,您会看到新规范只是将必填字段添加到 Pod 模板;应用程序没有重大变化。
现在就试试,应用待办事项规范的生产就绪版本,并确认应用程序真正运行。
```
# 此规范满足所有生产策略:
kubectl apply -f todo-list/production/update
# 等待Pod启动:
kubectl wait --for=condition=ContainersReady pod -l app=todo-web -n
kiamol-ch16
# 确认它正在运行:
kubectl get pods -n kiamol-ch16 --show-labels
# 获取应用程序的URL并浏览:
kubectl get svc todo-web -n kiamol-ch16 -o jsonpath='http://{.status
.loadBalancer.ingress[0].*}:8019'
```
图 16.18 显示了在 OPA Gatekeeper 允许更新部署后应用程序正在运行。
![图16.18](./images/Figure16.18.png)
<center>图 16.18 约束很强大,但您需要确保应用程序能够真正遵守。 </center>
Open Policy Agent 是一种比自定义验证 webhook 更简洁的应用准入控制的方法,我们查看的示例策略只是一些简单的想法,可以帮助您入门。 Gatekeeper 没有突变功能,但如果您有明确的修改规范的案例,您可以将其与您自己的 webhooks 结合使用。您可以使用约束来确保每个 Pod 规范都包含一个应用程序配置文件标签,然后根据您的配置文件改变规范——将您的 .NET Core 应用程序设置为以非根用户身份运行,并为所有 Go 应用程序切换到只读文件系统。
保护你的应用程序就是关闭漏洞利用路径,一个彻底的方法包括我们在本章中介绍的所有工具等等。最后,我们将了解一个安全的 Kubernetes 环境。
## 16.5 深入了解 Kubernetes 中的安全性
构建管道可能会遭到破坏,容器镜像可能会被修改,容器可能会以特权用户身份运行易受攻击的软件,而有权访问 Kubernetes API 的攻击者甚至可能会控制您的集群。在您的应用程序被替换之前,您不会知道它是 100% 安全的,并且您可以确认在其运行期间没有发生安全漏洞。到达那个快乐的地方意味着在整个软件供应链中深入应用安全性。本章的重点是在运行时保护应用程序,但您应该在此之前扫描容器镜像以查找已知漏洞。
安全扫描器查看镜像内部,识别二进制文件,并在 CVE常见漏洞和暴露数据库中检查它们。扫描会告诉您是否存在已知漏洞位于镜像中的应用程序堆栈、依赖项或操作系统工具中。商业扫描器与托管注册表集成您可以将 Aqua Security 与 Azure 容器注册表结合使用或者您可以运行自己的Harbor 是 CNCF 注册表项目,它支持开源扫描器 Clair 和 TrivyDocker Desktop 与Snyk 用于本地扫描)。
您可以设置一个管道,只有在扫描清晰时镜像才会被推送到生产存储库。将其与存储库准入策略相结合,您可以有效地确保容器仅在镜像安全时运行。不过,在安全配置的容器中运行的安全镜像仍然是一个目标,您应该使用一种工具来查看运行时安全性,该工具可以监视容器的异常活动,并可以生成警报或关闭可疑行为。 Falco 是用于运行时安全的 CNCF 项目Aqua 和 Sysdig以及其他提供支持的商业选项。
现在就试试,删除我们创建的所有对象:
```
kubectl delete -f opa/constraints/ -f opa/templates/ -f
opa/gatekeeper.yaml
kubectl delete all,ns,secret,networkpolicy -l kiamol=ch16
```
## 16.6 实验室
在本章开头,我说过主机路径的卷挂载是一个潜在的攻击向量,但我们没有在练习中解决这个问题,所以我们将在实验室中进行。这是准入控制的完美场景,如果 Pod 使用在主机上挂载敏感路径的卷,则应该阻止它们。我们将使用 OPA Gatekeeper我已经为您编写了 Rego因此您只需编写一个约束即可。
- 首先在实验室文件夹中部署 gatekeeper.yaml。
- 然后在 restrictedPaths-template.yaml 中部署约束模板,您需要查看规范以了解如何构建约束。
- 编写并部署使用模板并限制这些主机路径的约束:/、/bin 和 /etc。该约束应仅适用于标签为 kiamol=ch16-lab 的 Pod。
- 在实验室文件夹中部署 sleep.yaml。您的约束应该阻止创建 Pod因为它使用受限的卷安装。
这一个相当简单,尽管您需要阅读匹配表达式,这就是 Gatekeeper 实现标签选择器的方式。我的解决方案在 GitHub 上: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch16/lab/README.md 。

View File

@ -0,0 +1,443 @@
# 第二章 Pods & Deployment 在 Kubernetes 中的应用
Kubernetes 通过容器来运行应用的工作负载,但是容器并不是在 Kubernetes 场景下你需要工作处理的对象。每个容器都属于某个叫做 Pod 的对象,它用于管理一个或多个容器,然后另外一个方面 Pods 被其它类型的资源所管理。这些高层级的对象抽象了具体的容器,它们提供了自我修复的程序并且提供了目标状态的工作流程:你告诉 Kubernetes 你想要实现的内容,然后 Kubernetes 来决定如何实现。
在本章中,我们将从 Kubernetes 基础的构建块开始Pods 用于运行容器,以及 Deployments 用于管理 Pods。我们将使用一个简单的 Web app 进行练习,让你亲自动手使用 Kubernetes 命令行工具来管理应用并且使用 Kubernetes YAML 配置文件来定义应用程序。
## 2.1 Kubernetes 如何运行并管理容器
一个容器通常情况下作为虚拟环境来运行单个应用程序的组件。Kubernetes 将容器包装在另一个虚拟环境中: Pod。Pod 是一个计算单元它在集群中的单个节点上运行。Pod 拥有受 Kubernetes 管理的的虚拟 IP 地址并且就算Pods 运行在不同节点,它们之间也可以通过 Kubernetes 的虚拟网络进行通信。
正常情况下你只会在一个 Pod 中运行一个容器,但是你可以在一个 Pod 中运行多个容器,这将会开启一些有趣的部署选项。一个 Pod 中的所有容器将拥有相同的虚拟环境,所以它们共享相同的网络地址并且可以通过 localhost 通信。图 2.1 显示了容器与 Pods 之间的关系。
![图2.1](./images/Figure2.1.png)
<center>图2.1 容器在 Pod 内运行。你管理 Pods, Pods 管理容器 </center>
多容器 Pods 的业务在早期介绍有点多,但如果我掩盖它,只谈论单容器 Pods你会理所当然地问为什么 Kubernetes 使用 Pods 而不是容器。让我们运行一个Pod然后看看使用容器上的这种抽象是什么样子的。
<b>现在就试试</b> 你可以通过 Kubernetes 命令行运行一个简单的 Pod(无需编写 YAML 文件)。语法类似于 Docker 运行容器: 你需要说明你要使用的容器的镜像以及任何用于配置 Pod 行为的其他参数。
```
# 运行一个单容器 Pod; restart 参数代表如果退出,不重启:
kubectl run hello-kiamol --image=kiamol/ch02-hello-kiamol --restart=Never
# 等待 Pod 就绪:
kubectl wait --for=condition=Ready pod hello-kiamol
# 查询所有的 Pod 清单:
kubectl get pods
# 查看 Pod 详细信息:
kubectl describe pod hello-kiamol
```
你可以看到图 2.2 是我执行时的输出,当你自己运行它时,你将在其中看到很多看起来晦涩难懂的信息,比如节点选择符和容错。他们都是 Pod 配置规范Kubernetes 已将缺省值应用于我们没有在 Run 命令中指定的配置。
![图2.2](./images/Figure2.2.png)
<center>图2.2 运行最简单的 Pods 并且使用 Kubectl 检查它的状态 </center>
现在你的集群中拥有一个应用容器,它运行在一个 Pod 内。如果你之前接触过 Docker这是一个熟悉的工作流程事实证明 Pods 并不像看起来那么复杂。你的大多数 Pods 都会运行单个容器(直到您开始探索更高级的选项),因此你可以有效地将 Pod 视为 Kubernetes 用来运行容器的一种机制。
Kubernetes 并不真正运行容器,它把这个责任交给节点上的容器运行时,可能会是 Docker 或者其他之类的。这就是为什么是抽象的:它是 Kubernetes 管理的资源,而容器则是 Kubernetes 之外的某个东西管理。你可以通过 Kubectl 来获取有关 Pod 的信息。
<b>现在就试试</b> Kubectl 从 get pod 命令返回基本的信息,你可以通过指定输出相关的参数来请求返回更多的内容。你可以说出要在输出参数中看到的各个字段,可以使用 JSONPath 查询语言或者 Go 模板来得到复杂的输出。
```
# 获取 Pod 基本信息:
kubectl get pod hello-kiamol
# 指定输出的自定义列, 选择了网络相关信息:
kubectl get pod hello-kiamol --output custom-
columns=NAME:metadata.name,NODE_IP:status.hostIP,POD_IP:status.podIP
# 指定查询 JSONPath ,
# 选择 Pod 中第一个容器的 ID:
kubectl get pod hello-kiamol -o
jsonpath='{.status.containerStatuses[0].containerID}'
```
我的输出如图 2.3 所示。我在 windows 上的 Docker Desktop 运行了一个单节点的 Kubernetes 集群,第二条命令中的节点 IP 是我的 Linux 虚机的 Ip 地址,然后 Pod IP 是集群中的 Pod 虚拟地址。第三条命令输出的 container ID 以容器运行时的名字作为前缀,我的是 Docker。
![图2.3](./images/Figure2.3.png)
<center>图2.3 Kubectl 有很多选项可以自定义各种对象包括 Pods 的输出 </center>
这可能会让人觉得很枯燥但它有两个重要的要点。首先kubectl 是一个非常强大的工具,作为你与 Kubernetes 的主要联系点,你将花费大量时间使用它,非常值得去理解它能做什么。查询命令的输出是查看您关心的信息的有用方法,因为您可以访问所有资源的详细信息,这对自动化也很有用。第二个要点是提醒 Kubernetes不运行容器Pod 中的容器 ID 是引用自另一个运行容器的系统。
Pods 在创建时被分配给一个节点,该节点负责管理 Pod 及其容器它通过使用容器运行时中的称为容器运行时接口CRI的已知API 来实现此操作。CRI 让节点以相同的方式为所有不同的容器运行时管理容器。它使用一个标准API来创建和删除容器并查询它们的状态。当 Pod 运行时,节点与容器运行时一起工作,以确保 Pod 有它需要的所有容器。
<b>现在就试试</b> 所有 Kubernetes 环境都使用相同的 CRI 机制来管理容器,但不是所有的容器运行时都允许您访问 Kubernetes 之外的容器。本练习向您展示Kubernetes 节点如何确保其 Pod 容器的运行,这里使用的是 Docker 容器运行时进行演示。
```
# 查询 Pod 中的容器:
docker container ls -q --filter
label=io.kubernetes.container.name=hello-kiamol
# 现在,删除容器:
docker container rm -f $(docker container ls -q --filter
label=io.kubernetes.container.name=hello-kiamol)
# 检查 Pod 状态:
kubectl get pod hello-kiamol
# 可以再次看到容器:
docker container ls -q --filter
label=io.kubernetes.container.name=hello-kiamol
```
从图 2.4 中可以看到,当我删除 Docker 容器时Kubernetes做出了反应一时间 Pod 没有了容器,但 Kubernetes 立即创建了一个替代容器来修复 Pod 并将其恢复到正确的状态。
![图2.4](./images/Figure2.4.png)
<center>图2.4 Kubernetes 确保 Pods 可以始终拥有它所需的容器 </center>
从容器到 Pods 的抽象可以让 Kubernetes 修复此类问题。一个失败的容器可能是临时的故障Pod 仍然存在,并且 Pod 可以用一个新的容器使其符合目标。这只是 Kubernetes 提供的自我修复的一个级别Kubernetes 在 Pods 之上提供了进一步的抽象,使你的应用程序更具弹性。
其中一个抽象是 Deployment我们将在下一节中讨论。在我们继续之前让我们看看Pod中到底在运行什么。这是一个 web 应用程序,但你还不能访问它,因为我们还没有配置 Kubernetes 来路由网络到 Pod 的流量。我们可以使用 kubectl 的另一个特性来解决这个问题。
<b>现在就试试</b> Kubectl 可以将流量从节点转发到Pod这是一个从集群外部与 Pod 通信的快速方式。你可以监听在计算机上的特定端口,该端口是集群中的单个节点上的——并将流量转发到 Pod 中运行的应用程序。
```
# 监听你机器的 8080 端口,并将流量发送到 Pod 的 80 端口
kubectl port-forward pod/hello-kiamol 8080:80
# 现在访问 http://localhost:8080
# 当你结束之后,可以通过 ctrl-c 结束端口转发
```
我的输出如图 2.5 所示,你可以发现它是一个非常基本的 web 网站。这个 Web服务器和所有的内容都已打包到 Docker Hub 上的容器镜像中该镜像是公开可用的。所有CRI-兼容的容器运行时可以提取镜像并从中运行容器,因此当您运行应用程序是,它对你的工作方式与对我的工作方式是相同的。
![图2.5](./images/Figure2.5.png)
<center>图 2.5 这个应用并没有配置接收网络流量,但是 Kubectl 可以转发网络流量 </center>
现在我们很好地了解了Pod它是Kubernetes 中最小的计算单元。你需要了解这一切是如何运作的但Pod是一个原始的资源在正常使用中您永远不会直接运行Pod您总是创建一个控制器对象来管理 Pod。
## 2.2 通过控制器运行 Pods
这只是第二章的第二节,我们即将开始讨论另一个 Kubernetes 对象它是对其他对象的抽象。Kubernetes 确实很快就会变得复杂,但这种复杂性是这种强大且可配置系统的必要组成部分。
单独使用 Pods 太简单了;它们是应用程序的隔离实例,每个 Pod 都分配给一个节点。如果该节点离线Pod 将丢失Kubernetes 不会替换它。您可以通过运行多个 Pods 来尝试获得高可用性,但不能保证 Kubernetes 不会将它们全部运行在同一个节点上。即使您将 Pods 分散在多个节点上,您仍需要自己管理它们。当您有一个可以为您管理它们的编排器时,为什么要这样做呢?
那就是控制器的作用了。控制器是一种 Kubernetes 资源,用于管理其他资源。它与 Kubernetes API 一起工作以观察系统的当前状态将其与其资源的期望状态进行比较并进行任何必要的更改。Kubernetes 有许多控制器,但用于管理 Pods 的主要控制器是 Deployment它可以解决我刚才描述的问题。如果一个节点离线并且你失去了一个 PodDeployment 就会在另一个节点上创建一个替换 Pod如果你想扩展你的 Deployment你可以指定你想要多少 PodsDeployment 控制器会将它们运行在许多节点上。图 2.6 显示了 Deployments、Pods 和容器之间的关系。
![图2.6](./images/Figure2.6.png)
<center>图2.6 Deployment 控制器管理 Pods, Pods 管理 容器</center>
您可以使用 kubectl 创建 Deployment 资源,指定要运行的容器镜像和 Pod 的任何其他配置。Kubernetes 创建 DeploymentDeployment 创建 Pod。
<b>现在就试试</b> 使用 Deployment 创建另一个 web 应用的实例。唯一需要的参数是 Deployment 的名称和要运行的镜像。
```
# 创建名为 "hello-kiamol-2" 的 deployment, 运行同样的 web 应用:
kubectl create deployment hello-kiamol-2 --image=kiamol/ch02-hello-kiamol
# 查询所有的 Pods:
kubectl get pods
```
您可以在图 2.7 中查看我的输出。现在您的集群中有两个 Pod使用 kubectl run 命令创建的原始 Pod以及由 Deployment 创建的新 Pod。由 Deployment 管理的 Pod 具有 Kubernetes 生成的名称,该名称是 Deployment 名称后面跟随一个随机后缀。
![图2.7](./images/Figure2.7.png)
<center>图2.7 创建一个控制器资源, 它负责创建自己的资源—Deployments 创建 Pods</center>
从这次练习中需要意识到的一个重要事情:您创建了 Deployment但您没有直接创建 Pod。Deployment 规范描述了您想要的 Pod并由 Deployment 创建了该 Pod。Deployment 是一个控制器,它使用 Kubernetes API 检查哪些资源正在运行,意识到应该管理的 Pod 不存在,并使用 Kubernetes API 创建它。确切的机制并不重要;您可以只与 Deployment 一起工作,并依靠它来创建您的 Pod。
但是Deployment 是如何跟踪其资源的确很重要,因为这是 Kubernetes 经常使用的一种模式。任何 Kubernetes 资源都可以应用简单的键值对标签。您可以添加标签来记录您自己的数据。例如,您可以为 Deployment 添加一个名为 release 的标签,并将值设置为 20.04,表示这个 Deployment 来自 20.04 发行周期。Kubernetes 也使用标签来松散耦合资源,映射 Deployment 及其 Pods 之类对象之间的关系。
<b>现在就试试</b> Deployment 在它管理的 Pods 上添加了标签,使用 Kubectl 来输出 Deployment 添加的标签,然后查询标签匹配的 Pods:
```
# 打印 Deployment 添加到 Pods 上的标签:
kubectl get deploy hello-kiamol-2 -o
jsonpath='{.spec.template.metadata.labels}'
# 列出标签匹配的 Pods:
kubectl get pods -l app=hello-kiamol-2
```
我的输出如图 2.8 所示您可以看到资源的配置方式的一些内部细节。Deployments 使用模板来创建 Pods该模板的一部分是元数据字段其中包括 Pod 的标签。在这种情况下Deployment 向 Pod 添加了一个名为 app 的标签,值为 hello-kiamol-2。查询具有匹配标签的 Pod 会返回由 Deployment 管理的单个 Pod。
![图2.8](./images/Figure2.8.png)
<center> 图2.8 当 Deployments 创建 Pods 时添加标签, 然后你可以使用这些标签进行过滤 </center>
使用标签来识别资源之间的关系是 Kubernetes 中的核心模式,因此值得显示一个图表来确保它是明确的。资源在创建时可以应用标签,然后在其生命周期内添加、删除或编辑。控制器使用标签选择器来识别其管理的资源。这可以是一个匹配具有特定标签的资源的简单查询,如图 2.9 所示。
![图2.9](./images/Figure2.9.png)
<center>图2.9 控制器通过使用标签和选择器来识别他们管理的资源.</center>
这个过程是灵活的,因为这意味着控制器不需要维护它们管理的所有资源的列表;标签选择器是控制器规范的一部分,控制器可以通过查询 Kubernetes API 来随时找到匹配的资源。这也是您需要小心的地方,因为您可以编辑资源的标签,最终导致它与其控制器之间的关系中断。
<b>现在就试试</b> Deployment 与它创建的 Pod 之间没有直接关系;它只需要知道一个带有与其标签选择器匹配的标签的 Pod。如果您编辑了 Pod 的标签Deployment 就不再识别它了。
```
# 查询所有 Pods, 显示 Pod 名称和标签:
kubectl get pods -o custom-
columns=NAME:metadata.name,LABELS:metadata.labels
# 更新 Deployment 的 Pod "app" 标签:
kubectl label pods -l app=hello-kiamol-2 --overwrite app=hello-kiamol-x
# 再次查询 Pods:
kubectl get pods -o custom-
columns=NAME:metadata.name,LABELS:metadata.labels
```
您期望发生什么您可以从图2.10中显示的输出中看出,更改 Pod 标签有效地将 Pod 从 Deployment 中删除。在那个时候Deployment 看到没有匹配其标签选择器的 Pod 存在,因此它创建了一个新的 Pod。Deployment 已经完成了它的工作,但通过直接编辑 Pod您现在拥有了一个不受管理的 Pod。
![图2.10](./images/Figure2.10.png)
<center>图2.10 如果您干扰了 Pod 上的标签,您可以将其从部署的控制中删除</center>
在调试中,这可能是一种有用的技术——将 Pod 从控制器中删除,这样您就可以连接并调查问题,而控制器启动了一个新的 Pod这样您的应用程序就可以按照所需的规模运行。您也可以做相反的事情编辑 Pod 的标签,使控制器被骗到把该 Pod 作为其管理的集合的一部分。
<b>现在就试试</b> 将原始 Pod 通过将其 app 标签设置为与标签选择器匹配来返回 Deployment 的控制。
```
# 查询包含标签 “app” 的 Pods, 显示 Pod 名称和标签:
kubectl get pods -l app -o custom-
columns=NAME:metadata.name,LABELS:metadata.labels
# 更新不受管理的 Pod 的 “app” 标签:
kubectl label pods -l app=hello-kiamol-x --overwrite app=hello-kiamol-2
# 再次查询 Pods:
kubectl get pods -l app -o custom-
columns=NAME:metadata.name,LABELS:metadata.labels
```
此练习实际上逆转了前一个练习,将 app 标签重新设置为 Deployment 中原始 Pod 的 hello-kiamol-2。现在当 Deployment 控制器与 API 检查时,它会看到两个与其标签选择器匹配的 Pod。然而它应该只管理一个 Pod因此它删除了其中一个使用一组删除规则来决定哪一个。您可以在图2.11中看到Deployment 删除了第二个 Pod并保留了原始 Pod。现在您拥有一个不受管理的 Pod。
![图2.11](./images/Figure2.11.png)
<center>图2.11 更多标签干扰—如果标签匹配,您可以强制部署采用 Pod </center>
Pod 运行您的应用程序容器但与容器一样Pod 的生命周期也很短。您通常会使用更高级别的资源(如 Deployment来为您管理 Pod。这样做会使 Kubernetes 在容器或节点出现问题时更容易维护您的应用程序,但最终 Pod 运行的容器与您自己运行的容器相同,而您的应用程序的最终用户体验也是相同的。
<b>现在就试试</b> Kubectl 的 port-forward 命令将流量发送到 Pod但您不必为 Deployment 找到随机的 Pod 名称。您可以在 Deployment 资源上配置端口转发,并且 Deployment 会选择其中一个 Pod 作为目标。
```
# 在你的本地机器运行一个 Deployment 的端口转发:
kubectl port-forward deploy/hello-kiamol-2 8080:80
# 访问 http://localhost:8080
# 结束之后, 通过 ctrl-c 退出
```
您可以看到我的输出如图2.12所示,容器中运行的同一个应用程序来自同一个 Docker 镜像,但这一次,它在由 Deployment 管理的 Pod 中运行。
![图2.12](./images/Figure2.12.png)
<center>图2.12 Pod 和 Deployment 是容器上面的一层,但应用程序仍然在容器中运行</center>
Pod 和 Deployment 是本章中唯一要介绍的资源。您可以使用 kubectl run 和 create 命令部署非常简单的应用程序,但更复杂的应用程序需要更多的配置,而这些命令将无法完成。是时候进入 Kubernetes YAML 的世界了。
## 2.3 在清单文件中定义 Deployments
应用程序清单是 Kubernetes 最具吸引力的方面之一,但也是最令人沮丧的方面之一。当您在几百行 YAML 中挣扎时,试图找到破坏应用程序的小配置错误,可能会认为 API 是故意写来混淆和激怒您的。在这些时候,请记住 Kubernetes 清单是应用程序的完整描述,可以对其进行版本控制并跟踪源控制,并且在任何 Kubernetes 集群上部署相同的应用程序。
清单可以用 JSON 或 YAML 编写JSON 是 Kubernetes API 的原生语言,但 YAML 更适合清单,因为它更容易阅读,允许您在单个文件中定义多个资源,最重要的是,它可以在规范中记录注释。清单 2.1 是您可以编写的最简单的应用程序清单。它定义了一个使用本章已经使用过的相同容器镜像的单个 Pod。
> 清单 2.1 pod.yaml, 单个 Pod 运行单个容器
```
# 清单同时指定了 Kubernetes API 的版本以及资源类型
apiVersion: v1
kind: Pod
# 资源的元数据包括名称(必填项)和标签(可选)
metadata:
name: hello-kiamol-3
# spec 是资源的实际规格,对于 Pod 最小值是要运行的容器:包括容器名称以及镜像
spec:
containers:
- name: web
image: kiamol/ch02-hello-kiamol
```
这比 kubectl run 命令所需的信息要多得多但应用程序清单的大优势是它是声明性的。Kubectl run 和 create 是命令性操作——你告诉 Kubernetes 做些什么。清单是声明性的——你告诉 Kubernetes 最终结果应该是什么,它就会去决定它需要做什么来使这种情况发生。
<b>现在就试试</b> 您仍然使用 kubectl 从清单文件部署应用程序,但您使用 apply 命令,告诉 Kubernetes 将文件中的配置应用于集群。使用与清单 2.1 相同的内容的 YAML 文件运行本章示例应用程序的另一个 pod。
```
# 切换到第二章练习源码目录:
cd ch02
# 基于清单文件部署应用:
kubectl apply -f pod.yaml
# 查询 Pods:
kubectl get pods
```
新的 Pod 与使用 kubectl run 命令创建的 Pod 工作方式相同它分配给一个节点并运行容器。图2.13 的输出显示当我应用清单时Kubernetes 决定需要创建一个 Pod 来使集群的当前状态达到我的期望状态。这是因为清单指定了一个名为 hello-kiamol-3 的 Pod但没有这样的 Pod。
![图2.13](./images/Figure2.13.png)
<center>图2.13 应用清单将YAML文件发送到Kubernetes API该API应用更改</center>
现在 Pod 正在运行,您可以使用 kubectl 以相同的方式进行管理:通过列出 Pod 的详细信息并运行端口转发来向 Pod 发送流量。大的不同在于清单易于共享,基于清单的部署是可重复的。我可以多次运行相同的 kubectl apply 命令和相同的清单,结果总是一样的:一个名为 hello-kiamol-3 的 Pod 运行我的 web 容器。
<b>现在就试试</b> Kubectl 并不一定需要本地的清单文件,它可以获取远程的 URL 信息,让我们通过 GitHub 中的文件来部署同样的 Pod。
```
# 从清单文件部署:
kubectl apply -f https://github.com/yyong-brs/learn-kubernetes/blob/master/kiamol/ch02/pod.yaml
```
图2.14 显示了输出。资源定义与集群中运行的 Pod 匹配,因此 Kubernetes 不需要做任何事情,而 kubectl 显示匹配的资源未更改。
![图2.14](./images/Figure2.14.png)
<center>图2.14 Kubectl 可以通过从远程服务器下载清单文件,并发送到 Kubernetes API 执行</center>
应用程序清单在与高级资源一起工作时变得更有趣。当您在 YAML 文件中定义 Deployment 时,其中一个必填字段是 Deployment 应运行的 Pod 的规范。该 Pod 规范是独立定义 Pod 的相同 API因此 Deployment 定义是一个复合体,包括 Pod spec。清单 2.2 显示了运行同一个 web 应用程序的另一个版本的 Deployment 资源的最小定义。
> 清单 2.2 deployment.yaml, Deployment 及 Pod 配置
```
# Deployments 是 apps 版本v1 API 规范的一部分
apiVersion: apps/v1
kind: Deployment
# Deployments 需要一个名称
metadata:
name: hello-kiamol-4
# spec 包括 Deployment 使用的标签选择器来找到它自己的资源 - 我正在使用 app 标签,但这可能是任何组合的键值对.
spec:
selector:
matchLabels:
app: hello-kiamol-4
# template 在 Deployment 创建 Pod 模板时使用.
# PDeployment 中的 Pods 没有名称,但它们需要指定与选择器匹配的标签
labels:
app: hello-kiamol-4
# Pod 规范列出容器名称和镜像规范
containers:
- name: web
image: kiamol/ch02-hello-kiamol
```
这个清单是用于一个完全不同的资源(它只是运行相同应用程序的巧合),但所有 Kubernetes 清单都是使用 kubectl apply 以相同方式部署的。这为您的所有应用程序提供了一个很好的一致性层,无论它们有多复杂,您都可以在一个或多个 YAML 文件中定义它们,并使用相同的 kubectl 命令部署它们。
<b>现在就试试</b> 这个指令是要求将 Deployment 清单应用于创建一个新的 Deployment这样可以创建一个新的 Pod。
```
# 使用 deployment 清单运行应用程序:
kubectl apply -f deployment.yaml
# 找到新 deployment 管理的 Pods:
kubectl get pods -l app=hello-kiamol-4
```
图 2.15 输出结果与使用 kubectl create 创建的 Deployment 相同,但整个应用程序规范都定义在单个 YAML 文件中。
![图2.15](./images/Figure2.15.png)
<center>图2.15 通过 deployment 清单可以创建一个 deployment因为没有匹配的资源存在</center>
随着应用程序复杂度的增加,我需要指定我想要的副本数量,应用的 CPU 和内存限制,以及 Kubernetes 如何检查应用程序是否健康,应用程序配置设置来自哪里,数据写入哪里——我可以通过添加 YAML 来实现所有这些。
## 2.4 应用在 Pods 中运行
Pods 和 Deployments 只是为了让你的应用程序运行,但真正的工作发生在容器里。容器运行时可能不允许你直接使用容器 ——一个托管的Kubernetes集群不会给你 Docker 或者 Containerd 的控制权-但您仍然可以使用kubectl在Pods中使用容器。Kubernetes命令行允许您在容器中运行命令查看应用程序日志复制文件。
<b>现在就试试</b> 您可以使用 kubectl 在容器内运行命令并连接终端会话,这样您就可以像连接远程计算机一样连接到 Pod 的容器。
```
# 检查我们运行的第一个 Pod 的内部 IP 地址:
kubectl get pod hello-kiamol -o custom-columns=NAME:metadata.name,POD_IP:status.podIP
# 在 Pod 中运行交互式 shell 命令:
kubectl exec -it hello-kiamol -- sh
# 在 Pod 中, 检查 IP address:
hostname -i
# 测试 web app:
wget -O - http://localhost | head -n 4
# 退出 shell:
exit
```
我的输出如图2.16所示,在图中可以看到,容器环境中的 IP 地址是 Kubernetes 设置的,容器中运行的 Web 服务器可以通过 localhost 地址访问。
![图2.16](./images/Figure2.16.png)
<center>图2.16 您可以使用 kubectl 在 Pod 容器内运行命令,包括交互式 shells.</center>
在 Pod 容器内部运行一个交互式 shell 是查看 Pod 中的世界的样子最有用的办法。你可以查看文件内容以检查相关配置是否正确,运行 DNS 查询以验证服务是否被正常识别,以及 ping 一些端点以测试网络。如上所述的所有都是很好的故障排除方法但对于持续的管理一个更简单的选择是读取应用程序日志kubectl有一个专门的命令就是这个为了日志的目的。
<b>现在就试试</b> Kubernetes从容器运行时获取应用程序日志。您可以使用kubectl阅读日志如果您有权访问容器运行时可以验证它们与容器日志是相同的。
```
# 打印 kuberneters 容器最新的日志:
kubectl logs --tail=2 hello-kiamol
# 和 Docker 原生的日志对比:
docker container logs --tail=2 $(docker container ls -q --filter
label=io.kubernetes.container.name=hello-kiamol)
```
从我的输出中可以看出如图2.17所示Kubernetes只是将日志条目完全转发自容器运行时。
![图2.17](./images/Figure2.17.png)
<center>图2.17 kubernetes 从容器读取日志,所以你不需要自己去访问容器运行时</center>
所有 Pod 都可以使用相同的功能,无论它们是如何创建的。由控制器管理的 Pod 名称是随机的,因此您不能直接引用它们。相反,您可以通过它们的控制器或标签来访问它们。
<b>现在就试试</b> 您可以在由 Deployment 管理的 Pod 中运行命令,而无需知道 Pod 名称,并且可以查看与标签选择器匹配的所有 Pod 的日志。
```
# 调用我们通过 Deployment YAML 创建的 POD 容器中的 web app:
kubectl exec deploy/hello-kiamol-4 -- sh -c 'wget -O - http://localhost
> /dev/null'
# 然后查看 Pod 日志:
kubectl logs --tail=1 -l app=hello-kiamol-4
```
图 2.18 显示了在 Pod 容器内运行的命令,该命令导致应用程序写入日志条目。我们可以在 Pod 日志中看到信息。
![图2.18](./images/Figure2.18.png)
<center>图2.18 通过 Kubect 命令无需知道 POD 的名称也可以操作 Pod.</center>
在生产环境中,您可以收集所有 Pod 的所有日志,并将它们发送到中央存储系统,但在这之前,这是一种读取应用程序日志的有用且简单的方法。您还在那个练习中看到,有不同的方法可以访问由控制器管理的 Pod。Kubectl 可以在大多数命令中提供标签选择器,并且一些命令 - 比如 exec - 可以针对不同的目标运行。
您最有可能使用的最后一个 Pod 功能是与文件系统交互。Kubectl 允许您在本地计算机和 Pod 中的容器之间复制文件。
<b>现在就试试</b> 创建您的计算机上的临时目录,并从 Pod 容器中复制文件到该目录。
```
# 创建本地目录:
mkdir -p /tmp/kiamol/ch02
# 从 Pod 中拷贝 web 界面源文件:
kubectl cp hello-kiamol:/usr/share/nginx/html/index.html
/tmp/kiamol/ch02/index.html
# 检查本地文件内容:
cat /tmp/kiamol/ch02/index.html
```
在图2.19中,您可以看到 kubectl 将文件从 Pod 容器复制到我的本地计算机上。无论您的 Kubernetes 集群是在本地运行还是在远程服务器上运行,它都是双向的,因此您可以使用相同的命令将本地文件复制到 Pod 中。这可能是一种有用的解决应用程序问题的方法。
![图2.19](./images/Figure2.19.png)
<center>图2.19. 在本地文件系统与 POD 之间拷贝文件可能会帮你解决一些问题</center>
在本章中,这就是我们要涵盖的所有内容,但在我们继续之前,我们需要删除我们正在运行的 Pod这比你想象的要复杂一些。
## 2.5 了解 Kubernetes 资源管理
您可以使用 kubectl 轻松删除 Kubernetes 资源,但该资源可能无法被真正删除掉。如果您使用控制器创建了资源,那么管理该资源就是控制器的工作。它拥有资源生命周期,并且不希望有任何外部干涉。如果您删除了受管资源,那么它的控制器将创建替代品。
<b>现在就试试</b> 使用 kubectl delete 命令删除所有 Pod 并验证它们是否真的已被删除。
```
# 查询所有运行的 Pods:
kubectl get pods
# 删除所有的 Pods:
kubectl delete pods --all
# 再次检查:
kubectl get pods
```
你可以看到 图 2.20 是我的输出,是你期望看到的结果吗?
![图2.20](./images/Figure2.20.png)
<center>图2.20 控制器拥有它们的资源,如果其它操作删除了它们,控制器将创建替换对象 </center>
这其中的两个 Pod 是通过 run 命令和 YAML Pod 规范直接创建的。它们没有控制器来管理它们,因此当您删除它们时,它们会保持删除状态。另外两个 Pod 是由 Deployment 创建的,当您删除 Pod 时Deployment 控制器仍然存在。它们会发现没有匹配它们标签选择器的 Pod因此它们会创建新的 Pod。
当您了解它时,这似乎是显而易见的问题,但这是一个很容易让人误解的地方,在您与 Kubernetes 合作的所有日子里都会出现。如果您想删除一个由控制器管理的资源,您需要删除控制器。当删除控制器时,它们会清理它们的资源,因此删除 Deployment 就像是一个级联删除,它也会删除 Deployment 的所有 Pod。
<b>现在就试试</b> 检查你运行的 Deployments然后删除它们最后确认剩余的 Pod 已经被删除。
```
# 查看 Deployments:
kubectl get deploy
# 删除所有的 Deployments:
kubectl delete deploy --all
# 查看 Pods:
kubectl get pods
# 检查所有的 resources:
kubectl get all
```
图 2.21 显示了我的输出。我的速度足够快可以看到Pods正在被删除因此它们处于终止状态。几秒钟后Pods和Deployment被删除因此我运行的唯一资源就是Kubernetes API服务器本身。
![图2.21](./images/Figure2.21.png)
<center>图2.21 删除控制器启动级联动作,控制器将删除所有它管理的资源</center>
现在您的Kubernetes集群没有运行任何应用程序并且已回到原始状态。
我们在本章中讲了很多内容。您对Kubernetes如何使用Pods和Deployments管理容器有了很好的理解同时对 YAML 规范有了介绍,并且使用 kubectl 与 Kubernetes API 进行了大量交互。我们逐步建立了核心概念但是现在您可能已经大致了解Kubernetes是一个复杂的系统。如果您有时间完成以下实验这将有助于巩固您所学的内容。
## 2.6 实验室
这是您的第一个实验,挑战你自己完成它。目标是编写一个 Kubernetes YAML规范用于在Pod中运行应用程序的Deployment然后测试应用程序以确保它按预期运行。以下是一些提示帮助您开始
- 在 ch02/lab 文件夹中,有一个名为 pod.yaml 的文件您可以尝试。它运行应用程序但定义了Pod 而不是 Deployment。
- 应用容器运行监听端口 80 的网站。
- 当您将流量转发到端口时Web 应用程序会以它正在运行的机器的主机名作为响应。
- 该主机名实际上是 Pod 名称您可以使用kubectl验证。
如果您觉得这有点棘手,我在 GitHub 上提供了以下示例解决方案您可以用作参考https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch02/lab/README.md。

View File

@ -0,0 +1,458 @@
# 第三章 通过 Service 网络连接 Pods
Pods 是 Kubernetes 中运行的应用程序的基本构建块。大多数应用是跨多组件分布式运行的,你通过在 kubernetes 中模式化pods 来对应每个组件。例如,你可能有一个 web 站点 Pod 以及一个 API Pod或者你有一打微服务架构的 Pods。它们之间需要通信kubernetes 支持标准的网络协议tcp 以及 UDP它们都通过 IP 地址来路由流量,但是当 Pods 被替换之后 IP 就变了,所以 Kubernetes 通过 Services 对象来提供网络地址发现机制。
Services 是支持 Pods 之间路由流量的灵活资源,可实现路由集群外的流量到 Pods 中,以及 Pods 到外部系统的流量。在本章,您将了解 Kubernetes 为将系统粘合在一起提供的所有不同 Service 配置, 你将会明白它们是如何透明地为你的应用程序工作的。
## 3.1 Kubernetes 如何路由网络流量
在前一章,你学到了两个关于 Pods 的重点:一个 Pod 一个拥有 Kubernetes 指定的 IP 地址的虚拟环境,以及 Pods 的生命周期是可以被其它类型资源自由控制的。如果一个 Pod 想要和其它 Pod 通信,可以使用 Ip 地址。然而这么干是有问题的,两个原因:第一,当 Pod 被替换时 Ip 会改变,第二,查找 Pod 的IP地址没有简单的方法只能通过 Kubernetes API。
<b>现在就试试</b> 如果你部署了两个 Pods你可以由其中一个 Ping 另外一个,前提是需要知道 IP 地址。
```
# 启动你的实验环境— 运行 Docker Desktop ,然后进入本章的源码目录:
cd ch03
# 创建两个 Deployments, 它们每一个都运行了一个 Pod:
kubectl apply -f sleep/sleep1.yaml -f sleep/sleep2.yaml
# 等待 Pod 到达 ready 状态:
kubectl wait --for=condition=Ready pod -l app=sleep-2
# 检查第二个 Pod 的 Ip 地址:
kubectl get pod -l app=sleep-2 --output
jsonpath='{.items[0].status.podIP}'
# 使用返回的 ip 地址,在 第一个 Pod ping 该 ip 地址:
kubectl exec deploy/sleep-1 -- ping -c 2 $(kubectl get pod -l app=sleep-2
--output jsonpath='{.items[0].status.podIP}')
```
我的输出如图 3.1 所示. 容器中的 Ping 工作的很好, 第一个 Pod 成功 ping 通第二个 Pod, 前提是我必须使用 kubectl 获取ip地址并作为 ping命令的入参。
![图3.1 使用 IP 地址实现 Pod 网络通信—你可以使用 kubernetes API 获取 ip 地址.](./images/Figure3.1.png)
<center>图3.1 使用 IP 地址实现 Pod 网络通信—你可以使用 kubernetes API 获取 ip 地址.</center>
Kubernetes 中的虚拟网络覆盖整个集群,因此 Pods 即使在不同的节点上运行也可以通过IP地址进行通信。此示例在单节点 K3s 集群和 100 节点AKS集群上的工作方式相同。这是一个有用的练习可以帮助您了解Kubernetes没有任何特殊的网络魔力它只是使用你的应用程序已经使用的标准协议。但是您通常不会这样做因为IP地址是特定于一个Pod的并且当Pod被替换时替换将具有新的IP地址。
<b>现在就试试</b> 这些 Pods 由 Deployment 控制器管理。如果您删除了第二个Pod它的控制器将开始使用新的IP地址进行 POD 替换。
```
# 检查当前 Pod 的 IP 地址:
kubectl get pod -l app=sleep-2 --output
jsonpath='{.items[0].status.podIP}'
# 删除 poddeployment 将替换创建新的 pod:
kubectl delete pods -l app=sleep-2
# 检查替换产生的 POD ip:
kubectl get pod -l app=sleep-2 --output
jsonpath='{.items[0].status.podIP}'
```
在图 3.2, 我的输出显示了新的 POD 拥有了新的 IP 地址,如果你 ping 旧的地址,将会失败。
![图3.2 Pod IP 地址并不是其配置的一部分; 替换的 Pod 拥有新的地址.](./images/Figure3.2.png)
<center>图3.2 Pod IP 地址并不是其配置的一部分; 替换的 Pod 拥有新的地址.</center>
需要一个可以更改的资源的永久地址的问题是一个老问题互联网使用DNS域名系统解决了这个问题将友好名称映射到IP地址Kubernetes使用相同的系统。Kubernetes集群内置了DNS服务器它将服务名称映射到IP地址。图3.3显示了域名查找如何用于Pod-to-Pod通信。
![图3.3 Services 允许 Pods 使用固定的域名通信.](./images/Figure3.3.png)
<center>图3.3 Services 允许 Pods 使用固定的域名通信.</center>
这种类型的 Service 是对Pod及其网络地址的抽象就像 Deployment 是对Pod及其容器的抽象一样。Service 有自己的IP地址它是静态的。当消费者向该地址发出网络请求时Kubernetes将其路由到Pod的实际IP地址。Service 和它的Pod之间的链接是用标签选择器设置的就像Deployments和Pods之间的链接一样。
清单 3.1 显示了 Service 的最小YAML规范使用 app 标签标识PodPod是网络流量的最终目标。
> 清单 3.1 sleep2-service.yaml, 最简单的 Service 定义
```
apiVersion: v1
kind: Service
metadata:
name: sleep-2 # Service 的名称用作DNS域名
# 该spec 配置需要一个选择器和一个端口列表。
spec:
selector:
app: sleep-2 # 匹配所有 app 标签值为 sleep-2 的 pods
ports:
- port: 80 # 监听端口 80 并发送到 Pod 上的端口80
```
此 Service 定义适用于我们在上一练习中运行的一个 Deployments。当您部署它时Kubernetes会创建一个名为sleep-2的DNS条目将流量路由到sleep-2 Deployment 创建的Pod中。其他Pod可以使用 Service 名称作为域名向该Pod发送流量。
<b>现在就试试</b> 使用 YAML 文件和通常的kubectl apply命令部署 Service并验证网络流量是否路由到Pod。
```
# 部署清单 3.1 中的 Service :
kubectl apply -f sleep/sleep2-service.yaml
# 查看 service 基本信息:
kubectl get svc sleep-2
# 运行 ping 命令检查连通性—这将会失败:
kubectl exec deploy/sleep-1 -- ping -c 1 sleep-2
```
我的输出如图 3.4 所示, 你可以看到名称查找正常, ping命令没有按预期工作因为ping使用的是Kubernetes Services不支持的网络协议。
![图3.4 部署一个 Service 以创建一个 DNS 入口, 为 Service 名称提供固定的IP地址.](./images/Figure3.4.png)
<center>图3.4 部署一个 Service 以创建一个 DNS 入口, 为 Service 名称提供固定的IP地址.</center>
这是Kubernetes中服务发现背后的基本概念部署Service 资源,并使用 Service 名称作为组件通信的域名。
不同类型的 Service 支持不同的网络模式但您可以以相同的方式使用它们。接下来我们将通过一个简单的分布式应用程序的工作示例更深入地研究Pod到Pod的网络。
## 3.2 在 Pods 间路由流量
Kubernetes 中的默认 Service 类型称为ClusterIP。它创建了一个集群范围的IP地址任何节点上的Pods都可以访问该地址。IP地址仅在集群内工作因此ClusterIP Service 仅用于Pod之间的通信。这正是您想要的分布式系统其中一些组件是内部的不应该在集群外部访问。我们将使用一个使用内部API组件的简单网站来演示这一点。
<b>现在就试试</b> 运行两个Deployments一个用于web应用程序另一个用于API。此应用程序还没有 Service ,无法正常工作,因为 Web 网站找不到API。
```
# 部署两个分类的 deployment :
kubectl apply -f numbers/api.yaml -f numbers/web.yaml
# 等待 Pod ready:
kubectl wait --for=condition=Ready pod -l app=numbers-web
# 转发端口到 web app:
kubectl port-forward deploy/numbers-web 8080:80
# 访问网站 http://localhost:8080 然后点击 Go 按钮
# —你将会看到错误消息
# 退出端口转发:
ctrl-c
```
您可以从图3.5所示的输出中看到应用程序失败并显示一条消息说明API不可用。
![图3.5 web应用程序无法正常运行因为对API的网络调用失败.](./images/Figure3.5.png)
<center>图3.5 web应用程序无法正常运行因为对API的网络调用失败.</center>
错误页面还显示了站点希望在其中查找API http://numbers-api 的域名。这不是一个完全限定的域名比如blog.sixeed.com这是一个应该由本地网络解析的地址但Kubernetes中的DNS服务器无法解析它因为没有名称为numbersapi的Service。清单3.2中的配置显示了一个名称正确的Service以及一个与API Pod匹配的标签选择器。
> 清单 3.2 api-service.yaml
```
apiVersion: v1
kind: Service
metadata:
name: numbers-api # Service 使用域名 numbers-api.
spec:
ports:
- port: 80
selector:
app: numbers-api # 流量被路由到带有此标签的Pods.
type: ClusterIP # 此Services 仅适用于其他Pod 通信.
```
此 Service 与清单3.1中的 Service 类似只是名称已更改并且明确说明了ClusterIP的服务类型。这可以省略因为它是默认的服务类型但我认为如果包含它它会使配置更清晰Service 将在web Pod和API Pod之间路由流量在不更改 Deployment 或 Pod的情况下修复应用程序。
<b>现在就试试</b> 为 API 创建一个 Service以便域查找可以正常工作并将流量从web Pod发送到API Pod。
```
# 部署清单 3.2 中的 Service:
kubectl apply -f numbers/api-service.yaml
# 查看Service 信息:
kubectl get svc numbers-api
# 转发端口到 web app:
kubectl port-forward deploy/numbers-web 8080:80
# 访问 http://localhost:8080 然后点击 Go 按钮
# 退出端口转发:
ctrl-c
```
我的输出如图3.6所示显示了应用程序正常工作网站显示了API生成的随机数。
![图3.6 部署 Service 可修复web应用程序和API之间的断开链接.](./images/Figure3.6.png)
<center>图3.6 部署 Service 可修复web应用程序和API之间的断开链接.</center>
除了 Services、Deployments 和Pods之外这里的重要教训是YAML 规范描述了Kubernetes中的整个应用程序包括所有组件和它们之间的网络。Kubernetes不会对应用程序架构进行假设您需要在YAML中指定它。这个简单的web应用程序需要定义三个Kubernetes资源以便在当前状态下工作—两个Deployments和一个Service—但拥有所有这些移动部件的优势是增加了弹性。
<b>现在就试试</b> API Pod由 Deployment 控制器管理因此您可以删除Pod并创建替换。该替换也与API服务中的标签选择器相匹配因此流量被路由到新的Pod应用程序继续工作。
```
# 检查 API Pod 的名字和 ip 地址:
kubectl get pod -l app=numbers-api -o custom-columns=NAME:metadata.name,POD_IP:status.podIP
# 删除 Pod:
kubectl delete pod -l app=numbers-api
# 检查替换的 Pod:
kubectl get pod -l app=numbers-api -o custom-columns=NAME:metadata.name,POD_IP:status.podIP
# 转发端口到 web app:
kubectl port-forward deploy/numbers-web 8080:80
# 访问 http://localhost:8080 然后点击 Go 按钮
# 退出端口转发:
ctrl-c
```
图3.7显示了 Deployment 控制器创建的替换Pod。它是相同的API Pod规范但在具有新IP地址的新Pod中运行。不过API Service的IP地址没有改变web Pod可以在相同的网络地址到达新的API Pod。
![图3.7 该 Service 将web Pod与API Pod隔离因此API Pod是否更改无关紧要.](./images/Figure3.7.png)
<center>图3.7 该 Service 将web Pod与API Pod隔离因此API Pod是否更改无关紧要.</center>
我们在这些练习中手动删除Pod以触发控制器创建替换但在Kubernetes应用程序的正常生命周期中Pod替换总是发生。每当您更新应用程序的组件以添加功能、修复错误或发布对依赖项的更新时您都将替换Pods。每当一个节点宕机时它的Pod就会在其他节点上被替换。Service 抽象以通过这些替换保持应用程序的通信。
这个演示应用程序还没有完成因为它没有任何配置来从集群外部接收流量并将其发送到web Pod。到目前为止我们已经使用了端口转发但这确实是一个调试技巧。真正的解决方案是为web Pod部署 Service。
## 3.3 路由外部流量到 Pods
您有几个选项来配置 Kubernetes 以监听进入集群的流量并将其转发到Pod。我们将从一种简单而灵活的方法开始。这是一种叫做LoadBalancer 类型的 Service它解决了将流量发送到Pod的问题Pod可能运行在与接收流量的节点不同的节点上图3.8显示了它的工作方式。
![图3.8 LoadBalancer 类型 Service 将外部流量从任何节点路由到匹配的Pod.](./images/Figure3.8.png)
<center>图3.8 LoadBalancer 类型 Service 将外部流量从任何节点路由到匹配的Pod.</center>
这看起来是一个棘手的问题特别是因为您可能有许多Pod与 Service 的标签选择器匹配所以集群需要选择一个节点来发送流量然后在该节点上选择一个Pod。Kubernetes为您提供了世界级的编排因此您需要做的就是部署LoadBalancer Service。清单3.3显示了web应用程序的 Service 配置。
> 清单 3.3 web-service.yaml, 一个 LoadBalancer Service 用于外部流量
```
apiVersion: v1
kind: Service
metadata:
name: numbers-web
spec:
ports:
- port: 8080 # Service 监听的端口
targetPort: 80 # 发送到的目标 POD 流量端口
selector:
app: numbers-web
type: LoadBalancer # LoadBalancer 类型.
```
该 Service 在端口8080上侦听并在端口 80 上向web Pod发送流量。当您部署它时您将能够使用web应用程序而无需在kubectl中设置端口转发但如何访问该应用程序的确切细节将取决于您运行Kubernetes的方式。
<b>现在就试试</b> 部署 Service然后使用 kubectl 查找 Service 的地址
```
# 为网站部署LoadBalancer Service:
kubectl apply -f numbers/web-service.yaml
# 查看 Service 信息:
kubectl get svc numbers-web
# 使用格式设置从 EXTERNAL-IP 字段获取应用程序URL:
kubectl get svc numbers-web -o
jsonpath=http://{.status.loadBalancer.ingress[0].*}:8080
```
图3.9显示了我在Docker Desktop Kubernetes集群上运行该练习的结果在这里我可以浏览到地址为 http://localhost:8080.
![图3.9 Kubernetes从其运行的平台请求LoadBalancer Services的IP地址](./images/Figure3.9.png)
<center>图3.9 Kubernetes从其运行的平台请求LoadBalancer Services的IP地址.</center>
使用K3s或云中的托管Kubernetes集群输出是不同的其中Service部署为负载平衡器创建了一个专用的外部IP地址。图3.10显示了在我的LinuxVM上使用K3s集群的相同练习使用相同的YAML规范的输出网站位于http://172.28.132.127:8080.
![图3.10 Different Kubernetes platforms use different addresses for LoadBalancer Services](./images/Figure3.10.png)
<center>图3.10 不同的Kubernetes平台为LoadBalancer服务使用不同的地址.</center>
对于相同的应用程序清单结果如何不同我在第1章中说过您可以以不同的方式部署Kubernetes这都是相同的Kubernete我的重点但这并不是绝对正确的。Kubernetes包含许多扩展点发行版在如何实现某些特性方面具有灵活性。LoadBalancer Services是一个很好的例子说明了实现的不同之处适合于发行版的目标。
- Docker Desktop是一个本地开发环境。它在一台机器上运行并与网络堆栈集成因此LoadBalancer Service 在本地主机地址可用。每个LoadBalancer Service 都发布到localhost因此如果部署许多 LoadBalancer Service则需要使用不同的端口。
- K3s支持带有自定义组件的LoadBalancer Services该组件在您的计算机上设置路由表。每个LoadBalancer Service 都发布到您的计算机或VM的IP地址因此您可以使用本地主机或从网络上的远程计算机访问服务。像Docker Desktop一样您需要为每个 load balancer 使用不同的端口。
- 像AKS和EKS这样的云Kubernetes平台是高度可用的多节点集群。部署Kubernetes LoadBalancer 服务会在云中创建一个实际的负载均衡器它跨越集群中的所有节点。云负载均衡器将传入流量发送到其中一个节点然后Kubernete将其路由到Pod。您将为每个LoadBalancer服务获得不同的IP地址并且它将是一个公共地址可从internet访问。
这是我们将在其他Kubernetes特性中再次看到的模式其中发行版具有不同的可用资源和不同的目标。最终YAML清单是相同的最终结果是一致的但Kubernetes允许发行版在到达目的地的方式上有所不同。
回到标准Kubernetes的世界您可以使用另一种 NodePort Service 类型来监听进入集群的网络流量,并将其引导到 Pod。NodePort 类型 Service 不需要外部负载均衡器,集群中的每个节点都侦听 Service 中指定的端口并将流量发送到Pod上的目标端口。图3.11显示了它的工作原理。
![图3.11 NodePort 类型 Service 还将外部流量路由到Pod但它们不需要负载均衡器.](./images/Figure3.11.png)
<center>图3.11 NodePort 类型 Service 还将外部流量路由到Pod但它们不需要负载均衡器.</center>
NodePort Services 没有 LoadBalancer Services 的灵活性,因为您需要为每个 Service 提供不同的端口您的节点需要可公开访问并且无法跨多节点集群实现负载均衡。NodePort Services在发行版中也有不同级别的支持因此它们在K3s和Docker Desktop中的工作效果与预期一致但在Kind中却不太好。清单3.4显示了一个NodePort规范以供参考。
> 清单 3.4 web-service-nodePort.yaml, NodePort 类型 Service 配置
```
apiVersion: v1
kind: Service
metadata:
name: numbers-web-node
spec:
ports:
- port: 8080 # Service 侦听端口
targetPort: 80 # 目标 Pod 端口
nodePort: 30080 # 外部访问使用端口
selector:
app: numbers-web
type: NodePort # Node 节点的 IP 可直接用于访问.
```
没有部署这个 NodePort Service 的练习尽管如果您想尝试YAML文件在章节的文件夹中。这部分是因为它在每个发行版上的工作方式不同因此本节将以许多if 分支结尾;你需要试着弄明白。但有一个更重要的原因是您通常不会在生产中使用NodePort而且最好在不同的环境中保持清单尽可能一致。坚持使用LoadBalancer 服务意味着从开发到生产都有相同的规范这意味着需要维护和保持同步的YAML文件更少。
我们将通过深入了解 Service 在幕后的工作方式来完成本章但在此之前我们将再看一种使用服务的方式即从Pods到集群外部组件的通信。
## 3.4 将流量路由到 Kubernetes 外面
您可以在 Kubernetes 中运行几乎任何服务器软件但这并不意味着您应该这样做。像数据库这样的存储组件是在Kubernetes之外运行的典型候选组件特别是如果您部署到云并且可以使用托管数据库服务。或者您可能正在数据中心运行需要与不会迁移到Kubernetes的现有系统集成。无论您使用的是什么架构您仍然可以使用Kubernetes Services对集群外部的组件进行域名解析。
第一个选项是使用 ExternalName 类型 Service它就像一个域到另一个域的别名。ExternalName Services允许您在应用程序Pod中使用本地名称当Pod发出查找请求时Kubernetes中的DNS服务器将本地名称解析为完全限定的外部名称。图3.12显示了如何工作Pod使用解析为外部系统地址的本地名称。
![图3.12 使用ExternalName Service 可以为远程组件使用本地群集地址.](./images/Figure3.12.png)
<center>图3.12 使用ExternalName Service 可以为远程组件使用本地群集地址.</center>
本章的演示应用程序希望使用本地 API 生成随机数但只需部署ExternalName服务即可切换为从GitHub上的文本文件读取静态数。
<b>现在就试试</b> 在Kubernetes的每个版本中您不能将服务从一种类型切换到另一种类型因此在部署ExternalName服务之前您需要删除API的原始ClusterIP服务。
```
# 删除当前的 API Service:
kubectl delete svc numbers-api
# 部署一个新的 ExternalName 类型 Service:
kubectl apply -f numbers-services/api-service-externalName.yaml
# 检查 Service 配置:
kubectl get svc numbers-api
# 刷新网站,通过 Go 按钮进行测试
```
我的输出如图3.13所示。您可以看到该应用程序以相同的方式工作并且它使用相同的API URL。然而如果您刷新页面您会发现它总是返回相同的数字因为它不再使用随机数API。
![图3.13 ExternalName Services可以用作重定向以在集群外部发送请求.](./images/Figure3.13.png)
<center>图3.13 ExternalName Services可以用作重定向以在集群外部发送请求.</center>
ExternalName Services是处理应用程序配置中无法解决的环境之间差异的有用方法。也许您有一个应用程序组件它使用硬编码字符串作为数据库服务器的名称。在开发环境中您可以使用预期的域名创建ClusterIP服务该域名解析为在Pod中运行的测试数据库在生产环境中可以使用解析为数据库服务器的真实域名的ExternalName Service。清单3.5显示了API外部名称的YAML规范。
> 清单 3.5 api-service-externalName.yaml, 一个 ExternalName 类型 Service
```
apiVersion: v1
kind: Service
metadata:
name: numbers-api # 集群中 Service 的本地域名
spec:
type: ExternalName
externalName: raw.githubusercontent.com # 要解析的域名
```
Kubernetes使用DNS规范名称CNAME的标准特性实现ExternalName Services。当web Pod对 numbers-api 域名进行DNS查找时Kubernetes DNS服务器返回CNAME即raw.githubusercontent.com。然后DNS解析将继续使用节点上配置的DNS服务器因此它将连接到互联网以查找IP-即GitHub服务器的地址。
<b>现在就试试</b> Services 是集群范围Kubernetes Pod网络的一部分因此任何Pod都可以使用 Service。本章第一个练习中的sleep Pods在容器镜像中有一个DNS查找命令您可以使用它来检查API Service。
```
# 运行DNS查找工具以解析 Service 名称:
kubectl exec deploy/sleep-1 -- sh -c nslookup numbers-api | tail -n 5
```
当你尝试这样做时你可能会得到看起来像错误的混乱结果因为Nslokup工具会返回很多信息而且每次运行它的顺序都不一样。不过你想要的数据就在那里。我重复了该命令几次以获得如图3.14所示的适合打印输出的效果。
![图3.14 在Kubernetes中应用程序默认不会被隔离因此任何Pod都可以查找任何 Service.](./images/Figure3.14.png)
<center>图3.14 在Kubernetes中应用程序默认不会被隔离因此任何Pod都可以查找任何 Service.</center>
关于ExternalName Services有一件重要的事情需要了解您可以从本练习中看到它们最终只是为应用程序提供一个使用地址但实际上并不会改变应用程序发出的请求。这对于通过TCP通信的数据库等组件来说很好但对于HTTP服务来说就不那么简单了。HTTP请求在头字段中包含目标主机名这与ExternalName响应中的实际域不匹配因此客户端调用可能会失败。本章中的随机数应用程序有一些黑客代码来解决这个问题手动设置主机标头但这种方法最适合非HTTP服务。
还有一个选项用于将集群中的本地域名路由到外部系统。它不能解决HTTP头问题但当您希望路由到IP地址而不是域名时它允许您使用与ExternalName Services类似的方法。他们是 headless 类型 Service它们被定义为ClusterIP Service 类型但没有设置标签选择器因此它们永远不会匹配任何Pod。相反Service 部署有一个端点资源,该资源明确列出了 Service 应该解析的IP地址。
清单3.6显示了一个 headless Service 以及拥有一个 ip地址的 endpoint 。它还显示了YAML的新用法定义了多个资源用三个破折号分隔。
> 清单 3.6 api-service-headless.yaml, 具有显式地址的 Service
```
apiVersion: v1
kind: Service
metadata:
name: numbers-api
spec:
type: ClusterIP # 没有选择器字段使其成为 headless Service.(另外可以指定 ClusterIP: None,来提供 headless service,可以具备 selector)
ports:
- port: 80
---
kind: Endpoints # Endpoints 是新的资源类型.
apiVersion: v1
metadata:
name: numbers-api
subsets:
- addresses: # 静态 ip 地址集合
- ip: 192.168.123.234
ports:
- port: 80 # 以及它们监听的 端口.
```
该端点规范中的 IP地址是假的但Kubernetes不会验证该地址是否可访问因此该代码将无错误地部署。
<b>现在就试试</b> 使用此无头服务替换ExternalName服务。这将导致应用程序失败因为API域名现在解析为无法访问的IP地址。
```
# 删除之前的 Service:
kubectl delete svc numbers-api
# 部署 headless Service:
kubectl apply -f numbers-services/api-service-headless.yaml
# 检查 Service:
kubectl get svc numbers-api
# 检查 endpoint:
kubectl get endpoints numbers-api
# 检查 DNS lookup:
kubectl exec deploy/sleep-1 -- sh -c nslookup numbers-api | grep
"^[^*]"
# 访问网站—当你尝试获取数字时返回失败
```
我的输出如图3.15所示证实了Kubernetes将很乐意让您部署一个破坏应用程序的服务变更。域名解析内部群集IP地址但对该地址的任何网络调用都会失败因为它们被路由到端点中不存在的实际IP地址。
![图3.15 Service 中的错误配置可能会破坏您的应用程序,即使不部署应用程序更改.](./images/Figure3.15.png)
<center>图3.15 Service 中的错误配置可能会破坏您的应用程序,即使不部署应用程序更改.</center>
该练习的输出提出了几个有趣的问题DNS查找如何返回集群IP地址而不是端点地址为什么域名以.default.svc.cluster.local结尾使用Kubernetes服务不需要网络工程背景但如果您了解服务解决方案的实际工作方式这将有助于您跟踪问题这就是我们将如何完成本章的内容。
## 3.5 理解 Kubernetes Service 解析
Kubernetes支持您的应用程序可能需要的所有网络配置这些配置使用基于已建立的网络技术的服务。应用程序组件在Pod中运行并使用标准传输协议和DNS名称与其他Pod进行通信以进行发现。您不需要任何特殊的代码或库您的应用程序在Kubernetes中的工作方式与您在物理服务器或虚拟机上部署的方式相同。
在本章中,我们已经介绍了所有 Service 类型及其典型用例,因此现在您已经很好地了解了可以使用的模式。如果您觉得这里有太多的细节,请放心,大多数时候您都会部署 ClusterIP 类型服务这几乎不需要配置。它们大部分都是无缝工作的但深入一层来理解堆栈是很有用的。图3.16显示了下一层次的细节。
![图3.16 Kubernetes运行DNS服务器和代理并将它们与标准网络工具一起使用。](./images/Figure3.16.png)
<center>图3.16 Kubernetes运行DNS服务器和代理并将它们与标准网络工具一起使用.</center>
关键是 ClusterIP 是网络上不存在的虚拟IP地址。Pods通过运行在节点上的 kube-proxy 访问网络并使用数据包过滤将虚拟IP发送到真实端点。Kubernetes services 只要它们存在就保留它们的IP地址并且 Service 可以独立于应用程序的任何其他部分而存在。Service 有一个控制器每当Pods发生更改时该控制器会更新端点列表因此客户端始终使用静态虚拟IP地址kube-proxy 始终拥有最新的端点列表。
<b>现在就试试</b> 您可以看到Kubernetes如何在Pod更改时通过列出Pod更改之间 Service 的端点来保持端点列表的即时更新。端点使用与 Service 相同的名称您可以使用kubectl查看端点详细信息。
```
# 查看 sleep-2 Service 端点信息:
kubectl get endpoints sleep-2
# 删除 pod:
kubectl delete pods -l app=sleep-2
# 检查 端点使用了新的 POD ip 进行了更新:
kubectl get endpoints sleep-2
# 删除整个 Deployment:
kubectl delete deploy sleep-2
# 检查 endpoint 仍然存在, 没有 IP 地址清单:
kubectl get endpoints sleep-2
```
您可以在图3.17中看到我的输出这是第一个问题的答案Kubernetes DNS返回Cluster IP地址而不是端点因为端点地址发生了变化。
![图3.17 Service 的Cluster IP地址不会更改但端点列表始终在更新。.](./images/Figure3.17.png)
<center>图3.17 Service 的Cluster IP地址不会更改但端点列表始终在更新.</center>
使用静态虚拟IP意味着客户端可以无限期地缓存DNS查找响应许多应用程序这样做是错误的性能节省并且无论一段时间内发生多少Pod替换IP地址都将继续工作。关于域名后缀的第二个问题需要通过横向步骤来回答以查看Kubernetes命名空间。
每个Kubernetes资源都位于一个命名空间中这是一个可以用来对其他资源进行分组的资源。命名空间是对Kubernetes集群进行逻辑分区的一种方式您可以为每个产品、每个团队或单个共享命名空间。我们暂时还不会使用名称空间但我在这里介绍它们因为它们在DNS解析中发挥作用。图3.18显示了命名空间在服务名称中的位置。
![图3.18 命名空间对集群进行逻辑分区,但 Service 可以跨命名空间访问.](./images/Figure3.18.png)
<center>图3.18 命名空间对集群进行逻辑分区,但 Service 可以跨命名空间访问.</center>
您的集群中已经有多个命名空间,到目前为止我们部署的所有资源都是在默认名称空间中创建的(称为 default这就是为什么我们不需要在YAML文件中指定命名空间。内部Kubernetes组件如DNS服务器和KubernetesAPI也在kube-system 命名空间的Pods中运行。
<b>现在就试试</b> Kubectl支持命名空间您可以使用命名空间标志处理默认命名空间之外的资源。
```
# 在 default 命名空间检查 Service:
kubectl get svc --namespace default
# 检查 system 命名空间 Service :
kubectl get svc -n kube-system
# 尝试DNS查找完全限定的服务名称:
kubectl exec deploy/sleep-1 -- sh -c 'nslookup numbers-
api.default.svc.cluster.local | grep "^[^*]"'
# 以及 kube-system 下 的 Service:
kubectl exec deploy/sleep-1 -- sh -c 'nslookup kube-dns.kube-
system.svc.cluster.local | grep "^[^*]"'
```
我的输出如图3.19所示回答了第二个问题Service 的本地域名只是服务名称但这是包含Kubernetes命名空间的完全限定域名的别名。
![图3.19 可以使用相同的kubectl命令查看不同命名空间中的资源.](./images/Figure3.19.png)
<center>图3.19 可以使用相同的kubectl命令查看不同命名空间中的资源.</center>
在Kubernetes之旅的早期了解命名空间很重要因为它可以帮助您看到Kubernete的核心功能也可以作为Kubernets应用程序运行但除非您明确设置了命名空间否则在kubectl中看不到它们。命名空间是细分集群以提高利用率而不损害安全性的一种强大方式我们将在第11章中再次讨论它们。
现在我们已经完成了命名空间和Service。在本章中您已经了解到每个Pod都有自己的IP地址Pod通信最终使用标准TCP和UDP协议的IP地址。您永远不会直接使用Pod IP地址尽管您总是创建一个Service 资源Kubernetes使用它通过DNS提供服务发现。服务支持多种网络模式不同的服务类型配置Pod之间的网络流量从外部世界进入Pod以及从Pod到外部世界。您还了解到服务有自己的生命周期独立于Pod和Deployments因此在我们继续之前最后要做的就是清理。
<b>现在就试试</b> 删除 Deployment 也会删除其所有Pod但 Service 没有级联删除。它们是需要单独删除的独立对象。
```
# 删除 Deployments:
kubectl delete deploy --all
# 删除 Services:
kubectl delete svc --all
# 检查还有什么在运行:
kubectl get all
```
现在集群又干净了尽管如图3.20所示您需要小心使用其中一些kubectl命令。
![图3.20 您需要显式删除创建的任何服务但要注意all参数.](./images/Figure3.20.png)
<center>图3.20 您需要显式删除创建的任何服务但要注意all参数.</center>
## 3.6 实验室
这个实验室将为您提供一些创建 Service 的实践但它也将让您思考标签和选择器这是Kubernetes的强大功能。目标是为更新版本的随机数应用程序部署服务该应用程序已经进行了UI改造。以下是您的提示
- 本章的实验室文件夹包含deployments.yaml文件。通过它来使用kubectl部署应用程序。
- 检查Pods有两个版本的web应用程序正在运行.
- 编写一个 Service使API可用于域名 numbers-api w为其他Pod 服务.
- 在端口8088上编写一个 Service使网站的版本2可以从外部访问。.
- 您需要仔细查看Pod标签以获得正确的结果.
这个实验室是本章练习的扩展如果你想检查我的解决方案它可以在GitHub上的仓库中找到https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch03/lab/README.md 。

View File

@ -0,0 +1,608 @@
# 第四章 通过 ConfigMaps 和 Secrets 配置应用程序
在容器中运行应用程序的最大优势之一是消除了环境之间的差距。部署过程在所有测试环境直到生产环境中都使用相同的容器镜像,因此每个部署都使用与前一个环境完全相同的二进制文件集。您再也不会看到生产部署失败,因为服务器缺少某人手动安装在测试服务器上并且忘记记录的依赖项。当然,环境之间确实存在差异,您可以通过将配置设置注入到容器中来提供这种差异。
Kubernetes支持两种资源类型的配置注入:ConfigMap和Secrets。这两种类型都可以以任何合理的格式存储数据并且这些数据独立于任何其他资源存在于集群中。可以通过访问ConfigMaps和Secrets中的数据来定义Pods并为数据如何展现提供不同的选项。在本章中您将学习在Kubernetes中管理配置的所有方法这些方法足够灵活可以满足任何应用程序的需求。
## 4.1 Kubernetes 如何为应用提供配置
使用 kubectl 创建ConfigMap和Secret对象就像在kubernete中创建其他资源一样可以使用create命令也可以应用YAML规范。不像其他资源它们什么都不做;它们只是存储少量数据的存储单元。这些存储单元可以加载到Pod中成为容器环境的一部分因此容器中的应用程序可以读取数据。在讨论这些对象之前我们先来看看提供配置设置的最简单方法:使用环境变量。
<b>现在就试试</b> 环境变量是Linux和Windows中的核心操作系统特性它们可以在机器级别设置以便任何应用程序都可以读取它们。环境变量是常用的所有容器都有一些环境变量由容器内的操作系统和Kubernetes设置。确保您的Kubernetes实验室已经启动并运行。
```
# 切换到本章练习目录:
cd ch04
# 使用 sleep image 部署一个没有额外配置的 Pod:
kubectl apply -f sleep/sleep.yaml
# 等待 pod ready:
kubectl wait --for=condition=Ready pod -l app=sleep
# 检查容器中的一些环境变量:
kubectl exec deploy/sleep -- printenv HOSTNAME KIAMOL_CHAPTER
```
从图4.1所示的输出中可以看到容器中存在hostname变量并由Kubernetes填充但自定义Kiamol变量不存在。
![图4.1 所有Pod容器都有Kubernetes和容器操作系统设置的一些环境变量.](./images/Figure4.1.png)
<center>图4.1 所有Pod容器都有Kubernetes和容器操作系统设置的一些环境变量.</center>
在本练习中应用程序只是Linux printenv工具但原理对任何应用程序都是相同的。许多技术栈使用环境变量作为基本配置系统。在Kubernetes中提供这些设置的最简单方法是在Pod Spec 中添加环境变量。清单4.1显示了 sleep Deployment 的更新后的 Pod Spec其中添加了Kiamol环境变量。
> Listing 4.1 sleep-with-env.yaml, 一个 Pod Spec 带了环境变量
```
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
env: # 设置环境变量
- name: KIAMOL_CHAPTER # 定义环境变量名称
value: "04" # 定义环境变量值
```
环境变量在Pod的生命周期中是静态的;在Pod运行时你不能更新任何值。如果需要更改配置则需要使用替换Pod执行更新。你应该习惯这样的想法:部署不仅仅是为了新特性的发布;你也会使用它们来进行配置更改和软件补丁你必须设计你的应用程序来处理频繁的Pod更换。
<b>现在就试试</b> 使用清单4.1中的新Pod 配置更新 sleep Deployment添加一个Pod容器内可见的环境变量。
```
# 更新 Deployment:
kubectl apply -f sleep/sleep-with-env.yaml
# 在新的 Pod 中检查同样的环境变量:
kubectl exec deploy/sleep -- printenv HOSTNAME KIAMOL_CHAPTER
```
我的输出(如图4.2所示)显示了结果——一个设置了Kiamol环境变量的新容器在一个新的Pod中运行。
![图4.2 将环境变量添加到Pod Spec 中可以在Pod容器中使用这些值.](./images/Figure4.2.png)
<center>图4.2 将环境变量添加到Pod Spec 中可以在Pod容器中使用这些值.</center>
关于前面的练习重要的是新应用程序使用相同的Docker 镜像;这是一个具有相同二进制文件的相同应用程序——只是配置设置在部署之间发生了更改。在Pod Spec 中内联设置环境值对于简单设置很好但实际应用程序通常有更复杂的配置需求这就是使用ConfigMaps时的情况。
ConfigMap只是一个资源它存储了一些可以加载到Pod中的数据。数据可以是一组键-值对、文本简介,甚至是二进制文件。您可以使用键-值对加载带有环境变量的Pods使用文本加载任何类型的配置文件—json、XML、YAML、TOML、ini—以及二进制文件加载许可密钥。一个Pod可以使用多个ConfigMap每个ConfigMap可以被多个Pod使用。图4.3显示了其中的一些选项。
![图4.3 configmap是单独的资源可以附加到0个或多个pod.](./images/Figure4.3.png)
<center>图4.3 configmap是单独的资源可以附加到0个或多个pod.</center>
我们将继续使用简单的 sleep Deployment以展示创建和使用configmap的基础知识。清单4.2显示了更新后Pod Spec的环境部分其中使用了一个在YAML中定义的环境变量和一个从ConfigMap中加载的环境变量。
> 清单 4.2 sleep-with-configMap-env.yaml, 加载 ConfigMap 到 Pod 中
```
env: # 容器 Spec 环境变量配置部分
- name: KIAMOL_CHAPTER
value: "04" # 变量值.
- name: KIAMOL_SECTION
valueFrom:
configMapKeyRef: # 值来自于 ConfigMap.
name: sleep-config-literal # ConfigMap 名称
key: kiamol.section # 加载的数据项名称
```
如果在 Pod Spec 中引用了ConfigMap那么在部署Pod之前ConfigMap必须已经存在。该配置期望在数据中找到一个名为sleep-config-literal的具有键值对的ConfigMap最简单的创建方法是将键和值传递给kubectl命令。
<b>现在就试试</b> 通过指定命令中的数据创建ConfigMap然后检查数据并部署更新后的sleep 应用程序来使用ConfigMap。
```
# 基于命令行创建 ConfigMap:
kubectl create configmap sleep-config-literal --from-literal=kiamol.section='4.1'
# 检查 ConfigMap 详情:
kubectl get cm sleep-config-literal
# 显示 ConfigMap 友好的描述信息:
kubectl describe cm sleep-config-literal
# 基于 清单 4.2 部署更新后的 Pod 配置:
kubectl apply -f sleep/sleep-with-configMap-env.yaml
# 检查 Kiamol 环境变量:
kubectl exec deploy/sleep -- sh -c 'printenv | grep "^KIAMOL"'
```
在本书中我们不会经常使用kubectl describe 命令因为输出通常很冗长会占用大部分屏幕但它绝对是值得尝试的东西。描述Services和Pods以可读的格式为您提供了许多有用的信息。您可以在图4.4中看到我的输出其中包括描述ConfigMap时显示的键值数据。
![图4.4 Pods可以从ConfigMaps加载单独的数据项并重命名键.](./images/Figure4.4.png)
<center>图4.4 Pods可以从ConfigMaps加载单独的数据项并重命名键.</center>
从文字值创建 ConfigMap 对于单独的设置来说很好但是如果您有很多配置数据它会变得非常麻烦。除了在命令行上指定文本值外Kubernetes还允许你从文件中加载configmap。
## 4.2 在 ConfigMaps 中存储和使用配置文件
在许多 Kubernetes 版本中创建和使用configmap的选项已经得到了发展所以它们现在几乎支持您能想到的所有配置变体。这些 sleep Pod 练习是展示变化的好方法但它们有点无聊所以在我们进入更有趣的内容之前我们再做一个。清单4.3显示了一个环境文件——一个具有键-值对的文本文件可以加载它来创建一个带有多个数据项的ConfigMap。
> 清单 4.3 ch04.env, 一个包含环境变量的文件
```
# 环境变量文件,使用每个新行定义每个变量
KIAMOL_CHAPTER=ch04
KIAMOL_SECTION=ch04-4.1
KIAMOL_EXERCISE=try it now
```
环境文件是将多个设置分组的有效方法Kubernetes明确支持将它们加载到ConfigMaps中并将所有设置作为Pod容器中的环境变量呈现出来。
<b>现在就试试</b> 创建一个从清单4.3中的环境文件填充的新的ConfigMap然后将更新部署到 sleep 应用程序以使用新的设置。
```
# 加载环境变量到新的 ConfigMap:
kubectl create configmap sleep-config-env-file --from-env-file=sleep/ch04.env
# 检查 ConfigMap 明细信息:
kubectl get cm sleep-config-env-file
# 更新 Pod 使用新的 ConfigMap:
kubectl apply -f sleep/sleep-with-configMap-env-file.yaml
# 在容器中检查值:
kubectl exec deploy/sleep -- sh -c 'printenv | grep "^KIAMOL"'
```
在图4.5中,我的输出显示 printenv 命令读取所有环境变量并显示具有Kiamol名称的变量但这可能不是您所期望的结果。
![图4.5 一个ConfigMap可以有多个数据项Pod可以全部加载它们.](./images/Figure4.5.png)
<center>图4.5 一个ConfigMap可以有多个数据项Pod可以全部加载它们.</center>
这个练习向您展示了如何从文件创建ConfigMap。它还向您展示了Kubernetes在应用环境变量时具有优先级规则。您刚刚部署的Pod规范(如清单4.4所示)从ConfigMap加载所有环境变量但它也使用一些相同的键显式指定环境值。
> 清单 4.4 sleep-with-configMap-env-file.yaml, 一个 Pod 关联多个 ConfigMaps
```
env: # 已经存在的环境配置部分
- name: KIAMOL_CHAPTER
value: "04"
- name: KIAMOL_SECTION
valueFrom:
configMapKeyRef:
name: sleep-config-literal
key: kiamol.section
envFrom: # envFrom 加载多个变量
- configMapRef: # 基于 ConfigMap
name: sleep-config-env-file
```
因此如果存在重复的键Pod规范中用env定义的环境变量将覆盖用envFrom定义的值。记住一点很有用您可以通过显式地在Pod规范中设置容器镜像或ConfigMaps中设置的任何环境变量来覆盖它们——这是在跟踪问题时更改配置设置的快速方法。
环境变量得到了很好的支持但它们只能提供到此为止大多数应用程序平台更喜欢一种更结构化的方法。在本章的其余练习中我们将使用一个支持配置源层次结构的web应用程序。默认设置被打包在Docker 镜像中的JSON文件中应用程序在运行时在其他位置寻找具有覆盖默认设置的JSON文件——所有JSON设置都可以用环境变量覆盖。清单4.5显示了我们将使用的第一个部署的Pod规范。
> 清单 4.5 todo-web.yaml, 带有配置设置的web应用程序
```
spec:
containers:
- name: web
image: kiamol/ch04-todo-list
env:
- name: Logging__LogLevel__Default
value: Warning
```
这个应用程序的运行将使用镜像中JSON配置文件中的所有默认设置除了默认日志级别它被设置为一个环境变量。
<b>现在就试试</b> 在没有任何额外配置的情况下运行应用程序,并检查其行为。
```
# 部署应用程序和 Service 来访问它:
kubectl apply -f todo-list/todo-web.yaml
# 等待 Pod ready:
kubectl wait --for=condition=Ready pod -l app=todo-web
# 获取应用 地址:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080'
# 浏览应用程序,玩一玩,然后尝试浏览/config 检查应用程序日志
kubectl logs -l app=todo-web
```
演示应用程序是一个简单的待办事项列表(对于《在一个月的午餐中学习Docker》的读者来说这将是令人痛苦的熟悉)。在当前的设置中,它允许您添加和查看项,但是还应该有一个/config页面我们可以在非生产环境中使用它来查看所有的配置设置。如图4.6所示,该页面为空,应用程序记录了有人试图访问它的警告。
![图4.6 应用程序大部分工作,但我们需要设置额外的配置值.](./images/Figure4.6.png)
<center>图4.6 应用程序大部分工作,但我们需要设置额外的配置值.</center>
这里使用的配置层次结构是一种非常常见的方法。如果你对它不熟悉电子书的附录C是《Learn Docker in a Month of lunch》中的“容器中的应用程序配置管理”章节其中详细解释了它。这个例子是一个使用JSON的. net Core应用程序但是你可以在Java Spring应用程序、Node.js、Go、Python等中看到使用各种文件格式的类似配置系统。在Kubernetes中你使用相同的应用配置方法。
- 这可能只是适用于每个环境的设置,也可能是一组完整的配置选项,所以不需要任何额外的设置,应用程序就可以在开发模式下运行(这对那些可以用简单的Docker run命令快速启动应用程序的开发人员很有帮助)。
- 每个环境的实际设置存储在ConfigMap中并呈现到容器文件系统中。Kubernetes将配置数据显示为一个位于已知位置的文件应用程序检查并将其与默认文件中的内容合并。
- 任何需要调整的设置都可以作为部署Pod规范中的环境变量应用。
清单4.6显示了todo应用程序开发配置的YAML规范。它包含一个JSON文件的内容应用程序将该JSON文件与容器镜像中的默认JSON配置文件合并并设置使配置页面可见。
> 清单 4.6 todo-web-config-dev.yaml, 一个 ConfigMap的配置信息
```
apiVersion: v1
kind: ConfigMap # ConfigMap 是一种资源类型
metadata:
name: todo-web-config-dev # ConfigMap 名字
data:
config.json: |- # 数据 key 是文件名
{ # 文件内容可以是任何格式
"ConfigController": {
"Enabled" : true
}
}
```
您可以将任何类型的文本配置文件嵌入到YAML规范中只要小心处理空白即可。我更喜欢直接从配置文件加载ConfigMaps因为这意味着你可以始终使用kubectl apply命令来部署应用程序的每个部分。如果我想直接加载JSON文件我需要使用kubectl create命令来配置资源并应用其他所有内容。
清单4.6中的ConfigMap定义只包含一个设置但它以应用程序的本机配置格式存储。当我们部署更新的Pod规范时该设置将被应用配置页面将可见。
<b>现在就试试</b> 新的Pod规范引用了ConfigMap因此需要首先通过应用YAML来创建ConfigMap然后我们更新待办事项应用部署。
```
# 创建 JSON ConfigMap:
kubectl apply -f todo-list/configMaps/todo-web-config-dev.yaml
# 更新应用使用ConfigMap:
kubectl apply -f todo-list/todo-web-dev.yaml
# 刷新网页指向 /config 路由页面
```
可以在图4.7中看到我的输出。配置页面现在可以正确加载因此新的部署配置正在合并ConfigMap中的设置以覆盖镜像中的默认设置该设置阻止了对该页的访问。
![图4.7 将ConfigMap数据加载到容器文件系统中应用程序将在其中加载配置文件.](./images/Figure4.7.png)
<center>图4.7 将ConfigMap数据加载到容器文件系统中应用程序将在其中加载配置文件.</center>
这种方法需要两个前提:应用程序需要能够合并ConfigMap数据Pod规范需要将ConfigMap中的数据加载到容器文件系统中的预期文件路径中。我们将在下一节中看到它是如何工作的。
## 4.3 从 ConfigMaps 中查找配置数据
将配置项加载到环境变量中的替代方法是将它们作为容器目录中的文件表示。容器文件系统是一个虚拟构造由容器镜像和其他源构建。Kubernetes可以使用ConfigMaps作为文件系统源——它们作为一个目录挂载每个数据项都有一个文件。图4.8显示了您刚刚部署的设置其中ConfigMap中的数据项显示为一个文件。
![图4.8 configmap可以作为容器文件系统中的目录加载.](./images/Figure4.8.png)
<center>图4.8 configmap可以作为容器文件系统中的目录加载.</center>
Kubernetes通过Pod 配置 Spec 的两个特性来管理这个奇怪的魔法:卷它使ConfigMap的内容对Pod可用卷挂载它将ConfigMap卷的内容加载到Pod容器的指定路径中。清单4.7显示了在前面的练习中部署的卷和挂载。
> 清单 4.7 todo-web-dev.yaml, 将 ConfigMap 作为卷挂载
```
spec:
containers:
- name: web
image: kiamol/ch04-todo-list
volumeMounts: # 挂载卷到容器中
- name: config # 卷名称
mountPath: "/app/config" # 卷挂载的目录
readOnly: true # 卷 read-only 配置
volumes: # Pod 层面定义卷
- name: config # 匹配 volumeMount 部分的名称.
configMap: # 卷来源是一个 ConfigMap.
name: todo-web-config-dev # ConfigMap 名称
```
这里需要意识到的重要一点是ConfigMap被视为一个目录具有多个数据项每个数据项都成为容器文件系统中的文件。在这个例子中应用程序从/app/appsettings.json文件中加载它的默认设置然后它在/app/config 目录下寻找一个文件config.Json它可以包含覆盖默认值的设置。容器镜像中不存在/app/config目录;它是由Kubernetes创建和填充的。
<b>现在就试试</b> 容器文件系统对于应用程序来说是一个单独的存储单元但是它是由镜像和ConfigMap构建的。这些源有不同的行为。
```
# 显示默认的 config 文件:
kubectl exec deploy/todo-web -- sh -c 'ls -l /app/app*.json'
# 显示卷挂载进来的 config 文件:
kubectl exec deploy/todo-web -- sh -c 'ls -l /app/config/*.json'
# 检查它是否真的只读:
kubectl exec deploy/todo-web -- sh -c 'echo ch04 >> /app/config/config.json'
```
如图4.9所示我的输出显示JSON配置文件存在于应用程序的预期位置但ConfigMap文件由Kubernetes管理并作为只读文件交付。
![图4.9 容器文件系统由Kubernetes从镜像和ConfigMap构建。.](./images/Figure4.9.png)
<center>图4.9 容器文件系统由Kubernetes从镜像和ConfigMap构建.</center>
将ConfigMaps加载为目录是很灵活的你可以使用它来支持不同的应用配置方法。如果您的配置被拆分到多个文件中您可以将其全部存储在一个ConfigMap中并将其全部加载到容器中。清单4.8显示了使用两个JSON文件更新to-do ConfigMap的数据项这些JSON文件分离了应用程序行为和日志记录的设置。
> 清单 4.8 todo-web-config-dev-with-logging.yaml, 包含两个文件的 ConfigMap
```
data:
config.json: |- # 原始的 app 配置文件
{
"ConfigController": {
"Enabled" : true
}
}
logging.json: |- # 第二个 JSON file, 将会出现在卷挂载中
{
"Logging": {
"LogLevel": {
"ToDoList.Pages" : "Debug"
}
}
}
```
当您将一个更新部署到一个live Pod正在使用的ConfigMap时会发生什么?Kubernetes将更新后的文件交付到容器但接下来会发生什么取决于应用程序。一些应用程序在启动时将配置文件加载到内存中然后忽略配置目录中的任何更改因此更改ConfigMap实际上不会改变应用程序的配置直到替换Pods。这个应用程序更加周到——它监视配置目录并重新加载任何文件更改因此将更新部署到ConfigMap将更新应用程序配置。
<b>现在就试试</b> 使用清单4.9中的ConfigMap更新应用程序配置。这增加了日志记录级别所以同一个Pod现在将开始写入更多的日志条目。
```
# 检查当前应用日志:
kubectl logs -l app=todo-web
# 部署更新后的 ConfigMap:
kubectl apply -f todo-list/configMaps/todo-web-config-dev-with-logging.yaml
# 等待配置变更生效到 Pod :
sleep 120
# 检查新的设置:
kubectl exec deploy/todo-web -- sh -c 'ls -l /app/config/*.json'
# 从您的 Service IP地址的站点加载几个页面再次检查日志
kubectl logs -l app=todo-web
```
可以在图4.10中看到我的输出。sleep 是为了让Kubernetes API有时间将新的配置文件在Pod 重新加载;几分钟后,新配置被加载,应用程序在增强的日志记录下运行。
![图4.10 ConfigMap数据是缓存的所以更新需要几分钟才能到达Pods.](./images/Figure4.10.png)
<center>图4.10 ConfigMap数据是缓存的所以更新需要几分钟才能到达Pods.</center>
卷是加载配置文件的一个强大的选项尤其是像这样的应用程序它会对变化做出反应并实时更新设置。在不重启应用程序的情况下提高日志级别对跟踪问题有很大帮助。但是您需要小心您的配置因为卷挂载不一定以您期望的方式工作。如果容器镜像中已经存在卷的挂载路径那么ConfigMap目录将覆盖它替换所有内容这可能导致应用程序以令人兴奋的方式失败。清单4.9显示了一个示例。
> 清单 4.9 todo-web-dev-broken.yaml, 一个 Pod配置了错误的挂载
```
spec:
containers:
- name: web
image: kiamol/ch04-todo-list
volumeMounts:
- name: config # 挂载 ConfigMap 卷
mountPath: "/app" # 覆盖目录
```
这是一个坏的Pod spec 配置其中ConfigMap被加载到/app目录而不是/app/config目录。作者可能是想合并目录将JSON配置文件添加到现有的应用目录中。相反它将清除应用程序二进制文件。
<b>现在就试试</b> 清单4.9中的Pod 配置删除了所有应用程序二进制文件因此替换的Pod将无法启动。看看接下来会发生什么。
```
# 部署错误配置的 Pod:
kubectl apply -f todo-list/todo-web-dev-broken.yaml
# 访问应用程序,看看它的样子
# 检查应用日志:
kubectl logs -l app=todo-web
# 检查 pod 状态:
kubectl get pods -l app=todo-web
```
这里的结果很有趣:部署破坏了应用程序但应用程序继续工作。这是Kubernetes在保护你。应用更改会创建一个新的Pod该Pod中的容器立即退出并报错因为它试图加载的二进制文件不再存在于app目录中。Kubernetes重新启动容器几次给它一个机会但它一直失败。经过三次尝试后Kubernetes开始休息如图4.11所示。
![图4.11 如果更新的deployment 失败则不会替换原来的Pod](./images/Figure4.11.png)
<center>图4.11 如果更新的deployment 失败则不会替换原来的Pod.</center>
现在我们有了两个Pod但Kubernetes不会删除旧的Pod直到替换的Pod成功运行在这种情况下它永远不会删除因为我们破坏了容器设置。旧的Pod没有被移除仍然愉快地服务于请求;新的Pod处于失败状态但Kubernetes周期性地重新启动容器希望它可能已经自我修复。这是一种需要注意的情况:apply命令似乎可以工作应用程序继续工作但它没有使用您所应用的清单。
现在我们将修复这个问题并展示在容器文件系统中显示ConfigMaps的最后一个选项。您可以有选择地将数据项加载到目标目录中而不是将每个数据项作为其自己的文件加载。清单4.10显示了更新后的Pod规范。挂载路径已经固定但是卷被设置为只交付一个项。
> 清单 4.10 todo-web-dev-no-logging.yaml, 挂载 ConfigMap 单项
```
spec:
containers:
- name: web
image: kiamol/ch04-todo-list
volumeMounts:
- name: config # 挂载 ConfigMap 卷到正常的目录
mountPath: "/app/config"
readOnly: true
volumes:
- name: config
configMap:
name: todo-web-config-dev # 加载 ConfigMap 卷
items: # 指定数据项进行加载
- key: config.json # 加载 config.json 项
path: config.json # 作为 文件 config.json
```
该规范使用相同的ConfigMap因此它只是对部署的更新。这将是一个级联更新:它将创建一个新的Pod它将正确启动并且然后Kubernetes会移除之前的两个pod。
<b>现在就试试</b> 部署清单4.10中的 spec该 spec 推出更新后的卷挂载以修复应用程序但也忽略了ConfigMap中的日志JSON文件。
```
# 应用变更:
kubectl apply -f todo-list/todo-web-dev-no-logging.yaml
# 列出配置文件夹的内容:
kubectl exec deploy/todo-web -- sh -c 'ls /app/config'
# 现在浏览应用的几个页面,
# 检查日志:
kubectl logs -l app=todo-web
# 检查 Pods:
kubectl get pods -l app=todo-web
```
图4.12显示了我的输出。应用程序又开始工作了,但它只看到一个配置文件,所以没有应用增强的日志记录设置。
![图4.12 卷可以将ConfigMap中选定的项显示到挂载目录中.](./images/Figure4.12.png)
<center>图4.12 卷可以将ConfigMap中选定的项显示到挂载目录中.</center>
configmap支持广泛的配置系统。在环境变量和卷挂载之间你应该能够在ConfigMaps中存储应用程序设置并根据应用程序的需要应用它们。配置规范和应用规范之间的分离还支持不同的发布工作流允许不同的团队拥有流程的不同部分。但是有一件事不应该使用ConfigMaps那就是任何敏感数据——它们实际上是文本文件的包装器没有额外的安全语义。对于需要保护的配置数据Kubernetes提供了Secrets。
## 4.4 使用 Secrets 配置敏感数据
Secrets 是一种单独的资源类型但它们具有与ConfigMaps类似的API。你以同样的方式使用它们但因为它们是用来存储敏感信息的所以Kubernetes以不同的方式管理它们。主要的区别在于最大限度地减少接触。Secrets 只被发送到需要使用它们的节点,并且存储在内存中而不是磁盘上;Kubernetes还支持在传输和静止时对Secrets进行加密。
然而Secrets 并不是100%加密的。任何可以访问集群中的 Secret 对象的人都可以读取未加密的值。有一个混淆层:Kubernetes可以读写Base64编码的secret 数据,这并不是一个真正的安全功能,但确实可以防止 secrets 意外暴露给那些在你背后看着你的人。
<b>现在就试试</b> 您可以从一个文字值创建Secrets并将键和数据传递给kubectl命令。检索到的数据是Base64编码的。
```
# 对于 WINDOWS 这个脚本将一个Base64命令添加到会话中:
. .\base64.ps1
# 现在从纯文本文字创建一个 secret:
kubectl create secret generic sleep-secret-literal --from-literal=secret=shh...
# 友好展示 Secret 信息:
kubectl describe secret sleep-secret-literal
# 查看已编码的 Secret 值:
kubectl get secret sleep-secret-literal -o jsonpath='{.data.secret}'
# 然后解码数据:
kubectl get secret sleep-secret-literal -o jsonpath='{.data.secret}' | base64 -d
```
从图4.13的输出中可以看出Kubernetes对Secrets的处理与ConfigMaps不同。kubectl describe命令中不显示数据值只显示键值的名称并且在获取数据时显示为编码因此需要将其输送到解码器中进行读取。
![图4.13 Secrets有一个类似ConfigMaps的API但是Kubernetes尽量避免意外暴露它.](./images/Figure4.13.png)
<center>图4.13 Secrets有一个类似ConfigMaps的API但是Kubernetes尽量避免意外暴露它.</center>
当 Secerts 在 Pod 容器内浮出水面时这种预防措施并不适用。容器环境看到原始的纯文本数据。清单4.11显示了 sleep 应用程序的返回配置为将新的Secret作为环境变量加载。
> 清单 4.11 sleep-with-secret.yaml, 一个 Pod 配置加载了 Secret
```
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
env: # 环境变量
- name: KIAMOL_SECRET # 容器中的变量名
valueFrom: # 从外部源加载
secretKeyRef: # 来自 Secret
name: sleep-secret-literal # Secret 名称
key: secret # secret 数据项的key
```
使用 Secrets 的配置与使用configmaps的配置几乎相同——可以从Secret中的命名项加载命名环境变量。这个Pod 配置将 Secret 数据项以其原始形式交付到容器。
<b>现在就试试</b> 运行一个简单的sleep Pod使用Secret作为环境变量。
```
# 更新 sleep Deployment:
kubectl apply -f sleep/sleep-with-secret.yaml
# 检查 pod 中的环境变量:
kubectl exec deploy/sleep -- printenv KIAMOL_SECRET
```
图4.14显示了输出结果。在这种情况下Pod只使用Secret但是Secrets和ConfigMaps可以混合在同一个Pod 配置中,填充环境变量或文件或两者。
![图4.14 加载到Pods中的 Secret 不是Base64编码的.](./images/Figure4.14.png)
<center>图4.14 加载到Pods中的 Secret 不是Base64编码的.</center>
您应该警惕将Secrets加载到环境变量中。保护敏感数据的关键在于最大限度地减少其暴露。可以从任何进程读取环境变量,在Pod容器中一些应用程序平台在遇到严重错误时记录所有环境变量的值。另一种方法是将Secrets显示为文件(如果应用程序支持的话),这让您可以选择使用文件权限来保护访问。
为了圆满完成本章我们将在不同的配置中运行待办事项应用程序其中它使用单独的数据库来存储项目运行在自己的Pod中。数据库服务器是使用Docker Hub上的官方镜像的 Postgres它从环境中的配置值读取登录凭据。清单4.12显示了用于将数据库密码创建为Secret的YAML规范。
> 清单 4.12 -todo-db-secret-test.yaml, 数据库用户的 Secret
```
apiVersion: v1
kind: Secret # Secret 是资源类型
metadata:
name: todo-db-secret-test # 命名 Secret
type: Opaque # Opaque 类型 secrets 用于文本数据.
stringData: # stringData用于纯文本.
POSTGRES_PASSWORD: "kiamol-2*2*" # secret key 以及 value.
```
这种方法在stringData字段中以纯文本形式声明密码在创建Secret时将其编码为Base64。为Secrets使用YAML文件带来了一个棘手的问题:它为您提供了一种良好的一致部署方法,代价是在源代码控制中可见所有敏感数据。
在生产场景中您会将真实数据排除在YAML文件之外而是使用占位符并在部署过程中执行一些额外的处理——比如从GitHub Secret中将数据注入占位符。无论您采用哪种方法请记住一旦Secret存在于Kubernetes中任何人都可以轻松读取该值。
<b>现在就试试</b>从清单4.12中的清单创建一个Secret并检查数据
```
# 部署 Secret:
kubectl apply -f todo-list/secrets/todo-db-secret-test.yaml
# 检查数据是否已编码:
kubectl get secret todo-db-secret-test -o jsonpath='{.data.POSTGRES_PASSWORD}'
# 查看存储了哪些 annotations:
kubectl get secret todo-db-secret-test -o jsonpath='{.metadata.annotations}'
```
在图4.15中可以看到字符串被编码为Base64。其结果与规范使用普通数据字段并直接在YAML中设置Base64中的密码值相同。
![图4.15 从字符串数据创建的 Secret 被编码,但原始数据也存储在对象中.](./images/Figure4.15.png)
<center>图4.15 从字符串数据创建的 Secret 被编码,但原始数据也存储在对象中.</center>
要使用Secret作为Postgres密码镜像为我们提供了两个选项。我们可以将这个值加载到一个名为POSTGRES_PASSWORD的环境变量中——这并不理想——或者我们可以将它加载到一个文件中并通过设置POSTGRES_PASSWORD_FILE环境变量告诉Postgres加载文件的位置。使用文件意味着我们可以在卷级别上控制访问权限这就是清单4.13代码中配置数据库的方式。
> 清单 4.13 todo-db-test.yaml, 一个 Pod spec mount 了来自于 secret 的卷
```
spec:
containers:
- name: db
image: postgres:11.6-alpine
env:
- name: POSTGRES_PASSWORD_FILE # 设置环境变量文件
value: /secrets/postgres_password
volumeMounts: # 挂载 Secret 卷
- name: secret # volume name
mountPath: "/secrets"
volumes:
- name: secret
secret: # 从 Secret加载的 volume
secretName: todo-db-secret-test # Secret name
defaultMode: 0400 # 文件权限
items: # 可选 命名数据项
- key: POSTGRES_PASSWORD
path: postgres_password
```
当部署这个Pod时Kubernetes将Secret项的值加载到路径为/secrets/postgres_password的文件中。该文件将设置为0400权限这意味着容器用户可以读取它但任何其他用户都不能读取。环境变量被设置为让Postgres从Postgres用户有权访问的文件中加载密码因此数据库将从Secret中设置的凭据开始。
<b>现在就试试</b> 部署数据库Pod并验证数据库是否正确启动。
```
# 部署清单 4.13 的 YAML
kubectl apply -f todo-list/todo-db-test.yaml
# 检查数据库日志:
kubectl logs -l app=todo-db --tail 1
# 检查密码文件权限:
kubectl exec deploy/todo-db -- sh -c 'ls -l $(readlink -f /secrets/postgres_password)'
```
图4.16显示了数据库启动和等待连接的过程——这表明数据库已正确配置——最终输出验证文件权限是否按预期设置。
![图4.16 如果应用程序支持配置设置可以从Secrets中填充的文件读取.](./images/Figure4.16.png)
<center>图4.16 如果应用程序支持配置设置可以从Secrets中填充的文件读取.</center>
剩下的就是在测试配置中运行应用程序本身这样它就可以连接到Postgres数据库而不是使用本地数据库文件进行存储。还有很多YAML可以用来创建ConfigMap、Secret、Deployment和Service但是这些都是使用我们已经介绍过的特性所以我们只需要继续部署即可。
<b>现在就试试</b>运行 to-do 应用程序使其使用Postgres数据库进行存储。
```
# ConfigMap配置应用使用Postgres:
kubectl apply -f todo-list/configMaps/todo-web-config-test.yaml
# Secret包含连接到Postgres的凭据:
kubectl apply -f todo-list/secrets/todo-web-secret-test.yaml
# 部署Pod规范使用ConfigMap和Secret:
kubectl apply -f todo-list/todo-web-test.yaml
# 检查应用程序中是否设置了数据库凭据:
kubectl exec deploy/todo-web-test -- cat /app/secrets/secrets.json
# 浏览应用程序并添加一些项目
```
我的输出如图4.17所示其中Secret JSON文件的纯文本内容显示在web Pod容器中。
![图4.17 加载应用程序配置到Pods和将ConfigMaps和Secrets 作为JSON文件](./images/Figure4.17.png)
<center>图4.17 加载应用程序配置到Pods和将ConfigMaps和Secrets 作为JSON文件.</center>
现在当你在应用程序中添加待办事项时它们被存储在Postgres数据库中因此存储与应用程序运行时分离。你可以删除web Pod;它的控制器将启动一个具有相同配置的替换它连接到相同的数据库Pod因此所有来自原始web Pod的数据仍然可用。
这是Kubernetes中配置选项的一个非常详尽的介绍。原理非常简单—将configmap或secret加载到环境变量或文件中—但是有很多变化。你需要很好地理解这些细微差别以便以一致的方式管理应用程序配置即使你的应用程序都有不同的配置模型。
## 4.5 管理 Kubernetes 中的应用程序配置
Kubernetes 为您提供了管理应用程序配置的工具使用任何适合您的组织的工作流。核心需求是让应用程序从环境中读取配置设置理想情况下是使用文件和环境变量的层次结构。然后您就可以灵活地使用configmap和Secrets来支持您的部署过程。在你的设计中有两个因素需要考虑:你是否需要你的应用程序响应实时配置更新以及你将如何管理Secrets?
如果对您而言,不替换 Pod 的实时更新很重要,那么您的选项就很有限了。您无法使用环境变量设置,因为对这些变量的任何更改都会导致 Pod 替换。您可以使用卷挂载并从文件加载配置更改,但是您需要通过更新现有的 ConfigMap 或 Secret 对象来部署更改。您不能将卷更改为指向新的配置对象,因为这也是 Pod 替换。
更新相同配置对象的替代方法是每次部署新对象,在对象名称中使用某种版本方案,并更新 app 部署以引用新对象。您会失去实时更新,但可以轻松地获得配置更改的审计记录,并有一个方便的选项来返回到先前的设置。图 4.18 显示了这些选项。
![图4.18 您可以选择自己的配置管理方法由Kubernetes支持](./images/Figure4.18.png)
<center>图4.18 您可以选择自己的配置管理方法由Kubernetes支持.</center>
另一个问题是如何管理敏感数据。大型组织可能有专门的配置管理团队他们拥有部署配置文件的过程。这非常适合ConfigMaps和Secrets的版本化方法在这种方法中配置管理团队在部署之前从文字或受控文件部署新对象。
另一种选择是完全自动化的部署其中configmap和secret是从源代码控制中的YAML模板创建的。YAML文件包含占位符而不是敏感数据在应用它们之前部署过程会使用来自安全存储(如Azure KeyVault)的真实值替换它们。图4.19比较了这些选项。
![图4.19 Secrets 管理可以在部署时自动化,也可以由单独的团队严格控制.](./images/Figure4.19.png)
<center>图4.19 Secrets 管理可以在部署时自动化,也可以由单独的团队严格控制.</center>
您可以使用任何适用于您的团队和应用程序堆栈的方法,记住目标是从平台加载所有配置设置,以便在每个环境中部署相同的容器镜像。现在是清理集群的时候了。如果您已经完成了所有的练习(当然您已经完成了!)那么您将有几十个资源需要删除。我将介绍kubectl的一些有用特性以帮助理清所有问题。
<b>现在就试试</b>kubectl delete命令可以读取YAML文件并删除文件中定义的资源。如果在一个目录中有多个YAML文件可以使用目录名作为删除(或应用)的参数,它将遍历所有文件。
```
# 删除所有目录下所有文件中的所有资源:
kubectl delete -f sleep/
kubectl delete -f todo-list/
kubectl delete -f todo-list/configMaps/
kubectl delete -f todo-list/secrets/
```
## 4.6 实验室
如果你对Kubernetes提供的所有配置应用程序的选项感到困惑这个实验室将会有所帮助。实际上您的应用程序将有自己的配置管理想法您需要对Kubernetes部署进行建模以适应应用程序预期的配置方式。这就是你在这个实验室里需要用一个叫做Adminer的简单应用程序来做的。开始吧:
- Adminer 是一个用于管理SQL数据库的web UI在解决数据库问题时可以在Kubernetes中运行.
- 首先在 ch04/lab/postgres 文件夹中部署YAML文件然后部署 ch04/lab/adminer.yaml 在基本状态下运行Adminer.
- 找到Adminer Service的 external IP并浏览到端口8082。请注意您需要指定一个数据库服务器并且UI设计停留在20世纪90年代。可以使用Postgres作为数据库名、用户名和密码来确认与Postgres的连接.
- 您的工作是在Adminer Deployment中创建和使用一些配置对象以便数据库服务器名称默认为实验室的Postgres Service并且UI使用称为price的更好的设计.
- 可以在名为ADMINER_DEFAULT_SERVER的环境变量中设置默认数据库服务器。让我们称其为敏感数据因此它应该使用Secret.
- UI设计设置在环境变量ADMINER_DESIGN中;这是不敏感的所以ConfigMap会做得很好.
这将花费一些研究和思考如何显示配置设置因此对于实际应用程序配置来说这是一个很好的实践。我的解决方案发布在GitHub上供你检查方法:https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch04/lab/README.md。

View File

@ -0,0 +1,585 @@
# 第五章 通过 volumes,mounts,claims 存储数据
在集群环境中访问数据是困难的。计算的移动是简单的部分Kubernetes API与节点保持着持续的联系如果一个节点停止响应Kubernetes就会认为它已离线并在其他节点上为其所有Pod启动替代品。但如果其中一个Pod中的应用程序将数据存储在节点上那么在其他节点上启动时替代品将无法访问该数据如果该数据包含客户尚未完成的大订单那将是令人失望的。你真的需要全集群存储这样Pod就可以从任何节点访问相同的数据。
Kubernetes 没有内置集群范围的存储因为没有一个解决方案适用于所有场景。应用程序有不同的存储需求可以运行Kubernetes的平台有不同的存储能力。数据始终是访问速度和持久性之间的平衡Kubernetes支持这一点它允许您定义集群提供的不同存储类(storage class)并为应用程序请求特定的存储类。在本章中您将学习如何使用不同类型的存储以及Kubernetes如何抽象出存储实现细节。
## 5.1 Kubernetes 如何构建容器文件系统
Pod 中的容器的文件系统由Kubernetes使用多个源构建。容器镜像提供文件系统的初始内容并且每个容器都有一个可写存储层用于从镜像写入新文件或更新任何文件。(Docker镜像是只读的所以当容器从镜像中更新文件时它实际上是在自己的可写层中更新文件的副本。)图5.1显示了Pod内部的情况。
![图5.1](./images/Figure5.1.png)
<center>图5.1 容器并不知道这一点但它们的文件系统是由Kubernetes构建的虚拟结构.</center>
在容器中运行的应用程序只看到一个它具有读写访问权限的文件系统所有这些层的细节都是隐藏的。这对于将应用程序迁移到Kubernetes来说是很好的因为它们不需要更改就可以在Pod中运行。但如果你的应用确实需要写入数据你需要了解它们如何使用存储并设计Pods来支持它们的需求。否则你的应用程序看起来运行正常但当任何意外发生时(如使用新容器重新启动Pod),你将面临数据丢失的风险。
<b>现在就试试</b> 如果容器内的应用程序崩溃并且容器退出Pod将开始替换。新容器将从容器镜像和新的可写层开始从文件系统开始前一个容器在其可写层中写入的任何数据都将消失。
```
# 进入本章练习目录:
cd ch05
# 部署一个 sleep Pod:
kubectl apply -f sleep/sleep.yaml
# 在容器内写入一个文件:
kubectl exec deploy/sleep -- sh -c 'echo ch05 > /file.txt; ls /*.txt'
# 检查容器 id:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
# kill 容器中所有的进程, 触发 Pod 重启:
kubectl exec -it deploy/sleep -- killall5
# 检查替换的容器 id:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
# 查看你之前写入的文件,已经不存在:
kubectl exec deploy/sleep -- ls /*.txt
```
在这个练习中记住两件重要的事情:Pod容器的文件系统具有容器的生命周期而不是Pod并且当Kubernetes谈论Pod重启时它实际上指的是一个替换容器。如果您的应用程序愉快地在容器中写入数据这些数据不会存储在Pod级别如果Pod使用新容器重新启动所有数据都将消失。输出如图5.2所示。
![图5.2](./images/Figure5.2.png)
<center>图5.2 可写层具有容器的生命周期而不是Pod.</center>
我们已经知道 Kubernetes 可以从其他来源构建容器文件系统,我们在第 4 章中提到过ConfigMaps和Secrets文件系统目录。其机制是在Pod级别定义一个卷使另一个存储源可用然后将其挂载到容器文件系统的指定路径上。ConfigMaps和Secrets 是只读存储单元但Kubernetes支持许多其他类型的可写卷。图5.3展示了如何设计一个Pod使用卷来存储重启之间的数据甚至可以在整个集群范围内访问。
![图5.3](./images/Figure5.3.png)
<center>图5.3 虚拟文件系统可以从引用外部存储块的卷构建.</center>
我们将在本章的后面讨论集群范围的卷但现在我们将从一个更简单的卷类型开始这对许多场景仍然有用。清单5.1显示了一个Pod 配置,它使用了一种名为 EmptyDir 的卷类型这只是一个空目录但是它存储在Pod级别而不是容器级别。它作为一个卷被挂载到容器中因此它作为一个目录可见但它不是镜像层或容器层之一。
> 清单 5.1 sleep-with-emptyDir.yaml, 一个简单的 volume 配置
```
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: data # 挂载一个名为 data 的卷
mountPath: /data # 挂载到 /data 目录
volumes:
- name: data # 这个就是 data 卷配置,
emptyDir: {} # 指定 EmptyDir 类型.
```
空目录听起来是你能想到的最没用的存储空间但实际上它有很多用途因为它与Pod具有相同的生命周期。任何存储在EmptyDir卷中的数据都会在重启之间保留在Pod中这样替换容器就可以访问前一个容器写入的数据。
<b>现在就试试</b> 使用代码清单 5.1 中的配置更新 sleep 部署添加一个EmptyDir卷。现在可以写入数据并杀死容器替换容器可以读取数据。
```
# 更新 sleep Pod 使用一个 EmptyDir volume:
kubectl apply -f sleep/sleep-with-emptyDir.yaml
# 查看 volume mount 的内容:
kubectl exec deploy/sleep -- ls /data
# 在空目录下创建一个文件:
kubectl exec deploy/sleep -- sh -c 'echo ch05 > /data/file.txt; ls /data'
# 检查容器 ID:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
# kill 容器进程:
kubectl exec deploy/sleep -- killall5
# 检查替换的容器 ID:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[0].containerID}'
# 查看 volume 挂载的文件:
kubectl exec deploy/sleep -- cat /data/file.txt
```
输出如图 5.4 所示。容器只是在文件系统中看到一个目录但它指向一个存储单元它是Pod的一部分。
![图5.4](./images/Figure5.4.png)
<center>图5.4 像空目录这样的基本目录仍然是有用的,因为它可以由容器共享.</center>
任何使用文件系统作为临时存储的应用程序都可以使用 EmptyDir 卷——也许你的应用程序会调用一个API这个API需要几秒钟才能响应而且响应需要很长一段时间。应用程序可能会将API响应保存在本地文件中因为从磁盘读取比重复调用API更快。EmptyDir卷是本地缓存的一个合理来源因为如果应用程序崩溃那么替换容器仍然拥有缓存的文件仍然可以从速度提升中受益。
EmptyDir 卷只共享Pod的生命周期所以如果替换了Pod那么新的Pod就从一个空目录开始。如果你希望数据在Pods之间持久化那么你可以挂载其他类型的卷它们有自己的生命周期。
## 5.2 在节点使用 volumes 及 mounts 存储数据
这是数据处理比计算处理更为复杂的地方,因为我们需要考虑数据是否与特定节点相关联 - 这意味着任何替换 Pod 都需要在该节点上运行以查看数据 - 或数据是否具有集群范围的访问权限,并且 Pod 可以在任何节点上运行。Kubernetes 支持许多变体,但您需要知道您想要什么以及集群支持什么,并为 Pod 指定。
最简单的存储选项是使用映射到节点上目录的卷这样当容器写入卷挂载时数据实际上存储在节点磁盘上的一个已知目录中。我们将通过运行一个使用EmptyDir卷缓存数据的实际应用程序来演示这一点了解其限制然后将其升级为使用节点级存储。
<b>现在就试试</b> 运行一个使用代理组件来提高性能的web应用程序。web应用程序运行在一个带有内部服务的Pod中代理运行在另一个Pod中该Pod通过LoadBalancer服务公开可用。
```
# 部署 Pi 应用:
kubectl apply -f pi/v1/
# 等待 web Pod ready:
kubectl wait --for=condition=Ready pod -l app=pi-web
# 从 LoadBalancer 获取应用 URL:
kubectl get svc pi-proxy -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/?dp=30000'
# 访问 URL, 等待响应然后刷新页面
# 检查 proxy 中的缓存
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache
```
这是 web 应用程序的常见设置其中代理通过直接从本地缓存中提供响应来提高性能这也减少了web应用程序的负载。你可以在图5.5中看到我的输出。第一个Pi计算花费了1秒多的时间来响应并且刷新实际上是即时的因为它来自代理不需要计算。
![图5.5](./images/Figure5.5.png)
<center>图5.5 在 EmptyDir 卷中缓存文件意味着Pod重启后缓存仍然存在.</center>
对于这样的应用程序来说EmptyDir卷可能是一种合理的方法因为存储在卷中的数据不是关键数据。如果Pod重启缓存会存活下来新的代理容器可以为前一个容器缓存的响应提供服务。如果Pod被替换那么缓存就会丢失。替换的Pod从一个空的缓存目录开始但缓存不是必需的——应用程序仍然可以正常运行;它只是缓慢地开始,直到缓存再次被填充。
<b>现在就试试</b> 移除 proxy Pod。它将被替换因为它由部署控制器管理。替换从一个新的EmptyDir卷开始对于这个应用程序来说这意味着一个空的代理缓存因此请求被发送到web Pod。
```
# 删除 proxy Pod:
kubectl delete pod -l app=pi-proxy
# 检查替换的 Pod 缓存目录:
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache
# 刷新界面
```
输出如图 5.6 所示。结果是相同的但我不得不等待另一秒以等待web应用程序计算因为替换代理Pod启动时没有缓存。
![图5.6](./images/Figure5.6.png)
<center>图5.6 一个新的Pod从一个新的空目录开始.</center>
下一个级别的持久性来自于使用映射到节点磁盘上目录的卷Kubernetes将其称为HostPath卷。hostpath被指定为Pod中的一个卷它以通常的方式装载到容器文件系统中。容器将数据写入挂载目录时实际上是将数据写入节点上的磁盘。图5.7展示了node、Pod和volume之间的关系。
![图5.7](./images/Figure5.7.png)
<center>图5.7 HostPath卷维护Pod替换之间的数据但前提是Pod使用相同的节点.</center>
HostPath 卷可能很有用但您需要了解它们的局限性。数据物理存储在节点上仅此而已。Kubernetes不会神奇地将数据复制到集群中的所有其他节点。代码清单5.2是更新后的Pod 配置它使用HostPath卷代替了EmptyDir。当代理容器将缓存文件写入/data/nginx/cache时它们实际上会存储在节点上的目录 /volumes/nginx/cache。
> 清单 5.2 nginx-with-hostPath.yaml, 挂载 HostPath volume
```
spec:
containers: # 完整的配置 还包含一个configMap卷挂载
- image: nginx:1.17-alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- name: cache-volume
mountPath: /data/nginx/cache # proxy 缓存路径
volumes:
- name: cache-volume
hostPath: # 使用节点上的目录
path: /volumes/nginx/cache # 节点路径
type: DirectoryOrCreate # 不存在则创建路径
```
这种方法将数据的持久性从Pod的生命周期扩展到节点磁盘的生命周期前提是替换Pod总是在同一个节点上运行。在单节点的实验室集群中会出现这种情况因为只有一个节点。替换Pod将在启动时加载HostPath卷如果它使用前一个Pod的缓存数据填充那么新的代理可以立即开始提供缓存数据。
<b>现在就试试</b> 更新 proxy 部署以使用清单5.2中的Pod 配置然后使用应用程序并删除Pod。替换将使用现有缓存进行响应。
```
# 更新 proxy Pod 使用 HostPath volume:
kubectl apply -f pi/nginx-with-hostPath.yaml
# 列出缓存目录内容:
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache
# 访问 app URL
# 删除 proxy Pod:
kubectl delete pod -l app=pi-proxy
# 检查替换 Pod 下的缓存目录:
kubectl exec deploy/pi-proxy -- ls -l /data/nginx/cache
# 刷新浏览器
```
输出如图5.8所示。最初的请求只花了不到一秒的时间来响应但刷新几乎是即时的因为新的Pod继承了旧Pod缓存的响应并存储在节点上。
![图5.8](./images/Figure5.8.png)
<center>图5.8 在单节点的集群中Pods总是运行在同一个节点上所以它们都可以使用HostPath.</center>
HostPath卷的一个明显的问题是它们在有多个节点的集群中没有意义几乎每个集群都有一个简单的实验室环境。你可以在Pod 配置中包含一个要求要求Pod应该始终运行在同一个节点上以确保它与数据在一起但这样做限制了你的解决方案的弹性——如果节点离线Pod将无法运行你的应用程序将丢失。
一个不太明显的问题是该方法提供了一个很好的安全漏洞。Kubernetes没有限制节点上的哪些目录可用于HostPath卷。代码清单5.3中的Pod配置是完全有效的它使得Pod容器可以访问该节点上的整个文件系统。
> 清单 5.3 sleep-with-hostPath.yaml, 一个可以完全访问节点磁盘的 Pod
```
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: node-root
mountPath: /node-root
volumes:
- name: node-root
hostPath:
path: / # 节点文件系统的根目录
type: Directory # 目录必须存在.
```
任何有权限根据该配置创建Pod的人现在都可以访问Pod运行所在节点的整个文件系统。你可能想使用这样的卷挂载来快速读取主机上的多个路径但如果你的应用程序受到攻击攻击者可以在容器中执行命令那么他们也可以访问节点的磁盘。
<b>现在就试试</b> 运行代码清单 5.3 中的YAML文件中的Pod然后在 Pod 容器中运行一些命令来浏览node的文件系统。
```
# 运行一个带卷挂载到主机的Pod:
kubectl apply -f sleep/sleep-with-hostPath.yaml
# 检查容器内的日志文件:
kubectl exec deploy/sleep -- ls -l /var/log
# 检查卷所在节点的日志:
kubectl exec deploy/sleep -- ls -l /node-root/var/log
# 检查容器用户:
kubectl exec deploy/sleep -- whoami
```
如图 5.9 所示Pod容器可以看到节点上的日志文件在本例中包括Kubernetes日志。这是相当无害的但是这个容器以root用户的身份运行该用户映射到节点上的root用户因此容器可以完全访问文件系统。
![图5.9](./images/Figure5.9.png)
<center>图5.9 危险!挂载HostPath可以让你完全访问节点上的数据.</center>
如果这一切看起来像是一个糟糕的想法请记住Kubernetes是一个具有广泛功能的平台可以适应许多应用程序。您可能有一个旧的应用程序需要访问其运行节点上的特定文件路径而HostPath卷允许您这样做。在这种情况下您可以采用一种更安全的方法使用可以访问节点上一条路径的卷通过声明卷挂载的子路径来限制容器可以看到的内容。清单5.4显示了这一点。
> 清单 5.4 sleep-with-hostPath-subPath.yaml, 用子路径限制挂载
```
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: node-root # 挂载的卷名称
mountPath: /pod-logs # 容器的路径
subPath: var/log/pods # 卷内的路径
- name: node-root
mountPath: /container-logs
subPath: var/log/containers
volumes:
- name: node-root
hostPath:
path: /
type: Directory
```
在这里卷仍然定义在节点的根路径上但访问它的唯一方式是通过容器中的卷挂载这些挂载仅限于定义的子路径。在volume 配置和mount 配置之间,在构建和映射容器文件系统方面有很大的灵活性。
<b>现在就试试</b> 更新sleep Pod使容器的卷挂载限制在清单5.4中定义的子路径中,并检查文件内容。
```
# 更新 Pod spec:
kubectl apply -f sleep/sleep-with-hostPath-subPath.yaml
# 检查 node 上 Pod 日志:
kubectl exec deploy/sleep -- sh -c 'ls /pod-logs | grep _pi-'
# 检查容器日志 logs:
kubectl exec deploy/sleep -- sh -c 'ls /container-logs | grep nginx'
```
在这个练习中,除了通过挂载到日志目录之外,没有其他方法可以查看节点的文件系统。如图 5.10 所示,容器只能访问子路径中的文件。
HostPath卷是创建有状态应用程序的好方法;它们易于使用,并且在任何集群上都以相同的方式工作。它们在现实世界的应用程序中也很有用,但只有当你的应用程序使用状态作为临时存储时才有用。对于永久存储,我们需要迁移到集群中任何节点都可以访问的卷。
![图5.10](./images/Figure5.10.png)
<center>图5.10 限制对带子路径的卷的访问限制了容器可以做的事情.</center>
## 5.3 使用 persistent volumes 及 claims 存储集群范围数据
Kubernetes 集群就像一个资源池:它有许多节点,每个节点都有一些 CPU 和内存容量供集群使用Kubernetes使用它们来运行你的应用程序。存储只是Kubernetes提供给你的应用程序的另一种资源但只有当节点可以插入分布式存储系统时它才能提供集群范围的存储。图 5.11 展示了如果卷使用分布式存储Pods 如何从任何节点访问卷。
![图5.11](./images/Figure5.11.png)
<center>图5.11 分布式存储使 Pod 可以访问来自任何节点的数据,但它需要平台支持.</center>
Kubernetes 支持由分布式存储系统支持的许多卷类型: AKS集群可以使用 Azure 文件或 Azure 磁盘EKS集群可以使用弹性块存储并且在数据中心您可以使用简单的网络文件系统(NFS)共享,或像 GlusterFS 这样的网络文件系统。所有这些系统都有不同的配置要求,您可以在 Pod 的卷配置中指定它们。这样做将使您的应用程序配置与一种存储实现紧密耦合Kubernetes提供了一种更灵活的方法。
Pods 是位于计算层之上的抽象而Services是位于网络层之上的抽象。在存储层中抽象是PersistentVolumes (PV)和PersistentVolumeClaims。PersistentVolume是一个Kubernetes对象它定义了一个可用的存储空间。集群管理员可以创建一组持久化卷其中每个卷包含底层存储系统的卷规格。代码清单5.5展示了使用NFS存储的 PersistentVolume 配置。
> 清单 5.5 persistentVolume-nfs.yaml, 由NFS挂载支持的卷
```
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01 # 具有通用名称的通用存储单元
spec:
capacity:
storage: 50Mi # PV 提供的存储空间
accessModes: # 如何通过Pods访问卷
- ReadWriteOnce # 它只能被一个Pod使用。
nfs: # 该PV由NFS支持.
server: nfs.my.network # NFS 服务的域名
path: "/kubernetes-volumes" # NFS 共享的路径
```
您无法在实验室环境中部署该配置除非您的网络中恰好有一个NFS服务器其域名为 nfs.my.network共享名为 kubernetes-volumes。您可以在任何平台上运行Kubernetes因此在接下来的练习中我们将使用可以在任何地方工作的本地卷。(如果我在练习中使用Azure文件它们只能在AKS集群上工作因为EKS和Docker Desktop以及其他Kubernetes发行版没有为Azure卷类型配置。)
<b>现在就试试</b> 创建一个使用本地存储的 PV。PV是集群范围内的但卷是本地的一个节点因此我们需要确保PV连接到卷所在的节点。我们用标签来做。
```
# 将自定义标签应用到集群中的第一个节点:
kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') kiamol=ch05
# 使用标签选择器检查节点:
kubectl get nodes -l kiamol=ch05
# 在标记的节点上部署一个使用本地卷的PV:
kubectl apply -f todo-list/persistentVolume.yaml
# 检查 PV:
kubectl get pv
```
输出如图5.12所示。节点标记是必要的,因为我没有使用分布式存储系统;您通常只需要指定NFS或Azure磁盘卷配置这些配置可以从任何节点访问。本地卷仅存在于一个节点上PV使用标签标识该节点。
![图5.12](./images/Figure5.12.png)
<center>图5.12 如果没有分布式存储可以通过将PV固定到本地卷来作弊.</center>
现在 PV 作为一个可用的存储单元存在于集群中具有一组已知的特性包括大小和访问模式。pod不能直接使用PV;相反他们需要使用PersistentVolumeClaim (PVC)来声明它。PVC是Pods使用的存储抽象它只是为应用程序请求一些存储空间。Kubernetes将PVC与PV匹配并将底层的存储细节留给PV。代码清单5.6展示了一个与我们创建的PV相匹配的存储空间声明。
> 清单 5.6 postgres-persistentVolumeClaim.yaml, PVC 匹配 PV
```
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc # 该声明将由特定的应用程序使用。
spec:
accessModes: # 必要的 access mode
- ReadWriteOnce
resources:
requests:
storage: 40Mi # 请求的存储大小
storageClassName: "" # 一个空白类意味着PV需要存在.
```
PVC 配置包括访问模式、存储量和存储类。如果没有指定存储类Kubernetes会尝试找到与声明中要求匹配的现有PV。如果有匹配则PVC绑定到PV—有一对一的链接一旦PV 被声明使用就不能供任何其他PVC使用。
<b>现在就试试</b> 部署代码清单 5.6 中的PVC。它的要求由我们在前一个练习中创建的 PV 满足,因此 claim 将绑定到该 volume。
```
# 创建一个绑定到 pv 的pvc:
kubectl apply -f todo-list/postgres-persistentVolumeClaim.yaml
# 检查 PVCs:
kubectl get pvc
# 检查 PVs:
kubectl get pv
```
我的输出出现在图 5.13 中,在这里可以看到一对一的绑定:PVC 绑定到卷PV 绑定到 claim。
![图5.13](./images/Figure5.13.png)
<center>图5.13 PV 只是集群中的存储单元;你可以用 PVC 来为你的应用认领它.</center>
这是一种静态配置方法PV 需要显式创建以便Kubernetes可以绑定到它。如果在创建PVC时没有匹配的PV则仍然创建了声明但它是不可用的。它将停留在系统中等待满足其要求的PV被创建。
<b>现在就试试</b> 集群中的 PV 已经绑定到某个 claim 因此不能再次使用。创建另一个PVC将保持未绑定
```
# 创建一个不匹配任何可用 PV 的 PVC:
kubectl apply -f todo-list/postgres-persistentVolumeClaim-too-big.yaml
# 检查 claims:
kubectl get pvc
```
在图 5.14 中可以看到新的 PVC 处于挂起状态。这种情况会一直持续下去直到集群中出现一个容量至少为100 MB的PV(即本声明中的存储需求)。
![图5.14](./images/Figure5.14.png)
<center>图5.14 对于静态配置static provisioningPVC将不可用直到有一个PV可以绑定到它.</center>
在 Pod 使用PVC之前需要先把它绑起来。如果你部署了一个引用未绑定PVC的Pod在PVC绑定之前Pod将保持挂起状态因此你的应用程序将永远无法运行直到它拥有所需的存储空间。我们创造的第一个PVC已经绑定所以它可以使用但只能绑定到一个 Pod。声明的访问模式是ReadWriteOnce这意味着卷是可写的但只能由一个Pod挂载。代码清单5.7是Postgres数据库的简短Pod 配置使用PVC存储。
> 清单 5.7 todo-db.yaml, 一个 Pod 配置消费 PVC**
```
spec:
containers:
- name: db
image: postgres:11.6-alpine
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim: # 卷使用某 PVC
claimName: postgres-pvc # 使用的 pvc
```
现在我们已经准备好了使用卷部署Postgres数据库Pod的所有部件卷可能支持也可能不支持分布式存储。应用程序设计人员拥有Pod 配置和PVC并不关心pv——pv依赖于Kubernetes集群的基础设施可以由不同的团队管理。在我们的实验室环境中我们拥有一切。我们还需要采取另一个步骤:在卷预期使用的节点上创建目录路径。
<b>现在就试试</b> 你可能无法登录到真正的Kubernetes集群中的节点所以我们在这里通过运行sleep Pod来作弊它将HostPath挂载到节点的根目录并使用挂载创建目录。
```
# 运行 sleep Pod, 它可以访问节点磁盘:
kubectl apply -f sleep/sleep-with-hostPath.yaml
# 等待 Pod ready:
kubectl wait --for=condition=Ready pod -l app=sleep
# 创建节点的目录, 来自 PV 指定的值:
kubectl exec deploy/sleep -- mkdir -p /node-root/volumes/pv01
```
图 5.15 展示了以 root 权限运行的 sleep Pod因此它可以在节点上创建目录即使我没有直接访问该节点的权限。
![图5.15](./images/Figure5.15.png)
<center>图5.15 在本例中HostPath是访问节点PV源的另一种方式.</center>
现在一切都准备好了可以使用持久存储运行待办事项列表应用程序。通常情况下你不需要经历这么多步骤因为你知道集群提供的功能。不过我不知道你的集群能做什么所以这些练习可以在任何集群上运行它们是对所有存储资源的有用介绍。图5.16展示了到目前为止部署的内容,以及即将部署的数据库。
![图5.16](./images/Figure5.16.png)
<center>图5.16 只是有点复杂——将PV和HostPath映射到相同的存储位置.</center>
让我们运行数据库。当创建 Postgres 容器时它将卷挂载到由PVC支持的Pod中。这个新的数据库容器连接到一个空卷因此当它启动时它将初始化数据库创建预写日志(writeahead log, WAL)这是主数据文件。Postgres Pod并不知道但是PVC是由节点上的本地卷支持的在这里我们也有一个sleep Pod在运行我们可以使用它来查看Postgres文件。
<b>现在就试试</b> 部署数据库并给它时间来初始化数据文件然后使用sleep Pod检查卷中写入了什么。
```
# 部署 database:
kubectl apply -f todo-list/postgres/
# 等待 Postgres 初始化:
sleep 30
# 检查 database 日志:
kubectl logs -l app=todo-db --tail 1
# 检查卷中的数据文件:
kubectl exec deploy/sleep -- sh -c 'ls -l /node-root/volumes/pv01 | grep wal'
```
图 5.17 中的输出显示数据库服务器已经正确启动并等待连接,已经将所有数据文件写入卷中。
![图5.17](./images/Figure5.17.png)
<center>图5.17 数据库容器写入本地数据路径但这实际上是PVC的挂载.</center>
最后要做的是运行应用程序测试它并确认如果替换了数据库Pod数据仍然存在。
<b>现在就试试</b> 运行待办事项应用程序的web Pod它连接到Postgres数据库。
```
# 部署 web app 组件:
kubectl apply -f todo-list/web/
# 等待 web Pod:
kubectl wait --for=condition=Ready pod -l app=todo-web
# 从 Service 获取访问 url:
kubectl get svc todo-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8081/new'
# 访问 app, 添加新的条目
# 删除 database Pod:
kubectl delete pod -l app=todo-db
# 检查 node 上的 卷的内容:
kubectl exec deploy/sleep -- ls -l /node-root/volumes/pv01/pg_wal
# 检查新增加的条目是不是还在 to-do list
```
在图5.18中您可以看到我的待办事项应用程序显示了一些数据您只需相信我的话这些数据被添加到第一个数据库Pod中并从第二个数据库Pod中重新加载。
![图5.18](./images/Figure5.18.png)
<center>图5.18 存储抽象意味着数据库只需挂载PVC就可以获得持久存储.</center>
我们现在有了一个很好的解耦应用程序它有一个web Pod可以独立于数据库进行更新和扩展还有一个数据库Pod它在Pod生命周期之外使用持久存储。本练习使用本地卷作为持久数据的备份存储但是对于生产部署惟一需要做的更改是将PV中的卷配置替换为集群支持的分布式卷。
是否应该在 Kubernetes 中运行关系型数据库是我们将在本章末尾解决的问题,但在此之前,我们先来看看真正的存储:让集群根据抽象的存储类动态配置卷。
## 5.4 动态 volume provisioning 及 storage classes
到目前为止我们使用的是静态配置过程。我们明确地创建了PV然后创建了PVC, Kubernetes将其绑定到PV。这适用于所有Kubernetes集群并且可能是对存储访问受到严格控制的组织的首选工作方式但大多数Kubernetes平台支持一个更简单的动态配置dynamic provisioning替代方案。
在动态配置工作流中您只需创建PVC而支持PVC的PV则由集群按需创建。集群可以配置多个存储类这些存储类反映所提供的不同卷功能也可以配置一个默认存储类。pvc可以指定他们想要的存储类的名称或者如果他们想使用默认类那么他们在声明配置中省略存储类字段如清单5.8所示。
> 清单 5.8 postgres-persistentVolumeClaim-dynamic.yaml, 动态 PVC
```
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc-dynamic
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
# 没有指定 storageClassName 字段, 所以此处使用 default class.
```
您可以在不创建 PV 的情况下将这个 PVC 部署到您的集群中—但是我不能告诉您将会发生什么,因为这取决于您的集群的设置。如果您的 Kubernetes 平台支持使用默认存储类的动态配置,那么您将看到一个 PV 被创建并绑定到 PVC并且该PV将使用集群中配置的默认类型。
<b>现在就试试</b> 部署一个 PVC并查看它是否是动态供应的。
```
# 从清单 5.8 部署 PVC:
kubectl apply -f todo-list/postgres-persistentVolumeClaim-dynamic.yaml
# 检查 claims 和 volumes:
kubectl get pvc
kubectl get pv
# 删除 claim:
kubectl delete pvc postgres-pvc-dynamic
# 再次检查卷:
kubectl get pv
```
当你运行这个练习时会发生什么?Docker Desktop使用默认存储类中的HostPath卷来动态分配 PV;AKS使用Azure文件;K3s使用HostPath但配置与Docker Desktop不同这意味着您将看不到 PV因为它仅在创建使用 PVC 的 Pod 时创建。图5.19显示了Docker Desktop的输出。PV被创建并绑定到PVC当PVC被删除时PV也被删除。
![图5.19](./images/Figure5.19.png)
<center>图5.19 Docker Desktop 有一组默认 storage class 的行为;其他平台有所不同.</center>
Storage class 提供了很大的灵活性。您将它们创建为标准的 Kubernetes 资源,并且在 spec 中,您准确地定义了存储类如何使用以下三个字段
- provisioner—按需创建 PV 的组件。不同的平台有不同的供应器例如默认AKS存储类中的供应器与Azure文件集成以创建新的文件共享。
- reclaimPolicy—定义删除 claims 时如何处理动态创建的卷。底层卷也可以被删除,也可以被保留。
- volumeBindingMode—决定是否在 PVC 创建时立即创建PV或者直到使用PVC的Pod创建时才创建PV。
通过组合这些属性,可以在集群中选择存储类,因此应用程序可以请求所需的属性(从快速本地存储到高可用集群存储)而无需指定卷或卷类型的确切细节。我无法为您提供一个可以在您的集群上工作的存储类YAML因为集群并不是所有的集群都有相同的可用供应程序。相反我们将通过克隆默认类来创建一个新的存储类。
<b>现在就试试</b> 获取默认 storage class 并克隆它包含一些令人讨厌的细节,因此我将这些步骤封装在一个脚本中。如果你好奇,你可以检查脚本内容,但之后你可能需要躺下休息一下。
```
# 查询集群 storage classes :
kubectl get storageclass
# 在 Windows 上 clone 默认的类型:
Set-ExecutionPolicy Bypass -Scope Process -Force;
./cloneDefaultStorageClass.ps1
# 或者在 Mac/Linux:
chmod +x cloneDefaultStorageClass.sh && ./cloneDefaultStorageClass.sh
# 查询 storage classes:
kubectl get sc
```
从列出存储类中看到的输出显示了集群已配置的内容。运行脚本后,您应该有一个名为 kiamol 的新类型它具有与默认存储类相同的设置。Docker Desktop的输出如图 5.20 所示。
![图5.20](./images/Figure5.20.png)
<center>图5.20 克隆 default storage class 以创建可在PVC spec 中使用的自定义类型.</center>
现在你有了一个自定义存储类你的应用程序可以在PVC中请求它。这是一种更加直观和灵活的存储管理方式特别是在动态供应简单快速的云平台中。清单5.9显示了请求新存储类的PVC规范。
> 清单 5.9 postgres-persistentVolumeClaim-storageClass.yaml
```
spec:
accessModes:
- ReadWriteOnce
storageClassName: kiamol # 这个存储类型是抽象的
resources:
requests:
storage: 100Mi
```
生产集群中的存储类将有更有意义的名称但我们现在在集群中都有一个具有相同名称的存储类因此我们可以更新Postgres数据库以使用该显式类。
<b>现在就试试</b> 创建新的PVC并更新数据库Pod Spec 以使用它。
```
# 使用自定义存储类创建一个新的PVC:
kubectl apply -f storageClass/postgres-persistentVolumeClaim-storageClass.yaml
# 更新数据库以使用新的 PVC:
kubectl apply -f storageClass/todo-db.yaml
# 检查 storage:
kubectl get pvc
kubectl get pv
# 检查 Pods:
kubectl get pods -l app=todo-db
# 刷新 to-do 应用清单
```
这个练习将数据库 Pod 切换为使用新的动态配置的PVC如图5.21中的输出所示。新的PVC由一个新的卷支持因此它将开始为空您将丢失以前的数据。以前的卷仍然存在因此您可以将另一个更新部署到数据库Pod将其恢复到旧的PVC并查看您的条目。
![图5.21](./images/Figure5.21.png)
<center>图5.21 使用 storage classes 极大地简化了你的应用配置;你只需在你的PVC中命名这个类别.</center>
## 5.5 理解 Kubernetes 中存储的选择
这就是Kubernetes的存储。在您的日常工作中您将为 pod 定义PersistentVolumeClaims并指定所需的大小和存储类这可以是一个自定义值如FastLocal或Replicated。在本章中我们花了很长时间才了解到这一点因为了解在声明存储时实际发生了什么、涉及到哪些其他资源以及如何配置它们非常重要。
我们还介绍了卷类型这是您需要深入研究的一个领域以便了解您的Kubernetes平台上有哪些可用选项以及它们提供哪些功能。如果您处于云环境中您应该拥有多个集群范围内的存储选项但请记住存储成本以及快速存储的成本很高。您需要理解您可以使用快速存储类创建PVC这些存储类可以配置为保留底层卷这意味着您在删除部署时仍然需要支付存储费用。
这给我们带来了一个大问题:你是否应该使用 Kubernetes 来运行像数据库这样的有状态应用程序?这些功能都是为了给你提供高可用性的复制存储(如果你的平台提供的话)但这并不意味着你应该急于退役你的Oracle资产用运行在Kubernetes中的MySQL来代替它。管理数据给Kubernetes应用程序增加了很多复杂性而运行有状态应用程序只是问题的一部分。需要考虑数据备份、快照和回滚如果您在云中运行托管数据库服务可能会为您提供开箱即用的服务。但是在Kubernetes清单中定义整个堆栈是非常诱人的而且一些现代数据库服务器被设计为在容器平台中运行;TiDB和CockroachDB是值得一看的选项。
现在剩下的就是在我们进入实验室之前整理您的实验室集群。
<b>现在就试试</b> 从本章使用的清单中删除所有对象。您可以忽略所得到的任何错误,因为在运行此操作时,并非所有对象都将存在。
```
# 删除 deployments, PVCs, PVs, and Services:
kubectl delete -f pi/v1 -f sleep/ -f storageClass/ -f todo-list/web -f todo-list/postgres -f todo-list/
# 删除自定义 storage class:
kubectl delete sc kiamol
```
## 5.6 实验室
这些实验旨在为您提供一些实际Kubernetes问题的经验因此我不会要求您复制这个练习来克隆默认存储类。相反我们有一个新的待办事项应用程序部署它有几个问题。我们在web Pod前使用 Proxy 来提高性能并在web Pod内使用本地数据库文件因为这只是一个开发部署。我们需要在代理层和web层配置一些持久存储这样你可以删除Pods和部署而数据仍然可以保存。
- 首先在 ch05/lab/todo-list 文件夹中部署应用清单;它为代理和web组件创建 Service 和 Deployment。
- 找到 LoadBalancer 的URL并尝试使用该应用程序。你会发现它没有响应你需要深入日志以找出问题所在.
- 您的任务是为 Proxy 缓存文件和web Pod中的数据库文件配置持久存储。您应该能够从日志条目和Pod Spec 中找到挂载目标。
- 当应用程序运行时你应该能够添加一些数据删除所有Pods刷新浏览器并看到你的数据仍然在那里。
- 您可以使用任何您喜欢的卷类型或存储类。这是一个探索你的平台提供什么的好机会。
我的解决方案像往常一样在GitHub上供你检查如果你需要: https://github.com/yyong-brs/learn-kubernetes/blob/master/kiamol/ch05/lab/README.md.

View File

@ -0,0 +1,432 @@
# 第六章 通过 controllers 在多个 Pod 之间扩展应用
扩展应用程序的基本思想很简单:运行更多的pod。Kubernetes 将网络和存储从计算层中抽象出来,所以你可以运行许多 pod它们是同一个应用程序的副本只需将它们插入相同的抽象中即可。Kubernetes称这些pod为副本在多节点集群中它们将分布在许多节点上。这为您提供了规模的所有好处:更大的负载处理能力和故障时的高可用性——所有这些都在一个可以在几秒钟内扩大和缩小的平台中。
Kubernetes 还提供了一些可替代的扩展选项来满足不同的应用程序需求,我们将在本章中详细介绍这些选项。您最常使用的是 Deployment 控制器,它实际上是最简单的,但我们也将花时间讨论其他控制器,以便您了解如何在集群中扩展不同类型的应用程序。
## 6.1 Kubernetes 如何大规模运行应用程序
Pod 是 Kubernetes 中的计算单元你在第二章中了解到你通常不会直接运行Pod;相反,您可以定义另一个资源来为您管理它们。该资源是一个控制器,从那时起我们就一直使用 Deployment 控制器。控制器 spec 配置包括一个 Pod Template用于创建和替换Pod。它可以使用相同的 Template 创建Pod的多个副本。
Deployments 可能是您在Kubernetes中使用最多的资源并且您已经有了很多使用它们的经验。现在是时候深入挖掘并了解 Deployment 实际上并不直接管理Pods——这是由另一个称为 ReplicaSet 的资源完成的。图6.1显示了Deployment、ReplicaSet和Pods之间的关系。
![图6.1](./images/Figure6.1.png)
<center>图 6.1 每个软件问题都可以通过添加另一个抽象层来解决</center>
在大多数情况下,你会使用 Deployment 来描述你的应用; Deployment 是一个管理 ReplicaSet 的控制器ReplicaSet 是一个管理 Pods 的控制器。您可以直接创建 ReplicaSet而不是使用Deployment我们将在前几个练习中这样做只是为了了解如何进行伸缩。ReplicaSet 的 YAML 与 Deployment 的 YAML 几乎相同;它需要一个选择器来查找它拥有的资源,需要一个 Pod 模板来创建资源。清单 6.1显 示了一个简短的 spec 配置。
> 清单 6.1 whoami.yaml, 一个不包含 Deployment 的 ReplicaSet
```
apiVersion: apps/v1
kind: ReplicaSet # Spec 配置与 deployment 基本相同
metadata:
name: whoami-web
spec:
replicas: 1
selector: # selector 用于匹配 Pods
matchLabels:
app: whoami-web
template: # 后面就是常规的 pod temlate.
```
在这个 spec 中,与我们使用的与 deployment 定义唯一不同的地方是对象类型 ReplicaSet 和 replicas字段它声明要运行多少个pod。该 spec 使用单个副本这意味着Kubernetes将运行单个Pod。
<b>现在就试试</b> 部署 ReplicaSet 和 LoadBalancer Service它使用与 ReplicaSet 相同的标签选择器将流量发送到Pods。
```
# 进入本章练习目录:
cd ch06
# 部署 ReplicaSet 和 Service:
kubectl apply -f whoami/
# 检查资源:
kubectl get replicaset whoami-web
# 向 service 发起 http 请求:
curl $(kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8088')
# 删除所有的 Pods:
kubectl delete pods -l app=whoami-web
# 发起请求:
curl $(kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8088')
# 查看 ReplicaSet 明细:
kubectl describe rs whoami-web
```
可以在图6.2中看到我的输出。这里没有什么新东西;ReplicaSet拥有一个Pod当你删除那个Pod时ReplicaSet会替换它。我在最后的命令中删除了kubectl描述输出但是如果运行它您将看到它以一个事件列表结束ReplicaSet在其中写入关于它如何创建Pods的活动日志。
![图6.2](./images/Figure6.2.png)
<center>图 6.2 使用 ReplicaSet 就像使用 Deployment: 它创建并管理Pods</center>
ReplicaSet 替换删除的 Pods 是因为它不断地运行一个控制循环检查它拥有的对象数量是否与它应该拥有的副本数量相匹配。当您扩展应用程序时您使用相同的机制—您更新ReplicaSet规范以设置新的副本数量然后控制循环看到它需要更多副本并从相同的 Pod 模板创建它们。
<b>现在就试试</b> 通过部署指定三个副本的更新 ReplicaSet 定义来扩展应用程序。
```
# 部署更新后的应用:
kubectl apply -f whoami/update/whoami-replicas-3.yaml
# 检查 Pods:
kubectl get pods -l app=whoami-web
# 删除所有 Pods:
kubectl delete pods -l app=whoami-web
# 再次检查:
kubectl get pods -l app=whoami-web
# 多发起几次 http 请求:
curl $(kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8088')
```
如图6.3所示,我的输出提出了几个问题:Kubernetes是如何快速扩展应用程序的以及HTTP响应是如何来自不同的pod的?
![图6.3](./images/Figure6.3.png)
<center>图 6.3 扩展 ReplicaSets 是快速的,并且在规模上,一个 Service 可以将请求分发到许多pod</center>
第一个问题很简单:这是一个单节点集群,所以每个 Pod 都将运行在同一个节点上,并且该节点已经为应用程序拉取了 Docker 镜像。当你在生产集群中扩展时,很可能会安排新的 Pod 运行在本地没有镜像的节点上,它们需要在运行 Pod 之前拉取镜像。你可以缩放的速度受限于你的镜像可以被拉取的速度,这就是为什么你需要投入时间来优化你的镜像。
至于我们如何向同一个 Kubernetes 服务发出 HTTP 请求,并从不同的 pod 获得响应,这都取决于服务和 Pods 之间的松散耦合。当你扩大 ReplicaSet 时,突然有多个 pod 与服务的标签选择器匹配当这种情况发生时Kubernetes 在 pod 之间平衡负载请求。图 6.4 显示了相同的标签选择器如何维护 ReplicaSet 和 Pods 之间以及 Service 和 Pods 之间的关系。
![图6.4](./images/Figure6.4.png)
<center>图 6.4 具有与 ReplicaSet 相同标签选择器的 Service 将使用其所有 pod</center>
网络和计算之间的抽象使得 Kubernetes 的扩展如此容易。您现在可能正在经历一种温暖的感觉——突然,所有的复杂性都开始适应,您会看到资源之间的分离是如何促成一些非常强大的功能的。这是扩展的核心:你运行尽可能多的 pod它们都位于一个 Service 后面。当消费者访问服务时Kubernetes在Pods之间分配负载。
负载均衡是 Kubernetes 中所有服务类型的特性。在这些练习中,我们已经部署了一个 LoadBalancer Service它接收到集群中的流量并将其发送到Pods。它还创建了一个 ClusterIP 供其他 pod 使用,当 pod 在集群内通信时,它们也受益于负载平衡。
<b>现在就试试</b> 部署一个新的 Pod并使用它在内部调用 who-am-I 服务,使用 ClusterIP, Kubernetes从服务名称解析ClusterIP。
```
# 运行一个 sleep Pod:
kubectl apply -f sleep.yaml
# 检查 who-am-i Service 信息:
kubectl get svc whoami-web
# 在 sleep Pod 中为服务运行DNS查找:
kubectl exec deploy/sleep -- sh -c 'nslookup whoami-web | grep "^[^*]"'
# 发起一些 http 请求:
kubectl exec deploy/sleep -- sh -c 'for i in 1 2 3; do curl -w \\n -s http://whoami-web:8088; done;'
```
如图6.5所示Pod 使用内部服务的行为与外部消费者的行为相同并且请求在Pod之间是负载平衡的。当您运行这个练习时您可能会看到请求完全均匀地分布或者您可能会看到一些pod响应不止一次这取决于网络的变化。
![图6.5](./images/Figure6.5.png)
<center>图 6.5 集群内部的世界:Pod-to-Pod网络还受益于服务负载均衡。</center>
在第3章中我们讨论了服务以及ClusterIP地址是如何从Pod的IP地址抽象出来的因此当Pod被替换时应用程序仍然可以使用相同的服务地址访问。现在您可以看到服务可以是跨许多Pod的抽象将流量路由到任何节点上的Pod的同一网络层可以跨多个Pod进行负载平衡。
## 6.2 使用 Deployments 和 ReplicaSets 来扩展负载
ReplicaSets 使得扩展应用变得非常容易:你可以在几秒钟内通过改变 spec 中的副本数量来扩展或缩小应用。它非常适合运行在小型精简容器中的无状态组件,这就是为什么 Kubernetes 构建的应用程序通常使用分布式架构,将功能分解为许多块,可以单独更新和扩展。
部署在 ReplicaSets 之上添加了一个有用的管理层。既然我们知道了它们的工作原理,我们就不再直接使用 ReplicaSets 了—— Deployments 应该是定义应用程序的首选。在第9章介绍应用程序升级和回滚之前我们不会探讨 deployment 的所有特性但是准确地理解额外的抽象会给您带来什么是很有用的。图6.6显示了这一点。
![图6.6](./images/Figure6.6.png)
<center>图 6.6 0 是所需副本的有效数量; Deployment 将旧的ReplicaSets缩小到零</center>
Deployment 是 ReplicaSet 的一个控制器为了大规模运行您需要在Deployment spec 中包含相同的replicas字段并将其传递给ReplicaSet。清单6.2显示了Pi web应用程序的缩略YAML它显式地设置了两个副本。
> 清单 6.2 web.yaml, 一个 Deployment 运行多个副本
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: pi-web
spec:
replicas: 2 # replicas 是可选项; 默认值 1.
selector:
matchLabels:
app: pi-web
template: # 后续就是 Pod 的 Template 配置.
```
Deployment 的标签选择器需要匹配 Pod 模板中定义的标签,这些标签用于表示从 Pod 到 ReplicaSet 到 Deployment 的所有权链。当您扩展一个 Deployment 时,它会更新现有的 ReplicaSet 以设置新的副本数量,但是如果您更改了 Deployment 中的 Pod spec它会替换 ReplicaSet 并将之前的ReplicaSet缩小到零。这使得 Deployment 在如何管理更新以及如何处理任何问题方面拥有很大的控制权。
<b>现在就试试</b> 为Pi web应用程序创建一个 Deployment和 Service并进行一些更新以查看如何管理ReplicaSets。
```
# 部署 Pi app:
kubectl apply -f pi/web/
# 检查 ReplicaSet:
kubectl get rs -l app=pi-web
# 增加副本 replicas:
kubectl apply -f pi/web/update/web-replicas-3.yaml
# 检查 RS:
kubectl get rs -l app=pi-web
# 使用增强的日志记录部署更改的 Pod Spec:
kubectl apply -f pi/web/update/web-logging-level.yaml
# 再次检查 ReplicaSets:
kubectl get rs -l app=pi-web
```
这个练习表明 ReplicaSet 仍然是缩放机制:当您增加或减少部署中的副本数量时它只是更新ReplicaSet。Deployment是部署机制它通过多个replicaset管理应用程序更新。我的输出(如图6.7所示)显示了Deployment如何等待新的ReplicaSet完全可操作然后再完全缩小旧的ReplicaSet。
![图6.7](./images/Figure6.7.png)
<center>图 6.7 Deployments 管理 ReplicaSets 以在更新期间保持所需数量的pod可用</center>
可以使用 kubectl scale 命令作为缩放控制器的快捷方式。你应该谨慎使用它因为它是一种强制的工作方式使用声明性YAML文件要好得多这样你的应用程序在生产中的状态总是与源代码控制中存储的规范完全匹配。但是如果您的应用程序性能不佳并且自动部署需要90秒那么这是一种快速扩展的方法——只要您还记得更新YAML文件。
<b>现在就试试</b> 直接使用 kubectl 扩展 Pi 应用程序然后看看在另一个完全部署发生时ReplicaSets会发生什么。
```
# 我们需要快速扩展 Pi app:
kubectl scale --replicas=4 deploy/pi-web
# 检查哪一个 ReplicaSet 发生了变化:
kubectl get rs -l app=pi-web
# 现在我们可以恢复到原始的日志级别:
kubectl apply -f pi/web/update/web-replicas-3.yaml
# 但这将撤销我们手动设置的副本:
kubectl get rs -l app=pi-web
# 检查 Pods:
kubectl get pods -l app=pi-web
```
当你应用更新后的 YAML 时,你会看到两件事:应用程序缩小到三个副本部署通过将新的ReplicaSet缩小到0个pod和将旧的ReplicaSet 回归到3个pod来实现这一点。图 6.8显示了更新后的Deployment将创建三个新的pod。
![图6.8](./images/Figure6.8.png)
<center>图 6.8 Deployment 知道它们的 ReplicaSet 的 spec 信息并且可以通过扩展旧的ReplicaSet来回滚。</center>
部署更新覆盖了手动缩放级别,这并不奇怪;YAML定义是理想的状态如果两者不一致Kubernetes不会试图保留当前规范的任何部分。部署重用旧的ReplicaSet而不是创建一个新的ReplicaSet这可能更令人惊讶但这是Kubernetes工作的更有效的方式因为有更多的标签。
从部署中创建的Pods有一个生成的名称看起来是随机的但实际上不是。Pod名称包含部署的Pod spec 中的模板哈希,因此如果您对 spec 进行了与以前的部署相匹配的更改,那么它将具有与缩小的 ReplicaSet 相同的模板哈希,并且部署可以找到该 ReplicaSet 并再次扩大它以实现更改。Pod模板哈希存储在一个标签中。
<b>现在就试试</b> 查看Pi Pods和ReplicaSets的标签以查看 template hash 信息。
```
# 查询 ReplicaSets 并显示 labels:
kubectl get rs -l app=pi-web --show-labels
# 查询 Pods 显示 labels:
kubectl get po -l app=pi-web --show-labels
```
图6.9显示 template hash 包含在对象名称中但这只是为了方便——kubernetes使用标签进行管理。
![图6.9](./images/Figure6.9.png)
<center>图 6.9 Kubernetes 生成的对象名称不仅仅是随机的——它们包括 template hash 值</center>
了解一个 Deployment 如何与它的 Pods 相关的内部机制将帮助您理解更改是如何推出的并在您看到大量Pod数为零的ReplicaSets时消除任何困惑。但是Pods中的计算层和服务中的网络层之间的交互是以相同的方式工作的。
在典型的分布式应用程序中,每个组件都有不同的规模需求,您将使用 Service 来实现它们之间的多层负载平衡。到目前为止我们部署的Pi应用程序只有一个ClusterIP Service——它不是面向公众的组件。公共组件是一个代理(实际上,它是一个反向代理,因为它处理传入流量而不是传出流量)并且使用LoadBalancer Service。我们可以大规模地运行web组件和代理并实现从客户端到代理Pods以及从代理到应用程序Pods的负载平衡。
<b>现在就试试</b> 创建代理 Deployment它运行两个副本以及一个Service和ConfigMap用于设置与Pi web应用程序的集成。
```
# 部署 proxy 资源:
kubectl apply -f pi/proxy/
# 获取代理应用 URL:
kubectl get svc whoami-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8080/?dp=10000'
# 访问 app, 多尝试几个不同的 dp 参数值
```
如果您在浏览器中打开开发人员工具并查看网络请求,您可以找到代理发送的响应头。其中包括代理服务器的主机名(实际上是Pod名)以及网页本身包括生成响应的web应用程序Pod的名称。我的输出(如图6.10所示)显示了来自代理缓存的响应。
![图6.10](./images/Figure6.10.png)
<center>图 6.10 Pi的响应包括发送它们的Pod的名称所以你可以看到负载均衡在工作。</center>
这个配置很简单很容易扩展。代理的Pod spec 使用两个卷:ConfigMap用于加载代理配置文件EmptyDir用于存储缓存的响应。ConfigMap是只读的所以一个ConfigMap可以被所有的代理pod共享。EmptyDir卷是可写的但它们对于Pod是唯一的因此每个代理都有自己的卷用于缓存文件。图6.11显示了设置过程。
![图6.11](./images/Figure6.11.png)
<center>图 6.11 大规模运行Pod——一些类型的卷可以共享而另一些则是Pod独有的。</center>
这种架构提出了一个问题,如果您请求将 Pi 移到小数点后很多位并不断刷新浏览器您就会看到这个问题。第一个请求会很慢因为它是由web应用程序计算的;随后的响应将很快因为它们来自代理缓存但很快你的请求将转到另一个代理Pod它的缓存中没有响应所以页面将再次加载缓慢。
通过使用共享存储来解决这个问题会很好这样每个代理Pod都可以访问相同的缓存。这样做将把我们带回到分布式存储的棘手领域我们认为我们已经在第5章中离开了但是让我们从一个简单的方法开始看看它会给我们带来什么。
<b>现在就试试</b> 部署对代理 spec 的更新该更新使用HostPath卷来缓存文件而不是EmptyDir卷。同一节点上的多个pod将使用相同的卷这意味着它们将有一个共享的代理缓存。
```
# 部署更新后的 spec:
kubectl apply -f pi/proxy/update/nginx-hostPath.yaml
# 检查 Pods—新的配置有 3个副本:
kubectl get po -l app=pi-proxy
# 访问 Pi app, 多刷新几次
# 检查 proxy 的日志:
kubectl logs -l app=pi-proxy --tail 1
```
现在你应该能够刷新到你想要的内容并且响应总是来自缓存无论你被指向哪个代理Pod。图6.12显示了所有响应请求的代理pod这些请求由服务在它们之间共享。
对于大多数有状态应用程序这种方法不起作用。写数据的应用程序倾向于假设它们对文件有独占访问权如果同一个应用程序的另一个实例试图使用相同的文件位置你会得到意想不到但令人失望的结果——比如应用程序崩溃或数据损坏。我使用的反向代理叫做Nginx;它在这里非常宽容,并且它很乐意与它自己的其他实例共享它的缓存目录。
![图6.12](./images/Figure6.12.png)
<center>图 6.12 在规模上您可以使用标签选择器使用kubectl查看所有Pod日志</center>
如果你的应用需要伸缩性和存储空间你可以选择使用不同类型的控制器。在本章的剩余部分我们将查看DaemonSet;最后一种类型是StatefulSet它很快就会变得复杂我们将在第8章讲到它在那里它得到了本章的大部分内容。DaemonSets和StatefulSets都是Pod控制器尽管使用它们的频率远低于 Deployment ,但您需要知道可以使用它们做什么,因为它们支持一些强大的模式。
## 6.3 使用 DaemonSets 实现高可用性
DaemonSet 得名于 Linux 守护进程,它通常是一个在后台作为单个实例不断运行的系统进程(相当于Windows世界中的Windows Service)。在Kubernetes中DaemonSet 在集群中的每个节点上运行 Pod 的单个副本,如果在 spec 中添加选择器,则在节点的子集上运行 Pod 的单个副本。
daemonset 通常用于基础设施级的关注点您可能希望从每个节点获取信息并将其发送到中央收集器。Pod 在每个节点上运行只为该节点抓取数据。您不需要担心任何资源冲突因为节点上只有一个Pod。我们将在本书后面使用 DaemonSets 从 Pods 收集日志,以及关于节点活动的度量。
当您希望获得高可用性,而不需要在每个节点上加载多个副本时,也可以在自己的设计中使用它们。反向代理就是一个很好的例子:单个Nginx Pod可以处理数千个并发连接所以你不一定需要很多但你可能想确保每个节点上都有一个运行这样本地Pod就可以响应任何流量到达的地方。清单6.3显示了daemonset的缩略YAML——它看起来很像其他控制器但没有 replica 计数。
> 清单 6.3 nginx-ds.yaml, 代理组件的 DamonSet
```
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: pi-proxy
spec:
selector:
matchLabels: # DaemonSets使用相同的标签选择器机制.
app: pi-proxy
template:
metadata:
labels:
app: pi-proxy # 应用于Pods的标签必须与选择器匹配。
spec:
# 后续就是 pod 常规的 spec 信息
```
代理的这个 spec 仍然使用HostPath卷。这意味着每个Pod都有自己的代理缓存所以我们无法从共享缓存中获得最佳性能。这种方法适用于其他有状态应用它们比Nginx更麻烦因为多个实例使用相同的数据文件没有问题。
<b>现在就试试</b> 你不能从一种类型的控制器转换为另一种类型的控制器但是我们可以在不破坏应用程序的情况下将Deployment转换为DaemonSet。
```
# 部署 DaemonSet:
kubectl apply -f pi/proxy/daemonset/nginx-ds.yaml
# 检查代理服务中使用的端点:
kubectl get endpoints pi-proxy
# 删除 Deployment:
kubectl delete deploy pi-proxy
# 检查 DaemonSet:
kubectl get daemonset pi-proxy
# 检查 Pods:
kubectl get po -l app=pi-proxy
# 在浏览器上刷新最新的圆周率计算
```
图 6.13 显示了我的输出。在删除 Deployment 之前创建DaemonSet意味着始终有可用的Pods来接收来自服务的请求。首先删除Deployment将使应用程序不可用直到DaemonSet启动。如果您检查HTTP响应头您还应该看到您的请求来自代理缓存因为新的DaemonSet Pod使用与Deployment Pods相同的HostPath卷。
![图6.13](./images/Figure6.13.png)
<center>图 6.13 你需要为一个大的改变计划部署的顺序,以保持你的应用在线</center>
我使用的是单节点集群,所以我的 DaemonSet 运行一个Pod;如果节点更多每个节点上就有一个Pod。控制循环监视加入集群的节点任何新节点都将在加入集群后立即启动一个副本Pod。控制器还会监视Pod状态因此如果Pod被移除则会启动一个替换。
<b>现在就试试</b> 手动删除代理Pod。DaemonSet将启动一个替换。
```
# 检查 DaemonSet 状态:
kubectl get ds pi-proxy
# 删除 Pod:
kubectl delete po -l app=pi-proxy
# 检查 Pods:
kubectl get po -l app=pi-proxy
```
如果你在 Pod 被删除时刷新浏览器,你会看到它在 DaemonSet 启动替换之前没有响应。这是因为您使用的是单节点实验室集群。服务只向正在运行的Pod发送流量因此在多节点环境中请求将发送到仍然拥有健康Pod的节点。图6.14显示了我的输出。
![图6.14](./images/Figure6.14.png)
<center>图 6.14 DaemonSets 监视节点和 pod以确保始终满足所需的副本计数。</center>
需要 DaemonSet 的情况通常比只想在每个节点上运行Pod更加微妙。在这个代理示例中您的生产集群可能只有一个可以从internet接收流量的节点子集因此您希望只在这些节点上运行代理Pods。您可以通过标签来实现这一点添加任何您想要标识节点的任意标签然后在Pod spec 中选择该标签。清单6.4使用 nodeSelector字段显示了这一点。
> 清单 6.4 nginx-ds-nodeSelector.yaml, 配置了 node selection 的 daemonset
```
# 这是在DaemonSet的 template 域中的Pod spec。
spec:
containers:
# ...
volumes:
# ...
nodeSelector: # Pods 将只在某些节点上运行.
kiamol: ch06
```
DaemonSet 控制器不只是观察加入集群的节点;它会查看所有节点看看它们是否符合Pod spec 中的要求。当你部署这个更改时你会告诉DaemonSet只在标签kiamol设置为ch06值的节点上运行。集群中没有匹配的节点因此DaemonSet将缩小到零。
<b>现在就试试</b> 更新 DaemonSet 以包含清单 6.4 中的节点选择器。现在没有符合要求的节点所以现有Pod将被移除。然后标记一个节点一个新的Pod将被调度。
```
# 更新 DaemonSet spec:
kubectl apply -f pi/proxy/daemonset/nginx-ds-nodeSelector.yaml
# 检查 DS:
kubectl get ds pi-proxy
# 检查 Pods:
kubectl get po -l app=pi-proxy
# 现在标记集群中的一个节点,使其与选择器匹配:
kubectl label node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') kiamol=ch06 --overwrite
# 再次检查 pod:
kubectl get ds pi-proxy
```
您可以在图 6.15 中看到 DaemonSet 的控制循环。当应用节点选择器时没有节点符合选择器因此DaemonSet所需的副本计数降为零。现有Pod对于所需的计数来说多了一个因此将其删除。然后当节点被标记时有一个匹配的选择器所需的计数增加到1因此创建一个新的Pod。
![图6.15](./images/Figure6.15.png)
<center>图 6.15 DaemonSets 监视节点及其标签,以及当前 Pod 状态。</center>
DaemonSets 与 ReplicaSets 具有不同的控制循环因为它们的逻辑需要监视节点活动以及Pod计数但从根本上讲它们都是管理Pod的控制器。所有控制器都对其托管对象的生命周期负责但这些链接可能会断开。我们将在另一个练习中使用DaemonSet来演示如何将pod从控制器中释放出来。
<b>现在就试试</b> Kubectl 在 delete 命令上有一个级联选项您可以使用该选项删除控制器而不删除其管理对象。这样做会留下孤儿pod如果它们与之前的主人匹配就可以被另一个控制器收养。
```
# 删除 DaemonSet, 留下 Pod:
kubectl delete ds pi-proxy --cascade=false
# 检查 pod:
kubectl get po -l app=pi-proxy
# 重新创建 DS:
kubectl apply -f pi/proxy/daemonset/nginx-ds-nodeSelector.yaml
# 检查 DS and Pod:
kubectl get ds pi-proxy
kubectl get po -l app=pi-proxy
# 再次删除 DS, 不增加 cascade 选项:
kubectl delete ds pi-proxy
# 检查 Pods:
kubectl get po -l app=pi-proxy
```
图 6.16 显示了同一个 Pod 在 DaemonSet 被删除和重新创建的过程中仍然存在。新的 DaemonSet 需要一个Pod现有的Pod与它的 template 匹配因此它成为Pod的管理器。当这个DaemonSet被删除时Pod也会被删除。
![图6.16](./images/Figure6.16.png)
<center>图 6.16 孤儿 Pod 已经失去了他们的控制器,所以他们不再是高可用集的一部分。</center>
暂停级联删除是您将很少使用的功能之一但当您确实需要它时您将非常高兴了解它。在这种情况下您可能对现有的所有pod都很满意但在节点上有一些维护任务。与让DaemonSet在处理节点时添加和删除Pods不同您可以在维护完成后删除它并恢复它。
我们在这里为DaemonSets使用的示例是关于高可用性的但它仅限于某些类型的应用程序——您需要多个实例并且每个实例都有自己独立的数据存储是可以接受的。其他需要高可用性的应用程序可能需要在实例之间保持数据同步对于这些应用程序您可以使用StatefulSets。不过不要直接跳到第8章因为在第7章中你也会学到一些有助于有状态应用的简洁模式。
statfulsets, DaemonSets, ReplicaSets和deployment是你用来建模应用的工具它们应该给你足够的灵活性让你在Kubernetes中运行几乎任何东西。我们将快速浏览一下Kubernetes实际上是如何管理拥有其他的对象来结束本章然后我们将回顾本书第一部分的内容。
## 6.4 理解 Kubernetes 中的对象所有权
控制器使用标签选择器来查找它们管理的对象,对象本身在 metadata 字段中保存其所有者的记录。删除控制器后其管理对象仍然存在但时间不会太长。Kubernetes运行一个垃圾收集器进程该进程查找已删除所有者的对象并删除它们。对象所有权可以建模层次结构:Pods由replicaset拥有replicaset由deployment拥有。
<b>现在就试试</b> 查看所有 Pods 和 ReplicaSets 的 metadata 字段中的所有者引用。
```
# 检查哪些对象拥有Pods:
kubectl get po -o custom-columns=NAME:'{.metadata.name}', OWNER:'{.metadata.ownerReferences[0].name}',OWNER_KIND:'{.metadata.ownerReferences[0].kind}'
# 检查哪些对象拥有 ReplicaSets:
kubectl get rs -o custom-columns=NAME:'{.metadata.name}', OWNER:'{.metadata.ownerReferences[0].name}',OWNER_KIND:'{.metadata.ownerReferences[0].kind}'
```
图6.17显示了我的输出其中我的所有pod都由其他对象拥有而我的ReplicaSets中只有一个是由Deployment拥有的。
![图6.17](./images/Figure6.17.png)
<center>图 6.17 对象知道它们的所有者是谁——您可以在对象 metadata 中找到这一点。</center>
Kubernetes 在管理关系方面做得很好但您需要记住控制器仅使用标签选择器跟踪它们的依赖项因此如果您更改了标签可能会破坏这种关系。大多数情况下默认的删除行为是您想要的但是您可以使用kubectl停止级联删除只删除控制器—这将删除依赖项元数据中的所有者引用因此它们不会被垃圾收集器拾取。
我们将以我们在本章中部署的Pi应用程序的最新版本的架构来结束。图6.18显示了它的所有信息。
![图6.18](./images/Figure6.18.png)
<center>图 6.18 Pi应用程序:不需要注释——图应该非常清晰。</center>
这个图中包含了很多内容:它是一个简单的应用程序,但部署起来很复杂,因为它使用了大量 Kubernetes 特性来获得高可用性、伸缩性和灵活性。到目前为止您应该已经熟悉了所有这些Kubernetes资源并且应该了解它们是如何组合在一起的以及何时使用它们。大约有150行YAML定义了这个应用程序但是这些YAML文件是在您的笔记本电脑或云中的50个节点集群上运行这个应用程序所需要的。当有人新加入这个项目时如果他们有扎实的Kubernetes经验或者如果他们已经阅读了本书的前六章他们就可以立即高效地工作。
以上就是第一部分的全部内容。如果你这周的午餐时间延长了我很抱歉但现在你已经掌握了Kubernetes的所有基础知识并内置了最佳实践。我们要做的就是在你进入实验室之前清理一下。
<b>现在就试试</b> 本章中所有顶级对象都应用了kiamol标签。现在您已经理解了级联删除您将知道当您删除所有这些对象时它们的所有依赖项也将被删除。
```
# 删除所有的 controllers and Services:
kubectl delete all -l kiamol=ch06
```
## 6.5 实验室
Kubernetes 在过去的几年里发生了很大的变化。我们在本章中使用的控制器是推荐的,但在过去也有替代方案。在这个实验室中,您的工作是使用一些旧方法的应用程序规范,并将其更新为使用您所了解的控制器。
- 首先在ch06/lab/numbers中部署应用程序-这是第3章中的随机数应用程序但配置很奇怪。它坏了。
- 需要更新web组件以使用支持高负载的控制器。我们希望在生产环境中运行几十个这样的测试。
- API也需要更新。它需要被复制以获得高可用性但该应用程序使用了连接到服务器的硬件随机数生成器一次只能被一个Pod使用。具有正确硬件的节点具有标签rng=hw(您需要在您的集群中模拟该标签)。
- 这不是一个干净的升级所以你需要计划你的部署以确保没有停机的web应用程序。
听起来很可怕但你不应该觉得这太糟糕。我的解决方案在GitHub上供你查看: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch06/lab/README.md.

View File

@ -0,0 +1,522 @@
# 第七章 使用多容器 Pods 扩展应用程序
我们在第 2 章中见识了 Pod那时你知道你可以在一个 Pod 中运行多个容器,但实际上你并没有这样做。在本章中,您将看到它是如何工作的,并理解它所支持的模式。这是本书这一部分的第一个更高级的主题,但它不是一个复杂的主题——它只是帮助您了解前面章节的所有背景知识。从概念上讲,这很简单:一个Pod 运行许多容器,通常是您的应用程序容器加上一些 helper 容器。正是使用这些 helper 可以做的事情使得这个特性如此有趣。
Pod 中的容器共享相同的虚拟环境因此当一个容器执行一个操作时其他容器可以看到它并对其做出反应。它们甚至可以在原始容器不知道的情况下修改预期的操作。这种行为允许您对应用程序进行建模这样应用程序容器就非常简单了——它只专注于自己的工作并且有帮助程序负责将应用程序与其他组件和Kubernetes平台集成。这是向所有应用程序(无论是新应用程序还是旧应用程序)添加一致的管理API的好方法。
## 7.1 Pod 中多个容器如何通信
Pod 是一个虚拟环境,为一个或多个容器创建共享网络和文件系统空间。容器是孤立的单元;它们有自己的流程和环境变量并且可以使用不同技术堆栈的不同镜像。Pod 是一个单独的单元,因此当它被分配到一个节点上运行时,所有 Pod 容器都运行在同一个节点上。你可以让一个容器运行 Python另一个运行 Java但你不能让一些 Linux 容器和一些 Windows 容器在同一个Pod中(目前),因为 Linux 容器需要运行在 Linux 节点上而Windows 容器需要运行在 Windows 节点上。
Pod 中的容器共享网络因此每个容器都有相同的IP地址——Pod 的 IP 地址。多个容器可以接收外部流量但它们需要侦听不同的端口Pod 中的容器可以使用本地主机地址进行通信。每个容器都有自己的文件系统,但它可以从 Pod 挂载卷因此容器可以通过共享相同的挂载来交换信息。图7.1显示了带有两个容器的 Pod 的布局。
![图7.1](./images/Figure7.1.png)
<center>图 7.1 Pod 是许多容器的共享网络和存储环境</center>
这就是我们现在需要的所有理论,当我们阅读本章时,您会惊讶地发现仅使用共享网络和磁盘就可以完成一些聪明的事情。在本节中,我们将从一些简单的练习开始探索 Pod 环境。清单7.1显示了一个 Deployment 的多容器 Pod Spec 配置。定义了恰好使用相同镜像的两个容器,它们都挂载了 Pod 中定义的 EmptyDir 卷。
> 清单 7.1 sleep-with-file-reader.yaml, 一个简单的多容器 Pod spec
```
spec:
containers: # containers 字段是个数组
- name: sleep
image: kiamol/ch03-sleep
volumeMounts:
- name: data
mountPath: /data-rw # 可写挂载一个卷
- name: file-reader # 多个容器的名字不能一样
image: kiamol/ch03-sleep # 但是可以使用相同或者不同的镜像
volumeMounts:
- name: data
mountPath: /data-ro
readOnly: true # 只读挂载同一个卷
volumes:
- name: data # Volumes 可以被多个目标挂载
containers.
emptyDir: {}
```
这是一个运行两个容器的 Pod Spec。当您部署它时您将看到在如何使用多容器 Pods 方面存在的一些差异。
<b>现在就试试</b> 部署清单 7.1, 运行两个容器的 Pod
```
# 进入到章节练习目录:
cd ch07
# 部署 Pod:
kubectl apply -f sleep/sleep-with-file-reader.yaml
# 查看 Pod 详细信息:
kubectl get pod -l app=sleep -o wide
# 显示 container 名称:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[*].name}'
# 检查日志—将会失败:
kubectl logs -l app=sleep
```
我的输出(如图 7.2 所示)显示Pod有两个具有单个IP地址的容器它们都运行在同一个节点上。您可以看到 Pod 作为单个单元的详细信息但不能在Pod级别打印日志;您需要指定一个容器,从中获取日志。
![图7.2](./images/Figure7.2.png)
<center>图 7.2 您总是将 Pod 作为单个单元使用,除非需要指定容器</center>
该练习中的两个容器都使用 sleep 镜像因此它们没有做任何事情但容器继续运行Pod保持可用。这两个容器都从 Pod 挂载 EmptyDir 卷,因此这是文件系统的共享部分,您可以在两个容器中使用它。
<b>现在就试试</b> 一个容器以读写方式挂载卷,另一个容器以只读方式挂载卷。您可以在一个容器中写入文件,在另一个容器中读取文件。
```
# 使用一个容器将文件写入共享卷:
kubectl exec deploy/sleep -c sleep -- sh -c 'echo ${HOSTNAME} > /data-rw/hostname.txt'
# 使用相同的容器读取文件:
kubectl exec deploy/sleep -c sleep -- cat /data-rw/hostname.txt
# 使用另一个容器读取文件:
kubectl exec deploy/sleep -c file-reader -- cat /data-ro/hostname.txt
# 尝试将文件添加到只读容器-这将失败:
kubectl exec deploy/sleep -c file-reader -- sh -c 'echo more >> /data-ro/hostname.txt'
```
在运行这个练习时您将看到第一个容器可以向共享卷写入数据第二个容器可以读取数据但它本身不能写入数据。这是因为在Pod spec 中,第二个容器的卷挂载被定义为只读。这不是一般的 Pod 限制;如果需要可以将挂载定义为多个容器的可写。图7.3显示了我的输出。
![图7.3](./images/Figure7.3.png)
<center>图 7.3 容器可以挂载相同的Pod卷以共享数据但具有不同的访问级别。</center>
一个好的 empty 卷在这里再次显示了它的价值;这是一个简单的便签,所有的 Pod 容器都可以访问。卷是在 Pod 级别定义的并安装在容器级别这意味着您可以使用任何类型的卷或PVC并使其可供许多容器使用。将卷定义与卷挂载解耦还允许选择性共享因此一个容器可能能够看到Secrets而其他容器则不能。
另一个共享空间是网络其中容器可以侦听不同的端口并提供独立的功能。如果你的应用容器正在做一些后台工作但没有任何功能来报告进度这是很有用的。同一Pod中的另一个容器可以提供REST API该API报告应用程序容器正在做什么。
清单7.2显示了这个过程的简化版本。这是对sleep deployment 的更新,它将文件共享容器替换为运行简单 HTTP 服务的新容器 spec。
> 清单 7.2 sleep-with-server.yaml, 在另一个容器里面运行 web 服务
```
spec:
containers:
- name: sleep
image: kiamol/ch03-sleep # 和清单 7.1 相同的容器
- name: server
image: kiamol/ch03-sleep # 第二个容器不一样
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 ..."]
ports:
- containerPort: 8080 # 包括端口只记录应用程序使用的端口。
```
现在 Pod 将使用原始的应用程序容器(sleep 容器,实际上什么都不做)和服务器容器(在端口8080上提供HTTP端点)运行。这两个容器共享相同的网络空间,因此 Sleep 容器可以使用本地主机地址访问服务器。
<b>现在就试试</b> 使用清单 7.2 中的文件更新 sleep Deployment并确认 server 容器是可访问的。
```
# 部署更新的内容:
kubectl apply -f sleep/sleep-with-server.yaml
# 检查 Pod 状态:
kubectl get pods -l app=sleep
# 列出新 Pod 中容器的名称:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[*].name}'
# 在容器之间进行网络调用:
kubectl exec deploy/sleep -c sleep -- wget -q -O - localhost:8080
# 检查 server 容器 logs:
kubectl logs -l app=sleep -c server
```
可以在图 7.4 中看到我的输出。虽然它们是独立的容器,但在网络级别上,它们的功能就像运行在同一台机器上的不同进程一样,使用本地地址进行通信。
![图7.4](./images/Figure7.4.png)
<center>图 7.4 同一 Pod 中的容器之间的网络通信是通过本地主机进行的 </center>
网络不仅在 Pod 内部是共享的。Pod 在集群上有一个 IP 地址,如果 Pod 中的任何容器正在侦听端口,那么其他 Pod 就可以访问它们。您可以创建一个Service将流量路由到特定端口上的 Pod在该端口上侦听的容器将接收请求。
<b>现在就试试</b> 使用 kubectl 命令公开 Pod 端口——这是一种无需编写yaml就可以快速创建服务的方法然后测试HTTP服务是否可以从外部访问。
```
# 创建一个针对 server 容器端口的 Service:
kubectl expose -f sleep/sleep-with-server.yaml --type LoadBalancer --port 8020 --target-port 8080
# 获取你的 service 的URL:
kubectl get svc sleep -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8020'
# 打开浏览器输入地址
# 检查 server 容器日志:
kubectl logs -l app=sleep -c server
```
图 7.5 显示了我的输出。从外部世界来看它只是流向服务的网络流量服务被路由到Pod。Pod正在运行多个容器但这是一个对消费者隐藏的细节。
![图7.5](./images/Figure7.5.png)
<center>图 7.5 Service 可以将网络请求路由到任何已发布端口的Pod容器</center>
你应该能感受到在 Pod 中运行多个容器是多么强大,在本章的剩余部分,我们将把这些想法应用到现实场景中。有一件事需要强调:Pod不是 VM 的替代品,所以不要认为你可以在一个 Pod 中运行应用程序的所有组件。你可能会想用一个web服务器容器和一个 API 容器在同一个 pod 中运行来建模一个应用程序。Pod是一个单独的单元它应该用于应用程序的单个组件。可以使用其他容器来支持应用程序容器但你不应该在同一个 Pod 中运行不同的应用程序。这样做会破坏您独立更新、扩展和管理这些组件的能力。
## 7.2 使用 init 容器设置应用程序
到目前为止,我们已经运行了带有多个容器的 Pod其中所有容器都是并行运行的:它们一起启动直到所有容器都准备好了Pod 才被认为是准备好了。您将听到一种被称为 sidecar 的模式,它强化了附加容器(sidecar)对应用程序容器(摩托车)发挥支持作用的想法。Kubernetes 还支持另一种模式,当你需要在应用容器之前运行一个容器来设置部分环境时。这被称为 init 容器。
Init 容器的工作方式与 sidecars 不同。你可以为 Pod 定义多个 init 容器,它们按照 Pod spec 中写的顺序依次运行。每个 init 容器需要在下一个容器启动之前成功完成,并且所有的 init 容器都必须在 Pod 容器启动之前成功完成。图 7.6 显示了带有 init 容器的 Pod 的启动顺序。
![图7.6](./images/Figure7.6.png)
<center>图 7.6 Init 容器对于启动任务很有用,可以为应用程序容器准备 Pod</center>
所有容器都可以访问 Pod 中定义的卷,因此主要的用例是 init 容器写入为应用程序容器准备环境的数据。清单 7.3 显示了前面练习中的 sleep Pod 中对HTTP服务的简单扩展。init 容器运行并生成 HTML 文件,它将该文件写入 EmptyDir 卷的挂载中。server 容器通过发送该文件的内容来响应 HTTP 请求。
> 清单 7.3 sleep-with-html-server.yaml, Pod spec 配置中的 init 容器
```
spec: # Deployment template 下的 Pod spec 配置
initContainers: # init 容器同样也是一个数组
- name: init-html # 它们顺序运行
image: kiamol/ch03-sleep
command: ['sh', '-c', "echo '<!DOCTYPE html...' > /data/index.html"]
volumeMounts:
- name: data
mountPath: /data # Init 容器同样可以挂在 Pod volumes.
```
本例为 init 容器使用相同的 sleep 镜像,但它可以是任何镜像。您可以使用 init 容器来设置应用程序环境使用的工具不希望出现在正在运行的应用程序中。init 容器可以使用安装了 Git 命令行的Docker 镜像并将存储库克隆到共享文件系统中。应用程序容器可以访问这些文件而无需在应用程序镜像中设置Git客户端。
<b>现在就试试</b> 部署清单 7.3 的更新, 看一下 init 容器是如何工作的
```
# 部署 init 容器的部署更新:
kubectl apply -f sleep/sleep-with-html-server.yaml
# 检查 Pod 容器:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.containerStatuses[*].name}'
# 检查 init 容器:
kubectl get pod -l app=sleep -o jsonpath='{.items[0].status.initContainerStatuses[*].name}'
# 检查 init container 的日志—什么都没有:
kubectl logs -l app=sleep -c init-html
# 检查 pod 容器中的那个文件是否可用:
kubectl exec deploy/sleep -c server -- ls -l /data-ro
```
从这个练习中你会学到一些东西。App 容器被保证在 init 容器成功完成之前不会运行,所以你的应用可以安全地假设 init 容器准备的环境。在这种情况下HTML 文件必须在 Server 容器启动之前存在。Init 容器是Pod Spec 配置的不同部分,但一些管理特性的工作方式与应用程序容器相同——即使在 Init 容器退出后,您也可以从 Init 容器读取日志。我的输出如图 7.7 所示。
![图7.7](./images/Figure7.7.png)
<center>图 7.7 Init 容器对于为 app 和 sidecar 容器准备 Pod 环境非常有用。</center>
不过这仍然不是一个非常真实的例子所以让我们做一些更好的事情。我们在第4章讨论了应用程序配置并了解了如何使用环境变量、ConfigMaps和Secrets来构建配置设置的层次结构。如果你的应用程序支持这种灵活性那就太好了但许多老应用程序没有这种灵活性;他们希望在一个地方找到一个配置文件,而不会去其他地方寻找。让我们看看这样一个应用程序。
<b>现在就试试</b> 这一章有一个新的演示应用程序,因为如果我已经厌倦了看圆周率,那么你一定也厌倦了。这款游戏并不有趣,但至少有所不同。它只是每隔几秒钟向日志文件写入一个时间戳。它有一个老式的配置框架,所以我们不能使用到目前为止学到的任何配置技术。
```
# 运行应用,它使用到了一个配置文件:
kubectl apply -f timecheck/timecheck.yaml
# 检查容器日志—不会有任何东西:
kubectl logs -l app=timecheck
# 检查容器内的日志文件:
kubectl exec deploy/timecheck -- cat /logs/timecheck.log
# 检查配置信息:
kubectl exec deploy/timecheck -- cat /config/appsettings.json
```
您可以在图 7.8 中看到我的输出。有限的配置框架并不是这个应用程序在容器平台中不是一个好公民的唯一原因——Pod中也没有日志——但我们可以通过Pod中的其他容器解决所有问题。
![图7.8](./images/Figure7.8.png)
<center>图 7.8 使用单一配置源的旧应用程序无法从配置层次结构中受益</center>
init 容器是一个完美的工具可以让这个应用程序与我们想要用于所有应用程序的配置方法保持一致。我们可以将设置存储在ConfigMaps、Secrets和环境变量中并使用init容器从所有不同的输入中读取合并内容并将输出写入应用程序使用的单个文件位置。清单7.4显示了Pod spec 中的init容器。
> 清单 7.4 timecheck-with-config.yaml, 一个 init 容器写入了配置
```
spec:
initContainers:
- name: init-config
image: kiamol/ch03-sleep # 镜像中存在 jq 工具
command: ['sh', '-c', "cat /config-in/appsettings.json | jq --arg
APP_ENV \"$APP_ENVIRONMENT\" '.Application.Environment=$APP_ENV' >
/config-out/appsettings.json"]
env:
- name: APP_ENVIRONMENT # 所有容器有它们自己的环境变量,它们不在 POd 中共享
value: TEST
volumeMounts:
- name: config-map # 挂载一个 ConfigMap 用于读
mountPath: /config-in
- name: config-dir
mountPath: /config-out # 挂载一个 EmptyDir volume 用于写
```
在我们更新 deployment 之前,有几件事需要注意:
- init 容器使用 jq 工具,应用程序不需要 jq 工具。容器使用不同的镜像,每个镜像都带有运行该步骤所需的工具。
- init容器中的命令从 ConfigMap 卷挂载中读取合并环境变量值并写入EmptyDir卷挂载中。
- 应用程序容器将 EmptyDir 卷挂载到需要配置文件的路径。init 容器生成的文件隐藏了应用镜像中的默认配置。
- 容器不共享环境变量。这些设置是为 init 容器指定的;应用程序容器看不到这些。
- 容器映射它们所需要的卷。两个容器都挂载它们共享的EmptyDir卷但只有init容器挂载ConfigMap。
当我们应用此更新时应用程序的行为将随着ConfigMap和环境变量而改变即使应用程序容器没有将它们用作配置源。
<b>现在就试试</b> 使用清单 7.4 更新 timecheck 应用程序,以便从多个源配置应用程序容器。
```
# 部署 ConfigMap 以及新的 Deployment spec:
kubectl apply -f timecheck/timecheck-configMap.yaml -f timecheck/timecheck-with-config.yaml
# 等待容器启动:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck,version=v2
# 检查新的应用容器的日志:
kubectl exec deploy/timecheck -- cat /logs/timecheck.log
# 查看 init 容器创建的配置文件:
kubectl exec deploy/timecheck -- cat /config/appsettings.json
```
当您运行这个时,您将看到应用程序与新配置一起工作,应用程序容器 spec 的唯一变化是配置目录从EmptyDir卷中挂载。我的输出如图7.9所示。
![图7.9](./images/Figure7.9.png)
<center>图 7.9 Init 容器可以在不改变应用代码或 Docker 镜像的情况下改变应用行为。</center>
这种方法有效,因为配置文件是从专用目录加载的。请记住,卷挂载将覆盖镜像中的目录(如果该目录已经存在)。如果应用程序从与应用程序二进制文件相同的目录加载配置文件你不能这样做因为EmptyDir挂载会覆盖整个应用程序文件夹。在这种情况下您需要在应用程序容器启动中执行额外的步骤将配置文件从挂载复制到应用程序目录中。
将标准配置方法应用于非标准应用程序是init容器的一大用处但旧的应用程序仍然不能在现代平台上很好地运行这就是 sidecar 容器可以帮助的地方。
## 7.3 通过 adapter 容器以应用一致性
将应用程序迁移到 Kubernetes 是一个很好的机会,可以在所有应用程序之间添加一致性层,因此无论应用程序正在做什么,或者它使用什么技术堆栈,或者它是什么时候开发的,您都可以使用相同的工具以相同的方式部署和管理它们。我的同事、码头工人队长苏恩·凯勒(Sune Keller)谈到过Alm Brand使用的服务型酒店(https://bit.ly/376rBcF)概念。他们的容器平台为“客户”提供了一组保证(比如高可用性和安全性),前提是他们遵守规则(比如从平台中提取配置并将日志写入其中)。
并不是所有的应用程序都知道这些规则,其中一些规则不能从外部被平台应用,但 sidecar 容器与应用程序容器一起运行,因此它们具有特权地位。你可以将它们作为适配器使用,它们理解应用程序工作方式的某些方面,并使其适应平台希望它工作的方式。日志记录就是一个经典的例子。
每个应用程序都写一些输出到日志条目——或者应该;否则它将完全无法管理您应该拒绝使用它。像Node.js和.net Core这样的现代应用平台写入标准输出流这是Docker获取容器日志的地方也是Kubernetes获取Pod日志的地方。旧的应用程序对日志有不同的想法它们可能会写入文件或其他目标这些目标永远不会作为容器日志出现所以你永远不会看到任何Pod日志。这就是timcheck应用程序所做的我们可以用一个非常简单的sidecar容器来修复它。spec 如清单7.5所示。
> 清单 7.5 timecheck-with-logging.yaml, 通过 sidecar 容器导出日志
```
containers:
- name: timecheck
image: kiamol/ch07-timecheck
volumeMounts:
- name: logs-dir # 应用容器写入日志到一个挂载的 EmptyDir 卷
mountPath: /logs
- name: logger
image: kiamol/ch03-sleep # sidecar 容器仅仅 watch 日志文件
command: ['sh', '-c', 'tail -f /logs-ro/timecheck.log']
volumeMounts:
- name: logs-dir
mountPath: /logs-ro # 使用与 APP 容器相同的卷
readOnly: true
```
sidecar 所做的就是挂载日志卷(进入EmptyDir!)并使用标准的Linux tail命令从日志文件中读取。-f选项表示命令将跟随文件;实际上它只是等待并观察新的写入当任何行写入文件时它们将被回显为标准输出。它是一个中继使应用程序的实际日志实现适应Kubernetes的期望。
<b>现在就试试</b> 应用清单 7.5 中的更新,并检查应用程序日志是否可用。
```
# 添加sidecar日志记录容器:
kubectl apply -f timecheck/timecheck-with-logging.yaml
# 等待容器启动:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck,version=v3
# 检查 Pods:
kubectl get pods -l app=timecheck
# 检查 Pod 中的容器:
kubectl get pod -l app=timecheck -o jsonpath='{.items[0].status.containerStatuses[*].name}'
# 现在你可以看到 Pod 中的应用日志:
kubectl logs -l app=timecheck -c logger
```
这里有一些低效率因为应用程序容器将日志写入文件然后日志容器再次将它们读回来。这将有一个小的时间延迟可能会浪费很多磁盘但Pod将在下一次应用程序更新中被替换卷中使用的所有空间将被回收。好处是这个Pod现在像其他Pod一样使应用程序日志可用于Kubernetes但不需要对应用程序本身进行任何更改如图7.10所示。
![图7.10](./images/Figure7.10.png)
<center>图 7.10 适配器为Pods带来了一层一致性使旧应用程序像新应用程序一样运行。</center>
从平台接收配置并将日志写入平台几乎是任何应用程序的基础,但随着平台的成熟,您将对标准行为有更多的期望。您希望能够测试容器内的应用程序是否健康,还希望能够从应用程序中提取指标,以查看它正在做什么以及它的工作强度。
Sidecars 也可以提供帮助,要么运行自定义容器,为应用程序提供定制的信息,要么拥有标准的健康和指标容器镜像,应用于所有 Pod spec。我们将使用时间检查应用程序来完成练习并添加这些功能使其成为Kubernetes的好公民。但是我们将使用一些更静态的HTTP Server 容器如清单7.6所示。
> 清单 7.6 timecheck-good-citizen.yaml, 更多的 sidecars 扩展应用
```
containers: # 之前的 应用和 日志容器都是一样的
- name: timecheck
# ...
- name: logger
# ...
- name: healthz # 一个新的 sidecar 容器提供了健康检查 API
image: kiamol/ch03-sleep # 仅仅是一个静态的响应
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-
Type: application/json\nContent-Length: 17\n\n{\"status\": \"OK\"}' | nc
-l -p 8080; done"]
ports:
- containerPort: 8080 # 在 Pod 的 8080端口进行暴露
- name: metrics # 另一个 sidecar, 添加了 metrics API
image: kiamol/ch03-sleep # 内容同样是静态的.
command: ['sh', '-c', "while true; do echo -e 'HTTP/1.1 200 OK\nContent-
Type: text/plain\nContent-Length: 104\n\n# HELP timechecks_total The
total number timechecks.\n# TYPE timechecks_total
counter\ntimechecks_total 6' | nc -l -p 8081; done"]
ports:
- containerPort: 8081 # 暴露在不同的端口
```
完整的 YAML 文件还包括一个 ClusterIP Service它在 health 端点的端口8080上发布在metrics端点的端口8081上发布。在生产集群中其他组件将使用这些数据来收集监视统计信息。部署是以前版本的扩展所以应用程序使用init容器进行配置并有一个日志sidecar和新的sidecar。
<b>现在就试试</b> 部署更新,并检查应用程序的运行状况和性能的新管理端点。
```
# 应用更新的内容:
kubectl apply -f timecheck/timecheck-good-citizen.yaml
# 等待所有容器 ready:
kubectl wait --for=condition=ContainersReady pod -l app=timecheck,version=v4
# 检查运行的容器:
kubectl get pod -l app=timecheck -o jsonpath='{.items[0].status.containerStatuses[*].name}'
# 通过 sleep 容器检查 timecheck 应用的健康:
kubectl exec deploy/sleep -c sleep -- wget -q -O - http://timecheck:8080
# 检查 metrics:
kubectl exec deploy/sleep -c sleep -- wget -q -O - http://timecheck:8081
```
当您运行这个练习时您将看到一切都按预期运行如图7.11所示。你可能还会发现更新速度不如你习惯的那么快新Pod启动的时间更长旧Pod终止的时间更长。额外的启动时间来自初始化容器、应用程序容器和所有 sidecar——在新Pod被认为准备好之前它们都需要准备好。额外的终止时间是因为被替换的Pod也有多个容器每个容器都有一个关闭容器进程的宽限期。
![图7.11](./images/Figure7.11.png)
<center>图 7.11 多个适配器sidecars给应用程序一个一致的管理API。</center>
将所有这些sidecar容器作为适配器运行是有开销的。您已经看到它增加了部署时间但也增加了应用程序的持续计算需求——甚至存储和基本的sidecars(它只是跟踪日志文件并提供简单的HTTP响应)都使用内存和计算周期。但如果你想把现有的应用程序转移到没有这些功能的Kubernetes上让所有应用程序都以相同的方式运行是一种可以接受的方法如图7.12所示。
![图7.12](./images/Figure7.12.png)
<center>图 7.12 一个一致的管理API可以很容易地与Pod一起工作——不管API是如何在Pod中提供的。</center>
在前面的练习中,我们使用了一个旧的 sleep Pod来调用时间检查应用程序的新HTTP端点。请记住Kubernetes有一个平面网络模型其中Pod可以通过服务向任何其他Pod发送流量。你可能想要在你的应用程序中对网络通信有更多的控制你也可以用sidecars来做到这一点通过运行一个代理容器来管理从你的应用程序容器传出的流量。
## 7.4 通过 ambassador 容器抽象连接
ambassador 模式允许您控制和简化应用程序的对外连接:应用程序向 localhost 地址发出网络请求,这些请求由 ambassador 接收和执行。在几种情况下,您可以使用通用的 ambassador 容器,或者特定于应用程序组件的 ambassador 容器。图7.13显示了一些例子。ambassador 中的逻辑可能用于提高性能或提高可靠性或安全性。
![图7.13](./images/Figure7.13.png)
<center>图 7.13 ambassador 模式有很多潜力,从简化应用程序逻辑到提高性能。</center>
从应用程序中控制网络是非常强大的。代理容器可以在未加密的通道上执行服务发现、负载平衡、重试甚至层加密。也许您听说过使用Linkerd和istio等技术的服务网格体系结构——它们都是由 ambassador 模式变体中的代理 sidecar 容器提供支持的。
这里我们不打算使用服务网格体系结构因为那会让我们度过午餐时间一直到晚上但是我们将通过一个简化的示例了解它可以做什么。起点是我们之前用过的随机数应用程序。有一个web应用程序在一个Pod中运行它消耗一个在另一个Pod中运行的 API。API是web应用程序使用的唯一组件所以理想情况下我们会将网络调用限制到任何其他地址但在最初的部署中这种情况不会发生。
<b>现在就试试</b> 运行随机数应用程序,验证 web 应用程序容器可以使用任何网络地址。
```
# 部署应用和 Service:
kubectl apply -f numbers/
# 获取应用访问 url:
kubectl get svc numbers-web -o jsonpath='http://{.status.loadBalancer.ingress[0].*}:8090'
# 访问它,返回一个完美的随机数
# 检查 web 应用是否可以访问其他端点:
kubectl exec deploy/numbers-web -c web -- wget -q -O -http://timecheck:8080
```
web Pod 可以使用ClusterIP 服务和域名 numbers-api 到达API但它也可以访问任何其他地址这可能是公共互联网上的URL或另一个ClusterIP服务。图7.14显示了应用程序可以读取时间检查应用程序的健康端点——这应该是一个私有端点,它可能会暴露对某些人有用的信息。
![图7.14](./images/Figure7.14.png)
<center>图 7.14 Kubernetes 对 Pod 容器的传出连接没有任何默认限制 </center>
除了使用代理 sidecar 之外,还有许多限制网络访问的选项,但是 ambassador 模式带有一些额外的特性值得考虑。清单7.7显示了web应用程序 spec 的更新,使用一个简单的代理容器作为 ambassador。
> 清单 7.7 web-with-proxy.yaml, 使用一个代理容器作为 ambassador
```
containers:
- name: web
image: kiamol/ch03-numbers-web
env:
- name: http_proxy # 设置变量让容器使用代理服务
value: http://localhost:1080 # 因此流量到达 ambassador
- name: RngApi__Url
value: http://localhost/api # 为API使用本地 localhost 地址
- name: proxy
image: kiamol/ch07-simple-proxy # 这是一个基础的 HTTP proxy 服务
env:
- name: Proxy__Port # 路由来自应用程序的网络请求
value: "1080" # 使用配置的URI映射
- name: Proxy__Request__UriMap__Source
value: http://localhost/api
- name: Proxy__Request__UriMap__Target
value: http://numbers-api/sixeyed/kiamol/master/ch03/numbers/rng
```
这个例子展示了 ambassador 模式的主要部分:应用程序容器为它使用的任何服务使用本地主机地址,并且它被配置为通过代理容器路由所有网络调用。代理是一个自定义应用程序,它可以记录网络呼叫,将本地主机地址映射到真实地址,并阻止映射中未列出的任何地址。所有这些都成为 Pod 中的功能,但对应用程序容器来说是透明的。
<b>现在就试试</b> 更新随机数应用程序,并确认网络现在已锁定。
```
# 应用清单 7.5 中的更新:
kubectl apply -f numbers/update/web-with-proxy.yaml
# 刷新浏览器, 得到新的数字
# 检查 proxy 容器 logs:
kubectl logs -l app=numbers-web -c proxy
# 尝试访问 timcheck 应用的健康接口:
kubectl exec deploy/numbers-web -c web -- wget -q -O -http://timecheck:8080
# 再次查看 proxy 容器日志:
kubectl logs -l app=numbers-web -c proxy
```
现在 web 应用程序甚至与API进一步解耦因为它甚至不知道API的URL——这是在 ambassador 中设置的可以独立于应用程序进行配置。web应用程序也被限制使用单个地址进行外发请求所有这些调用都由代理记录如图7.15所示。
![图7.15](./images/Figure7.15.png)
<center>图 7.15 所有的网络访问都是通过 ambassadorambassador 可以实现自己的访问规则。</center>
这个 web 应用程序的 ambassador 代理 Pod 外部的 HTTP 调用,但 ambassador 模式比这更广泛。它在传输层插入网络,因此可以处理任何类型的流量。数据库 ambassador 可以做出一些明智的选择,比如将查询发送到只读数据库副本,并且只使用主数据库进行写操作。这将提高性能和规模,同时将复杂的逻辑排除在应用程序之外。
我们将进一步了解使用 Pod 作为许多容器的共享环境意味着什么,从而使本章圆满结束。
## 7.5 理解 Pod 环境
Pod 是围绕一个或多个容器的边界就像容器是围绕一个或多个进程的边界一样。pod 在不增加开销的情况下创建了虚拟化层因此它们灵活而高效。这种灵活性的代价是复杂性您需要了解使用多容器pod的一些细微差别。
需要理解的主要事情是Pod 仍然是单一的计算单元,即使有很多容器在其中运行。在 Pod 中的所有容器都准备好之前Pod才准备好服务只向准备好了的Pod发送流量。添加sidecars和init容器会增加应用程序的失败模式。
<b>现在就试试</b> 如果 init 容器失败,您可以中断应用程序。对 numbers 应用程序的更新将不会成功,因为 init 容器配置错误。
```
# 应用更新:
kubectl apply -f numbers/update/web-v2-broken-init-container.yaml
# 检查新 Pod:
kubectl get po -l app=numbers-web,version=v2
# 检查新的 init 容器的日志:
kubectl logs -l app=numbers-web,version=v2 -c init-version
# 检查 deployment 状态:
kubectl get deploy numbers-web
# 检查 ReplicaSets 状态:
kubectl get rs -l app=numbers-web
```
在本练习中,您可以看到失败的 init 容器有效地阻止了应用程序的更新。新的 Pod 永远不会进入运行状态也不会接收来自服务的流量。部署从不缩小旧的ReplicaSet因为新的ReplicaSet没有达到所需的可用性级别但是部署的基本细节看起来更新已经工作如图7.16所示。
![图7.16](./images/Figure7.16.png)
<center>图 7.16 在 Pod spec 中添加更多容器会增加 Pod 失败的可能性</center>
如果 sidecar 容器在启动时失败也会发生同样的情况——Pod没有运行所有的容器所以Pod本身还没有准备好。任何部署检查都需要扩展到多容器Pod以确保所有init容器都运行完毕所有Pod容器都在运行。您还需要注意以下重启条件:
- 如果替换了带有 init 容器的 Pod那么新的 Pod 将重新运行所有的 init 容器。您必须确保您的 init 逻辑可以重复运行。
- 如果您为 Pod 部署了 init 容器镜像的更改,则会重新启动 Pod。Init 容器全部重新执行app 容器被替换。
- 如果你将 Pod spec 更改部署到应用程序容器镜像应用程序容器将被替换但init容器不会再次执行。
- 如果应用程序容器退出Pod 将重新创建它。在容器被替换之前Pod 不会完全运行,不会接收服务流量。
Pod 是一个单一的计算环境,但当你在该环境中添加多个移动部件时,你需要测试所有的故障场景,并确保你的应用程序的行为符合你的预期。
Pod 环境的最后一部分我们还没有介绍:计算层。Pod容器有一个共享的网络可以共享部分文件系统但是它们不能访问彼此的进程——容器边界仍然提供了计算隔离。这是默认的行为但在某些情况下您希望 sidecar 能够访问应用程序容器中的进程,或者用于进程间通信,或者以便 sidecar 能够获取有关应用程序进程的指标。
你可以在 Pod Spec 中通过一个简单的设置来启用这种访问: shareProcess-Namespace: true。这意味着Pod中的每个容器共享相同的计算空间并且可以看到彼此的进程。
<b>现在就试试</b> 将更新部署到 sleep Pod以便容器使用共享的计算空间并可以访问彼此的进程。
```
# 检查当前容器进程:
kubectl exec deploy/sleep -c sleep -- ps
# 应用更新:
kubectl apply -f sleep/sleep-with-server-shared.yaml
# 等待新容器启动:
kubectl wait --for=condition=ContainersReady pod -l app=sleep,version=shared
# 再次查看进程:
kubectl exec deploy/sleep -c sleep -- ps
```
可以在图 7.17 中看到我的输出。sleep 容器可以看到所有 server 容器的进程它可以很高兴地杀死它们让Pod处于混乱的状态。
![图7.17](./images/Figure7.17.png)
<center>图 7.17 您可以配置 Pod这样所有容器都可以看到所有进程——使用时要小心。</center>
以上就是多容器 Pod 的全部内容。在本章中你已经看到你可以使用init容器为你的应用程序容器准备环境并运行sidecar容器为你的应用程序添加特性所有这些都不需要改变应用程序代码或Docker镜像。使用多个容器有一些注意事项但是您将经常使用这种模式来扩展应用程序。只要记住 Pod 应该是一个逻辑组件:我不希望看到你在一个Pod中运行Nginx, WordPress和MySQL只是因为你可以。我们现在收拾一下准备去实验室。
<b>现在就试试</b> 删除所有与本章标签匹配的内容。
```
kubectl delete all -l kiamol=ch07
```
## 7.6 实验室
回到这个实验室的 Pi 应用程序。Docker 镜像 kiamol/ch05-pi 实际上可以以不同的方式使用,并且要作为 web 应用程序运行它,你需要覆盖容器 spec 中的启动命令。我们在前几章的YAML文件中已经做到了这一点但现在我们被要求使用标准方法来设置pod。以下是要求和一些提示:
- 应用程序容器需要使用一个标准的启动命令,我们平台上的所有 Pod 都在使用。它应该运行 /init/startup.sh。
- Pod 应该使用 80 端口作为应用程序容器。
- Pod 还应该为 HTTP 服务器发布端口 8080它返回应用程序的版本号
- 应用程序容器镜像不包含启动脚本,所以你需要使用一些可以创建该脚本并使其可执行的应用程序容器运行的东西。
- 应用程序不会在端口 8080 (或其他任何地方)上发布版本API所以你需要一些可以提供的东西(它可以是任何静态文本)。
起点是 ch07/lab/pi 中的YAML目前它已经坏了。你需要对前面章节的应用程序是如何运行的做一些调查并应用我们在本章中学到的技术。您有很多方法来处理这个问题您可以在通常的位置找到我的示例解决方案:https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch07/lab/README.md。

View File

@ -0,0 +1,524 @@
# 第八章 使用 StatefulSets 和 Jobs 运行数据量大的应用
“数据量大” 不是一个很科学的术语,但本章是关于运行一个应用程序类,它不仅是有状态的,而且还要求它如何使用状态。数据库就是此类的一个例子。它们需要跨多个实例运行以实现高可用性,每个实例都需要一个本地数据存储以实现快速访问,而且这些独立的数据存储需要保持同步。数据有其自身的可用性要求,您需要定期运行备份以防止终端故障或损坏。其他数据密集型应用程序,如消息队列和分布式缓存,也有类似的需求。
你可以在 Kubernetes 中运行这些类型的应用程序,但你需要围绕一个固有的冲突进行设计:Kubernetes 是一个动态环境而数据量大的应用程序通常希望在一个稳定的环境中运行。群集应用程序希望在已知的网络地址中找到对等点但在ReplicaSet 中不能很好地工作,而备份作业希望从磁盘驱动器中读取数据,因此在 persistentvolumecclaims 中不能很好地工作。如果你的应用程序有严格的数据要求,你需要对它进行不同的建模,我们将在本章中介绍一些更高级的控制器:StatefulSets, Jobs 和 CronJobs。
## 8.1 Kubernetes 如何用 StatefulSets 建模稳定性
StatefulSet 是一个具有可预测管理特性的 Pod 控制器:它允许您在一个稳定的框架内大规模运行应用程序。当您部署 ReplicaSet 时,它会使用随机名称创建 Pod这些名称不能通过域名系统(DNS)单独寻址,并且它会并行启动它们。当你部署一个 StatefulSet 时,它会创建具有可预测名称的 Pod这些 Pod 可以通过 DNS 单独访问,并按顺序启动它们;第一个 Pod 需要启动并运行在第二个 Pod 创建之前。
集群应用程序是 StatefulSet的一个很好的候选。它们通常设计有一个主实例和一个或多个辅助实例这使它们具有高可用性。您可能能够扩展辅助服务器但它们都需要到达主服务器然后使用主服务器同步它们自己的数据。你不能用Deployment 来建模,因为在 ReplicaSet 中,没有办法将单个 Pod 识别为主节点,所以你最终会遇到多个主节点或零主节点的奇怪且不可预测的情况。
图 8.1 显示了一个例子,它可以用来运行我们在前几章中为待办事项列表应用程序使用的 Postgres 数据库,但是它使用了一个 StatefulSet 来实现复制数据和高可用性。
![图8.1](.\images\Figure8.1.png)
<center>图 8.1 在 StatefulSet 中,每个 Pod 都可以从第一个 Pod 复制自己的数据副本</center>
为此的设置相当复杂,我们将分阶段花几个部分来完成,以便您了解工作中的 StatefulSet 的所有部分是如何组合在一起的。这种模式不仅对数据库有用——许多旧的应用程序是为静态运行时环境设计的并且对稳定性做出了假设但这些假设在Kubernetes 中并不成立。StatfulSets 允许你对这种稳定性建模,如果你的目标是将你现有的应用程序转移到 Kubernetes那么它们可能是你在这一旅程的早期使用的东西。
让我们从一个简单的 StatefulSet 开始,它展示了基础知识。清单 8.1 显示了 StatefulSets 具有与其他 Pod 控制器几乎相同的 spec除了它们还需要包含服务的名称。
> 清单 8.1 todo-db.yaml, 一个简单的 StatefulSet
```
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: todo-db
spec:
selector: # StatefulSets 使用了相同的选择器机制
matchLabels:
app: todo-db
serviceName: todo-db # StatefulSets 必须关联 Service.
replicas: 2
template:
# pod spec...
```
当您部署这个 YAML 文件时,您将得到一个运行两个 Postgres pod 的 StatefulSet但不要太兴奋——它们只是两个单独的数据库服务器恰好由同一个控制器管理。要使两个 pod 成为一个复制的数据库集群,还需要做更多的工作,我们将在接下来的几节中实现这一点。
<b>现在就试试</b> 部署清单 8.1 中的 StatefulSet看看它创建的 pod 与 ReplicaSet 管理的 pod 有什么不同
```
# 切换到本章的源码目录:
cd ch08
# 部署 StatefulSet, Service, 以及用于 Postgres 密码的 Secret:
kubectl apply -f todo-list/db/
# 检查 StatefulSet:
kubectl get statefulset todo-db
# 检查 Pods:
kubectl get pods -l app=todo-db
# 查看 Pod 0 的主机名:
kubectl exec pod/todo-db-0 -- hostname
# 检查 Pod 1 的日志:
kubectl logs todo-db-1 --tail 1
```
从图 8.2 中可以看到StatefulSet 的工作方式与 ReplicaSet 或 DaemonSet 非常不同。Pod 有一个可预测的名称,即 StatefulSet 名称后面跟着 Pod 的索引,因此您可以使用它们的名称来管理 Pod而不必使用标签选择器。
Pod 仍然由控制器管理,但以一种比 ReplicaSet 更可预测的方式。Pod 是按照从 0 到 n 的顺序创建的;如果您缩小集合,控制器将以相反的顺序删除它们,从 n 开始向下。如果删除 Pod控制器将创建一个替换。它的名称和配置将与原来的相同但它将是一个新的Pod。
![图8.2](.\images\Figure8.2.png)
<center>图 8.2 StatefulSet 可以为集群应用程序创建环境,但应用程序需要自行配置</center>
<b>现在就试试</b> 删除Pod 0 的 StatefulSet并看到 Pod 0 再次返回
```
# 检查 Pod 0 的内部 id:
kubectl get pod todo-db-0 -o jsonpath='{.metadata.uid}'
# 删除 Pod:
kubectl delete pod todo-db-0
# 检查 Pods:
kubectl get pods -l app=todo-db
# 检查新的 Pod 完全是全新的:
kubectl get pod todo-db-0 -o jsonpath='{.metadata.uid}'
```
你可以在图 8.3 中看到StatefulSet 为 app 提供了一个稳定的环境。Pod 0 被替换为一个相同的Pod 0但这不会触发一个全新的设置; 原来的 Pod 1仍然存在。仅在创建和扩展时应用顺序不用于替换缺失的 Pod。
[图8.3](.\images\Figure8.3.png)
<center>图 8.3 StatefulSets 替换丢失的副本,就像它们之前的模样</center>
StatefulSet 只是对稳定环境建模的第一部分。您可以获取每个 Pod 的 DNS 名称,将 StatefulSet 链接到一个服务,这意味着您可以配置 Pod通过使用已知地址上的其他副本来初始化它们自己。
## 8.2 在 StatefulSets 中使用 init 容器引导 Pod
Kubernetes API 从其他对象中组合对象: 在 StatefulSet 定义中的 Pod 模板与在 Deployment 模板和 裸Pod定义中使用的对象类型相同。这意味着 Pod 的所有功能都可以用于 StatefulSets尽管 Pod 本身是以不同的方式管理的。我们在第7章学习了 init 容器,对于集群应用程序中经常需要的复杂初始化步骤,它们是一个完美的工具。
清单 8.2 显示了用于更新 Postgres 部署的第一个init容器。这个Pod Spec 中的多个 init 容器是按顺序运行的,而且由于 Pod 也是按顺序启动的所以可以保证Pod 1 中的第一个init容器在 Pod 0 完全初始化并准备就绪之前不会运行。
> 清单8.2 todo-db.yaml复制的 Postgres 设置初始化
```
initContainers:
- name: wait-service
image: kiamol/ch03-sleep
envFrom: # env 文件用于在容器之间共享
- configMapRef:
name: todo-db-env
command: ['/scripts/wait-service.sh']
volumeMounts:
- name: scripts # 卷从 ConfigMap 装载脚本
mountPath: "/scripts"
```
在这个 init 容器中运行的脚本有两个功能: 如果它在 Pod 0 中运行,它只是打印一个日志来确认这是数据库主容器,然后容器退出;如果它在任何其他 Pod 中运行,它会对主 Pod 进行 DNS 查找调用以确保在继续之前可以访问它。下一个init 容器将启动复制进程,因此这个容器将确保一切就绪。
本例中的具体步骤是特定于 Postgres 的但许多集群和复制应用程序的模式是相同的——mysql、Elasticsearch、RabbitMQ和nat都有大致相似的需求。图 8.4 显示了如何在 StatefulSet 中使用init容器对该模式建模。
[图8.4](.\images\Figure8.png)
<center>图 8.4 StatefulSet的稳定环境保证了您可以在初始化中使用</center>
您可以通过在 spec 中标识服务来为 StatefulSet 中的各个 Pods 定义 DNS 名称,但它需要是 headless Service 的特殊配置。清单 8.3 显示了如何在没有 ClusterIP 地址的情况下配置数据库服务,并为 Pods 配置一个选择器。
> 清单8.3 todo-db-service.yaml一个 StatefulSet 的 headless Service
```
apiVersion: v1
kind: Service
metadata:
name: todo-db
spec:
selector:
app: todo-db # Pod 选择器与 StatefulSet 相匹配.
clusterIP: None # Service 将不会拥有自己的 Ip 地址.
ports:
# ports follow
```
没有 ClusterIP 的 Service 仍然可以作为集群中的 DNS 条目,但它不会为该服务使用固定的 IP 地址。没有由网络层路由到真实目的地的虚拟 IP。相反服务的 DNS 条目为 StatefulSet 中的每个 Pod 返回一个 IP 地址,并且每个 Pod 还获得自己的 DNS 条目。
<b>现在就试试</b> 我们已经部署了 headless Service所以我们可以使用 sleep Deployment 来查询 StatefulSet 的 DNS看看它与典型的 ClusterIP 服务相比如何
```
# 查看 Service 明细:
kubectl get svc todo-db
# 运行一个 sleep Pod 用于网络查找:
kubectl apply -f sleep/sleep.yaml
# 运行 Service 名称的 DNS 查询:
kubectl exec deploy/sleep -- sh -c 'nslookup todo-db | grep "^[^*]"'
# 运行 Pod 0 的 DNS 查找:
kubectl exec deploy/sleep -- sh -c 'nslookup todo-db-0.todo-
db.default.svc.cluster.local | grep "^[^*]"'
```
在本练习中,您将看到 Service 的 DNS 查找返回两个 IP 地址,它们是 Pod 内部IP。Pods 本身有自己的格式为 pod-name 的 DNS 条目。带有通常集群域后缀的服务名称。图 8.5 显示了我的输出。
![图8.5](.\images\Figure8.5.png)
</center>图 8.5 StatefulSets 为每个 Pod 提供自己的 DNS 条目,因此它们是单独可寻址的</center>
可预测的启动顺序和单独可寻址的 Pod 是在 StatefulSet 中初始化集群应用的基础。具体细节在不同的应用程序之间会有很大的不同但从广义上讲Pod 的启动逻辑将是这样的: 如果我是Pod 0那么我是主设备所以我做所有的主设备设置工作;否则,我是一个辅助,所以我将给主服务器一些时间来设置,检查一切都正常工作,然后使用 Pod 0 地址进行同步。
Postgres 的实际设置相当复杂,所以我在这里略过。它使用 ConfigMaps 中的脚本和 init 容器来设置主服务器和备用服务器。到目前为止,我在 StatefulSet 的 spec 中使用了书中介绍的各种技术值得探索但脚本的细节都是特定于Postgres 的。
<b>现在就试试</b> 更新数据库使其成为一个复制的设置。ConfigMaps 中有配置文件和启动脚本,并且更新 StatefulSet 以在 init 容器中使用它们
```
# 部署副本方式的 StatefulSet 应用设置:
kubectl apply -f todo-list/db/replicated/
# 等待 Pods 就绪
kubectl wait --for=condition=Ready pod -l app=todo-db
# 检查 Pod 0日志—主节点:
kubectl logs todo-db-0 --tail 1
# 以及 Pod 1—从节点:
kubectl logs todo-db-1 --tail 2
```
Postgres 使用主-被动模型进行复制,因此主服务器用于数据库读写,从服务器从主服务器同步数据,可供客户端使用,但仅用于读访问。图 8.6 显示了 init 容器如何识别每个 Pod 的角色并初始化它们。
![图8.6](.\images\Figure8.6.png)
<center>图 8.6 Pod 是副本,但它们可以有不同的行为,使用 init 容器选择一个角色</center>
像这样初始化复制应用的大部分复杂性都是围绕工作流建模,这是特定于应用的。这里的 init 容器脚本使用 pg_isready 工具来验证主应用是否准备好接收连接,并使用 pb_basebackup 工具来启动复制。这些实现细节从管理系统的操作员那里抽象出来。他们可以通过扩大 StatefulSet 来添加更多的副本,就像使用任何其他复制控制器一样。
<b>现在就试试</b> 扩大数据库以添加另一个副本,并确认新的 Pod 也作为备用启动
```
# 添加另一个副本:
kubectl scale --replicas=3 statefulset/todo-db
# 等待 Pod 2 就绪
kubectl wait --for=condition=Ready pod -l app=todo-db
# 检查新 Pod 是否设置为另一个备用:
kubectl logs todo-db-2 --tail 2
```
我不认为这是一个企业级的产品设置,但这是一个很好的起点,真正的 Postgres 专家可以接管它。现在,您有了一个功能良好的复制 Postgres 数据库集群其中有一个主数据库和两个备用数据库——Postgres称它们为备用数据库。如图8.7所示,所有备用服务器都以相同的方式启动,从主服务器同步数据,客户端都可以使用它们进行只读访问。
![图8.7](.\images\Figure8.7.png)
<center>图 8.7 使用单独可寻址的 pod 意味着辅助设备总能找到主设备</center>
这里遗漏了一个明显的部分——数据的实际存储。我们所拥有的设置并不是真正可用的因为它没有任何用于存储的卷因此每个数据库容器都在自己的可写层中写入数据而不是在持久卷中写入数据。StatefulSets 有一种定义卷需求的简洁方法:您可以在 spec 中包含一组持久卷声明(PVC)模板。
## 8.3 使用卷声明模板请求存储
卷是标准 Pod Spec 的一部分,您可以为 StatefulSet 将 ConfigMaps 和 Secrets 加载到 Pod 中。你甚至可以包含一个 PVC 并将其挂载到应用程序容器中,但这将为你提供在所有 Pod之间共享的卷。在只读配置设置中您希望每个 Pod 都有相同的数据,但如果您为数据存储安装了标准 PVC那么每个 Pod 都将尝试写入相同的卷。
您实际上希望每个 Pod 都有自己的 PVC, Kubernetes 在 Spec 中为 StatefulSets 提供了 volumeClaimTemplates 字段,它可以包括存储类以及容量和访问模式需求。当您部署带有 volume claim templates 的 StatefulSet 时它为每个Pod 创建一个 PVC并且它们是链接的因此如果 Pod 0 被替换,新的 Pod 0 将附加到前一个Pod 0 使用的 PVC 上。
> 清单8.4 sleep-with-pvc.yaml一个带有 volume claim templates 的 StatefulSet
```
spec:
selector:
# pod selector...
serviceName:
# headless service name...
replicas: 2
template:
# pod template...
volumeClaimTemplates:
- metadata:
name: data # 卷挂载到 Pod 中的名字
spec: # 这个是标准的 PVC spec
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Mi
```
在将 StatfulSets 中的 volume claim templates 添加为数据库集群的存储层之前,我们将使用这个练习来了解它们在一个简单的环境中是如何工作的。
<b>现在就试试</b> 部署清单 8.4 中的 StatefulSet并探索它创建的 Pvc
```
# 部署带有 volume claim templates 的 StatefulSet:
kubectl apply -f sleep/sleep-with-pvc.yaml
# 检查创建的 PVC:
kubectl get pvc
# 在 Pod 0 挂载的 PVC 中写入一些数据:
kubectl exec sleep-with-pvc-0 -- sh -c 'echo Pod 0 > /data/pod.txt'
# 确认 Pod 0 可以读取到数据:
kubectl exec sleep-with-pvc-0 -- cat /data/pod.txt
# 确认 Pod 1 读取不到—将会失败:
kubectl exec sleep-with-pvc-1 -- cat /data/pod.txt
```
您将看到集合中的每个 Pod 都得到一个动态创建的 PVC而 PVC 又使用默认存储类(或者是请求的存储类,如果我在 spec 中包含了一个)创建了一个PersistentVolume。PVC 都具有相同的配置,并且它们使用与 StatfulSet中的 Pod 相同的稳定方法:它们具有可预测的名称并且如图8.8所示,每个 Pod 都有自己的PVC为副本提供独立的存储。
![图8.8](.\images\Figure8.8.png)
<center>图 8.8 volume claim templates 在 StatefulSets 中动态地为 Pods 创建存储空间</center>
Pod 和它的 PVC 之间的链接在替换 Pods 时得到维护,这才是真正赋予 StatefulSets 运行数据量大的应用程序的能力。当你推出一个更新到你的应用程序,新的 Pod 0 将附加到从以前的 Pod 0 的 PVC新的应用程序容器将访问与替换的应用程序容器完全相同的状态。
<b>现在就试试</b> 通过移除0号 Pod 触发 Pod 更换。它将被替换为另一个Pod 0连接到相同的PVC
```
# 删除 Pod:
kubectl delete pod sleep-with-pvc-0
# 检查替换的 Pod 已创建:
kubectl get pods -l app=sleep-with-pvc
# 检查新的 Pod 0 可以看到旧数据:
kubectl exec sleep-with-pvc-0 -- cat /data/pod.txt
```
这个简单的例子说明了这一点——在图 8.9 中可以看到,新的 Pod 0 可以访问原始 Pod 中的所有数据。在生产集群中,您可以指定一个存储类,该存储类使用任何节点都可以访问的卷类型,因此替换 Pods 可以在任何节点上运行,而应用程序容器仍然可以挂载 PVC。
![图8.9](.\images\Figure8.9.png)
<center>图 8.9 StatefulSet 中的稳定性扩展到保留 Pod 替换之间的 PVC 链接</center>
Volume claim templates 是我们需要添加到 Postgres 部署中的最后一部分以建模完全可靠的数据库。StatefulSet 旨在为您的应用程序提供一个稳定的环境,因此当涉及到更新时,它们不如其他控制器灵活—您不能更新现有的 StatefulSet 并进行根本性的更改,例如添加卷声明。您需要确保您的设计满足 StatefulSet的应用程序需求因为在大的更改期间很难保持服务水平。
<b>现在就试试</b> 我们将更新 Postgres 部署但首先我们需要删除现有的StatefulSet
```
# 对 volume claim templates 应用更新—这将失败:
kubectl apply -f todo-list/db/replicated/update/todo-db-pvc.yaml
# 删除 StatefulSet:
kubectl delete statefulset todo-db
# 创建一个包含 volume claims 的新的 StatefulSet:
kubectl apply -f todo-list/db/replicated/update/todo-db-pvc.yaml
# 检查 volume claims:
kubectl get pvc -l app=todo-db
```
当您运行这个练习时,您应该清楚地看到 StatefulSet 如何保持顺序,并在启动下一个 Pod 之前等待每个 Pod 运行。PVC 也是按顺序为每个 Pod 创建的从我的输出中可以看到如图8.10所示。
![图8.10](.\images\Figure8.10.png)
<center>图 8.10 PVC 被创建并分配给 Postgres Pods</center>
感觉我们在 StatefulSets 上花了很长时间,但这是一个你应该很好地理解的主题,所以当有人要求你将他们的数据库移动到 Kubernetes(他们会的)时你不会感到惊讶。StatefulSets 具有相当大的复杂性,您将在大多数情况下避免使用它们。但是如果你想把现有的应用程序迁移到 Kubernetes上StatefulSets可以让你在同一个平台上运行所有的东西或者不得不保留几个 vm 来运行一两个应用程序。
我们将通过一个练习来结束本节以展示集群数据库的强大功能。Postgres 辅助服务器从主服务器复制所有数据,客户端可以使用它进行只读访问。如果我们的待办事项列表应用程序出现了严重的生产问题,导致它丢失数据,我们可以选择切换到只读模式,并在调查问题时使用次要模式。这样可以让应用程序以最小的功能安全地运行,这绝对比让它脱机要好。
<b>现在就试试</b> 运行待办事项 web 应用程序并输入一些项目。在默认配置中,它连接到 statfulset 的 Pod 0 中的Postgres主节点。然后我们将切换应用程序配置将其设置为只读模式。这使得它连接到 Pod 1 中的只读Postgres备用后者已经从 Pod 0复制了所有数据
```
# 部署 web app:
kubectl apply -f todo-list/web/
# 获取应用访问 url:
kubectl get svc todo-web -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8081/new'
# 访问并添加新的待办项
# 切换到 read-only 模式, 使用数据库备用节点:
kubectl apply -f todo-list/web/update/todo-web-readonly.yaml
# 刷新应用—现在/new的页面只读;
# 访问 /list 你将看到原始的数据
# 检查 Pod 0 中是否没有使用主服务器的客户端:
kubectl exec -it todo-db-0 -- sh -c "psql -U postgres -t -c 'SELECT
datname, query FROM pg_stat_activity WHERE datid > 0'"
# 检查 web 应用程序是否真的在使用 Pod 1 中的备用:
kubectl exec -it todo-db-1 -- sh -c "psql -U postgres -t -c 'SELECT
datname, query FROM pg_stat_activity WHERE datid > 0'"
```
您可以在图 8.11 中看到我的输出,其中有一些小屏幕截图,显示应用程序以只读模式运行,但仍然可以访问所有数据。
![图8.11](.\images\Figure8.11.png)
<center>图 8.11 如果出现数据问题,将应用程序切换到只读模式是一个有用的选择</center>
Postgres 作为 SQL 数据库引擎存在于 1996 年,比 Kubernetes 早了将近 25 年。使用 StatefulSet您可以为适合 Postgres 和其他类似的集群应用程序的应用程序环境建模,从而在容器的动态世界中提供稳定的网络、存储和初始化。
## 8.4 使用 Jobs 和 CronJobs 运行维护任务
数据密集型应用程序需要复制数据,并将存储与计算对齐,它们通常还需要一些独立的存储层。数据备份和协调非常适合另一种类型的 Pod 控制器:Job。Kubernetes Job 是用 Pod Spec 定义的,它们将 Pod 作为批处理作业运行,确保它运行到完成。
Jobs 不仅仅适用于有状态应用;它们是为任何批处理问题提供标准的好方法,您可以将所有调度、监视和重试逻辑移交给集群。你可以在 Pod 中为 Job 运行任何容器镜像,但它应该启动一个结束的进程;否则,您的作业将永远运行。清单 8.5 显示了以批处理模式运行 Pi 应用程序的 Job Spec。
> 清单8.5 pi-job.yaml一个简单的计算Pi 的 Job
```
apiVersion: batch/v1
kind: Job # Job 是对象类型
metadata:
name: pi-job
spec:
template:
spec: # 标准的 Pod spec
containers:
- name: pi # 容器将运行并退出.
image: kiamol/ch05-pi
command: ["dotnet", "Pi.Web.dll", "-m", "console", "-dp", "50"]
restartPolicy: Never # 容器若失败,将替换 Pod.
```
Job template 包含一个标准 Pod spec并添加了一个必需的 restartPolicy 字段。该字段控制 Job 在响应失败时的行为。如果运行失败,您可以选择让 Kubernetes 用一个新容器重新启动同一个Pod或者总是在不同的节点上创建一个替换Pod。在 Pod 成功完成的正常作业运行中,作业和 Pod 将被保留,因此容器日志可用。
<b>现在就试试</b> 运行清单 8.5 中的Pi Job并检查 Pod 的输出
```
# 部署 Job:
kubectl apply -f pi/pi-job.yaml
# 检查 Pod 日志:
kubectl logs -l job-name=pi-job
# 检查 Job 状态:
kubectl get job pi-job
```
Jobs 将他们自己的标签添加到他们创建的 Pods 中。始终添加作业名称标签,因此您可以从作业导航到 Pods。图 8.12 中的输出显示 Job 已经成功完成了一次,计算结果可以在日志中找到。
![图8.12](.\images\Figure8.12.png)
<center>图 8.12 Job 创建 Pod确保它们完成然后将它们留在集群中</center>
有不同的选项来计算圆周率总是有用的,但这只是一个简单的例子。您可以使用 Pod spec 中的任何容器镜像,因此您可以使用 Job 运行任何类型的批处理。你可能有一组输入项,需要对它们做相同的工作;您可以为整个集合创建一个 Job它为每个项目创建一个 Pod, Kubernetes 将工作分布到整个集群中。Job spec 支持以下两个可选字段:
- completions - 指定 Job 应该运行多少次。如果 Job 正在处理一个工作队列那么应用程序容器需要了解如何获取下一个要处理的项。Job 本身只是确保它运行的 pod 数量等于所需的完成数量。
- parallelism - 指定在多个完成设置的作业中并行运行多少 pod。此设置允许您调整 Job 的运行速度,以平衡集群上的计算需求。
本章最后一个圆周率的例子:一个并行运行多个 pod 的新 Job spec每个 pod 都将圆周率计算到一个随机的小数点后数位。该 spec 使用 init 容器生成要使用的小数点后数位,应用程序容器使用共享 EmptyDir 挂载读取输入。这是一种很好的方法,因为应用程序容器不需要修改就可以在并行环境中工作。你可以用一个 init 容器来扩展它,从队列中获取一个工作项,这样应用程序本身就不需要知道队列。
<b>现在就试试</b> 运行另一个使用并行性的 Pi Job并显示来自相同 spec 的多个 pod 可以处理不同的工作负载
```
# 部署新 Job:
kubectl apply -f pi/pi-job-random.yaml
# 检查 Pod 状态:
kubectl get pods -l job-name=pi-job-random
# 检查 Job 状态:
kubectl get job pi-job-random
# 查询 Pod 输出日志:
kubectl logs -l job-name=pi-job-random
```
这个练习可能需要一段时间才能运行,这取决于您的硬件和它生成的小数位数。你会看到所有的 pod 都在并行运行各自进行计算。最终输出的是三组圆周率可能精确到小数点后几千位。我简化了结果如图8.13所示。
![图8.13](.\images\Figure8.13.png)
<center>图 8.13 Job 可以运行相同 spec 的多个 pod每个 pod 处理不同的工作负载</center>
Job 是你口袋里的好工具。它们非常适合任何计算密集型或IO密集型的情况在这些情况下您希望确保进程完成但不介意何时完成。你甚至可以从你自己的应用程序中提交 job——一个运行在Kubernetes中的web应用程序可以访问Kubernetes API服务它可以创建 job 来为用户运行任务。
Jobs 的真正强大之处在于它们在集群上下文中运行,因此它们拥有所有可用的集群资源。回到 Postgres 的例子,我们可以在 Job 中运行一个数据库备份进程,它运行的 Pod 可以访问 statfulset 中的 Pods 或 pvc这取决于它需要做什么。这就解决了这些数据密集型应用程序的培育方面的问题但这些 job 需要定期运行,这就是 CronJob 发挥作用的地方。CronJob 是一个 Jo b控制器它定期创建 Job。图 8.14 显示了工作流。
![图8.14](.\images\Figure8.14.png)
<center>图 8.14 CronJobs 是 Job Pods 的最终所有者,因此可以级联删除所有内容</center>
CronJob spec 包括一个 Job spec所以你可以在 CronJob 中做任何你可以在 Job 中做的事情,包括并行运行多个补全。运行 Job 的计划使用 Linux Cron 格式该格式允许您表达从简单的“每分钟”或“每天”计划到更复杂的“每个星期天早上4点和6点”例程的所有内容。清单 8.6 显示了运行数据库备份的部分 CronJob spec。
> 清单 8.6 todo-db-backup-cronjob.yaml, 用于数据库备份的 CronJob
```
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: todo-db-backup
spec:
schedule: "*/2 * * * *" # 每两分钟创建一个 Job
concurrencyPolicy: Forbid # 防止重叠,因此如果前一个作业正在运行,则不会创建新的作业
jobTemplate:
spec:
# job template...
```
完整 spec 使用 Postgres Docker 镜像,并使用命令运行 pg_dump 备份工具。Pod 从 StatefulSet 使用的相同 configmap 和 Secrets 中加载环境变量和密码,因此在配置文件中没有重复。它还使用自己的 PVC 作为存储位置来写入备份文件。
<b>现在就试试</b> 根据清单 8.6 中的 spec 创建一个 CronJob每两分钟运行一次数据库备份作业
```
# 部署 CronJob 以及为备份文件的 PVC:
kubectl apply -f todo-list/db/backup/
# 等待 Job 运行—可以乘机喝杯茶:
sleep 150
# 检查 CronJob 状态:
kubectl get cronjob todo-db-backup
# 现在运行 sleep Pod 挂载备份 PVC:
kubectl apply -f sleep/sleep-with-db-backup-mount.yaml
# 检查 CronJob Pod 已创建备份:
kubectl exec deploy/sleep -- ls -l /backup
```
CronJob 设置为每两分钟运行一次因此在这个练习中您需要给它一些时间来启动它。CronJob 按照计划创建一个 Job, Job 创建一个 Pod, Pod 运行备份命令。Job 确保 Pod 成功完成。您可以通过在另一个 Pod 中挂载相同的 PVC 来确认备份文件已被写入。您可以在图 8.15 中看到它都正常工作。
![图8.15](.\images\Figure8.15.png)
<center>图 8.15 CronJobs 运行 Pods可以访问其他 Kubernetes 对象。这个连接到一个数据库 Pod</center>
CronJobs 不会为 Pods 和 Jobs 执行自动清理。生存时间(TTL)控制器可以做到这一点,但这是一个 alpha 级功能,在许多 Kubernetes 平台上都无法使用。如果没有它,当您确定不再需要子对象时,您需要手动删除它们。您还可以将 CronJob移动到挂起状态这意味着对象 spec 仍然存在于集群中,但直到 CronJob 再次激活它才会运行。
<b>现在就试试</b> 暂停 CronJob这样它就不会一直创建备份 job然后查看 CronJob 及其 job 的状态
```
# 更新CronJob并设置为挂起:
kubectl apply -f todo-list/db/backup/update/todo-db-backup-cronjob-
suspend.yaml
# 检查 CronJob:
kubectl get cronjob todo-db-backup
# 找到 CronJob 拥有的 job:
kubectl get jobs -o jsonpath="{.items[?(@.metadata.ownerReferences[0]
.name=='todo-db-backup')].metadata.name}"
```
如果您研究对象层次结构,您会发现 CronJobs 不遵循标准控制器模型,没有标签选择器来标识它拥有的 job。您可以在 CronJob 的 Job template 中添加自己的标签,但如果不这样做,则需要标识所有者引用为 CronJob 的 Job如图8.16所示。
![图8.16](.\images\Figure8.16.png)
<center>图 8.16 CronJobs 不使用标签选择器来建模所有权,因为它们不跟踪 Jobs</center>
当您开始更多地使用 Jobs 和 CronJobs 时,您将意识到 spec 的简单性掩盖了过程中的一些复杂性并呈现了一些有趣的故障模式。Kubernetes 尽其所能确保批处理作业在您希望它们启动时启动,并运行到完成,这意味着您的容器需要具有弹性。完成一个 Job 可能意味着用一个新的容器重新启动 Pod或者在一个新的节点上替换 Pod对于 CronJobs如果进程花费的时间超过调度间隔则可能运行多个Pod。容器逻辑需要考虑所有这些场景。
现在你知道了如何在 Kubernetes 中运行数据量大的应用程序,使用 StatefulSets 来建模稳定的运行时环境并初始化应用程序,使用 CronJobs 来处理数据备份和其他常规维护工作。我们将在这一章的结尾思考这是否真的是个好主意。
## 8.5 为有状态应用程序选择平台
Kubernetes 最大的承诺是,它为你提供了一个单一的平台,可以在任何基础设施上运行你的所有应用。认为你可以建模所有的东西是非常吸引人的,使用kubectl命令部署它并且知道它将以相同的方式在任何集群上运行利用平台提供的所有扩展功能。但数据是宝贵的通常是不可替代的所以在决定将 Kubernetes 作为运行数据量大的应用程序的地方之前,你需要仔细考虑。
图 8.17 显示了我们在本章中构建的在 Kubernetes 中运行几乎是生产级 SQL 数据库的完整设置。只要看看这些活动的部分就行了.
![图8.17](.\images\Figure8.17.png)
<center>图 8.17 呵!这是一个简化没有显示卷或init容器</center>
你真想搞定这些吗?仅用自己的数据大小测试这个设置需要花费多少时间:验证副本正在正确同步,验证备份可以恢复,运行混乱实验以确保以您期望的方式处理故障?
将其与云中的托管数据库进行比较。Azure、AWS和GCP都为Postgres、MySQL和SQL Server提供托管服务以及他们自己的自定义云规模数据库。云提供商负责规模和高可用性包括备份到云存储的功能和更高级的选项如威胁检测。另一种架构只是使用 Kubernetes 进行计算,并插入托管云服务进行数据和通信。
哪个是更好的选择?我白天是个顾问,我知道唯一真实的答案是:“看情况而定。”如果您在云中运行,那么我认为您需要一个很好的理由不在生产环境中使用托管服务,因为在生产环境中数据至关重要。在非生产环境中,在 Kubernetes 中运行相同的服务通常是有意义的因此您可以在开发和测试环境中运行容器化的数据库和消息队列以降低成本和简化部署并在生产环境中切换到托管版本。Kubernetes 让这样的交换非常简单,包括所有 Pod 和 Service 配置选项。
在数据中心,情况略有不同。如果您已经投资在自己的基础设施上运行 Kubernetes那么您将承担大量的管理工作因此最大限度地利用集群并将它们用于所有事情可能是有意义的。如果您选择这样做Kubernetes 为您提供了将数据密集型应用程序迁移到集群的工具,并以所需的可用性和规模运行它们。只是不要低估实现这一目标的复杂性。
我们现在已经完成了 StatefulSets 和 job因此可以在进入实验室之前进行清理。
<b>现在就试试</b> 所有顶级对象都被标记,因此我们可以使用级联删除删除所有对象
```
# 删除本章相关的对象:
kubectl delete all -l kiamol=ch08
# 删除 PVCs
kubectl delete pvc -l kiamol=ch08
```
## 8.6 实验室
你还剩多少午餐时间来实验室?你能从头开始建模一个 MySQL 数据库,有备份吗?可能不会,但别担心,这个实验室没有那么复杂。我们的目标只是给你一些使用 StatefulSet 和 PVC 的经验所以我们将使用一个更简单的应用程序。你将在一个StatefulSet 中运行 Nginx web 服务,每个 Pod 将日志文件写入自己的 PVC然后你将运行一个 Job打印每个 Pod 的日志文件的大小。基本的部分都在那里,所以它是关于应用本章中的一些技巧。
- 起点是 ch08/lab/nginx 中的 Nginx Spec它运行一个 Pod将日志写入 EmptyDir 卷。
- Pod Spec 需要迁移到一个 StatefulSet 定义,它被配置为与三个 Pod 一起运行,并为每个 Pod 提供单独的存储。
- 当你有StatefulSet工作时你应该能够调用你的服务并看到正在写入 Pods 的日志文件。
- 在 disk-calc-job.yaml 文件中填写 Job Spec。添加卷挂载这样它就可以从 Nginx Pods 中读取日志文件。
它并不像看起来那么糟糕,它会让你想到存储和 Jobs。我的解决方案在 GitHub 上,你可以在通常的地方检查:https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch08/lab/README.md。

View File

@ -0,0 +1,534 @@
# 第九章 通过 rollouts 和 rollbacks 管理应用发布
你会更频繁地更新现有应用,而不是部署新应用。容器化应用程序从它们使用的基本镜像继承多个发布节奏;Docker Hub上针对操作系统、平台sdk和运行时的官方镜像通常每个月都会发布一个新版本。您应该有一个重建镜像的过程并在这些依赖项更新时发布更新因为它们可能包含关键的安全补丁。这个过程的关键是能够安全地推出更新并在出现错误时为自己提供暂停和回滚更新的选项。Kubernetes 为 Deployments、DaemonSets 和 StatefulSets 提供了这些场景。
单一的更新方法并不适用于所有类型的应用程序,因此 Kubernetes 为控制器提供了不同的更新策略,并提供了调整策略工作方式的选项。我们将在本章探讨所有这些选项。如果你正在考虑跳过这一点,因为你对 6000 字的应用程序更新不感兴趣,我建议你坚持下去。更新是导致应用程序宕机的最大原因,但如果您了解 Kubernetes 提供的工具,就可以显著降低风险。我会试着在这个过程中注入一些有趣的内容。
## 9.1 Kubernetes 如何管理 rollouts
我们将从 Deployments 开始——实际上,您已经完成了大量的 Deployment 更新。每次我们对现有的 Deployment 进行更改(我们一章要做10次)Kubernetes 都会进行一次 rollout。在展开过程中Deployment 将创建一个新的 ReplicaSet并将其扩展到所需的副本数量同时将先前的 ReplicaSet 缩小到零副本。图 9.1 显示了正在进行的更新。
![图9.1](.\images\Figure9.1.png)
<center>图 9.1 Deployment 控制多个 ReplicaSet以便它们可以管理滚动更新</center>
Rollouts arent triggered from every change to a Deployment, only from a change to the Pod spec. If you make a change that the Deployment can manage with the current ReplicaSet, like updating the number of replicas, thats done without a rollout.
不是 Deployment 的每次更改都会触发 rollout 的,只有 Pod spec 的更改才会触发 rollout。如果您做了一个 Deployemt 可以使用当前 ReplicaSet 管理的更改,比如更新副本的数量,则无需 rollout 即可完成。
<b>现在就试试</b> 部署一个简单的两个副本应用程序,然后更新它以增加规模,并查看如何管理 ReplicaSet
```
# 切换到章节练习目录:
cd ch09
# 部署一个简单的 web app:
kubectl apply -f vweb/
# 检查 ReplicaSets:
kubectl get rs -l app=vweb
# 现在扩展规模:
kubectl apply -f vweb/update/vweb-v1-scale.yaml
# 检查 ReplicaSets:
kubectl get rs -l app=vweb
# 检查 deployment 历史:
kubectl rollout history deploy/vweb
```
kubectl rollout 命令提供了查看和管理 rollout 的选项。您可以从图9.2的输出中看到本练习中只有一次rollout即创建ReplicaSet的初始部署。规模更新只更改了现有的ReplicaSet因此没有第二次 rollout。
![图9.2](.\images\Figure9.2.png)
<center>图 9.2 Deployment 通过 rollout 管理变更,但仅限 Pod spec 更改时</center>
您正在进行的应用程序更新将集中于部署运行容器镜像的更新版本的新 Pods。您应该通过更新 YAML spec 来管理这个问题,但是 kubectl 使用 set 命令提供了一个快速的替代方案。使用此命令是更新现有部署的必要方法您应该将其视为与scale 命令相同的方法—它是摆脱棘手情况的有用方法但需要随后更新YAML文件。
<b>现在就试试</b> 使用 kubectl 更新部署的镜像版本。这是对Pod spec 的更改,因此它将触发一个新的 rollout
```
# 更新web应用程序的镜像:
kubectl set image deployment/vweb web=kiamol/ch09-vweb:v2
# 再次检查 ReplicaSets:
kubectl get rs -l app=vweb
# 检查 rollouts:
kubectl rollout history deploy/vweb
```
kubectl set 命令改变一个现有对象的 spec 。您可以使用它来更改 Pod 的镜像或环境变量或 Service 的选择器。这是应用新的 YAML spec 的捷径,但它的实现方式是相同的。在这个练习中,更改导致了一个 rollout创建了一个新的ReplicaSet 来运行新的 Pod spec而旧的ReplicaSet缩小到零。在图9.3中可以看到这一点。
![图9.3](.\images\Figure9.3.png)
<center>图 9.3 必要的更新经过相同的 rollout 过程,但现在您的 YAML 和实际状态是不同步的</center>
Kubernetes 对其他 Pod 控制器(DaemonSets和StatefulSets)使用了相同的 rollout 概念。它们是API中一个奇怪的部分因为它们不直接映射到对象(您不需要创建具有“rollout”类型的资源),但是它们是用于您的版本的重要管理工具。您可以使用 rollout 来跟踪版本历史并恢复到以前的版本。
## 9.2 使用 rollouts 和 rollbacks 更新 Deployments
如果您再次查看图 9.3,您将看到 rollout 历史记录非常没有帮助。每次 rollout 都会记录一个修订号,但没有其他记录。目前还不清楚是什么原因导致了这个变化,也不清楚哪个 ReplicaSet 与哪个修订相关。最好包含一个版本号(或Git提交ID)作为 Pods 的标签,然后 Deployment 也将该标签添加到 ReplicaSet 中,这样可以更容易地跟踪更新。
<b>现在就试试</b> 对 Deployment 应用更新,它使用相同的 Docker 镜像,但更改 Pod 的版本标签。这是对 Pod spec 的更改,因此它将创建一个新的 rollout。
```
# 通过 record 参数应用新的变更:
kubectl apply -f vweb/update/vweb-v11.yaml --record
# 检查 ReplicaSets 以及它们的标签:
kubectl get rs -l app=vweb --show-labels
# 检查当前 rollout 状态:
kubectl rollout status deploy/vweb
# 检查 rollout history:
kubectl rollout history deploy/vweb
# 根据 ReplicaSets 查看 rollout revision:
kubectl get rs -l app=vweb -o=custom-columns=NAME:.metadata.name,
REPLICAS:.status.replicas,REVISION:.metadata.annotations.deployment
\.kubernetes\.io/revision
```
我的输出如图 9.4 所示。添加 record 标志将 kubectl 命令保存为 rollout 详细信息,如果您的 YAML 文件具有标识名称这将很有帮助。通常情况下他们不会这样做因为您将部署整个文件夹所以Pod spec 中的版本号标签是一个有用的补充。然后,您需要一些笨拙的 JSONPath 来查找 rollout revision 和ReplicaSet之间的链接。
![图9.4](.\images\Figure9.4.png)
<center>图 9.4 Kubernetes 使用标签作为关键信息,额外的细节存储在 annotations 中.</center>
随着 Kubernetes 成熟度的提高,您将希望拥有一组包含在所有对象 spec 中的标准标签。标签和选择器是一个核心特性您将一直使用它们来查找和管理对象。应用程序名称、组件名称和版本都是很好的标签但是区分为方便而包含的标签和Kubernetes用于映射对象关系的标签是很重要的。
清单 9.1 显示了 Pod 标签和前面练习中 Deployment 的选择器。app 标签在选择器中使用Deployment 使用该选择器来查找它的Pods。为了方便起见Pod 还包含一个版本标签但那不是选择器的一部分。如果是则Deployment将链接到一个版本因为一旦创建了Deployment就不能更改选择器。
> 清单9.1 vweb-v11。yaml在 Pod spec 中有附加标签的 Deployment
```
spec:
replicas: 3
selector:
matchLabels:
app: vweb # 用于选择器.
template:
metadata:
labels:
app: vweb
version: v1.1 # 多配置了一个 version 标签.
```
你需要事先仔细规划你的选择器,但你应该在 Pod spec 中添加任何你需要的标签以使你的更新易于管理。Deployment 会保留多个replicaset(10是默认值),并且名称中的 Pod 模板散列使得它们很难直接使用,即使在进行了几次更新之后也是如此。让我们看看我们部署的应用程序实际做了什么,然后在另一个 rollout 中查看ReplicaSets。
<b>现在就试试</b> 向web服务发起HTTP调用以查看响应然后启动另一次更新并再次检查响应
```
# 因为将会使用 url 很多次,所以把它保存到本地文件中:
kubectl get svc vweb -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8090/v.txt' >
url.txt
# 然后通过保存地址进行 HTTP 请求:
curl $(cat url.txt)
# 部署 v2 更新:
kubectl apply -f vweb/update/vweb-v2.yaml --record
# 再次检查响应:
curl $(cat url.txt)
# 检查 ReplicaSet 详细信息:
kubectl get rs -l app=vweb --show-labels
```
在本练习中,您将看到 replicaset 并不是易于管理的对象,因此需要使用标准化标签。通过检查 ReplicaSet 的标签,可以很容易地看到应用程序的哪个版本处于活动状态(如图9.5所示),但标签只是文本字段,因此需要流程保障以确保它们是可靠的。
![图9.5](.\images\Figure9.5.png)
<center>图 9.5 Kubernetes 为您管理 rollouts但如果您添加标签它会有所帮助.</center>
rollout 确实有助于抽象 ReplicaSets 的细节,但它们的主要用途是管理发布。我们已经看到了 kubectl 的 rollout 历史,您还可以运行命令暂停正在进行的 rollout 或将部署回滚到较早的版本。一个简单的命令将回滚到以前的部署但是如果您想回滚到特定的版本则需要更多的JSONPath技巧来查找所需的版本。现在我们将看到这一点并使用kubectl的一个非常方便的特性该特性可以告诉您运行命令时会发生什么而无需实际执行该命令。
<b>现在就试试</b> 检查应用的 rollout 历史记录并尝试回滚到应用的v1
```
# 查看 revisions:
kubectl rollout history deploy/vweb
# 查看 ReplicaSets,带出 revisions 信息:
kubectl get rs -l app=vweb -o=custom-
columns=NAME:.metadata.name,REPLICAS:.status.replicas,VERSION:.meta
data.labels.version,REVISION:.metadata.annotations.deployment\.kube
rnetes\.io/revision
# 看一下执行 rollout 会发生什么:
kubectl rollout undo deploy/vweb --dry-run
# 然后运行一个 rollback 到 revision 2:
kubectl rollout undo deploy/vweb --to-revision=2
# 检查 app—可能会惊到你:
curl $(cat url.txt)
```
如果您在运行该练习时看到图9.6所示的最终输出时感到困惑(这是本章令人兴奋的部分),请举手。我举起手来,我已经知道会发生什么。这就是为什么你需要持续的发布。
![图9.6](.\images\Figure9.6.png)
<center>图 9.6 标签是一个关键的管理特性,但它们是由人类设置的,所以它们很容易出错</center>
过程最好是完全自动化的过程因为一旦开始混合方法就会得到令人困惑的结果。我回滚到版本2从ReplicaSets上的标签判断应该已经恢复到应用程序的v1。但是修订2实际上来自9.1节中的kubectl集合镜像练习因此容器镜像是v2但ReplicaSet标签是v1。
您可以看到,发布过程的移动部分相当简单:部署创建和重用ReplicaSets根据需要扩大和缩小它们对ReplicaSets的更改被记录为rollout。Kubernetes让您可以控制部署策略中的关键因素但在我们继续讨论之前我们将看看还涉及配置更改的版本因为这增加了另一个复杂因素。
在第4章中我讨论了更新Config-Maps和Secrets内容的不同方法您所做的选择会影响您干净地回滚的能力。第一种方法是说配置是可变的因此发布可能包括ConfigMap更改这是对现有ConfigMap对象的更新。但是如果您的发布只是一个配置更改那么您就没有作为rollout的记录也没有回滚的选项。
<b>现在就试试</b>删除现有的部署这样我们就有了一个干净的历史记录然后部署一个使用ConfigMap的新版本看看当你更新相同的ConfigMap时会发生什么
```
# 删除 app:
kubectl delete deploy vweb
# 部署一个用到 configmap 的新应用:
kubectl apply -f vweb/update/vweb-v3-with-configMap.yaml --record
# 检查响应:
curl $(cat url.txt)
# 更新 ConfigMap, 等待一段时间成功:
kubectl apply -f vweb/update/vweb-configMap-v31.yaml --record
sleep 120
# 检查 app :
curl $(cat url.txt)
# 检查 rollout history:
kubectl rollout history deploy/vweb
```
如图9.7所示对ConfigMap的更新改变了应用程序的行为但它不是对Deployment的更改因此如果配置更改导致问题也没有回滚的修订。
![图9.7](.\images\Figure9.7.png)
<center>图 9.7 配置更新可能会改变应用程序的行为,但不会记录 rollout.</center>
这就是热重载方法如果您的应用程序支持它那么它工作得很好因为仅配置的更改不需要rollout。现有的pod和容器保持运行因此不存在服务中断的风险。代价是失去回滚选项您必须决定这是否比热重载更重要。
您的替代方案是将所有configmap和Secrets视为不可变的因此在对象名称中包含一些版本控制方案并且在配置对象创建后永远不要更新它。相反您可以创建一个具有新名称的新配置对象并将其与对Deployment的更新一起发布后者引用了新的配置对象。
<b>现在就试试</b> 使用不可更改的配置部署应用的新版本,这样你就可以比较发布过程
```
# 删除 old Deployment:
kubectl delete deploy vweb
# 使用不可变配置创建一个新的部署:
kubectl apply -f vweb/update/vweb-v4-with-configMap.yaml --record
# 检查输出:
curl $(cat url.txt)
# 发布一个新的 ConfigMap 并更新 Deployment:
kubectl apply -f vweb/update/vweb-v41-with-configMap.yaml --record
# 检查输出:
curl $(cat url.txt)
# 更新将产生完整的 rollout:
kubectl rollout history deploy/vweb
# 因此可以执行回滚:
kubectl rollout undo deploy/vweb
curl $(cat url.txt)
```
图9.8显示了我的输出其中配置更新伴随着Deployment更新后者保留了 rollout 历史并启用了回滚。
![图9.8](.\images\Figure9.8.png)
<center>图 9.8 不可变配置保留了滚出历史,但它意味着每次配置更改都要 rollout.</center>
Kubernetes 并不真正关心您采用哪种方法,您的选择在一定程度上取决于组织中谁拥有配置。如果项目团队还拥有部署和配置,那么您可能更喜欢可变配置对象,以简化发布过程和要管理的对象数量。如果一个单独的团队拥有配置,那么不可变的方法会更好,因为他们可以在发布之前部署新的配置对象。应用程序的规模也会影响决策:在规模大的情况下,你可能更倾向于减少应用程序部署的数量,并依赖可变配置。
这个决定有文化上的影响,因为它框定了应用程序发布是如何被感知的——把它看作是没什么大不了的日常事件,或者看作是要尽可能避免的有点可怕的事情。在容器世界中,发布应该是一些微不足道的事件,只要需要,您就会乐于以最小的仪式来完成。测试和调整你的发行策略会给你带来很大的信心。
## 9.3 为 Deployments 配置滚动更新
Deployment 支持两种更新策略:RollingUpdate 是默认的,也是我们目前使用的一种,另一种是 Recreate。您知道 rollout 更新是如何工作的——通过缩小旧的ReplicaSet同时扩大新的ReplicaSet这提供了服务连续性和在较长时间内错开更新的能力。而 Recreate 策略则不提供这两种情况。它仍然使用 ReplicaSets 来实现更改但是在扩展替换之前它将先前的设置缩小到零。清单9.2显示了部署 spec 中的recreate策略。这只是一个设置但它具有重大影响。
> 清单9.2 vweb-recreate-v2.yaml一个使用重建更新策略的部署
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: vweb
spec:
replicas: 3
strategy: # 更新策略.
type: Recreate
# selector & Pod spec follow
```
当你部署这个时你会看到它只是一个普通的应用程序有一个Deployment一个ReplicaSet和一些Pods。如果查看Deployment的详细信息您将看到它使用了 Recreate 更新策略,但这仅在更新 Deployment 时才会起作用。
<b>现在就试试</b> 部署清单9.2中的应用程序,并浏览对象。这就像一个正常的部署
```
# 删除 app:
kubectl delete deploy vweb
# 部署 Recreate 策略的应用:
kubectl apply -f vweb-strategies/vweb-recreate-v2.yaml
# 检查 ReplicaSets:
kubectl get rs -l app=vweb
# 测试 app:
curl $(cat url.txt)
# 查看 Deployment 详细:
kubectl describe deploy vweb
```
如图 9.9 所示这是同一个旧web应用程序的新部署使用容器镜像的版本2。有三个pod它们都在运行应用程序按预期运行——到目前为止还不错
![图9.9](.\images\Figure9.9.png)
<center>图 9.9 在你发布更新之前,重新创建更新策略不会影响行为</center>
不过这种配置很危险只有在应用程序的不同版本不能共存时才应该使用这种配置——就像数据库模式更新一样需要确保只有一个版本的应用程序连接到数据库。即使在这种情况下您也有更好的选择但如果您的场景确实需要这种方法那么您最好确保在发布之前测试所有更新。如果你部署了一个更新而新Pods失败了你会直到旧Pods全部被终止才知道你的应用将完全不可用。
<b>现在就试试</b>版本3的web应用程序已经准备好部署。当应用程序离线时你会看到它坏了因为没有Pods在运行
```
# 部署 v3:
kubectl apply -f vweb-strategies/vweb-recreate-v3.yaml
# 检查状态,有更新的时间限制:
kubectl rollout status deploy/vweb --timeout=2s
# 检查 ReplicaSets:
kubectl get rs -l app=vweb
# 检查 Pods:
kubectl get pods -l app=vweb
# 测试 app将会失败:
curl $(cat url.txt)
```
在这个练习中你会看到Kubernetes很乐意让你的应用脱机因为这是你所请求的。Recreate 策略使用更新后的Pod模板创建一个新的ReplicaSet然后将先前的ReplicaSet缩小到0并将新的ReplicaSet扩大到3。新的镜像被破坏了所以新的Pods失败了没有任何东西可以响应请求如图9.10所示。
![图9.10](.\images\Figure9.10.png)
<center>图 9.10 如果新的Pod规格被打破Recreate 策略会愉快地关闭你的应用 </center>
现在您已经看到了它,您可能应该尝试忘记 Recreate 策略。在某些情况下,它可能看起来很有吸引力,但当它出现时,您仍然应该考虑替代选项,即使这意味着重新查看您的体系结构。应用程序的大规模关闭将导致停机,而且停机时间可能比您计划的要长。
RollingUpdate 是默认的因为它们可以防止停机但即使这样默认行为也是相当激进的。对于产品版本您需要调优一些设置以设置发布的速度以及如何监控它。作为rollout 更新规范的一部分您可以使用以下两个值添加控制新ReplicaSet的扩展速度和旧ReplicaSet的缩小速度的选项:
- maxUnavailable 是用于缩小旧 ReplicaSet 的加速器。它定义了在更新期间相对于所需的Pod计数可以有多少Pod不可用。您可以把它看作是在旧的ReplicaSet中终止pod的批处理大小。在10个pod的部署中将此设置为30%意味着将立即终止3个pod。
- maxSurge 是扩展新ReplicaSet的加速器。它定义了在期望的副本计数上可以存在多少额外的pod就像在新的ReplicaSet中创建pod的批处理大小一样。在10个部署中将此设置为40%将创建4个新pod。
很好,很简单,除了这两种设置都是在 rollout 时使用的所以你有一个跷跷板的效果。新的ReplicaSet被放大直到Pod计数是所需的副本计数加上maxSurge值然后部署等待旧的Pod被删除。旧的ReplicaSet被缩小到所需的计数减去maxUnavailable计数然后Deployment等待新Pods达到就绪状态。你不能把两个值都设为0因为这意味着什么都不会改变。图9.11显示了如何组合这些设置,以便对新版本使用先创建再删除、先删除再创建或先删除再创建的方法。
![图9.11](.\images\Figure9.11.png)
<center>图 9.11 正在进行部署更新,使用不同的 rollout 选项</center>
如果您的集群中有空闲的计算能力,您可以调整这些设置以更快地 rollout。您还可以在您的比例设置上创建额外的pod但如果您对新版本有问题那么这样做的风险更大。缓慢的发布更保守:它使用更少的计算,给你更多的机会发现任何问题,但它在发布期间降低了应用程序的整体容量。让我们来看看这些是什么样子,首先通过保守的 rollout 来修复我们的坏应用程序。
<b>现在就试试</b>恢复到工作版本2的镜像使用 maxSurge=1 和 maxUnavailable=0 在RollingUpdate策略
```
# 更新 Deployment 使用 rolling updates 以及 v2 image:
kubectl apply -f vweb-strategies/vweb-rollingUpdate-v2.yaml
# 检查 Pods:
kubectl get po -l app=vweb
# 检查 rollout status:
kubectl rollout status deploy/vweb
# 检查 ReplicaSets:
kubectl get rs -l app=vweb
# 测试 app:
curl $(cat url.txt)
```
在本练习中新的部署规范将Pod镜像更改回版本2并将更新策略更改为滚动更新。你可以在图9.12中看到首先进行了策略更改然后根据新策略进行Pod更新一次创建一个新的Pod。
![图9.12](.\images\Figure9.12.png)
<center>图 9.12 如果Pod模板匹配新规范部署更新将使用现有的ReplicaSet</center>
在前面的练习中,您需要快速工作以查看 rollout 的进度因为这个简单的应用程序很快启动只要一个新的Pod正在运行就会继续使用另一个新Pod进行 rollout。在部署规范中您可以通过以下两个字段来控制部署的速度:
- minReadySeconds 增加了一个延迟即部署等待以确保新pod稳定。它指定了Pod在没有容器崩溃之前应该启动的秒数。默认值为0这就是为什么在 rollout 过程中会快速创建新pod的原因。
- progressDeadlineSeconds 指定部署更新在被视为进展失败之前可以运行的时间。默认值是600秒因此如果更新没有在10分钟内完成它将被标记为未进行。
监视发布需要多长时间听起来很有用但从Kubernetes 1.19开始超过最后期限实际上并不会影响部署—它只是在部署上设置了一个标志。Kubernetes没有自动回滚功能但是当该功能出现时它将由这个标志触发。等待和检查Pod中失败的容器是一个相当生硬的工具但这比根本不检查要好您应该考虑在所有部署中指定minReadySeconds。
这些安全措施可以添加到你的部署中但它们对我们的web应用没有真正的帮助因为新的Pods总是失败。我们可以使此部署安全并使用滚动更新保持应用程序在线。下一个版本3更新将maxUnavailable和maxSurge设置为1。这样做与默认值(每个25%)具有相同的效果但在规范中使用确切的值更清楚并且在小型部署中Pod计数比百分比更容易处理。
<b>现在就试试</b>再次部署版本3更新。它仍然会失败但通过使用RollingUpdate策略它不会让应用程序离线
```
# 更新失败的容器镜像:
kubectl apply -f vweb-strategies/vweb-rollingUpdate-v3.yaml
# 检查 Pods:
kubectl get po -l app=vweb
# 检查 rollout:
kubectl rollout status deploy/vweb
# 查看 ReplicaSets:
kubectl get rs -l app=vweb
# 测试 app:
curl $(cat url.txt)
```
当您运行这个练习时您将看到更新永远不会完成并且部署卡住了两个ReplicaSets其Pod数为2如图9.13所示。旧的ReplicaSet将不会进一步缩小因为部署已经将maxUnavailable设置为1;它的规模已经缩小了1不会有新的pod准备继续推出。新的ReplicaSet将不再扩展因为maxSurge被设置为1并且已经达到了部署的总Pod数。
![图9.13](.\images\Figure9.13.png)
<center>图 9.13 失败的更新不会自动回滚或暂停;他们只是一直在努力</center>
如果您在几分钟后检查新pod您将看到它们处于CrashLoopBackoff状态。Kubernetes通过创建替换容器来重新启动失败的Pods但它在每次重新启动之间添加了暂停这样就不会阻塞节点上的CPU。这个暂停就是后退时间它呈指数级增加——第一次重新启动时为10秒然后是20秒然后是40秒最多5分钟。这些第三版pod将永远无法成功重启但Kubernetes将继续尝试。
Deployments 是您使用最多的控制器,值得花时间研究更新策略和定时设置,以确保您理解.对你的应用的影响。DaemonSets和StatefulSets也有滚动更新功能因为它们有不同的管理pod的方式所以它们也有不同的 rollout 方法。
## 9.4 DaemonSets 和 StatefulSets 中的滚动更新
DaemonSets 和 StatefulSets 有两个可用的更新策略。默认的是RollingUpdate我们将在本节中探讨它。另一种方法是OnDelete它用于需要密切控制每个Pod何时更新的情况。你部署更新控制器会监视pod但它不会终止任何现有的pod。它会一直等待直到它们被另一个进程删除然后用新规范中的pod替换它们。
当您考虑这些控制器的用例时这并不像听起来那么毫无意义。您可能有一个StatefulSet其中每个Pod在删除之前都需要将数据刷新到磁盘您可以有一个自动化的过程来完成这一点。您可能有一个DaemonSet其中每个Pod都需要从硬件组件断开连接以便下一个Pod可以使用它。这种情况很少见但是OnDelete策略可以让你在删除Pods时拥有所有权并且仍然可以让Kubernetes自动创建替换。
在本节中,我们将重点关注 rollingupdate为此我们将部署一个版本的待办事项列表应用程序它在StatefulSet中运行数据库在Deployment中运行web应用程序在DaemonSet中运行web应用程序的反向代理。
<b>现在就试试</b> 待办事项应用程序可以在6个pod中运行所以首先清理现有的应用程序以腾出空间。然后部署应用程序并测试它是否正常工作
```
# 删除所有本章节应用:
kubectl delete all -l kiamol=ch09
# 部署 to-do app, database, and proxy:
kubectl apply -f todo-list/db/ -f todo-list/web/ -f todo-list/proxy/
# 获取应用访问url:
kubectl get svc todo-proxy -o
jsonpath='http://{.status.loadBalancer.ingress[0].*}:8091'
# 浏览应用, 添加条目
```
这只是为更新做准备。你现在应该有一个工作的应用程序你可以添加项目并看到列表。我的输出如图9.14所示。
![图9.14](.\images\Figure9.14.png)
<center>图 9.14 运行带有各种免费控制器的to-do应用程序 </center>
第一个更新是 DaemonSet在那里我们将退出一个新版本的Nginx代理镜像。DaemonSets在集群中的所有(或部分)节点上运行一个Pod通过滚动更新您没有激增选项。在更新期间节点永远不会运行两个pod因此这始终是一种先删除再删除的策略。您可以添加 maxUnavailable 设置来控制并行更新的节点数量但是如果您关闭多个pod您将以较低的容量运行直到替换准备就绪。
我们将使用 maxUnavailable 设置为1和 minReadySeconds 设置为90来更新代理。在单节点实验室集群上延迟不会产生任何影响——一个节点上只有一个Pod需要替换。在更大的集群中这意味着一次更换一个Pod并等待90秒让Pod证明它是稳定的然后再转移到下一个Pod。
<b>现在就试试</b> 启动DaemonSet的滚动更新。在单节点集群中更换Pod启动时会出现短暂停机
```
# 部署更新后的 DaemonSet :
kubectl apply -f todo-list/proxy/update/nginx-rollingUpdate.yaml
# watch Pod 更新:
kubectl get po -l app=todo-proxy --watch
```
kubectl 中的 watch 标志对于监视更改非常有用——它会一直查看对象,并在状态更改时打印更新行。在本练习中,您将看到旧的 Pod 在创建新Pod之前被终止这意味着新Pod启动时应用程序有停机时间。图9.15显示了我在发布中有1秒的停机时间。
![图9.15](.\images\Figure9.15.png)
<center>图 9.15 DaemonSets 更新通过删除现有Pod之前创建一个替换</center>
多节点集群不会有任何停机时间因为服务只向准备就绪的Pod发送流量并且一次只更新一个Pod因此其他Pod始终可用。但是您的容量将会减少如果您使用更高的maxUnavailable设置来优化更快的推出这意味着随着更多pod并行更新容量将会减少更多。这是你对DaemonSets的唯一设置所以这是一个简单的选择通过删除pod手动控制更新或让Kubernetes通过指定数量的pod并行推出更新。
statefulset 更有趣尽管它们只有一个选项来配置rollout。Pod由StatefulSet按顺序管理这也适用于更新——从集合中的最后一个Pod向后推出到第一个Pod。这对于Pod 0是主节点的集群应用程序特别有用因为它首先验证从节点上的更新。
StatefulSets 没有maxSurge或maxUnavailable设置。更新总是由一个Pod在一个时间。您的配置选项是使用分区设置定义总共应该更新多少pod。这个设置定义了滚出停止的截止点它对于执行有状态应用的分阶段滚出非常有用。如果你的set中有五个副本并且spec中包含partition=3那么只有Pod 4和Pod 3将被更新;pod 0、1和2仍然运行前一个规范。
<b>现在就试试</b> 在StatefulSet中部署一个分区更新到数据库镜像它在Pod 1之后停止所以Pod 0不会被更新
```
# 部署更新:
kubectl apply -f todo-list/db/update/todo-db-rollingUpdate-
partition.yaml
# 检查 rollout status:
kubectl rollout status statefulset/todo-db
# 查看 Pods, 显示镜像及启动时间:
kubectl get pods -l app=todo-db -o=custom-columns=NAME:.metadata.name,
IMAGE:.spec.containers[0].image,START_TIME:.status.startTime
# 切换 web app 到只读模式, 所以它会使用副本数据库:
kubectl apply -f todo-list/web/update/todo-web-readonly.yaml
# 测试 app—只不过是只读的
```
这个练习是一个分区更新它推出了Postgres容器镜像的新版本但只对次要Pod进行更新在本例中是单个Pod如图9.16所示。当你在只读模式下使用应用程序时你会看到它连接到更新后的备用程序备用程序仍然包含从上一个Pod复制的数据。
![图9.16](.\images\Figure9.16.png)
<center>图 9.16 对StatefulSets的分区更新允许您更新次要文件而保持主文件不变 </center>
这是完整的展示即使在一组pod运行不同的规格。对于StatefulSet中的数据量大的应用程序您可能需要在每个更新的Pod上运行一组验证作业然后才愿意继续推出而分区更新可以让您做到这一点。您可以通过运行分区值递减的连续更新来手动控制发布的节奏直到在最后的更新中完全删除分区以完成集合。
<b>现在就试试</b>将更新部署到数据库主数据库。此规范与前面的练习相同,但删除了分区设置
```
# 应用更新:
kubectl apply -f todo-list/db/update/todo-db-rollingUpdate.yaml
# 检查 progress:
kubectl rollout status statefulset/todo-db
# Pods 现在拥有相同的配置:
kubectl get pods -l app=todo-db -o=custom-
columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,START
_TIME:.status.startTime
# 重置 web app 回到读写模式:
kubectl apply -f todo-list/web/todo-web.yaml
# 测试应用程序是否工作并连接到更新后的主Pod
```
你可以在图9.17中看到我的输出其中完整的更新已经完成主服务器使用与备用服务器相同的更新版本的Postgres。如果您以前对复制的数据库进行过更新您就会知道这非常简单——当然除非您使用的是托管数据库服务。
![图9.17](.\images\Figure9.17.png)
<center>图 9.17 使用未分区的更新完成StatefulSet rollout</center>
滚动更新是 Deployment 、DaemonSets和StatefulSets的默认设置它们的工作方式大致相同:逐渐用运行新规范的Pods取代运行前一个应用程序规范的Pods。实际的细节不同因为控制器的工作方式不同目标也不同但它们对应用程序的要求是相同的:当多个版本处于活动状态时它需要正确工作。这并不总是可行的还有其他方法可以在Kubernetes中部署应用程序更新。
## 9.5 理解发布策略
以一个 web 应用程序为例。滚动更新非常棒因为它可以让每个Pod在处理完所有客户端请求后优雅地关闭并且可以按照您的喜好快速或保守地 rollout。rollout 的实际方面很简单,但您也必须考虑用户体验(UX)方面。
应用程序更新很可能会改变ux——使用不同的设计、新功能或更新的工作流。对于用户来说如果他们在推出过程中看到新版本然后刷新并发现自己使用了旧版本那么任何这样的更改都会非常奇怪因为这些请求是由运行不同版本应用程序的Pods提供的。
处理这种情况的策略超出了控制器中的RollingUpdate规范。您可以在web应用程序中设置cookie将客户端链接到特定的用户体验然后使用更高级的流量路由系统以确保用户一直看到新版本。当我们在第15章讲到它时你会看到它引入了更多的活动部分。对于这种方法过于复杂或不能解决双运行多个版本的问题的情况您可以使用蓝绿部署自己管理发布。
蓝绿部署是一个简单的概念:你同时部署了应用程序的新版本和旧版本但只有一个版本是活动的。您可以通过拨动开关来选择激活的版本。在Kubernetes中你可以通过更新Service中的标签选择器来将流量发送到不同部署中的Pods如图9.18所示。
![图9.18](.\images\Figure9.18.png)
<center>图 9.18 在蓝绿部署中运行多个版本的应用程序,但只有一个是活的</center>
你需要在你的集群中有能力运行两个完整的应用程序副本。如果它是一个web或API组件那么新版本应该使用最小的内存和CPU因为它没有接收任何流量。您可以通过更新服务的标签选择器来切换版本因此更新实际上是即时的因为所有Pods都在运行并准备接收流量。您可以轻松地来回翻转因此您可以回滚有问题的版本而无需等待ReplicaSets的伸缩。
蓝绿部署没有滚动更新那么复杂但正因为如此它们更简单。它们可能更适合那些迁移到Kubernetes的组织这些组织有大规模部署的历史但它们是一种计算密集型的方法需要多个步骤并且不能保存应用程序的推出历史。您应该将滚动更新作为首选的部署策略但蓝绿部署是一个很好的跳板可以在您获得信心的同时使用。
以上是关于滚动更新的全部内容,但是当我们讨论生产准备、网络进入和监视等主题时,我们将回到这些概念。我们只需要在去实验室之前整理一下集群。
<b>现在就试试</b> 删除为本章创建的所有对象
```
kubectl delete all -l kiamol=ch09
kubectl delete cm -l kiamol=ch09
kubectl delete pvc -l kiamol=ch09
```
## 9.6 实验室
我们在前一节中学习了蓝绿部署的理论现在在实验室中您将实现它。通过本实验将有助于您清楚地了解选择器如何将Pods与其他对象关联起来并为您提供使用滚动更新的替代方案的经验。
- 起点是web应用程序的版本1您可以从lab/v1文件夹部署。
- 你需要为应用程序的版本2创建一个蓝绿色的部署。规范将类似于版本1的规范但使用:v2容器镜像。
- 当您部署更新时您应该能够通过更改服务在版本1和版本2之间切换而无需对Pods进行任何更新。
这是复制YAML文件并尝试确定需要更改哪些字段的良好实践。你可以在GitHub上找到我的解决方案: https://github.com/yyong-brs/learn-kubernetes/tree/master/kiamol/ch09/lab/README.md。

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Some files were not shown because too many files have changed in this diff Show More