GitOps-与-Kubernetes-全-

GitOps 与 Kubernetes(全)

原文:GitOps and Kubernetes

译者:飞龙

协议:CC BY-NC-SA 4.0

前置内容

前言

当 Intuit 开始从本地化到云原生转型的旅程时,这个过程本身为我们重新发明构建和部署流程提供了机会。与许多大型企业类似,我们旧的部署流程以数据中心为中心,有独立的 QA、运维和基础设施团队。代码可能需要几周时间才能部署,当出现生产问题时,开发者无法访问基础设施。基础设施问题可能需要很长时间才能解决,需要许多团队的协作。

当马里安娜·特塞尔(Intuit CTO)和杰夫·布鲁尔(Intuit SBSEG 首席架构师)决定对 Kubernetes 和 Docker 进行大赌注时,我们很幸运地成为第一个完全使用 Kubernetes 和 Docker 迁移我们生产应用程序的团队。在这个过程中,我们得以重新发明我们的 CI/CD 管道并采用 GitOps 流程。杰西和亚历克斯创建了 Argo CD(CNCF 孵化器项目)以满足企业对 GitOps 的需求。托德和他的团队创建了世界级的集群管理工具,使我们能够轻松扩展到数百个集群。

拥有像 Kubernetes 和 Docker 这样的标准,使得所有工程师都能在基础设施和部署方面使用一种共同的语言。工程师可以轻松地为其他项目做出贡献,并在开发过程完成后立即部署。GitOps 还允许我们确切地知道在我们的环境中是谁和什么发生了变化,这对于您需要遵守合规要求的情况尤为重要。我们无法想象回到我们以前进行部署的方式,我们希望这本书能帮助加速您拥抱 GitOps 的旅程!

致谢

这本书最终成为了一段历时 18 个月的旅程,需要大量的工作和额外的研究来讲述完整的故事。我们相信我们已经完成了我们设定的目标,这是一本任何想要采用 GitOps 和 Kubernetes 的人都会喜欢的书。

有很多人我们想要感谢他们在旅途中给予的帮助。在 Manning,我们想要感谢我们的开发编辑达斯汀·阿奇博尔德(Dustin Archibald)、项目编辑迪尔德丽·希姆(Deirdre Hiam)、校对员凯蒂·滕南特(Katie Tennant)和审阅编辑亚历克斯·德拉戈萨夫列维奇(Aleks Dragosavljevic)。

我们要感谢马里安娜·特塞尔(Marianna Tessel)和杰夫·布鲁尔(Jeff Brewer),他们为我们提供了转换和实验 GitOps 和 Kubernetes 的机会和自由。我们还要感谢普拉蒂克·瓦德赫(Pratik Wadher)、萨拉达希·斯里吉里亚朱(Saradhi Sreegiriaju)、穆库莉卡·库帕斯(Mukulika Kupas)和爱德华·李(Edward Lee)在整个过程中的指导。我们还要特别指出维克托·法尔西奇(Viktor Farcic)和奥斯卡·梅迪纳(Oscar Medina)对 Jenkins X 章节的深刻贡献。

向所有审阅者:安德烈斯·达米安·萨科(Andres Damian Sacco)、安杰洛·西蒙内·斯科托(Angelo Simone Scotto)、比约恩·纽豪斯(Björn Neuhaus)、克里斯·维纳(Chris Viner)、克利福德·瑟伯(Clifford Thurber)、科诺尔·雷德蒙德(Conor Redmond)、迪戈·凯拉(Diego Casella)、刘家辉(James Liu)、豪梅·洛佩斯(Jaume López)、杰里米·布赖恩(Jeremy Bryan)、杰罗姆·梅耶(Jerome Meyer)、约翰·古德里奇(John Guthrie)、马可·马森齐奥(Marco Massenzio)、马蒂厄·埃文(Matthieu Evrin)、迈克·恩索尔(Mike Ensor)、迈克·詹森(Mike Jensen)、罗曼·祖扎(Roman Zhuzha)、塞缪尔·布朗(Samuel Brown)、萨特杰·库马尔·萨胡(Satej Kumar Sahu)、肖恩·T·博克(Sean T. Booker)、温德尔·贝克威斯(Wendell Beckwith)和佐罗德扎伊·穆库亚(Zorodzayi Mukuya),我们说谢谢。你们的建议帮助使这本书变得更好。

致 Jeff Brewer,他激励我们所有人踏上这段精彩的转型之旅!

关于本书

本书面向的对象

本书旨在为 Kubernetes 基础设施和运维工程师以及希望通过 GitOps 流程使用声明式模型部署应用程序的软件开发人员提供帮助。它将有助于任何希望提高其 Kubernetes 集群的稳定性、可靠性、安全性和可审计性,同时通过自动化的持续软件部署来降低运营成本的人。

预期读者具备 Kubernetes(例如 Deployment、Pod、Service 和 Ingress 资源)的实际操作知识,以及包括持续集成/持续交付 (CI/CD)、版本控制系统(如 Git)和部署/基础设施自动化在内的现代软件开发实践的理解。

本书不面向的对象

成功实施成熟 GitOps 系统的高级用户可能更适合阅读关于他们选择工具的更高级书籍。

本书并不旨在深入探讨 Kubernetes 的所有方面。虽然我们涵盖了与 GitOps 相关的许多 Kubernetes 概念,但寻求 Kubernetes 综合指南的读者应参考其他关于该主题的杰出书籍和在线资源。

本书组织方式:路线图

本书描述了 GitOps 在 Kubernetes 上的好处,包括灵活的配置管理、监控、健壮性、多环境支持和安全性。您将学习实现这些好处最佳实践、技术和工具,这些工具使企业能够使用 Kubernetes 加速应用程序开发,同时不牺牲稳定性、可靠性或安全性。

您还将深入了解以下主题:

  • 多环境管理,包括分支、命名空间和配置

  • 使用 Git、Kubernetes 和管道进行访问控制

  • CI/CD、晋升、推送/拉取和发布/回滚的管道考虑事项

  • 可观察性和漂移检测

  • 管理机密

  • 在滚动更新、蓝/绿、金丝雀和渐进式交付之间选择部署策略

本书采用动手实践的方法,通过教程和练习来培养您使用 Kubernetes 采用 GitOps 所需的技能。阅读本书后,您将了解如何为在 Kubernetes 上运行的应用程序实现声明式连续交付系统。本书包含以下动手教程:

  • 开始管理 Kubernetes 应用程序部署

  • 使用 Kustomize 进行配置和环境管理

  • 编写自己的基本 Kubernetes 连续交付 (CD) 操作符

  • 使用 Argo CD、1 Jenkins X、2 和 Flux3 实现持续集成/持续交付 (CI/CD)

命令式与声明式:部署 Kubernetes 有两种基本方式:命令式地使用多个kubectl命令或通过编写清单并使用kubectl apply进行声明式部署。前者适用于学习和交互式实验。后者最适合可重复部署和跟踪变更。

本书旨在让您跟随,运行教程的动手部分,使用您自己的测试 Kubernetes 集群。附录 A 描述了创建测试集群的几种选项。

书中包含许多代码示例。所有代码示例和附加支持材料都可以在本书的公开 GitHub 仓库中找到:

github.com/gitopsbook/resources

我们鼓励您克隆或分叉此仓库,并在完成书中的教程和练习时使用它。

以下工具和实用程序应在您的工作站上安装:

  • Kubectl (v1.16 或更高版本)

  • Minikube (v1.4 或更高版本)

  • Bash 或 Windows Subsystem for Linux (WSL)

大多数教程和练习都可以使用在您的工作站上运行的 minikube 完成。如果不是,我们将提及是否需要运行在云服务提供商上的集群,您可以通过附录 A 获取创建集群的详细信息。

注意:在云服务提供商上运行测试 Kubernetes 集群可能会产生额外费用。虽然我们已经尽可能降低了推荐测试配置的成本,但请记住,您对这些费用负责。我们建议您在完成每个教程或练习后删除您的测试集群。

本书分为 3 部分,共 11 章。第一部分涵盖了背景知识,并介绍了 GitOps 和 Kubernetes:

  • 第一章带您回顾软件部署的演变历程,以及 GitOps 如何成为最新的实践。它还涵盖了 GitOps 的许多关键概念和优势。

  • 第二章提供了 Kubernetes 的关键概念以及为什么其声明式特性非常适合 GitOps。它还涵盖了核心操作员概念以及如何实现一个简单的 GitOps 操作员。

第二部分介绍了采用 GitOps 流程的模式和过程:

  • 第三章讨论了环境的定义以及 Kubernetes Namespaces 如何很好地映射为环境。它还涵盖了分支策略和配置管理到环境实现。

  • 第四章深入探讨了 GitOps CI/CD 管道,全面描述了完整管道所需的所有阶段。它还涵盖了代码、镜像和环境升级,以及回滚机制。

  • 第五章描述了各种部署策略,包括滚动更新、蓝绿部署、金丝雀发布和渐进式交付。它还涵盖了如何通过使用原生 Kubernetes 资源和其它开源工具来实现每种策略。

  • 第六章讨论了 GitOps 驱动的部署的攻击面以及如何减轻每个区域。它还回顾了 Jsonnet、Kustomize 和 Helm,以及如何为您的用例选择合适的配置管理模式。

  • 第七章讨论了管理 GitOps 秘密的各种策略。它还涵盖了几个秘密管理工具以及原生的 Kubernetes 秘密。

  • 第八章解释了可观测性的核心概念以及为什么它对 GitOps 很重要。它还描述了使用 GitOps 和 Kubernetes 实现可观测性的各种方法。

第三部分介绍了几个企业级 GitOps 工具:

  • 第九章讨论了 Argo CD 的意图和架构。它还涵盖了使用 Argo CD 配置应用程序部署以及如何在生产中保护 Argo CD。

  • 第十章讨论了 Jenkins X 的意图和架构。它还涵盖了配置应用程序部署和升级到各种环境。

  • 第十一章讨论了 Flux 的意图和动机。它还涵盖了使用 Flux 和多云租户配置应用程序部署。

图片

本书组织结构是为了按顺序阅读所有章节。然而,如果您想直接跳到某个特定领域,我们建议您先阅读先决章节。例如,如果您想直接学习使用 Argo CD,我们建议您在阅读第九章之前先阅读第 1、2、3 和 5 章。

关于代码

本书包含许多源代码示例,既有编号列表,也有与普通文本内联。在这两种情况下,源代码都使用固定宽度字体格式化,以将其与普通文本区分开来。有时,代码也会加粗,以突出显示与章节中先前步骤相比已更改的代码,例如当新功能添加到现有代码行时。

在许多情况下,原始源代码已被重新格式化;我们已添加换行和重新整理缩进来适应书中的可用页面空间。此外,当代码在文本中描述时,源代码中的注释通常已从列表中删除。代码注释伴随许多列表,突出显示重要概念。本书中的示例源代码可以从github.com/gitopsbook /resources下载。

liveBook 讨论论坛

购买 GitOps 和 Kubernetes 包括免费访问由 Manning Publications 运行的私有网络论坛,您可以在论坛中就本书发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问livebook.manning.com/book/GitOps-and-Kubernetes/discussion。您还可以在 livebook.manning.com/#!/discussion了解更多关于 Manning 论坛和行为准则的信息。

曼宁对读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量承诺的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议您尝试向他们提出一些挑战性的问题,以免他们的兴趣偏离!只要这本书有售,论坛和先前讨论的存档将可通过出版社的网站访问。

关于作者

比利·袁(Billy Yuen)是 Intuit 平台团队的首席工程师,专注于 AWS 和 Kubernetes 的采用、系统弹性和监控。此前,比利曾在 Netflix 的边缘服务团队工作,构建下一代边缘服务基础设施,以支持数百万客户(每天超过 30 亿次请求),具有高可扩展性、故障恢复能力和快速创新。比利曾在 2016 年的 Java One 和 Velocity NY 2016 上发表演讲,主题是“使用 Netflix Hystrix 实现操作卓越”、“KubeCon 2018 的 CI/CD at Lightspeed”以及 2019 年 Container World 的“Automated Canary Release”。

亚历山大·马图申采夫(Alexander Matyushentsev)是 Intuit 平台团队的首席工程师,专注于构建使使用 Kubernetes 更简单的工具。亚历山大热衷于开源、云原生基础设施以及提高开发者生产力的工具。他是 Argo Workflows 和 Argo CD 项目的核心贡献者之一。亚历山大曾在 2019 年的 KubeCon 上发表演讲,主题是“Intuit 如何使用 K8s 控制器进行金丝雀和蓝绿部署”。

托德·伊肯斯塔姆(Todd Ekenstam)是 Intuit 公司的一名首席工程师,负责构建一个支持 Intuit 约 5000 万客户的应用程序的、安全的多租户 Kubernetes 基础设施平台。在超过 25 年的职业生涯中,他参与了许多大型分布式系统项目,包括分层存储管理、对等数据库复制、企业存储虚拟化和双因素认证 SaaS。托德曾在学术、政府和行业会议上发表演讲,最近在 2018 年的 KubeCon 上作为嘉宾演讲者,主题是“Open Policy Agent 简介”。

詹斯·苏恩(Jesse Suen)是 Intuit 平台团队的首席工程师,负责开发基于微服务的、适用于 Kubernetes 的分布式应用程序。他曾是 Applatix(被 Intuit 收购)的早期工程师,构建了一个平台,帮助用户在公共云中运行容器化工作负载。在此之前,他曾是 Tintri 和 Data Domain 的工程团队的一员,从事虚拟化基础设施、存储、工具和自动化工作。詹斯是开源项目 Argo Workflows 和 Argo CD 的核心贡献者之一。

关于封面插图

《GitOps 和 Kubernetes》封面上的插图标题为“Habitant de Styrie”,或斯泰里亚居民。这幅插图取自雅克·格拉塞·德·圣索沃尔(1757-1810)的作品集,名为《不同国家的服饰》,于 1797 年在法国出版。每一幅插图都是手工精心绘制和着色的。格拉塞·德·圣索沃尔收藏中的丰富多样性生动地提醒我们,200 年前世界的城镇和地区在文化上有多么不同。人们相互隔离,说着不同的方言和语言。在街道或乡村,仅凭他们的服饰就能轻易识别他们居住的地方以及他们的职业或社会地位。

自那以后,我们的着装方式已经改变,当时地区间的多样性已经消失。现在很难区分不同大陆、城镇、地区或国家的人们。也许我们用文化多样性换取了更加丰富多彩的个人生活——当然,是为了更加多样化和快节奏的技术生活。

在难以区分一本计算机书和另一本计算机书的今天,Manning 通过基于两百年前区域生活丰富多样性的封面,庆祝计算机行业的创新精神和主动性,这些封面被格拉塞·德·圣索沃尔的图画重新赋予了生命。


1.argoproj.github.io/argo-cd.

2.jenkins-x.io.

3.github.com/fluxcd/flux.

第一部分. 背景

本书这一部分涵盖了背景知识,并为您介绍了 GitOps 和 Kubernetes。

第一章带您回顾了软件部署的演变历程以及 GitOps 如何成为最新的实践。它还涵盖了 GitOps 的许多关键概念和优势。

第二章提供了 Kubernetes 的关键概念以及为什么其声明性特性非常适合 GitOps。它还涵盖了核心操作员概念以及如何实现一个简单的 GitOps 操作员。

在您掌握了 GitOps 和 Kubernetes 的核心概念之后,您将准备好深入探索在部署中采用 GitOps 所需的模式和流程。第二部分涵盖了 GitOps CI/CD 流水线,以及环境设置和升级,以及不同的部署策略。它还涵盖了如何确保您的部署过程的安全,并回顾了几个配置管理工具以及 GitOps 中管理 Secrets 的各种技术。此外,还有一个章节专门讨论可观察性,因为它与 GitOps 相关。

1 为什么选择 GitOps?

本章涵盖

  • 什么是 GitOps?

  • 为什么 GitOps 很重要

  • GitOps 与其他方法的比较

  • GitOps 的优势

Kubernetes 是一个非常流行的开源平台,它编排和自动化操作。尽管它提高了基础设施和应用程序的管理和扩展,但 Kubernetes 经常面临管理应用程序复杂性的挑战。

Git 是目前软件行业中应用最广泛的版本控制系统。GitOps 是一套使用 Git 的力量在 Kubernetes 平台上提供修订和变更控制的一系列流程。GitOps 策略在团队快速且轻松地管理其服务的环境创建、升级和运营方面可以发挥重要作用。

使用 GitOps 与 Kubernetes 是一种自然匹配,声明式 Kubernetes 清单文件的部署由常见的 Git 操作控制。GitOps 以直观、易于访问的方式,将基础设施即代码和不可变基础设施的核心优势带给 Kubernetes 应用的部署、监控和生命周期管理。

1.1 向 GitOps 的演变

管理和运营计算机系统的两个日常任务是基础设施配置和软件部署。基础设施配置 准备计算资源(如服务器、存储和负载均衡器),使软件应用能够正确运行。软件部署 是将特定版本的软件应用部署到计算基础设施上的过程。管理这两个过程是 GitOps 的核心。然而,在我们深入探讨 GitOps 中如何进行这种管理之前,了解导致行业走向 DevOps 和 GitOps 的不可变、声明式基础设施的挑战是有用的。

1.1.1 传统运维

在传统的信息技术运维模型中,开发团队负责定期向质量保证(QA)团队交付新的软件应用版本,该团队测试新版本并将其交付给运维团队进行部署。软件的新版本可能每年发布一次,每季度发布一次,或者更短的时间间隔。对于传统的运维模型来说,支持越来越压缩的发布周期变得越来越困难。

运维团队负责新软件应用版本的配置和部署到该基础设施。运维团队的主要关注点是确保运行软件的系统的可靠性、弹性和安全性。没有复杂的管理框架,基础设施管理可能是一项困难的任务,需要大量的专业知识。

图片

图 1.1 传统 IT 团队通常由独立的开发、质量保证(QA)和运维团队组成。每个团队专注于应用开发过程中的不同方面。

IT 运维IT operations是指由 IT 人员为内部或外部客户提供并由他们使用以提供企业技术需求的所有流程和服务。运维工作可以包括响应为维护工作或客户问题生成的票证。1

由于涉及三个团队,通常具有不同的管理-报告结构,因此需要一个详细的手递过程和对应用程序更改的彻底文档记录,以确保应用程序得到充分的测试,对基础设施进行适当的更改,并且应用程序被正确安装。然而,这些要求会导致部署时间延长,并减少可以进行的部署频率。此外,在每个团队之间的过渡中,关键细节可能无法得到有效沟通的可能性增加,可能导致测试中的差距或部署错误。

图片

图 1.2 在传统的部署流程中,开发团队为 QA 团队打开一个工单以测试新产品版本。当测试成功时,QA 团队为运维团队打开一个工单以将最新版本部署到生产环境。

幸运的是,大多数开发团队通过使用自动化构建系统和称为持续集成(CI)的过程来编译、测试和生成可部署的工件。但新代码的部署通常是由运维团队手动执行的,涉及漫长的手动程序或通过部署脚本的局部自动化。在最坏的情况下,运维工程师手动将可执行二进制文件复制到多个服务器上所需的位置,并手动重新启动应用程序以使新二进制版本生效。这个过程容易出错,并且很少提供如审查、批准、可审计性和回滚等控制选项。

持续集成(CI)CI涉及软件应用程序的自动化构建、测试和打包。在典型的发展工作流程中,软件工程师将代码更改提交到中央代码仓库。这些更改必须经过测试并与旨在部署到生产的主体代码分支集成。CI 系统简化了代码的审查、构建和测试,以确保在合并到主分支之前其质量。

随着云计算基础设施的兴起,管理和控制计算和网络资源的接口越来越多地基于应用程序编程接口(APIs),这使得自动化程度更高,但同时也需要更多的编程技能来实现。这一事实,加上许多组织寻求优化运营、缩短部署时间、增加部署频率以及提高其计算系统的可靠性、稳定性和性能的方法,导致了新的行业趋势:DevOps。

1.1.2 DevOps

DevOps 既是组织结构,也是一种注重自动化的思维模式变革。运维团队不再负责部署和运营;应用的开发团队承担这些责任。

devops DevOps 是一套软件开发实践,它将软件开发(Dev)和 IT 运营(Ops)相结合,以缩短系统开发周期,同时频繁地、与业务目标紧密一致地交付功能、修复和更新.^(2)

图 1.3 展示了在传统的运维模型中,组织是如何通过功能边界进行划分的,有不同团队负责开发、质量和运维。在 DevOps 模型中,团队是根据产品或组件进行划分的,并且是跨学科的,包含具有所有功能技能集的团队成员。尽管图 1.3 指出了具有特定角色的团队成员,但在实践 DevOps 的高效能团队中,所有成员都跨功能贡献;每个成员都能够编码、测试、部署和运营他们的产品或组件。

图 1.3 传统的组织模型有独立的团队负责开发、质量和运维。DevOps 组织模型允许以特定产品或组件为中心的跨学科团队。每个 DevOps 团队都是自给自足的,包含具有开发、测试和部署其应用程序技能的成员。

DevOps 的好处包括

  • 开发和运维之间更好的协作

  • 提高产品质量

  • 更频繁的发布

  • 新功能上市时间的缩短

  • 设计、开发和运维成本的降低

案例研究:Netflix

Netflix 是 DevOps 流程的早期采用者之一,每位工程师都负责其功能的编码、测试、部署和支持。Netflix 的文化推崇“自由与责任”,这意味着每位工程师都可以独立发布,但必须确保该发布的正确运行。所有部署流程都是完全自动化的,因此工程师可以一键部署和回滚。所有新功能在功能完成时立即交付给最终用户。

1.1.3 GitOps

术语 GitOps 于 2017 年 8 月由 Weaveworks 的联合创始人兼首席执行官 Alexis Richardson 在一系列博客文章中提出.^(3) 从那时起,该术语在云原生社区以及 Kubernetes 社区中获得了显著的认知。GitOps 是一种以 Git 为特色的 DevOps 流程,其特点为

  • 部署、管理和监控容器化应用程序的最佳实践

  • 以开发者为中心的应用管理体验,使用 Git 进行开发和运维的完全自动化管道/工作流程

  • 使用 Git 版本控制系统跟踪和批准对应用程序的基础设施和运行时环境的更改

图 1.4 GitOps 发布工作流程从创建包含系统期望状态定义更改的仓库分支开始。

GitHub(以及 GitLab、Bitbucket 等)是现代软件开发生命周期中的核心,因此它也被用于系统操作和管理似乎是自然而然的。

在 GitOps 模型中,系统的期望配置存储在版本控制系统(如 Git)中。工程师不是直接通过 UI 或 CLI 对系统进行更改,而是更改表示期望状态的配置文件。Git 中存储的期望状态与系统实际状态之间的差异表明并非所有更改都已部署。这些更改可以通过标准版本控制流程(如拉取请求、代码审查和合并到主分支)进行审查和批准。当更改被批准并合并到主分支后,操作软件进程负责根据 Git 中存储的配置将系统的当前状态更改为期望状态。

在 GitOps 的理想实现中,不允许手动更改系统,所有对配置的更改都必须是对存储在 Git 中的文件的更改。在极端情况下,更改系统的权限仅授予操作软件进程。在 GitOps 模型中,基础设施和运维工程师的角色从执行基础设施更改和应用部署转变为开发和维护 GitOps 自动化,并帮助团队使用 Git 审查和批准更改。

Git 具有许多功能和技术能力,使其成为 GitOps 的理想选择:

  • Git 存储每个提交。通过适当的访问控制和安全配置(在第六章中介绍),所有更改都是可审计的和防篡改的。

  • Git 中的每个提交都代表了一个系统在该时间点的完整配置。

  • Git 中的每个提交对象都与它的父提交相关联,这样在创建和合并分支时,提交历史在需要时是可用的。

注意 GitOps 的重要性,因为它使对环境所做的更改具有可追溯性,并使使用大多数开发者已经熟悉的 Git 工具轻松回滚、恢复和自我修复变得容易。

Git 为验证和审计部署提供了基础。尽管可能可以使用除 Git 之外的版本控制系统实现 GitOps,但 Git 的分布式特性、分支和合并策略以及广泛的应用使其成为理想选择。

GitOps 不需要特定的工具集,但工具必须提供以下标准功能:

  • 在 Git 中操作存储的系统期望状态

  • 检测期望状态与实际状态之间的差异

  • 对基础设施执行必要的操作,以同步实际状态与期望状态

尽管这本书主要关注 Kubernetes 中的 GitOps,但 GitOps 的许多原则可以独立于 Kubernetes 实现。

1.2 GitOps 的开发者好处

GitOps 为开发者提供了许多好处,因为它允许他们以与他们的软件开发过程管理方式相同的方式处理基础设施配置和代码部署,并且使用一个熟悉的工具:Git。

1.2.1 基础设施即代码

基础设施 即代码 (IaC) 是 GitOps 的基础范式。运行您应用程序的基础设施配置是通过执行自动化流程而不是手动步骤来完成的。4 在实践中,IaC 意味着基础设施更改被编码化,并且基础设施的源代码存储在版本控制系统之中。让我们来看看最显著的好处:

  • 可重复性——所有有手动配置基础设施经验的都知道这个过程既耗时又容易出错。不要忘记,同样的过程通常需要重复多次,因为应用程序通常部署到多个环境中。如果发现问题,使用可重复的过程回滚到早期的工作配置会更简单,从而允许更快的恢复。

  • 可靠性——自动化流程显著减少了不可避免的人类错误的可能性,从而降低了故障的可能性。当过程被编码化时,基础设施质量不再依赖于执行部署的特定工程师的知识和技能。基础设施配置的自动化可以稳步改进。

  • 效率——IaC 提高了团队的生产力。使用 IaC,工程师更有效率,因为他们使用熟悉的工具,如 API、软件开发工具包 (SDK)、版本控制系统和文本编辑器。工程师可以使用熟悉的过程并利用代码审查和自动化测试。

  • 节省——IaC 的初始实施需要大量的努力和时间投资。然而,尽管初始成本较高,但从长远来看,它更经济。为下一个环境提供基础设施不需要浪费宝贵的工程师时间进行手动配置。因为提供是快速且便宜的,所以没有必要保持未使用的环境运行。相反,每个环境可能根据需要创建,并在不再需要时销毁。

  • 可见性——当你定义 IaC 时,代码本身记录了基础设施应该如何看起来。

IaC 使开发者能够在节省时间和金钱的同时生产出高质量的软件。对于单一环境来说,手动配置基础设施可能更容易,但随着需要维护的应用程序环境数量不断增加,这将变得越来越具有挑战性。使用自动化的基础设施配置和遵循 IaC 原则可以实现可重复部署,并防止由配置漂移或缺失依赖项引起的运行时问题。

1.2.2 自助服务

如前所述,在传统的运维模型中,基础设施管理由专门的团队或公司内的独立组织执行。

然而,这种方法存在一个问题:它无法扩展。无论团队有多少成员,专用团队都会很快成为瓶颈。应用程序开发者不能自己进行基础设施更改,他们必须提交工单、发送电子邮件、安排会议并等待。无论过程如何,都会存在一个障碍,导致许多延迟并阻碍团队主动提出基础设施更改。GitOps 旨在通过自动化流程和实现自助服务来打破这个障碍。

在使用 GitOps 模型时,开发者独立工作,并在存储库中对基础设施的声明性配置提交更改,而不是发送工单。基础设施的更改不再需要跨团队沟通,这使得应用程序开发团队能够更快地前进,并拥有更多的自由进行实验。快速独立地做出基础设施更改的能力鼓励开发者承担其应用程序基础设施的责任。开发者不必向中央运维团队寻求解决方案,他们可以进行实验并开发出能够高效解决业务需求的设计。

图片

图 1.5 开发团队可以通过更新存储在 Git 存储库中的文件来更改系统的期望状态。这些更改将由其他团队成员进行代码审查,并在批准后合并到主分支。主分支由 GitOps 操作员处理,以部署集群的期望配置。

开发者不能完全控制所欲为,这可能会影响安全性或可靠性。每个更改都需要创建一个可以由应用程序开发团队的其他成员审查的拉取请求,如下文所述。

GitOps 的优势在于它允许自助服务基础设施更改,并在控制与开发速度之间提供了正确的平衡。

1.2.3 代码审查

代码审查 是一种软件开发实践,其中代码更改由第二双眼睛主动检查错误或遗漏,从而导致可预防的中断更少。执行代码审查是软件开发生命周期中的一个自然过程,DevOps/GitOps 的软件工程师应该熟悉。当 DevOps 工程师可以将基础设施视为代码时,逻辑上的下一步就是在部署之前对基础设施更改进行代码审查。当与 Kubernetes 一起使用 GitOps 时,“代码”审查可能主要是 Kubernetes YAML 清单或其他声明性配置文件,而不是用编程语言编写的传统代码。

除了错误预防外,代码审查还提供以下好处:

  • 教学和知识分享—在审查更改时,审查者不仅有机会提供反馈,还可以学习到一些东西。

  • 设计和实现的一致性—在审查过程中,团队可以确保更改与整体代码结构保持一致,并遵循公司的代码风格指南。

  • 团队凝聚力—代码审查不仅用于批评和请求更改。这个过程也是团队成员互相表扬、增进关系并确保每个人都充分参与的一个绝佳方式。

在适当的代码审查过程中,只有经过验证和批准的基础设施更改才会提交到主分支,从而防止错误和操作环境的错误修改。代码审查不一定完全由人类完成。代码审查过程还可以运行自动化工具,如代码检查器、^(5) 静态代码分析和安全工具。

注意:其他用于代码和漏洞分析的自动化工具在第四章中介绍。

代码审查长期以来一直被视为软件开发最佳实践的 critical 部分。GitOps 的关键前提是,应用于应用程序代码的代码审查的严谨性应该应用于应用程序操作环境中的更改。

1.2.4 Git 拉取请求

Git 版本控制系统提供了一种机制,可以将提议的更改提交到分支或分叉,然后通过拉取请求与主分支合并。2005 年,Git 引入了 request-pull 命令。此命令生成所有更改的易读摘要,可以手动邮寄给项目维护者。拉取请求收集存储库文件的所有更改,并展示代码审查和批准的差异。

可以使用拉取请求来强制执行预合并代码审查。可以实施控制措施,要求在拉取请求合并到主分支之前进行特定的测试或批准。与代码审查一样,拉取请求是软件开发生命周期中的一个熟悉的过程,软件工程师可能已经使用。

图 1.6 展示了典型的拉取请求生命周期:

  1. 开发者创建一个新的分支并开始对更改进行工作。

  2. 当更改准备就绪时,开发者会发送一个代码审查的拉取请求。

  3. 团队成员审查拉取请求,并根据需要要求更多更改。

  4. 开发者在分支中持续进行更改,直到拉取请求获得批准。

  5. 项目维护者将拉取请求合并到主分支。

  6. 合并后,用于拉取请求的分支可以被删除。

图片

图 1.6 拉取请求生命周期允许进行多轮代码审查和修订,直到更改获得批准。然后,更改可以合并到主分支,并删除拉取请求分支。

当应用于基础设施更改审查时,审查步骤尤其有趣。在创建拉取请求后,项目维护者会收到通知并审查提议的更改。因此,审查者会提出问题,接收答案,并可能要求更多更改。这些信息通常会被存储并可供将来参考,因此现在拉取请求是基础设施更改的实时文档。在发生事件的情况下,可以轻松地找出谁进行了更改以及为什么应用了该更改。

1.3 GitOps 的操作优势

将 GitOps 方法与 Kubernetes 的声明式配置和主动协调模型相结合,提供了许多操作优势,这些优势提供了更可预测和可靠的系统。

1.3.1 声明式

从 DevOps 运动中涌现出的最突出的范例之一是声明式系统和配置模型。简单来说,使用声明式模型,你描述的是想要实现什么,而不是如何实现。相比之下,在命令式模型中,你描述的是一系列指令,用于操纵系统以达到你希望的状态。

为了说明这种差异,想象两种电视遥控器的样式:一种命令式样式和一种声明式样式。两种遥控器都可以控制电视的电源、音量和频道。为了讨论方便,假设电视只有三个音量设置(响亮、柔和、静音)和三个频道(1、2、3)。

图片

图 1.7 此图说明了命令式和声明式远程控制之间的差异。命令式远程控制允许你执行“增加频道 1”和“切换电源状态”等操作。相比之下,声明式远程控制允许你执行“调谐到频道 2”或“将电源状态设置为关闭”等操作。

命令式远程示例

假设你有一个简单的任务,即使用两个远程端同时切换到频道 3。要使用命令式远程端完成这个任务,你会使用频道上调按钮,该按钮向电视发送信号,使其当前频道增加 1。要达到频道 3,你需要连续按几次频道上调按钮,直到电视达到所需的频道。

声明式远程示例

相比之下,声明式遥控器提供了可以直接跳转到特定数字频道的独立按钮。在这种情况下,要切换到频道 3,你只需按一次频道-3 按钮,电视就会调到正确的频道。你是在声明你想要达到的最终状态(我想电视调到频道 3)。而在命令式遥控器上,你描述了需要执行的动作以达到你想要的状态(一直按频道上调按钮,直到电视调到频道 3)。

你可能已经注意到,在命令式更改频道的方法中,用户必须考虑是否继续按频道上调按钮,这取决于电视当前调谐的频道。然而,在声明式方法中,你可以毫不犹豫地按下频道-3 按钮,因为声明式遥控器上的这个按钮被认为是幂等的(而命令式遥控器上的频道上调按钮则不是)。

幂等性(Idempotency)是操作的一个属性,即操作可以执行任意多次并产生相同的结果。换句话说,如果一个操作可以任意多次执行,并且系统状态与只执行一次操作时的状态相同,那么这个操作就是幂等的。幂等性是区分声明式系统和命令式系统属性之一。声明式系统是幂等的;命令式系统则不是。

1.3.2 可观察性

可观察性是检查和描述系统当前运行状态的能力,并在出现意外条件时发出警报。部署的环境预期应该是可观察的。换句话说,你应该始终能够检查环境,以查看当前正在运行的内容以及配置情况。为此,服务和云提供商提供了一系列方法来促进可观察性(包括 CLIs、APIs、GUIs、仪表板、警报和通知),使用户尽可能方便地了解环境的当前状态。

图片

图 1.8 可观察性是操作员(可能是人或自动化)确定环境运行状态的能力。如果已知环境的当前运行状态,操作员可以仅一次就做出关于需要改变环境哪些方面的明智决策。适当控制环境需要对该环境进行可观察性。

虽然这些可观察性机制可以帮助回答“我的环境中当前正在运行什么?”的问题,但它们不能回答“对于我环境中当前配置和运行的资源,它们应该以这种方式配置和运行吗?”的问题。如果您曾经担任过系统管理员或操作员的职责,您可能非常熟悉这个问题。在某个时候——通常是当调试环境时——您会遇到一个可疑的配置设置,并认为它似乎不正确。有人(可能是您自己)意外或错误地更改了这个设置,还是这个设置是有意的?

很可能,您已经在实践中遵循了 GitOps 的一个基本原则:将应用程序配置的副本存储在源代码控制中,并使用它作为应用程序期望状态的真相来源。您可能并不是为了驱动持续部署而将此配置存储在 Git 中——只是为了确保环境可以被复制,例如在灾难恢复场景中。这个副本可以被视为期望的应用程序状态,除了灾难恢复用例之外,它还服务于另一个有用的目的:它使操作员能够在任何时间点将实际运行状态与源代码控制中保存的期望状态进行比较,以验证状态是否匹配。

图片

图 1.9 如果可以观察到环境的运行状态,并且环境的期望状态在 Git 中定义,则可以通过比较这两个状态来验证环境。

验证您环境的能力是 GitOps 的一个核心原则,它已被正式化为一种实践。通过将期望状态存储在一个系统中(例如 Git),并定期将该期望状态与运行状态进行比较,您解锁了新的可观察性维度。您不仅拥有提供商提供的标准可观察性机制,而且还能检测到与期望状态的偏差。

与期望状态的偏差,也称为配置漂移,可能由任何数量的原因引起。常见的例子包括操作员犯的错误、由于自动化而产生的意外副作用以及错误场景。配置漂移甚至可能是预期的,例如由过渡期(例如维护模式)引起的临时状态。

但是,配置差异的最重要原因可能是恶意的。在最坏的情况下,一个恶意行为者可能已经破坏了环境并重新配置了系统以运行恶意镜像。因此,可观察性和验证对于系统的安全性至关重要。除非您已经建立了期望状态的真相来源,并且没有验证收敛到该真相来源的机制,否则您无法知道您的环境是否真正安全。

1.3.3 审计性和合规性

对于在受法律和法规影响信息管理和合规性评估框架的国家(在这个时代的大多数国家)开展业务的组织来说,确保合规性和可审计性是必不可少的。一些行业比其他行业监管更严格,但几乎所有公司都需要遵守基本的隐私和数据安全法律。许多组织必须大量投资于其流程和系统,以确保合规性和可审计性。使用 GitOps 和 Kubernetes,大多数合规性和可审计性要求都可以以最小的努力得到满足。

合规性是指验证组织的信息系统是否符合特定的行业标准,通常侧重于客户数据安全和遵守组织关于访问该客户数据的个人和系统的记录政策。第六章深入探讨了访问控制,第四章涵盖了管道来定义和执行您的合规性部署流程。

可审计性是指系统被验证为符合一系列标准的能力。如果一个系统无法向内部或外部审计员展示其符合性,则不能对该系统的符合性做出任何声明。第八章涵盖了可观察性,包括使用 Git 提交历史和 Kubernetes 事件来实现可审计性。

案例研究:Facebook 和剑桥分析公司

剑桥分析公司,一家受特朗普 2016 年竞选活动总统聘请的政治数据公司,未经许可获取了超过 5000 万 Facebook 用户的私人信息。这些数据被用来为每个用户生成一个性格评分,并将其与美国的选民记录相匹配。剑桥分析公司利用这些信息为其选民画像和定向广告服务。Facebook 被发现没有实施必要的控制措施来执行数据隐私,最终因违规行为被联邦贸易委员会罚款 50 亿美元。^a

^a www.ftc.gov/news-events/press-releases/2019/07/ftc-imposes-5-billion-penalty-sweeping-new-privacy-restrictions

可审计性也指审计员能够全面审查组织内部控制的程度。在典型的审计中,审计员会要求提供证据以确保规则和政策得到相应执行。证据可能包括限制用户数据访问的过程、处理个人可识别信息(PII)的方式以及软件发布流程的完整性。

图 1.10

图 1.10 在传统的审计过程中,往往很难确定系统的期望状态。审计员可能需要查看各种信息来源,包括文档、变更请求和部署脚本。

案例研究:支付卡行业数据安全标准

信用卡行业数据安全标准(PCI DSS)是为处理主要信用卡网络品牌信用卡的组织制定的信息安全标准。违反 PCI DSS 可能会导致高额罚款,在最坏的情况下,甚至可能被暂停信用卡处理。PCI DSS 规定,“访问控制系统配置为根据工作分类和职能分配给个人的权限进行权限管理。”在审计期间,组织需要提供证据证明已实施访问控制系统以符合 PCI 标准.^a

^a en.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard

所有这些与 GitOps 有什么关系?

Git 是一种版本控制软件,帮助组织管理代码的更改和访问控制。Git 通过一种特殊类型的数据库跟踪代码的每一次修改,该数据库旨在保护受管理源代码的完整性。Git 仓库中文件的 内容以及文件和目录之间、版本、标签和提交之间的真实关系都通过安全哈希算法(SHA)校验和散列算法得到保护。此算法保护代码和更改历史,防止意外和恶意更改,并确保历史记录可完全追溯。

Git 的历史跟踪还包括每个更改的作者、日期和关于更改目的的书面注释。通过良好的提交注释,你可以知道特定提交的 原因。Git 还可以与项目管理软件和缺陷跟踪软件集成,允许对所有更改进行完全追溯,并实现根本原因分析和其他法医分析。

如前所述,Git 支持拉取请求机制,这防止任何单一个人在没有第二个人批准的情况下更改系统。当拉取请求被批准时,更改会被记录在安全的 Git 更改历史中。Git 在变更控制、可追溯性和变更历史真实性方面的优势,加上 Kubernetes 的声明式配置,自然满足了审计性和合规性所需的 安全、可用性和处理完整性原则。

图 1.11 在 GitOps 中,审计过程可以简化,因为审计员可以通过检查源代码仓库来确定系统的期望状态。系统的当前状态可以通过审查托管服务和 Kubernetes 对象来确定。

1.3.4 灾难恢复

灾难可能由许多原因和多种形式引起。灾难可能是自然发生的(地震袭击数据中心),由设备故障引起(存储阵列中硬盘驱动器的丢失),意外发生(软件错误损坏关键数据库表),甚至可能是恶意的(网络攻击导致数据丢失)。

GitOps 通过将环境的声明性规范存储在源控制中作为真相来源,有助于恢复基础设施环境。拥有环境应具备的完整定义,便于在灾难发生时重新创建环境。灾难恢复变成了一种简单的练习,即(重新)应用存储在 Git 仓库中的所有配置。您可能会观察到,在灾难期间遵循的程序与日常升级和部署中使用的程序之间没有太大的区别。使用 GitOps,您实际上是在定期练习灾难恢复程序,这使得您在真正发生灾难时做好了充分的准备。

数据备份的重要性 虽然 GitOps 有助于简化计算和网络基础设施的灾难恢复,但持久性和有状态应用程序的恢复需要不同的处理方式。对于与存储相关的基础设施,没有传统的灾难恢复解决方案可以替代:备份、快照和复制。

摘要

  • GitOps 是一种使用 Git 作为记录系统的 DevOps 部署流程,用于管理复杂系统中的部署。

  • 传统运维需要单独的团队进行部署,新版本可能需要几天(如果不是几周)才能部署完成。

  • DevOps 使工程师能够在代码完成后立即部署新版本,而无需等待集中的运维团队。

  • GitOps 提供完整的可追溯性和发布控制。

  • 声明性模型描述了您想要实现的目标,而不是实现它的必要步骤。

  • 一致性是操作的一个属性,即操作可以执行任意次数并产生相同的结果。

  • GitOps 的其他好处包括

    • 代码质量和发布控制的拉取请求

    • 可观察的运行状态和期望状态

    • 简化的合规性和可审计性流程,具有历史真实性和可追溯性

    • 简单的灾难恢复和回滚程序,与熟悉的部署体验保持一致


1.en.wikipedia.org/wiki/Data_center_management#Operations.

2.en.wikipedia.org/wiki/DevOps.

3.www.weave.works/blog/gitops-operations-by-pull-request.

4.www.hashicorp.com/resources/what-is-infrastructure-as-code.

5.en.wikipedia.org/wiki/Lint_(software).

2 Kubernetes 和 GitOps

本章涵盖了

  • 使用 Kubernetes 解决问题

  • 在本地运行和管理 Kubernetes

  • 理解 GitOps 的基础知识

  • 实现一个简单的 Kubernetes GitOps 操作员

在第一章中,你学习了 Kubernetes 以及为什么它的声明式模型使其非常适合使用 GitOps 进行管理。本章将简要介绍 Kubernetes 架构和对象,以及声明式和命令式对象管理的区别。在本章结束时,你将实现一个基本的 GitOps Kubernetes 部署操作员。

2.1 Kubernetes 简介

在深入探讨为什么 Kubernetes 和 GitOps 能够如此良好地协同工作之前,让我们先谈谈 Kubernetes 本身。本节提供了 Kubernetes 的高级概述,包括它与其他容器编排系统的比较以及其架构。我们还将有一个练习,演示如何在本地运行 Kubernetes,这将被用于本书中的其他练习。本节仅是对 Kubernetes 的简要介绍和复习。如果你想有趣且信息丰富地了解 Kubernetes,可以查看云原生计算基金会出版的“儿童插图 Kubernetes 指南”和“Phippy 去动物园”^1。如果你对 Kubernetes 完全陌生,我们建议阅读 Marko Lukša(Manning,2020 年)的《Kubernetes 动作,第二版》,然后返回本书。如果你已经熟悉 Kubernetes 并运行 minikube,你可以跳到 2.1 节末的练习。

2.1.1 什么是 Kubernetes?

Kubernetes 是一个于 2014 年发布的开源容器编排系统。好的,但什么是容器,为什么你需要对它们进行编排?

容器提供了一种标准方式来打包你的应用程序代码、配置和依赖项到一个单一的资源中。这使得开发者可以确保应用程序无论在任何其他机器上都能正常运行,无论该机器可能有什么定制设置,这些设置可能与编写和测试代码所使用的机器不同。Docker 简化和普及了容器化,现在它被认为是一种用于构建分布式系统的基本技术。

chroot 是 UNIX 操作系统中可用的一种操作,它更改当前运行进程及其子进程的可见根目录。Chroot 提供了一种将进程及其子进程从系统其余部分隔离的方法。它是容器化的前身,也是 Docker 的基础。2

虽然 Docker 解决了单个应用程序的打包和隔离问题,但关于如何编排多个应用程序的操作以形成一个工作分布式系统的问题仍然很多:

  • 容器是如何进行通信的?

  • 容器之间的流量是如何路由的?

  • 容器是如何进行扩展以处理额外的应用负载的?

  • 集群的底层基础设施是如何进行扩展以运行所需的容器的?

所有这些操作都是容器编排系统的责任,并由 Kubernetes 提供。Kubernetes 帮助自动化使用容器部署、扩展和管理应用程序。

注意:Borg 是 Google 的内部容器集群管理系统,用于支持 Google 搜索、Gmail 和 YouTube 等在线服务。Kubernetes 利用 Borg 的创新和经验教训,解释了为什么它比竞争对手更稳定,发展得更快。3

Kubernetes 是基于 Google 使用其专有集群管理系统 Borg 进行容器编排的十年经验而开发和开源的。因此,对于如此复杂的系统来说,Kubernetes 相对稳定且成熟。由于其开放的 API 和可扩展的架构,Kubernetes 周围已经发展出一个庞大的社区,这进一步推动了其成功。它是 GitHub 上评分最高的项目之一(按星级衡量),提供了优秀的文档,并拥有庞大的 Slack 和 Stack Overflow 社区。社区成员的无尽博客和演示文稿分享了他们使用 Kubernetes 的知识。尽管 Kubernetes 是由 Google 启动的,但它不受单一供应商的影响。这使得社区开放、协作且具有创新性。

2.1.2 其他容器编排器

自 2016 年末以来,Kubernetes 已经成为业界公认的、事实上的行业标准容器编排系统,与 Docker 成为容器标准的方式相似。然而,有几个 Kubernetes 的替代方案解决了与 Kubernetes 相同的容器编排问题。Docker Swarm 是 Docker 在 2015 年发布的原生容器编排引擎,它与 Docker API 紧密集成,并使用基于 YAML 的部署模型 Docker Compose。Apache Mesos 于 2016 年正式发布(尽管在此之前已有很长时间的历史),支持大型集群,可扩展到数千个节点。

尽管将 GitOps 方法应用于使用其他容器编排系统部署应用程序可能是可行的,但本书的重点是 Kubernetes。

2.1.3 Kubernetes 架构

在本章结束时,你将完成一个练习,实现一个基本的 GitOps 持续部署操作符用于 Kubernetes。但要理解 GitOps 操作符是如何工作的,首先理解一些 Kubernetes 核心概念以及它在高层次上的组织方式是至关重要的。

Kubernetes 是一个庞大且健壮的系统,具有许多不同类型的资源和可以在这些资源上执行的操作。Kubernetes 在基础设施之上提供了一层抽象,并引入了以下一组基本对象,它们代表了所需的集群状态:

  • Pod—在同一主机上一起部署的一组容器。Pod 是节点上可部署的最小单元,提供挂载存储、设置环境变量和提供其他容器配置信息的方式。当一个 Pod 的所有容器退出时,该 Pod 也会死亡。

  • Service—定义了一组逻辑 Pod 及其访问策略的抽象。

  • Volume—Pod 中运行的容器可访问的目录。

Kubernetes 架构使用主要资源作为一组更高层次资源的基础层。更高层次资源实现了针对实际生产用例所需的功能,这些用例利用/扩展了主要资源的功能。在图 2.1 中,您可以看到 ReplicaSet 资源控制一个或多个 Pod 资源的创建。其他一些高级资源的例子包括

图 2.1 该图展示了在命名空间中部署的典型 Kubernetes 环境。ReplicaSet 是管理 Pod 生命周期的更高层次资源的一个例子,Pod 是更低层次、主要资源。

  • ReplicaSet—定义了所需数量的配置相同的 Pod 正在运行。如果 ReplicaSet 中的 Pod 终止,将启动一个新的 Pod,以将运行 Pod 的数量恢复到所需数量。

  • Deployment—为 Pods 和 ReplicaSets 启用声明式更新。

  • Job—创建一个或多个运行至完成的 Pod。

  • CronJob—基于时间表创建作业。

另一个重要的 Kubernetes 资源是命名空间。大多数类型的 Kubernetes 资源属于一个(且仅一个)命名空间。命名空间定义了一个命名范围,其中特定命名空间内的资源必须具有唯一名称。命名空间还提供了一种通过基于角色的访问控制(RBAC)、网络策略和资源配额来隔离用户和应用程序的方法。这些控制允许创建一个多租户 Kubernetes 集群,其中多个用户共享同一个集群,避免相互影响(例如,“嘈杂邻居”问题)。正如我们在第三章中将要看到的,命名空间在 GitOps 中对于定义应用程序环境也是必不可少的。

Kubernetes 对象存储在控制平面中,^(4) 它监控集群状态,进行更改,调度工作,并对事件做出响应。为了执行这些任务,每个 Kubernetes 控制平面运行以下三个进程:

  • kube-apiserver—集群的入口点,提供 REST API 以评估和更新所需的集群状态

  • kube-controller-manager—守护进程通过 API 服务器持续监控集群的共享状态,尝试将当前状态移动到所需状态以进行更改

  • kube-scheduler—一个负责在集群中可用的节点间调度工作负载的组件

  • etcd—一个高度可用的键值数据库,通常用作 Kubernetes 所有集群配置数据的后端存储

图 2.2 Kubernetes 集群由运行在控制平面主节点上的多个服务以及运行在集群工作节点上的多个其他服务组成。这些服务共同提供了构成 Kubernetes 集群的必要服务。

实际的集群工作负载使用 Kubernetes 节点的计算资源运行。节点是一个工作机器(无论是虚拟机还是物理机),它运行必要的软件以允许它被集群管理。类似于主节点,每个节点运行一组预定义的进程:

  • kubelet——管理节点上实际容器的“节点代理”

  • kube-proxy——一个网络代理,在每个节点上反映 Kubernetes API 中定义的服务,并且可以进行简单的 TCP、UDP 和 SCTP 流转发

2.1.4 部署到 Kubernetes

在这个练习中,你将使用 NGINX 在 Kubernetes 上部署一个网站。你将回顾一些基本的 Kubernetes 操作,并熟悉 minikube,这是你将在本书的大部分练习中使用的单节点 Kubernetes 环境。

Kubernetes 测试环境:minikube 请参考附录 A,使用 minikube 设置 Kubernetes 测试环境以完成此练习。

创建一个 Pod

正如本章前面提到的,Pod 是 Kubernetes 中最小的对象,代表特定的应用程序工作负载。Pod 代表在相同主机上运行并具有相同操作要求的容器组。单个 Pod 的所有容器共享相同的网络地址、端口空间和(可选)文件系统,这是通过 Kubernetes 卷实现的。

NGINX NGINX 是一个开源软件 Web 服务器,许多组织和企业使用它来托管他们的网站,因为它具有高性能和稳定性。

在这个练习中,你将创建一个 Pod,使用 NGINX 托管网站。在 Kubernetes 中,可以通过 YAML 文本文件“清单”定义对象,该文件提供了 Kubernetes 创建和管理对象所需的所有信息。以下是我们的 NGINX Pod 清单的列表。

列表 2.1 NGINX Pod 清单 (http://mng.bz/e5JJ)

kind: Pod                              ❶
apiVersion: v1
metadata:                              ❷
  name: nginx
spec:                                  ❸
  restartPolicy: Always
  volumes:                             ❹
    - name: data
      emptyDir: {}
  initContainers:
  - name: nginx-init                   ❺
    image: docker/whalesay
    command: [sh, -c]
    args: [echo "<pre>$(cowsay -b 'Hello Kubernetes')</pre>" > /data/index.html]
    volumeMounts:                        
    - name: data
      mountPath: /data
  containers:
  - name: nginx                        ❻
    image: nginx:1.11
    volumeMounts:
    - name: data
      mountPath: /usr/share/nginx/html

❶ 字段 kind 和 apiVersion 存在于每个 Kubernetes 资源中,并确定应该创建什么类型的对象以及如何处理它。

❷ 在这个例子中,元数据有一个名称字段,有助于识别每个 Kubernetes 资源。元数据还可能包含 UID、标签和其他将在以后介绍的字段。

❸ 规范部分包含特定于特定对象的配置。在 Pod 示例中,规范包括容器列表、容器之间共享的卷以及 Pod 的重启策略。

❹ 用于在容器之间共享数据的卷

❺ 初始化部分包含使用 cowsay^(5) 命令生成的 HTML。

❻ 主要容器,用于通过 NGINX 服务器提供生成的 HTML 文件

你可以输入此列表并将其保存为 nginx-Pod.yaml 文件名。然而,由于本书的目标不是提高你的打字技巧,我们建议克隆第一章中提到的公共 Git 仓库,该仓库包含本书中的所有列表,并直接使用这些文件:

github.com/gitopsbook/resources

让我们继续启动 minikube 集群并使用以下命令创建 NGINX Pod:

$ minikube start
(minikube/default)
😁  minikube v1.1.1 on darwin (amd64)
🔥  Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
🐳  Configuring environment for Kubernetes v1.14.3 on Docker 18.09.6
🚜  Pulling images ...
🚀  Launching Kubernetes ...
⏳   Verifying: apiserver proxy etcd scheduler controller dns
🏄  Done! kubectl is now configured to use "minikube"
$ kubectl create -f nginx-Pod.yaml
Pod/nginx created

图 2.3 显示了 Pod 在 minikube 内部运行的外观。

图 2.3 nginx-init 容器将所需的 index.html 文件写入挂载的卷。主要的 NGINX 容器也挂载了卷,并在接收到 HTTP 请求时显示生成的 index.html。

获取 Pod 状态

一旦 Pod 创建完成,Kubernetes 会检查spec字段,并尝试在集群中适当节点上运行配置的容器集。关于进度的信息可以在 Pod 的status字段中找到。kubectl 实用工具提供了多个命令来访问它。让我们尝试使用kubectl get Pods命令来获取 Pod 状态:

$ kubectl get Pods
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          36s

get Pods命令提供了特定命名空间中所有 Pod 的列表。在这种情况下,我们没有指定命名空间,因此它给出了在默认命名空间中运行的 Pod 列表。假设一切顺利,NGINX Pod 应该处于Running状态。

要了解 Pod 状态更多或调试 Pod 为何不在Running状态的原因,可以使用kubectl describe Pod命令输出详细信息,包括相关的 Kubernetes 事件:

$ kubectl describe Pod nginx
Name:         nginx
Namespace:    default
Priority:     0
Node:         minikube/192.168.99.101
Start Time:   Sat, 26 Oct 2019 21:58:43 -0700
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:

{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"nginx","Namespace":"default"},"spec":{"containers":[{"image":"nginx:1...
Status:       Running
IP:           172.17.0.4
Init Containers:
  nginx-init:
    Container ID:  docker://128c98e40bd6b840313f05435c7590df0eacfc6ce989ec15cb7b484dc60d9bca
    Image:         docker/whalesay
    Image ID:      docker-pullable://docker/whalesay@sha256:178598e51a26abbc958b8a2e48825c90bc22e641de3d31e18aaf55f3258ba93b
    Port:          <none>
    Host Port:     <none>
    Command:
      sh
      -c
    Args:
      echo "<pre>$(cowsay -b 'Hello Kubernetes')</pre>" > /data/index.html
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Sat, 26 Oct 2019 21:58:45 -0700
      Finished:     Sat, 26 Oct 2019 21:58:45 -0700
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /data from data (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-vbhsd (ro)
Containers:
  nginx:
    Container ID:   docker://071dd946709580003b728cef12a5d185660d929ebfeb84816dd060167853e245
    Image:          nginx:1.11
    Image ID:       docker-pullable://nginx@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Sat, 26 Oct 2019 21:58:46 -0700
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /usr/share/nginx/html from data (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-vbhsd (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  data:
    Type:       EmptyDir (a temporary directory that shares a Pod's lifetime)
    Medium:
    SizeLimit:  <unset>
  default-token-vbhsd:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-vbhsd
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  37m   default-scheduler  Successfully assigned default/nginx to minikube
  Normal  Pulling    37m   kubelet, minikube  Pulling image "docker/whalesay"
  Normal  Pulled     37m   kubelet, minikube  Successfully pulled image "docker/whalesay"
  Normal  Created    37m   kubelet, minikube  Created container nginx-init
  Normal  Started    37m   kubelet, minikube  Started container nginx-init
  Normal  Pulled     37m   kubelet, minikube  Container image "nginx:1.11" already present on machine
  Normal  Created    37m   kubelet, minikube  Created container nginx
  Normal  Started    37m   kubelet, minikube  Started container nginx

通常,事件部分将包含有关 Pod 为何不在Running状态的原因的线索。

最全面的信息可以通过kubectl get Pod nginx -o=yaml获取,它以 YAML 格式输出对象的完整内部表示。原始的 YAML 输出难以阅读,通常是为了程序化访问资源控制器。Kubernetes 资源控制器将在本章后面更详细地介绍。

访问 Pod

当 Pod 处于Running状态时,意味着所有容器都已成功启动,NGINX Pod 已准备好处理请求。如果我们的集群中的 NGINX Pod 正在运行,我们可以尝试访问它并证明它正在工作。

默认情况下,Pod 无法从集群外部访问。有多种配置外部访问的方式,包括 Kubernetes 服务、Ingress 等。为了简化,我们将使用kubectl port-forward命令,该命令将本地端口上的连接转发到 Pod 上的端口:

$ kubectl port-forward nginx 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

保持kubectl port-forward命令运行,并在浏览器中尝试打开 http://localhost:8080/。你应该会看到生成的 HTML 文件!

图 2.4 从 docker/whalesay 镜像生成的 HTML 文件内容是一个可爱的鲸鱼 ASCII 渲染,其问候语通过命令参数传递。port-forward命令允许 Pod(HTML)的 80 端口在本地主机的 8080 端口上访问。

练习 2.1

现在您的 NGINX Pod 正在运行,请使用kubectl exec命令在运行中的容器上获取一个 shell。

提示:命令可能类似于kubectl exec -it <POD_NAME> -- /bin/bash。在 shell 中四处探索。运行lsdfps -ef以及其他 Linux 命令。如果你终止 NGINX 进程会发生什么?

在这个练习的最终步骤中,让我们删除 Pod 以释放集群资源。可以使用以下命令删除 Pod:

$ kubectl delete Pod nginx
Pod "nginx" deleted

2.2 声明式与 imperative 对象管理

Kubernetes 的kubectl命令行工具用于创建、更新和管理 Kubernetes 对象,并支持 imperative 命令、imperative 对象配置和声明式对象配置。6 让我们通过一个真实世界的示例来了解 Kubernetes 中 imperative/procedural 配置与声明式配置之间的区别。首先,让我们看看 kubectl 如何被 imperatively 使用。

声明式与 imperative 请参阅第 1.3.1 节,以详细了解声明式与 imperative 的区别。

在以下示例中,让我们创建一个脚本,该脚本将部署一个具有三个副本和一些注释的 NGINX 服务。

列表 2.2 Imperative kubectl 命令 (imperative-deployment.sh)

#!/bin/sh
kubectl create deployment nginx-imperative --image=nginx:latest     ❶
kubectl scale deployment/nginx-imperative --replicas 3              ❷
kubectl annotate deployment/nginx-imperative environment=prod       ❸
kubectl annotate deployment/nginx-imperative organization=sales     ❹

❶ 创建一个名为 nginx-imperative 的新部署对象

❷ 将 nginx-imperative 部署扩展到具有三个 Pod 副本

❸ 在 nginx-imperative 部署中添加了一个带有键环境值 prod 的注释

❹ 在 nginx-imperative 部署中添加了一个带有关键组织和销售价值的注释

尝试在您的 minikube 集群上运行脚本,并检查部署是否成功创建:

$ imperative-deployment.sh
deployment.apps/nginx-imperative created
deployment.apps/nginx-imperative scaled
deployment.apps/nginx-imperative annotated
deployment.apps/nginx-imperative annotated
$ kubectl get deployments
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-imperative   3/3     3            3           27s

太好了!部署已按预期创建。但现在让我们编辑我们的deployment.sh脚本,将organization注释的值从sales更改为marketing,然后重新运行脚本:

$ imperative-deployment-new.sh
Error from server (AlreadyExists): deployments.apps "nginx-imperative" already exists
deployment.apps/nginx-imperative scaled
error: --overwrite is false but found the following declared annotation(s): 'environment' already has a value (prod)
error: --overwrite is false but found the following declared annotation(s): 'organization' already has a value (sales)

如您所见,新脚本失败了,因为部署和注释已经存在。为了使其工作,我们需要增强我们的脚本,添加额外的命令和逻辑来处理更新情况,而不仅仅是创建情况。当然,这可以做到,但结果是我们不必做所有这些工作,因为 kubectl 本身可以检查系统的当前状态,并使用声明式对象配置做正确的事情。

以下清单定义了一个与我们的脚本创建的部署相同的部署(除了部署的名称是nginx-declarative)。

列表 2.3 声明式 (http://mng.bz/OEpP)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-declarative
  annotations:
    environment: prod
    organization: sales
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest

我们可以使用半魔法般的 kubectl apply 命令来创建 nginx-declarative 部署:

$ kubectl apply -f declarative-deployment.yaml
deployment.apps/nginx-declarative created
$ kubectl get deployments
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
nginx-declarative   3/3     3            3           5m29s
nginx-imperative    3/3     3            3           24m

运行 apply 后,我们看到创建了 nginx-declarative 部署资源。但当我们再次运行 kubectl apply 时会发生什么呢?

$ kubectl apply -f declarative-deployment.yaml
deployment.apps/nginx-declarative unchanged

注意输出消息的变化。第二次运行 kubectl apply 时,程序检测到不需要进行任何更改,并随后报告部署未更改。这是 kubectl createkubectl apply 之间的一个微妙但关键的区别。如果资源已存在,kubectl create 将会失败。kubectl apply 命令首先检测资源是否存在,如果对象不存在,则执行创建操作;如果对象已存在,则执行更新操作。

与强制性的示例一样,如果我们想将组织注释的值从销售更改为营销怎么办?让我们编辑 declarative-deployment.yaml 文件,并将 metadata.annotations.organization 字段从 sales 更改为 marketing。但在我们再次运行 kubectl apply 之前,让我们先运行 kubectl diff

$ kubectl diff -f declarative-deployment.yaml
:
-    organization: sales                       ❶
+    organization: marketing
   creationTimestamp: "2019-10-15T00:57:44Z"
-  generation: 1                               ❷
+  generation: 2
   name: nginx-declarative
   Namespace: default
   resourceVersion: "347771"

$ kubectl apply -f declarative-deployment.yaml
deployment.apps/nginx-declarative configured

❶ 组织标签的值已从销售更改为营销。

❷ 在执行 kubectl apply 时,系统已更改了此资源的生成方式。

正如你所见,kubectl diff 正确识别出组织已从 sales 更改为 marketing。我们还可以看到 kubectl apply 成功应用了新的更改。

在这个练习中,强制性和声明性示例都导致以完全相同的方式配置了部署资源。乍一看,强制性的方法可能看起来要简单得多。与声明性部署规范的冗长性相比,它只包含几行代码,而声明性部署规范的冗长性是脚本大小的五倍。然而,它包含了一些问题,使得它在实践中使用时不是一个好的选择:

  • 代码不具有幂等性,如果多次执行可能会得到不同的结果。如果再次运行,将会抛出一个错误,抱怨部署 NGINX 已经存在。相比之下,部署规范是幂等的,这意味着可以根据需要多次应用,处理部署已存在的情况。

  • 随着时间的推移管理资源更改变得更加困难,尤其是在差异是减法的情况下。假设你不再希望组织被注释在部署上。简单地从脚本代码中移除 kubectl annotate 命令并不能解决问题,因为它对删除现有部署的注释没有任何帮助。需要单独的操作来删除它。另一方面,使用声明性方法,你只需从规范中移除注释行,Kubernetes 就会负责删除注释以反映你希望的状态。

  • 理解变化更为困难。如果一个团队成员发送了一个修改脚本的拉取请求,以执行不同的操作,那么这就像任何其他源代码审查一样。审查者需要心理上遍历脚本的逻辑来验证算法是否实现了预期的结果。脚本中甚至可能存在错误。另一方面,修改声明式部署规范的拉取请求清楚地显示了系统期望状态的变化。审查起来更简单,因为没有逻辑需要检查,只有配置更改。

  • 代码不是原子的,这意味着如果脚本中的四个命令之一失败,系统状态将部分更改,既不会回到原始状态,也不会达到期望状态。使用声明式方法,整个规范作为一个单一请求接收,系统尝试作为一个整体实现所有期望状态方面。

如你所想,最初只是一个简单的 shell 脚本,为了实现幂等性,需要变得越来越复杂。Kubernetes 部署规范中有数十个选项可用。使用脚本方法,需要在脚本中散布 if/else 检查,以了解现有状态并条件性地修改部署。

2.2.1 声明式配置的工作原理

正如我们在之前的练习中看到的,声明式配置管理是由 kubectl apply 命令驱动的。与 scaleannotate 这样的命令式 kubectl 命令相比,kubectl apply 命令有一个参数,即包含资源清单的文件的路径:

kubectl apply -f ./resource.yaml

该命令负责确定应该应用到 Kubernetes 集群中匹配资源上的哪些更改,并使用 Kubernetes API 更新资源。这是一个关键特性,使得 Kubernetes 成为 GitOps 的完美选择。让我们更深入地了解 kubectl apply 背后的逻辑,并了解它能做什么以及不能做什么。为了理解 kubectl apply 解决了哪些问题,让我们通过使用我们之前创建的 Deployment 资源来探讨不同的场景。

最简单的情况是当匹配的资源不存在于 Kubernetes 集群中。在这种情况下,kubectl 会使用存储在指定文件中的清单创建一个新的资源。

如果匹配的资源存在,为什么 kubectl 不替换它?如果你使用 kubectl get 命令查看完整的清单资源,答案就显而易见了。以下是示例中创建的 Deployment 资源的部分列表。为了清晰起见,清单的一些部分已被省略(用省略号表示):

$ kubectl get deployment nginx-declarative -o=yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    environment: prod
    kubectl.kubernetes.io/last-applied-configuration: |
      { ... }
    organization: marketing
  creationTimestamp: "2019-10-15T00:57:44Z"
  generation: 2
  name: nginx-declarative
  Namespace: default
  resourceVersion: "349411"
  selfLink: /apis/apps/v1/Namespaces/default/deployments/nginx-declarative
  uid: d41cf3dc-a3e8-40dd-bc81-76afd4a032b1
spec:
  progressDeadlineSeconds: 600
  replicas: 3
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: nginx-declarative
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    ...
status:
  ...

如您可能已经注意到的,实时资源清单包括文件中指定的所有字段以及数十个新字段,如额外的元数据、status字段和资源规范中的其他字段。所有这些附加字段都是由 Deployment 控制器填充的,并包含有关资源运行状态的重要信息。控制器在status字段中填充有关资源状态的信息,并为所有未指定的可选字段应用默认值,例如revisionHistoryLimitstrategy。为了保留这些信息,kubectl apply合并了指定文件和实时资源清单。因此,该命令只更新文件中指定的字段,保持其他一切不变。所以如果我们决定缩小部署并将replicas字段更改为1,那么 kubectl 只更改实时资源中的该字段,并使用更新 API 将其保存回 Kubernetes。

在现实生活中,我们不想以声明性方式控制所有可能影响资源行为的字段。留出一些空间用于强制性,并跳过那些应该动态更改的字段是有意义的。Deployment 资源的replicas字段是一个完美的例子。您可以使用水平 Pod 自动伸缩器动态地根据负载扩展或缩小应用程序,而不是硬编码您想要使用的副本数量。

水平 Pod 自动伸缩器水平 Pod 自动伸缩器根据观察到的 CPU 利用率(或,在自定义度量支持的情况下,根据某些其他应用程序提供的度量)自动调整副本控制器、部署或副本集中的 Pod 数量。

让我们继续,从 Deployment 清单中移除replicas字段。应用这个更改后,replicas字段将重置为默认值一个副本。但是等等!kubectl apply命令只更新文件中指定的字段,并忽略其他字段。它是如何知道replicas字段已被删除的呢?允许 kubectl 处理删除用例的附加信息隐藏在实时资源的注解中。每次kubectl apply命令更新资源时,它都会将输入清单保存到kubectl.kubernetes.io/last-applied-configuration注解中。因此,当命令下次执行时,它会从注解中检索最近应用的清单,这代表了新期望清单和实时资源清单的共同祖先。这允许 kubectl 执行三次合并/合并,并正确处理从资源清单中删除某些字段的情况。

三向合并三向合并是一种合并算法,它自动分析两个文件之间的差异,同时考虑两个文件的起源或共同祖先。

最后,让我们讨论一下kubectl apply可能无法按预期工作的情况,以及应该谨慎使用它的情况。

首先,通常你不应该将命令式命令,如kubectl editkubectl scale,与声明式资源管理混合使用。这会导致当前状态与last-applied-configuration注解不匹配,并会破坏 kubectl 用来确定已删除字段的合并算法。典型的场景是当你使用kubectl edit对资源进行实验,并希望通过应用存储在文件中的原始清单来回滚更改。不幸的是,这可能不起作用,因为kubectl edit命令所做的更改并未存储在任何地方。例如,如果你临时将resource limits字段添加到部署中,kubectl apply不会移除它,因为limits字段在last-applied-configuration注解或文件中的清单中都没有提到。同样,kubectl replace命令也会忽略last-applied-configuration注解,并在应用更改后完全删除该注解。因此,如果你以命令式方式做出任何更改,你应该准备好在继续声明式配置之前使用命令式命令来撤销更改。

当你想要停止声明式管理字段时,也应该小心。这个问题的一个典型例子是将水平 Pod 自动伸缩器添加到管理现有部署副本数量的缩放。通常,在引入水平 Pod 自动伸缩器之前,部署副本的数量是声明式管理的。要将replicas字段的控制权交给水平 Pod 自动伸缩器,必须首先从包含部署清单的文件中删除replicas字段。这样做是为了确保下一次kubectl apply不会覆盖水平 Pod 自动伸缩器设置的replicas值。然而,不要忘记replicas字段可能也存储在last-applied-configuration注解中。如果是这样,清单文件中缺失的replicas字段将被视为字段删除,因此每次运行kubectl apply时,水平 Pod 自动伸缩器以命令式方式设置的replicas值将被从实时部署中移除。部署将缩放到默认的单个副本。

在本节中,我们介绍了管理 Kubernetes 对象的不同机制:命令式和声明式。你还了解了一些关于 kubectl 内部结构和它如何识别应用于实时对象的变化。但在此阶段,你可能想知道所有这些与 GitOps 有什么关系。答案是简单的:一切!理解 kubectl 和 Kubernetes 如何管理实时对象的变化对于理解后续章节中讨论的 GitOps 工具如何识别包含 Kubernetes 配置的 Git 仓库是否与实时状态同步,以及它如何跟踪和应用更改至关重要。

2.3 控制器架构

到目前为止,我们已经了解了 Kubernetes 的声明式特性和它提供的优势。让我们来谈谈每个 Kubernetes 资源背后的内容:控制器架构。了解控制器的工作原理将帮助我们更有效地使用 Kubernetes,并理解它如何扩展。

控制器是理解特定类型资源清单含义的大脑,并执行必要的任务以使系统的实际状态与清单中描述的期望状态相匹配。每个控制器通常只负责一种资源类型。通过监听与被管理资源类型相关的 API 服务器事件,控制器持续监视资源配置的变化,并执行必要的任务以将当前状态移动到期望状态。Kubernetes 控制器的一个基本特性是能够将工作委派给其他控制器。这种分层架构非常强大,并允许你有效地重用不同资源类型提供的功能。让我们通过一个具体的例子来更好地理解委派的概念。

2.3.1 控制器委派

Deployment、ReplicaSet 和 Pod 资源完美地展示了委派如何赋予 Kubernetes 能力。Pod 提供了在集群中的节点上运行一个或多个请求资源的容器的功能。这使得 Pod 控制器可以专注于简单地运行一个应用程序实例,并抽象出与基础设施配置、扩展和缩减、网络以及其他复杂细节相关的逻辑,将这些留给其他控制器。尽管 Pod 资源提供了许多功能,但它仍然不足以在生产环境中运行应用程序。我们需要运行同一应用程序的多个实例(为了弹性和性能),这意味着我们需要多个 Pod。ReplicaSet 控制器解决了这个问题。它不是直接管理多个容器,而是编排多个 Pod,并将容器编排委派给 Pod 资源。同样,Deployment 控制器利用 ReplicaSet 提供的功能来实现各种部署策略,如滚动更新。

图 2.5 Kubernetes 资源分层

图 2.5 Kubernetes 允许资源分层。提供额外功能的高级资源,如 ReplicaSets 和 Deployments,可以管理其他高级资源或基本资源,如 Pods。这是通过一系列控制器实现的,每个控制器管理与其控制的资源相关的事件。

控制器委托的好处 通过控制器委托,Kubernetes 功能可以轻松扩展以支持新功能。例如,不向后兼容的服务只能使用蓝/绿策略(不是滚动更新)进行部署。控制器委托允许重新编写新的控制器以支持蓝/绿部署,并通过委托仍然可以利用 Deployment 控制器的功能,而无需重新实现 Deployment 控制器的核心功能。

所以,正如您可以从这个例子中看到的那样,控制器委托允许 Kubernetes 从简单的资源逐步构建更复杂的资源。

2.3.2 控制器模式

尽管所有控制器都有不同的职责,但每个控制器的实现都遵循相同的简单模式。每个控制器运行一个无限循环,并且每次迭代都会协调其负责的集群资源的所需状态和实际状态。在协调过程中,控制器正在寻找实际状态和所需状态之间的差异,并做出必要的更改,以将当前状态移动到所需状态。

所需状态由资源清单的 spec 字段表示。问题是,控制器如何知道实际状态?这个信息在 status 字段中可用。每次成功协调后,控制器都会更新 status 字段。status 字段为最终用户提供有关集群状态的信息,并使高级控制器的操作成为可能。图 2.6 展示了协调循环。

图 2.6 一个控制器在一个连续的协调循环中运行,它试图将 spec 中定义的所需状态与当前状态收敛。通过更新资源状态来报告资源的更改和更新。控制器可能将工作委托给其他 Kubernetes 控制器或执行其他操作,例如使用云提供商的 API 管理外部资源。

控制器与 operators 的比较

经常被混淆的两个术语是 operatorcontroller。在这本书中,术语 GitOps operator 用于描述持续交付工具,而不是 GitOps controller。这样做的原因是我们代表了一种特定的控制器,这种控制器是针对应用程序和特定领域的。

Kubernetes operators 一个 Kubernetes operator 是一个特定于应用程序的控制器,它扩展了 Kubernetes API,代表 Kubernetes 用户创建、配置和管理复杂有状态应用程序的实例。它建立在主要的 Kubernetes 资源和控制概念之上,并包括特定于领域或应用程序的知识,以自动化日常任务。

术语操作符控制器经常被混淆,因为它们有时可以互换使用,两者之间的界限通常模糊不清。然而,另一种思考方式是,术语操作符用于描述特定应用的控制器。所有操作符都使用控制器模式,但并非所有控制器都是操作符。一般来说,控制器倾向于管理较低级别的、可重用的构建块资源,而操作符在更高层次上运行,且针对特定应用。控制器的一些例子包括所有管理 Kubernetes 原生类型(如部署、作业、入口等)的内置控制器,以及第三方控制器,如 cert-manager(提供和管理 TLS 证书)和 Argo 工作流控制器,后者在集群中引入了一种新的类似作业的工作流资源。操作符的一个例子是 Prometheus,它管理 Prometheus 数据库安装。

2.3.3 NGINX 操作符

在了解了控制器基础知识和控制器与操作符之间的区别之后,我们准备实现一个操作符!这个示例操作符将解决一个现实生活中的任务:管理一组预配置静态内容的 NGINX 服务器。操作符将允许用户指定一组 NGINX 服务器并配置每个服务器上挂载的静态文件。这项任务并不简单,展示了 Kubernetes 的灵活性和强大功能。

设计

如本章前面所述,Kubernetes 的架构允许您通过委派利用现有控制器的功能。我们的 NGINX 控制器将利用 Deployment 资源来委派 NGINX 部署任务。

接下来的问题是应该使用哪种资源来配置服务器列表和自定义静态内容。最合适的现有资源是 ConfigMap。根据官方 Kubernetes 文档,ConfigMap 是“一个用于以键值对形式存储非机密数据的 API 对象。”^(7) ConfigMap 可以作为环境变量、命令行参数或卷中的配置文件来使用。控制器将为每个 ConfigMap 创建一个 Deployment,并将 ConfigMap 数据挂载到默认的 NGINX 静态网站目录中。

实现

一旦我们决定了主要构建块的设计,就是时候编写一些代码了。大多数与 Kubernetes 相关的项目,包括 Kubernetes 本身,都是使用 Go 语言实现的。然而,Kubernetes 控制器可以使用任何语言实现,包括 Java、C++,甚至是 JavaScript。为了简化,我们将使用您可能最熟悉的语言:Bash 脚本语言。

在 2.3.2 节中,我们提到每个控制器都维护一个无限循环,并持续地协调期望状态和实际状态。在我们的示例中,期望状态由 ConfigMap 的列表表示。遍历每个 ConfigMap 变化的最高效方式是使用 Kubernetes 的 watch API。watch 功能由 Kubernetes API 为大多数资源类型提供,允许调用者在资源被创建、修改或删除时收到通知。kubectl 工具允许使用带有 --watch 标志的 get 命令来监视资源变化。--output-watch-events 命令指示 kubectl 输出更改类型,它可以是以下值之一:ADDEDMODIFIEDDELETED

图 2.7 在 NGINX 操作符设计中,创建了一个包含 NGINX 将要提供的数据的 ConfigMap。NGINX 操作符为每个 ConfigMap 创建一个 Deployment。只需创建一个包含网页数据的 ConfigMap,就可以简单地创建额外的 NGINX 部署。

Kubectl 版本 确保您在本教程中使用 kubectl 的最新版本(版本 1.16 或更高版本)。--output-watch-events 选项是相对较新添加的。

列表 2.4 示例 ConfigMap (mng.bz/GxRN)

apiVersion: v1
kind: ConfigMap
metadata:
  name: sample
data:
  index.html: hello world

在一个窗口中,运行以下命令:

$ kubectl get --watch --output-watch-events configmap

在另一个终端窗口中,运行 kubectl apply -f sample.yaml 以创建示例 ConfigMap。注意运行 kubectl --watch 命令的窗口中的新输出。现在运行 kubectl delete -f sample.yaml。现在您应该看到一个 DELETED 事件出现:

$ kubectl get --watch --output-watch-events configmap
EVENT      NAME     DATA   AGE
ADDED      sample   1      3m30s
DELETED    sample   1      3m40s

手动运行此实验后,您应该能够看到我们如何将 NGINX 操作符编写为 Bash 脚本。

kubectl get --watch 命令在创建、更改或删除 ConfigMap 资源时输出新行。脚本将消耗 kubectl get --watch 的输出,并根据输出 ConfigMap 事件类型创建或删除新的 Deployment。现在,不拖延,完整的操作符实现如下代码所示。

列表 2.5 NGINX 控制器 (http://mng.bz/zxmZ)

#!/usr/bin/env bash

kubectl get --watch --output-watch-events configmap \      ❶
-o=custom-columns=type:type,name:object.metadata.name \
--no-headers | \
while read next; do                                        ❷

    NAME=$(echo $next | cut -d' ' -f2)                     ❸
    EVENT=$(echo $next | cut -d' ' -f1)

    case $EVENT in
        ADDED|MODIFIED)                                    ❹
            kubectl apply -f - << EOF
apiVersion: apps/v1
kind: Deployment
metadata: { name: $NAME }
spec:
  selector:
    matchLabels: { app: $NAME }
  template:
    metadata:
      labels: { app: $NAME }
      annotations: { kubectl.kubernetes.io/restartedAt: $(date) }
    spec:
      containers:
      - image: nginx:1.7.9
        name: $NAME
        ports:
        - containerPort: 80
        volumeMounts:
        - { name: data, mountPath: /usr/share/nginx/html }
      volumes:
      - name: data
        configMap:
          name: $NAME
EOF
            ;;
        DELETED)                                         ❺
            kubectl delete deploy $NAME
            ;;
    esac
done

❶ 此 kubectl 命令输出 configmap 对象发生的所有事件。

❷ kubectl 的输出由这个无限循环处理。

❸ 从 kubectl 输出中解析出 configmap 的名称和事件类型。

❹ 如果 configmap 已被添加或修改,则应用该 configmap 的 NGINX 部署清单(两个 EOF 标签之间的所有内容)。

❺ 如果 configmap 已被删除,则删除该 configmap 的 NGINX 部署。

测试

现在实施完成,我们准备测试我们的控制器。在现实生活中,控制器被打包成一个 Docker 镜像,并在集群内部运行。对于测试目的,在集群外部运行控制器是可以接受的,这正是我们即将要做的事情。使用附录 A 中的说明,启动一个 minikube 集群,将控制器代码保存到名为 controller.sh 的文件中,并使用以下 Bash 命令启动它:

$ bash controller.sh

注意:此示例需要 kubectl 版本 1.16 或更高版本。

控制器正在运行并等待 ConfigMap。让我们创建一个。请参阅 2.4 节中的 ConfigMap 的清单。

我们使用kubectl apply命令创建 ConfigMap:

$ kubectl apply -f sample.yaml
configmap/sample created

控制器注意到变化,并使用kubectl apply命令创建一个 Deployment 实例:

$ bash controller.sh
deployment.apps/sample created

练习 2.2

尝试通过在本地转发端口 80 来访问 NGINX 控制器,以确保控制器按预期工作。尝试删除或修改 ConfigMap,看看控制器如何相应地做出反应。

练习 2.3

创建额外的 ConfigMap,为您的家庭成员中的每个人启动一个显示Hello <name>!的 NGINX 服务器。同时,别忘了在现实生活中给他们打电话/发短信/Snapchat。

练习 2.4

编写一个 Dockerfile 来打包 NGINX 控制器。将其部署到您的测试 Kubernetes 集群中。提示:您需要为操作员创建 RBAC 资源。

2.4 Kubernetes + GitOps

GitOps 假设每个基础设施组件都表示为一个存储在版本控制系统中的文件,并且存在一个自动化的过程,可以无缝地将更改应用到应用程序的运行时环境中。如果没有像 Kubernetes 这样的系统,这很遗憾,说起来容易做起来难。有太多的事情需要担心,还有很多不同的技术不能很好地协同工作。这两个假设通常成为无法解决的障碍,阻碍了高效基础设施即代码过程的实施。

Kubernetes 显著改善了这种情况。随着 Kubernetes 被越来越广泛地采用,基础设施即代码(IaC)的概念也随之发展,这导致了新的工具的创建,这些工具实现了 GitOps。那么 Kubernetes 有什么特别之处,它是如何以及为什么导致了 GitOps 的兴起?

Kubernetes 通过完全采用声明式 API 作为其主要操作模式,并提供实现这些 API 所需的控制器模式和后端框架,从而实现了 GitOps。该系统从一开始就按照声明性规范和最终一致性以及收敛的原则进行设计。

最终一致性最终一致性是分布式计算中用于实现高可用性的一个一致性模型,非正式地保证,如果没有对给定数据项进行新的更新,最终所有对该项的访问都将返回最后更新的值。

这个决策导致了 GitOps 在 Kubernetes 中的突出地位。与传统的系统不同,在 Kubernetes 中几乎没有只能修改某些现有资源子集的 API。例如,没有(也永远不会)只更改 Pod 容器镜像的 API。相反,Kubernetes API 服务器期望所有 API 请求向 API 服务器提供资源的完整清单。这是一个有意向用户不提供任何便利 API 的决策。因此,Kubernetes 用户实际上被迫进入声明性操作模式,这导致这些用户需要将这些声明性规范存储在某个地方。Git 成为了存储这些规范的自然介质,GitOps 然后成为了从 Git 部署这些清单的自然交付工具。

2.5 开始使用 CI/CD

既然你已经学习了 Kubernetes 控制器的基本架构和原则,以及 Kubernetes 如何适合 GitOps,现在是时候实现自己的 GitOps 操作符了。在本教程中,我们将首先创建一个基本的 GitOps 操作符来驱动持续交付。之后,我们将举例说明如何将持续集成(CI)与基于 GitOps 的持续交付(CD)解决方案集成。

2.5.1 基本 GitOps 操作符

要实现自己的 GitOps 操作符,需要实现一个持续运行的控制循环,该循环执行图 2.8 中展示的三个步骤。

图片

图 2.8 GitOps 协调循环首先通过克隆仓库来获取配置仓库的最新版本到本地存储。接下来,清单发现步骤遍历克隆仓库的文件系统,寻找任何要应用到集群中的 Kubernetes 清单。最后,kubectl apply 步骤通过将所有发现的清单应用到集群来执行实际的部署。

虽然这个控制循环可以用多种方式实现,但最简单的方式是将其实现为一个 Kubernetes CronJob。

列表 2.6 CronJob GitOps 操作符 (http://mng.bz/0myz)

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: gitops-cron
  Namespace: gitops
spec:
  schedule: "*/5 * * * *"                                 ❶
  concurrencyPolicy: Forbid                               ❷
  jobTemplate:
    spec:
      backoffLimit: 0                                     ❸
      template:
        spec:
          restartPolicy: Never                            ❹
          serviceAccountName: gitops-serviceaccount       ❺
          containers:
          - name: gitops-operator
            image: gitopsbook/example-operator:v1.0       ❻
            command: [sh, -e, -c]                         ❼
            args:
            - git clone https://github.com/gitopsbook/sample-app-deployment.git /tmp/example &&
              find /tmp/example -name '*.yaml' -exec kubectl apply -f {} \;

❶ 每 5 分钟执行一次 GitOps 协调循环

❷ 防止作业并发执行

❸ 由于这是一个重复的 CronJob,不会重试失败的作业;重试会自然发生

❹ 容器完成时不会重新启动

❺ 一个具有足够权限在集群中创建和修改对象的 Kubernetes 服务账户

❻ 包含预装了 git、find 和 kubectl 二进制文件的 Docker 镜像

❼ 命令和参数字段包含 GitOps 协调循环的实际逻辑。

作业模板规范包含了操作逻辑的核心。CronJob gitops-cron包含了控制循环逻辑,定期从 Git 部署清单到集群。schedule字段是一个cron表达式,在这个例子中,它将导致作业每五分钟执行一次。将concurrencyPolicy设置为Forbid可以防止作业的并发执行,允许当前执行完成后再尝试启动第二个。请注意,这只会发生在单个执行时间超过五分钟的情况下。

jobTemplate是一个 Kubernetes 作业模板规范。作业模板规范包含一个 Pod 模板规范(jobTemplate.spec.template.spec),这是您可能从为部署、Pod、作业等编写 Kubernetes 清单时熟悉的规范。backoffLimit指定了在将作业视为失败之前重试的次数。零值表示不会重试。由于这是一个周期性的 CronJob,重试会自然发生,因此不需要立即重试。需要restartPolicyNever以防止作业在完成时重新启动容器,这是容器的正常行为。serviceAccountName字段引用了一个具有足够权限在集群中创建和修改对象的 Kubernetes Service account。由于此操作员可能部署任何类型的资源,gitops-operator Service account 应该绑定到具有管理员级别的 ClusterRole。

commandargs字段包含了 GitOps 协调循环的实际逻辑。它只包含两个命令:

  • git clone—将最新仓库克隆到本地存储

  • find—在仓库中查找 YAML 文件,并为每个找到的 YAML 文件执行kubectl apply命令

要使用此功能,只需将 CronJob 应用到集群中。请注意,您首先需要应用以下支持资源。

列表 2.7 CronJob GitOps 资源 (http://mng.bz/KMln)

apiVersion: v1                               ❶
kind: Namespace
metadata:
  name: gitops

---
apiVersion: v1                               ❷
kind: ServiceAccount
metadata:
  name: gitops-serviceaccount
  Namespace: gitops

---
apiVersion: rbac.authorization.k8s.io/v1     ❸
kind: ClusterRoleBinding
metadata:
  name: gitops-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admin
subjects:
- kind: ServiceAccount
  name: gitops-serviceaccount
  Namespace: gitops

gitops命名空间是 CronJob 和 ServiceAccount 将驻留的地方。

ServiceAccount gitops-serviceaccount是具有将权限部署到集群的 Kubernetes Service account。

ClusterRoleBinding gitops-operator将集群管理员级别的权限绑定/授予 ServiceAccount,gitops-serviceaccount。

多资源 YAML 文件管理可以通过将多个资源分组到同一个文件中(在 YAML 中以---分隔)来简化。列表 2.7 是一个示例,展示了单个 YAML 文件定义了多个相关资源。

此示例是原始的,旨在说明 GitOps 持续交付操作员的基本概念。它不适用于任何实际的生产使用,因为它缺少在现实世界生产环境中所需的功能。例如,它不能修剪在 Git 中未定义的资源。另一个限制是它不处理连接到 Git 仓库所需的任何凭证。

练习 2.5

修改 CronJob 以指向你自己的 GitHub 仓库。应用新的 CronJob,并将 YAML 文件添加到你的仓库中。验证相应的 Kubernetes 资源是否已创建。

2.5.2 持续集成流水线

在上一节中,我们实现了一个基本的 GitOps CD 机制,该机制将 Git 仓库中的清单持续交付到集群。下一步是将此过程与 CI 流水线集成,该流水线发布新的容器镜像,并使用新镜像更新 Kubernetes 清单。GitOps 与任何 CI 系统都集成得很好,因为过程或多或少与典型的构建流水线相同。主要区别是,CI 流水线不是直接与 Kubernetes API 服务器通信,而是将期望的更改提交到 Git,并相信在某个时候,GitOps 操作员会检测到新更改并将其应用。

图 2.9 GitOps CI 流水线类似于典型的 CI 流水线。代码被构建和测试,然后工件(标记过的 Docker 镜像)被推送到镜像仓库。额外的步骤是 GitOps CI 流水线还会更新配置仓库中的清单,以包含最新的镜像标签。此更新可能会触发 GitOps CD 作业,将更新的清单应用到集群中。

GitOps CI 流水线的目标是

  • 构建你的应用程序并根据需要运行单元测试

  • 将新的容器镜像发布到容器仓库

  • 更新 Git 中的 Kubernetes 清单以反映新镜像

以下示例是在 CI 流水线中执行的一系列典型命令,以实现此目的。

列表 2.8 示例 GitOps CI (mng.bz/9M18)

export VERSION=$(git rev-parse HEAD | cut -c1-7)                         ❶
make build                                                               ❷
make test

export NEW_IMAGE="gitopsbook/sample-app:${VERSION}"                      ❸
docker build -t ${NEW_IMAGE} .
docker push ${NEW_IMAGE}

git clone http://github.com/gitopsbook/sample-app-deployment.git         ❹
cd sample-app-deployment
                                                                         ❺
kubectl patch \
  --local \
  -o yaml \
  -f deployment.yaml \
  -p "spec:
        template:
          spec:
            containers:
            - name: sample-app
              image: ${NEW_IMAGE}" \
  > /tmp/newdeployment.yaml
mv /tmp/newdeployment.yaml deployment.yaml

git commit deployment.yaml -m "Update sample-app image to ${NEW_IMAGE}"  ❻
git push

❶ 使用当前提交-SHA 的前七个字符作为版本,以唯一标识此构建的工件

❷ 按照通常的方式构建和测试你的应用程序的二进制文件

❸ 构建容器镜像,将其推送到容器仓库,并将唯一版本作为容器镜像标签的一部分

❹ 克隆包含 Kubernetes 清单的 Git 部署仓库

❺ 使用新镜像更新清单

❻ 将清单更改提交并推送到部署配置仓库

此示例流水线是 GitOps CI 流水线可能看起来的一种方式。有一些重要的要点需要强调,这些不同的选择可能会更好地满足你的需求。

镜像标签和最新标签的陷阱

注意在示例管道的前两步中,当前应用程序 Git 仓库的 Git 提交 SHA 被用作版本变量,然后作为容器镜像标签的一部分被整合。示例管道中的结果容器镜像可能看起来像 gitopsbook/sample-app:cc52a36,其中 cc52a36 是构建时的提交 SHA。

使用独特的版本字符串(如提交 SHA)非常重要,因为版本被整合为容器镜像标签的一部分。人们常犯的一个常见错误是将 latest 作为他们的镜像标签(例如 gitopsbook/sample-app:latest)或从构建到构建重用相同的镜像标签。一个简单的管道可能会犯以下错误:

make build
docker build -t gitopsbook/sample-app:latest .
docker push gitopsbook/sample-app:latest

从构建到构建重用镜像标签是一种非常糟糕的做法,原因有几个。

容器标签不应重用的第一个原因是,当重用容器镜像标签时,Kubernetes 不会将新版本部署到集群中。这是因为第二次尝试应用清单时,Kubernetes 不会检测到清单中的任何变化,第二次 kubectl apply 将没有任何效果。例如,假设构建 #1 发布了镜像 gitopsbook/sample-app :latest 并将其部署到集群中。这个 Deployment 清单可能看起来像这样。

列表 2.9 示例应用程序部署 (http://mng.bz/j4m9)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - image: gitopsbook/sample-app:latest
        name: sample-app
        command:
          - /app/sample-app
        ports:
        - containerPort: 8080

当运行构建 #2 时,尽管已经将新的容器镜像 gitopsbook/sample-app:latest 推送到容器注册库,但应用程序的 Kubernetes Deployment YAML 与构建 #1 中的相同。从 Kubernetes 的角度来看,部署规范是相同的;构建 #1 和构建 #2 中应用的内容没有区别。Kubernetes 将第二次应用视为无操作(no-op)并且不执行任何操作。为了使 Kubernetes 重新部署,部署规范中必须从第一次构建到第二次构建有所不同。使用唯一的容器镜像标签确保存在差异。

将独特版本纳入镜像标签的另一个原因是它实现了可追溯性。通过将类似应用程序的 Git 提交 SHA 这样的内容纳入标签,集群中当前运行的软件版本永远不会产生疑问。例如,您可以运行以下 kubectl 命令,该命令输出命名空间中所有部署的镜像:

$ kubectl get deploy -o wide | awk '{print $1,$7}' | column -t
NAME        IMAGES
sample-app  gitopsbook/sample-app:508d3df

通过将容器镜像标签与应用程序仓库的 Git 提交 SHA 相关联的约定,您可以追踪当前运行的 sample-app 版本到提交 508d3df。从那里,您将完全了解集群中运行的应用程序的确切版本。

不重用像latest这样的图像标签的第三个,可能是最重要的原因,是回滚到旧版本变得不可能。当你重用图像标签时,你正在覆盖或重写被覆盖图像的含义。想象以下事件序列:

  1. 构建号 1 发布了容器镜像gitopsbook/sample-app:latest并将其部署到集群中。

  2. 构建号 2 重新发布了容器镜像gitopsbook/sample-app:latest,覆盖了构建号 1 中部署的图像标签。它将此镜像重新部署到集群中。

  3. 在部署构建号 2 之后,发现代码的最新版本存在一个严重的 bug,并且需要立即回滚到构建号 1 创建的版本。

由于没有代表该软件版本的图像标签,因此没有简单的方法可以重新部署构建号 1 期间创建的sample-app版本。第二次构建覆盖了latest图像标签,实际上使原始图像不可访问(至少不是没有极端措施)。

由于这些原因,不建议在生产环境中重用图像标签,如latest。话虽如此,在开发和测试环境中,持续创建新的唯一图像标签(这些标签可能永远不会被清理)可能会导致你的容器注册库的磁盘使用量过多,或者仅仅是因为图像标签的数量庞大而变得难以管理。在这些情况下,重用图像标签可能是合适的,理解 Kubernetes 在应用相同的规范两次时不会采取任何行动的行为。

Kubectl rollout restart Kubectl 有一个方便的命令,kubectl rollout restart,它会导致部署的所有 Pod 重启(即使图像标签相同)。这在图像标签被覆盖并希望重新部署的开发和测试场景中很有用。它通过在 Pod 模板元数据注解中注入任意时间戳来实现。这导致 Pod 规范与之前不同,从而引起 Pod 的常规滚动更新。

需要注意的一件事是,我们的 CI 示例使用 Git 提交-SHA 作为唯一的图像标签。但除了 Git 提交-SHA 之外,图像标签可以包含任何其他唯一标识符,例如语义版本、构建号、日期/时间字符串,甚至这些信息的组合。

语义版本 A 语义版本 是一种使用三位数约定(MAJOR.MINOR.PATCH)来传达版本含义(如 v2.0.1)的版本控制方法。当存在不兼容的 API 更改时,MAJOR 会增加。当以向后兼容的方式添加功能时,MINOR 会增加。当有向后兼容的 bug 修复时,PATCH 会增加。

摘要

  • Kubernetes 是一个用于部署、扩展和管理容器的容器编排系统。

  • 基本 Kubernetes 对象是 Pod、Service 和 Volume。

  • Kubernetes 控制平面由kube-apiserverkube-controller-managerkube-scheduler组成。

  • 每个 Kubernetes 工作节点都运行kubeletkube-proxy

  • 在 Pod 中运行的Running服务可以通过kubectl port-forward从您的计算机访问。

  • Pod 可以通过使用命令式或声明式语法进行部署。命令式部署不是幂等的,而声明式部署是幂等的。对于 GitOps,声明式是首选方法。

  • 控制器是 Kubernetes 的大脑,用于将Running状态转换为所需状态。

  • Kubernetes 操作符可以通过监控 ConfigMap 的更改并更新部署简单地实现为 shell 脚本。

  • Kubernetes 配置是声明式的。

  • 由于其声明式特性,GitOps 补充了 Kubernetes。

  • GitOps 操作符根据存储在 Git 中受版本控制的配置文件的变化触发对 Kubernetes 集群的部署。

  • 一个简单的 GitOps 操作符可以通过定期检查清单 Git 仓库的更改实现为脚本。

  • CI 管道可以通过一个脚本实现,该脚本包含构建 Docker 镜像和更新清单以包含新镜像标签的步骤。


1.www.cncf.io/phippy.

2.en.wikipedia.org/wiki/Chroot.

3.kubernetes.io/blog/2015/04/borg-predecessor-to-kubernetes.

4.kubernetes.io/docs/concepts/overview/components/#control-plane-components.

5.https://en.wikipedia.org/wiki/Cowsay.

6.mng.bz/pVdP.

7.mng.bz/Yq67.

第二部分. 模式和流程

现在你已经对 GitOps 和 Kubernetes 有了很好的理解,你准备好回顾采用 GitOps 所需的模式和流程了。

第三章讨论了环境的定义以及 Kubernetes Namespaces 如何很好地映射环境。它还涵盖了分支策略和配置管理以支持你的环境实施。

第四章深入探讨了 GitOps CI/CD 管道,对完整管道所需的所有阶段进行了全面描述。它还涵盖了代码、镜像和环境升级,以及回滚机制。

第五章描述了各种部署策略,包括滚动更新、蓝绿部署、金丝雀发布和渐进式交付。它还涵盖了如何通过使用原生的 Kubernetes 资源和其它开源工具来实现每种策略。

第六章讨论了 GitOps 驱动部署的攻击面以及如何减轻每个区域的风险。它还回顾了 Jsonnet、Kustomize 和 Helm,以及如何为你的用例选择正确的配置管理模式。

第七章讨论了 GitOps 管理秘密的各种策略。它还涵盖了几个秘密管理工具以及原生的 Kubernetes Secrets。

第八章解释了可观测性的核心概念以及为什么它对 GitOps 很重要。它还描述了使用 GitOps 和 Kubernetes 实现可观测性的各种方法。

在对 GitOps 模式和流程有更深入的理解后,你准备好改变你的部署流程了。你也具备了选择最适合你情况的 GitOps 工具的知识。

在第三部分,我们将介绍几个开源的 GitOps 工具(Argo CD、Jenkins X 和 Flux),这些工具可以简化并自动化你的 GitOps 流程。我们将讨论我们工具的动机和设计,并提供教程帮助你入门。

3 环境管理

本章涵盖

  • 理解环境

  • 使用命名空间设计正确环境

  • 组织你的 Git 仓库/分支策略以支持你的环境

  • 为你的环境实现配置管理

在第二章中,你学习了 GitOps 如何将应用程序部署到运行时环境。本章将让我们更深入地了解这些不同的运行时环境以及 Kubernetes 命名空间如何定义环境边界。我们还将了解几个配置管理工具(Helm、Kustomize 和 Jsonnet),以及它们如何帮助在多个环境中一致地管理应用程序的配置。

我们建议你在阅读本章之前先阅读第一章和第二章。

3.1 环境管理简介

在软件部署中,环境是代码部署和执行的地方。不同的环境在软件开发的生命周期中扮演不同的角色。例如,本地开发环境(也称为笔记本电脑)是工程师可以创建、测试和调试新代码版本的地方。工程师完成代码开发后,下一步是将更改提交到 Git,并启动部署到不同的环境进行集成测试和最终的生产发布。这个过程被称为持续集成/持续部署(CI/CD),通常包括以下环境:QA、E2E、Stage 和 Prod

QA 环境是新代码将针对硬件、数据和类似生产依赖项进行测试的地方,以确保你的服务的正确性。如果 QA 环境中的所有测试都通过,新代码将被提升到 E2E 环境,作为其他预发布服务的稳定环境进行测试/集成。QA 和 E2E 环境也被称为预生产(preprod)环境,因为它们不托管生产流量或使用生产数据。

当代码的新版本准备进行生产发布时,代码通常会首先部署到 Stage 环境(该环境可以访问实际的生产依赖项),以确保在代码在 Prod 环境中上线之前,所有生产依赖项都已就绪。例如,新代码可能需要新的数据库模式更新,而 Stage 环境可以用来验证新的模式是否到位。配置仅将测试流量直接导向 Stage 环境,以确保新代码引入的问题不会影响实际客户。然而,Stage 环境通常配置为使用“真实”的生产数据库操作。在 Stage 环境中进行的测试必须仔细审查,以确保它们可以在生产环境中安全执行。一旦 Stage 环境中的所有测试都通过,新代码最终将在 Prod 环境中部署,以处理实时生产流量。由于 Stage 和 Prod 都可以访问生产数据,它们都被视为生产环境。

图片

图 3.1 预生产环境有集成测试的 QA 环境,以及用于预发布功能集成的端到端 E2E 环境。生产环境可能有一个用于生产依赖测试的 Staging 环境,以及用于实时流量的实际生产环境。

3.1.1 环境的组成部分

环境由三个同等重要的组件组成:

  • 代码

  • 运行时先决条件

  • 配置

代码是应用程序执行特定任务的机器指令。为了执行代码,可能还需要运行时依赖。例如,Node.js 代码需要 Node.js 二进制文件和其他 npm 包才能成功执行。在 Kubernetes 的情况下,所有运行时依赖和代码都被打包成一个可部署单元(即 Docker 镜像),并通过 Docker 守护进程进行编排。应用程序的 Docker 镜像可以在任何环境中运行,从开发者的笔记本电脑到在云中运行的运行生产集群,因为镜像封装了代码和所有依赖,消除了环境之间的潜在不兼容性。

图片

图 3.2 左侧表示基于非容器的部署,在代码可以部署之前需要操作系统和运行时依赖。右侧表示基于容器的部署,包含代码和运行时依赖。

环境特定应用程序属性的配置通常与代码和运行时依赖一起部署,以便应用程序实例可以针对每个环境的行为和连接到正确的依赖项。每个环境都可能包含用于隔离的 DB 存储、分布式缓存或消息传递(如数据)。环境还有自己的网络策略,用于隔离流量和自定义访问控制。例如,可以配置入口和出口以阻止预生产和生产环境之间的流量,以增强安全性。可以配置访问控制以仅允许一小部分工程师访问生产环境,而预生产环境则对整个开发团队开放。

图片

图 3.3 环境由应用程序实例、网络入口/出口和用于保护其资源的访问控制组成。环境还包括应用程序依赖项,如缓存、DB 或消息传递。

选择合适的粒度

最终目标是所有新代码都能部署到生产环境中,以便客户和最终用户可以在代码通过质量测试后立即开始使用它。将代码部署到生产环境的延迟会导致开发团队产生的新代码的商业价值实现推迟。选择合适的环境粒度对于代码无延迟部署至关重要。需要考虑的因素包括

  • 发布独立性——如果代码需要与其他团队的代码捆绑在一起进行部署,则一个团队的部署周期将受制于其他团队生成的代码的可用性。正确的粒度应使您的代码能够独立于其他团队/代码进行部署。

  • 测试边界——类似于发布独立性,新代码的测试应该独立于其他代码发布。如果新代码测试依赖于其他团队/代码,则发布周期将受制于其他团队的准备情况。

  • 访问控制——除了预生产和生产环境的单独访问控制之外,每个环境还可以限制访问控制,仅限于积极在代码库上工作的团队。

  • 隔离——每个环境是一个逻辑工作单元,应该与其他环境隔离,以避免“嘈杂邻居”问题并限制出于安全原因来自不同环境的访问。

3.1.2 命名空间管理

命名空间是 Kubernetes 支持环境的自然结构。它们允许将集群资源分配给多个团队或项目。命名空间提供了独特资源命名、资源配额、RBAC、硬件隔离和网络配置的范围:

Kubernetes 命名空间 ~= 环境

在每个命名空间中,应用程序实例(即 Pod)是一个或多个在部署期间注入了特定环境应用程序属性的 Docker 容器。这些应用程序属性定义了环境应该如何运行(例如功能标志)以及应该使用哪些外部依赖项(例如数据库连接字符串)。

除了应用程序 Pod 之外,命名空间还可能包含其他提供环境所需额外功能的 Pod。

图 3.4 在 Kubernetes 中,命名空间相当于环境。命名空间可能包括 Pod(应用程序实例)、网络策略(入口/出口)和 RBAC(访问控制),以及运行在单独 Pod 中的应用程序依赖项。

RBAC 是一种基于企业内部个人用户角色的计算机或网络资源访问控制方法。在 Kubernetes 中,一个角色包含代表一组权限的规则。权限是纯粹累加的(没有拒绝规则)。一个角色可以在命名空间内定义,具有角色或集群范围定义的 ClusterRole。

命名空间还可以拥有专用硬件和网络策略,以根据应用程序需求优化其配置。例如,一个计算密集型应用程序可以在具有专用多核硬件的命名空间中部署。另一个需要大量磁盘 I/O 的服务可以部署在具有高速 SSD 的单独命名空间中。每个命名空间还可以定义其网络策略(入口/出口),以限制跨命名空间流量或使用未经验证的 DNS 名称访问集群内的其他命名空间。

在两个不同的环境中部署应用程序

在本节中,您将学习如何使用命名空间在两个不同的环境中部署相同的应用程序(一个名为 guestbook-qa 的测试环境和一个名为 guestbook-e2e 的预生产端到端环境),并使用不同的配置。我们将使用 Guestbook Kubernetes 示例应用程序进行此练习。1

图 3.5 Guestbook 前端架构将有一个服务来将 guestbook 网页前端暴露给实时流量。后端架构由 Redis 主节点和 Redis 从节点组成,用于数据。

练习概述

  1. 创建环境命名空间(guestbook-qa 和 guestbook-e2e)。

  2. 将 guestbook 应用程序部署到 guestbook-qa 环境。

  3. 测试 guestbook-qa 环境。

  4. 将 guestbook 应用程序提升到 guestbook-e2e 环境。

  5. 测试 guestbook-e2e 环境。

验证 Kubernetes 集群连接 在您开始之前,请确保您已正确配置 KUBECONFIG 环境变量以指向所需的 Kubernetes 集群。有关更多信息,请参阅附录 A。

首先,为您的每个 guestbook 环境创建 guestbook-qa 和 guestbook-e2e 命名空间:

$ kubectl create namespace guestbook-qa
namespace/guestbook-qa created
$ kubectl create namespace guestbook-e2e
namespace/guestbook-e2e created
$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   2m27s
guestbook-e2e     Active   9s
guestbook-qa      Active   19s
kube-node-lease   Active   2m30s
kube-public       Active   2m30s
kube-system       Active   2m30s

现在,您可以使用以下命令将 guestbook 应用程序部署到 guestbook-qa 环境:

$ export K8S_GUESTBOOK_URL=https://k8s.io/examples/application/guestbook
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-master-deployment.yaml
deployment.apps/redis-master created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-master-service.yaml
service/redis-master created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-slave-deployment.yaml
deployment.apps/redis-slave created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/redis-slave-service.yaml
service/redis-slave created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/frontend-deployment.yaml
deployment.apps/frontend created
$ kubectl apply -n guestbook-qa -f ${K8S_GUESTBOOK_URL}/frontend-service.yaml
service/frontend created

在我们继续之前,让我们测试一下 guestbook-qa 环境是否按预期工作。使用以下 minikube 命令查找 guestbook-qa 服务的 URL,然后在您的网页浏览器中打开该 URL:

$ minikube -n guestbook-qa service frontend --url
http://192.168.99.100:31671
$ open http://192.168.99.100:31671

在 guestbook 应用程序的 Messages 文本编辑中,输入类似 This is the guestbook-qa environment 的内容,然后按提交按钮。您的屏幕应该看起来像图 3.6 所示。

图 3.6 当您的 guestbook 应用程序已部署到 QA 时,您可以在浏览器中提交测试消息以验证您的部署。

现在,我们已经将 Guestbook 应用程序运行在 guestbook-qa 环境中,并已测试其工作正常,让我们将 guestbook-qa 提升到 guestbook-e2e 环境。在这种情况下,我们将使用与 guestbook-qa 环境中使用的完全相同的 YAML 文件。这类似于您的自动化 CD 管道的工作方式:

$ export K8S_GUESTBOOK_URL=https://k8s.io/examples/application/guestbook
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-master-deployment.yaml
deployment.apps/redis-master created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-master-service.yaml
service/redis-master created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-slave-deployment.yaml
deployment.apps/redis-slave created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/redis-slave-service.yaml
service/redis-slave created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/frontend-deployment.yaml
deployment.apps/frontend created
$ kubectl apply -n guestbook-e2e -f ${K8S_GUESTBOOK_URL}/frontend-service.yaml
service/frontend created

太好了!Guestbook 应用程序现在已部署到 guestbook-e2e 环境。现在让我们测试一下 guestbook-e2e 环境是否工作正常:

$ minikube -n guestbook-e2e service frontend --url
http://192.168.99.100:31090
$ open http://192.168.99.100:31090

与您在 guestbook-qa 环境中所做的一样,在 Messages 文本编辑中输入类似 This is the guestbook-e2e environment, NOT the guestbook-qa environment! 的内容,然后按提交按钮。您的屏幕应该看起来像图 3.7 所示。

图 3.7 当您的 guestbook 应用程序已部署到 QA 时,您可以在浏览器中提交测试消息以验证您的部署。

这里重要的是要意识到你在两个不同的环境中运行着相同的应用程序,这些环境由 Kubernetes 命名空间定义。请注意,每个应用程序都在维护其数据的独立副本。如果你在 QA Guestbook 中输入一条消息,它不会出现在 E2E Guestbook 中。这些是两个不同的环境。

练习 3.1

现在你已经创建了两个预生产环境,guestbook-qa 和 guestbook-e2e,在新的生产集群中创建另外两个生产环境,guestbook-stage 和 guestbook-prod。

提示:你可以使用命令minikube start -p production创建一个新的 minikube 集群,并使用kubectl config use-context <name>在它们之间切换。

案例研究:Intuit 环境管理

在 Intuit,我们根据服务和环境在每个 AWS 区域中组织命名空间,并在预生产集群和 prod 集群之间进行分离。一个典型的服务将拥有六个命名空间:QA、E2E、Stage/Prod West 和 Stage/Prod East。QA 和 E2E 命名空间将在预生产集群中,对相应团队开放访问。Stage/Prod West 和 Stage/Prod East 将位于生产集群中,访问受限.^a

^a www.cncf.io/case-study/intuit.

3.1.3 网络隔离

定义部署应用程序的环境的一个关键方面是确保只有预期的客户端可以访问特定的环境。默认情况下,所有命名空间都可以连接到所有其他命名空间中运行的服务。但在 QA 和 Prod 这样的两个不同环境的情况下,你不想这些环境之间有交叉通信。幸运的是,可以应用命名空间网络策略来限制命名空间之间的网络通信。让我们看看我们如何将应用程序部署到两个不同的命名空间,并使用网络策略来控制访问。

我们将介绍在两个不同的命名空间中部署服务的步骤。你还将修改网络策略并观察其效果。

概述

  1. 创建环境命名空间(qa 和 prod)。

  2. 将 curl 部署到 qa 和 prod 命名空间。

  3. 将 NGINX 部署到 prod 命名空间。

  4. 从 qa 和 prod 命名空间(都可行)使用 curl 访问 NGINX。

  5. 从 qa 命名空间阻止对 prod 命名空间的入站流量。

  6. 从 qa 命名空间(被阻止)使用 curl 访问 NGINX。

出站Egress流量是开始于网络内部并通过其路由器流向网络外部的网络流量。

入站Ingress流量由所有来自外部网络的数据通信和网络流量组成。

在开始之前,请验证你是否已正确配置了KUBECONFIG环境变量以指向所需的 Kubernetes 集群。请参阅附录 A 获取更多信息。

图 3.8 对于 QA 中的 Curl Pod 要到达生产环境中的 Web Pod,Curl Pod 需要通过 QA 出口到达生产入口。然后生产入口将流量路由到生产环境中的 Web Pod。

首先,为您的每个环境创建命名空间:

$ kubectl create namespace qa
namespace/qa created
$ kubectl create namespace prod
namespace/prod created
$ kubectl get namespaces
NAME              STATUS   AGE
qa           Active   2m27s
prod         Active   9s

现在,我们将同时在两个命名空间中创建一个 Pod,从该 Pod 我们可以运行 Linux 命令curl

$ kubectl -n qa apply -f curlpod.yaml
$ kubectl -n prod apply -f curlpod.yaml

列表 3.1 curlpod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: curl-pod
spec:
  containers:
  - name: curlpod
    image: radial/busyboxplus:curl
    command:
    - sh
    - -c
    - while true; do sleep 1; done     

在生产命名空间中,我们将运行一个 NGINX 服务器,该服务器将接收curl HTTP 请求:

$ kubectl -n prod apply -f web.yaml

列表 3.2 web.yaml

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
  - image: nginx
    imagePullPolicy: Always
    name: nginx
    ports:
    - containerPort: 80
      protocol: TCP

默认情况下,在命名空间中运行的 Pod 可以向运行在不同命名空间中的其他 Pod 发送网络流量。让我们通过从 qa 命名空间中的 Pod 执行curl命令到生产命名空间中的 NGINX Pod 来证明这一点:

$ kubectl describe pod web -n prod | grep IP                        ❶
$ kubectl -n qa  exec curl-pod -- curl -I http://<web pod ip>       ❷
$ kubectl -n prod  exec curl-pod -- curl -I http://<web pod ip>     ❸

❶ 获取 Web Pod 的 IP 地址

❷ 返回 HTTP 200

❸ 返回 HTTP 200

通常,您不希望 qa 和生产环境之间存在依赖关系。可能的情况是,如果应用程序的两个实例都正确配置,qa 和生产之间可能没有依赖关系,但如果有 qa 配置中的错误,意外地向生产发送流量怎么办?您可能会损坏生产数据。或者,即使在生产环境中,如果一个环境托管您的营销网站,另一个环境托管一个包含敏感数据的 HR 应用程序,这种情况可能也是合适的。在这些情况下,可能有必要在命名空间之间阻止网络流量,或者仅允许特定命名空间之间的网络流量。这可以通过向命名空间添加一个NetworkPolicy来实现。

让我们在每个命名空间中的 Pod 上添加一个NetworkPolicy

$ kubectl apply -f block-other-namespace.yaml

容器网络接口 Network policy 仅在配置了容器网络接口(CNI)^(2)的情况下受支持(minikube 和 Docker desktop 均不支持)。请参阅附录 A 以获取有关测试网络策略配置的更多信息。

列表 3.3 网络策略 (http://mng.bz/WdAX)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  namespace: prod               ❶
  name: block-other-namespace
spec:
  podSelector: {}               ❷
  ingress:
  - from:
    - podSelector: {}           ❸

❶ 适用于生产命名空间

❷ 选择生产命名空间中的所有 Pod

❸ 仅允许来自生产命名空间的请求入站。来自其他命名空间的请求将被阻止。

NetworkPolicy应用于生产命名空间,并且只允许来自生产命名空间的入站(入网流量)。正确使用NetworkPolicy约束是定义环境边界的关键方面。

应用了NetworkPolicy后,我们可以重新运行我们的curl命令来验证每个命名空间现在是否已与其他命名空间隔离:

$ kubectl -n qa exec curl-pod -- curl -I http://<web pod ip>    ❶
$ kubectl -n prod exec curl-pod -- curl -I http://<web pod ip>  ❷

❶ 从 qa 命名空间发起的 Curl 请求被阻止!

❷ 返回 Http 200

3.1.4 预生产和生产集群

现在你已经知道了如何使用命名空间创建多个环境,可能觉得在一个集群上创建所有所需的环境是一件微不足道的事情。例如,你可能需要为你的应用程序提供 QA、E2E、阶段和 Prod 环境。然而,根据你的具体用例,这可能不是最佳方法。我们的建议是拥有两个集群来托管你的环境,一个用于预生产环境的预生产集群,一个用于生产环境的生产集群。

拥有两个独立的集群来托管你的环境的主要原因是保护你的生产环境免受预生产环境工作中意外中断或其他影响。

亚马逊网络服务(AWS)中的集群隔离。在 AWS 中,可以创建一个单独的 VPC 来作为逻辑边界,用于隔离预生产和生产环境的流量和数据。为了获得更强的隔离和更多对生产凭据和访问的控制,应将单独的生产虚拟私有云托管在不同的生产 AWS 账户中。

有人可能会问为什么我们应该有这么多环境,以及预生产和生产集群的分离。简单的答案是,需要一个预生产集群来在发布到生产集群之前测试代码。在 Intuit,我们使用我们的 QA 环境进行集成测试,将 E2E 环境作为其他服务测试预发布功能的稳定环境。如果你正在进行多分支并发开发,你也可以为每个分支配置额外的预生产测试环境。

使用 Kubernetes 进行配置管理的关键优势在于,由于它使用不可变的可移植镜像 Docker 容器,因此不同环境之间的部署差异仅限于命名空间配置、特定环境的属性以及如缓存或数据库之类的应用程序依赖。预生产测试可以验证你的服务代码的正确性,而生产集群中的阶段环境可以用来验证你的应用程序依赖的正确性。

预生产和生产集群应遵循相同的最佳安全实践和操作严谨性。安全问题可以在开发周期的早期被发现,如果预生产集群以与生产相同的标准运行,则不会打断开发者的生产力。

3.2 Git 策略

使用单独的 Git 仓库来存储你的 Kubernetes 清单(即配置),将配置与应用程序源代码分离,出于以下原因强烈推荐:

  • 它提供了应用代码和应用配置的清晰分离。有时你可能希望修改清单而不触发整个 CI 构建。例如,如果你只是想增加 Deployment 规范中副本的数量,你可能不想触发构建。

    应用程序配置与机密信息 在 GitOps 中,应用程序配置通常不包括机密信息,因为使用 Git 存储机密是一种不良做法。在第七章中详细讨论了处理敏感信息(密码、证书等)的几种方法。

  • 审计日志更清晰。出于审计目的,仅持有配置的仓库将有一个更干净的 Git 历史记录,记录了所做的更改,而没有来自常规开发活动的检查入的噪音。

  • 您的应用程序可能由多个 Git 仓库构建的服务组成,但作为单个单元部署。通常,微服务应用程序由具有不同版本控制和发布周期的服务组成(例如 ELK、Kafka 和 Zookeeper)。将清单存储在单个组件的源代码仓库中可能没有意义。

  • 访问是分开的。正在开发应用程序的开发人员可能不是可以/应该推送到生产环境的人,无论是故意还是无意。拥有单独的仓库允许将提交访问权限给予源代码仓库,而不是应用程序配置仓库,后者可以保留给更精选的团队成员组。

  • 如果您正在自动化您的 CI 管道,将清单更改推送到同一个 Git 仓库可能会触发构建作业和 Git 提交触发的无限循环。有一个单独的仓库来推送配置更改可以防止这种情况发生。

对于您的代码仓库,您可以使用您喜欢的任何分支策略(例如 GitFlow),因为这只是用于您的 CI。对于您的配置仓库(将用于您的 CD),您需要根据您的组织规模和工具考虑以下策略。

3.2.1 单分支(多个目录)

使用单分支策略时,主分支将始终包含每个环境中使用的确切配置。所有环境将有一个默认配置,并在单独的环境特定目录中定义环境特定覆盖。单分支策略可以很容易地通过 Kustomize 等工具(第 3.3 节)得到支持。

图片

图 3.9 单分支策略将有一个主分支和每个环境的子目录。每个子目录将包含环境特定的覆盖。

在我们的 CI/CD 示例中,我们将为 qa、e2e、stage 和 prod 环境设置特定的覆盖目录。每个目录将包含特定环境的设置,例如副本数量、CPU 和内存请求/限制。

图片

图 3.10 使用 qal、e2e、stage 和生产子目录的示例。每个子目录将包含副本数量、CPU 和内存请求/限制等覆盖。

3.2.2 多分支

在多分支策略中,每个分支都相当于一个环境。这里的优势是每个分支都将拥有该环境的精确清单,而无需使用任何工具,如 Kustomize。每个分支还将拥有独立的提交历史,以便进行审计跟踪和需要时回滚。缺点是,由于工具如 Kustomize 不与 Git 分支一起工作,因此环境之间将无法共享通用配置。

图 3.11

图 3.11 在多分支策略中,每个分支都相当于一个环境。每个分支将包含确切的清单,而不是覆盖。

可能可以在多个分支之间合并常见的基础设施更改。假设需要将新资源添加到所有环境中。在这种情况下,该资源可以首先添加到 QA 分支并测试,然后在适当的测试完成后合并( cherry-picked)到每个后续分支。

3.2.3 多仓库与单仓库对比

如果你在一个只有一个敏捷团队的初创环境中,你可能不希望(或需要)多个仓库的复杂性。所有代码都可以在一个代码仓库中,所有部署配置在一个部署仓库中。

然而,如果你在一个拥有数十(或数百)开发者的企业环境中,你可能会希望拥有多个仓库,以便团队可以相互解耦,并且各自以自己的速度运行。例如,组织内部的不同团队将会有不同的代码节奏和发布流程。如果使用单一配置仓库,某些功能可能需要几周时间才能完成,但需要等待计划中的发布。这可能会导致将功能交付给最终用户的时间延迟,并发现潜在的代码问题。回滚也存在问题,因为一个代码缺陷将需要回滚每个团队的所有更改。

图 3.12

图 3.12 单仓库是一个包含多个项目的单一 Git 仓库。在多仓库中,每个项目都将有一个专门的 Git 仓库。

使用多个仓库的另一个考虑因素是根据功能组织应用程序。如果仓库专注于离散的可部署功能,那么在团队之间(如重组后)移动这些功能的责任将更容易。

3.3 配置管理

正如我们在 3.1 节中的教程和练习中看到的,环境配置管理可以像为每个环境创建一个目录那样简单,该目录包含应部署的所有资源的 YAML 清单。这些 YAML 清单中的所有值都可以硬编码为特定环境的特定值。要部署,你运行kubectl apply -f <directory>

然而,现实情况是,以那种方式管理多个配置很快就会变得难以控制且容易出错。如果你需要添加一个新资源呢?你需要确保将那个资源添加到每个环境中的 YAML 文件中。如果那个资源需要特定属性(如副本)在不同环境中具有不同的值呢?你需要仔细地在所有正确的文件中做出所有正确的自定义设置。

已经开发出一些工具来满足配置管理的需求。我们将在本节稍后回顾每个更受欢迎的配置管理工具。但首先,让我们讨论一下在选择特定工具时应考虑的因素。

良好的 Kubernetes 配置工具具有以下特性:

  • 声明式—配置是明确的、确定的,并且不依赖于系统。

  • 可读性—配置是以易于理解的方式编写的。

  • 灵活—该工具有助于促进并不会妨碍你完成想要做的事情。

  • 可维护性—工具应促进重用和可组合性。

Kubernetes 配置管理之所以如此具有挑战性,有几个原因:看似简单的部署应用程序的行为可能会有截然不同,甚至相反的要求,而单个工具很难满足所有这些要求。想象以下用例:

  • 集群管理员将第三方现成应用程序(如 WordPress)部署到他们的集群中,对这些应用程序几乎没有或没有进行定制。对于这个用例,最重要的标准是能够轻松地从上游源接收更新,并以尽可能快速和无缝的方式升级他们的应用程序(新版本、安全补丁等)。

  • 一个软件即服务(SaaS)应用开发者将他们定制的应用程序部署到一个或多个环境中(开发、测试、Prod-West、Prod-East)。这些环境可能分布在不同的账户、集群和命名空间中,它们之间有细微的差别,因此配置重用至关重要。对于这个用例,从代码库中的 Git 提交到完全自动地将应用程序部署到每个环境,并以简单和可维护的方式管理环境,这一点非常重要。这些开发者对他们的发布版本没有语义版本化的兴趣,因为他们可能每天部署多次。主要版本、次要版本和补丁版本的概念最终对他们应用程序没有意义。

如您所见,这些是完全不同的用例,而且往往一个在某个方面表现优异的工具在其他方面处理得并不好。

3.3.1 Helm

无论你喜欢它还是讨厌它,Helm 作为第一个配置工具,是 Kubernetes 生态系统的一个基本组成部分,你很可能在某个时候通过运行helm install安装了某些东西。

关于 Helm,需要注意的是,它是一个自称为 Kubernetes 的包管理器,并不声称自己是配置管理工具。然而,由于许多人使用 Helm 模板正是为了这个目的,它属于这次讨论的范围。这些用户不可避免地会维护几个 values.yaml 文件,每个环境一个(例如 values-base.yaml、values-prod.yaml 和 values-dev.yaml),然后以这种方式参数化他们的图表,以便可以在图表中使用特定环境的值。这种方法或多或少是可行的,但它使得模板难以管理,因为 Go 模板是扁平的,需要支持每个环境的每个可能的参数,这最终会在整个模板中充斥着{{-if / else}}开关。

优点:

  • 有图表就能解决。毫无疑问,Helm 最大的优势是其出色的图表仓库。最近,我们需要运行一个高可用的 Redis,不使用持久卷,用作临时缓存。能够将redis-ha图表直接放入你的命名空间,设置persistentVolume.enabled:false,并将你的服务指向它,这已经有人完成了在 Kubernetes 集群上可靠运行 Redis 的艰苦工作。

缺点:

  • Go 模板—“看看那个美丽而优雅的 Helm 模板!”没有人这么说。众所周知,Helm 模板存在可读性问题。我们相信 Helm 3 对 Lua 的支持将解决这个问题,但在此之前,嗯,我们希望你喜欢花括号。

  • 复杂的 SaaS CD 管道—对于 SaaS CI/CD 管道,假设你正按照预期的方式使用 Helm(即通过运行helm install/upgrade),你的管道中的自动化部署可能会有几种不同的方式。在最佳情况下,从你的管道中部署将像这样简单

    $ docker push mycompany/guestbook:v2
    $ helm upgrade guestbook --set guestbook.image.tag=v2
    

    但在最坏的情况下,如果现有的图表参数无法支持你想要的清单更改,你将不得不经历一系列的步骤:打包一个新的 Helm 图表,增加其语义版本,将其发布到图表仓库,然后使用 Helm 升级重新部署。在 Linux 世界中,这类似于构建一个新的 RPM,将其发布到 Yum 仓库,然后运行yum install,所有这些只是为了让你可以将闪亮的新 CLI 放入/usr/bin。虽然这种模型对于打包和分发来说效果很好,但对于定制 SaaS 应用程序的部署来说,这是一个不必要的复杂且迂回的方式。因此,许多人选择运行helm template并将输出管道传输到kubectl apply,但到那时,你最好使用一些专门为此目的设计的其他工具。

  • 默认非声明性—如果你曾经在任何 Helm 部署中添加了 --set param=value,很抱歉地告诉你,你的部署过程不是声明性的。这些值只记录在 Helm ConfigMap 的另一个世界(以及可能你的 Bash 历史记录)中,所以希望你在某处记录了这些值。如果你需要从头开始重新创建你的集群,这远远不是理想的。一个稍微好一点的方法是将所有参数记录在一个新的自定义 values.yaml 文件中,你可以将其存储在 Git 中并使用 -f my-values.yaml 进行部署。然而,当你从 Helm 稳定版部署 OTS 图表时,这会变得很烦人,你没有明显的地方来存储与相关图表并排的 values.yaml。我想出的最佳解决方案是将上游图表作为依赖项创建一个新的虚拟图表。尽管如此,我们还没有找到一种标准的方法来在管道中使用单行命令更新 values.yaml 中的参数,除非运行 sed

使用 Helm 配置预生产和生产 manifest

在这个练习中,我们将使用 Helm 管理我们在本章早期部署的 guestbook 应用程序在不同环境中的配置。

Helm 使用以下目录结构来组织其图表。

列表 3.4 Helm 图表目录结构

├──  Chart.yaml            ❶
├──  templates             ❷
│   └──    guestbook.yaml
├──  values-prod.yaml      ❸
└──  values-qa.yaml        ❸

❶ Chart.yaml 是 Helm 对图表的描述。

❷ 一个模板目录,当与值结合时,将生成有效的 Kubernetes manifest 文件

❸ 图表的各个配置值,可用于特定环境的配置

Helm 模板文件使用文本模板语言来生成 Kubernetes YAML。Helm 模板文件看起来像 Kubernetes YAML,但文件中散布着模板变量。因此,即使是基本的 Helm 模板文件最终也会看起来像这样。

列表 3.5 示例 Helm 模板

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "sample-app.fullname" . }}
  labels:
    {{- include "sample-app.labels" . | nindent 4 }}
spec:
  selector:
    matchLabels:
      {{- include "sample-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "sample-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        {{- with .Values.environmentVars }}
          env:
            {{- toYaml . | nindent 12 }}
        {{- end }}

如你所见,Helm 模板的可读性不是很好。但它们非常灵活,因为最终生成的 YAML 可以根据用户的意愿进行任何方式的定制。

最后,当使用 Helm 图表自定义特定环境时,会创建一个包含用于该环境的值的特定环境值文件。例如,对于此应用程序的生产版本,值文件可能看起来像这样。

列表 3.6 示例 Helm 值

# Default values for sample-app.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

image:
  repository: gitopsbook/sample-app
  tag: "v0.2"                        ❶

nameOverride: "sample-app"
fullnameOverride: "sample-app"

podAnnotations: {}

environmentVars: [                   ❷
  {
    name: "DEBUG",
    value: "true"
  }
]

❶ 覆盖默认的图表 appVersion 的镜像标签

❷ 将 DEBUG 环境变量设置为 true

最终的 qa manifest 可以使用以下命令安装到 qa-heml 命名空间下的 minikube:

$ kubectl create namespace qa-helm
$ helm template . --values values.yaml | kubectl apply -n qa-helm -f -
deployment.apps/sample-app created
$ kubectl get all -n qa-helm
NAME                               READY   STATUS       RESTARTS    AGE
pod/sample-app-7595985689-46fbj    1/1     Running      0           11s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample-app         1/1     1            1           11s

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-app-7595985689   1         1         1       11s

练习 3.2

在前面的教程中,我们使用 Helm 对 QA 和生产环境中的 guestbook 镜像标签进行了参数化。为每个 guestbook 部署所需的副本数量添加额外的参数化。将 QA 的副本数设置为 1,将生产环境的副本数设置为 3。

3.3.2 Kustomize

Kustomize 是围绕 Brian Grant 优秀的关于声明式应用程序管理的论文中描述的设计原则创建的。^(3) Kustomize 的受欢迎程度急剧上升,自从它开始以来,已经合并到 kubectl 中。无论你是否同意它的合并方式,不言而喻的是,Kustomize 应用程序现在将成为 Kubernetes 生态系统中的永久性主流,并将成为用户在配置管理中倾向于选择的默认选项。是的,成为 kubectl 的一部分有帮助!

好处:

  • 无参数和模板——Kustomize 应用程序非常易于推理,实际上,看起来非常愉快。它几乎可以接近 Kubernetes YAML,因为您用来执行定制的覆盖只是 Kubernetes YAML 的子集。

坏处:

  • 无参数和模板——使 Kustomize 应用程序如此易于阅读的同一特性也可能使其受限。例如,我最近试图让 Kustomize CLI 为自定义资源设置一个镜像标签而不是 Deployment,但未能成功。Kustomize 确实有一个名为 vars 的概念,它看起来很像参数,但不知何故并不是,并且只能在 Kustomize 的授权白名单字段路径中使用。我们觉得这是那些解决方案,尽管让困难的事情变得容易,但最终让容易的事情变得困难的时候之一。

使用 Kustomize 配置预生产和生产环境

在这个练习中,我们将使用一个示例应用程序,我们将在第三部分中使用它,并使用 Kustomize 来部署它。

我们将把我们的配置文件组织成以下目录结构。

列表 3.7 Kustomize 目录结构

├──  base                          ❶
│   ├── deployment.yaml
│   └── kustomization.yaml
└──  envs
 ├── prod                       ❷
 │   └── kustomization.yaml
 └── qa                         ❸
 ├── debug.yaml
 └── kustomization.yaml

❶ 基础目录包含将在不同环境中共享的通用配置。

❷ envs/prod 目录包含生产环境的配置。

❸ envs/qa 目录包含 QA 环境的配置。

基础目录中的清单包含所有环境共有的资源。在这个简单的示例中,我们有一个单一的 Deployment 资源。

列表 3.8 基础部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - command:
        - /app/sample-app
        image: gitopsbook/sample-app:REPLACEME     ❶
        name: sample-app
        ports:
        - containerPort: 8080

❶ 基础配置中定义的镜像是不相关的。由于本例中的子覆盖环境将覆盖此值,因此此版本的镜像永远不会被部署。

要将基础目录作为其他环境的基础,目录中必须存在 kustomization.yaml 文件。以下是最简单的 kustomization.yaml 文件。它仅将 guestbook.yaml 列为构成应用程序的单个资源。

列表 3.9 基础 Kustomization

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- deployment.yaml

现在我们已经建立了 kustomize 基础目录,我们可以开始定制我们的环境。为了定制和修改特定环境的资源,我们定义了一个包含所有要应用于基础资源的补丁和定制的 overlay 目录。我们的第一个 overlay 是 envs/qa 目录。在这个目录中还有一个 kustomization.yaml,它指定了应该应用于基础的补丁。以下两个列表提供了一个示例,说明了一个qa overlay

  • 设置一个不同的 guestbook 镜像以部署到新的标签(v0.2)

  • 向 guestbook 容器添加环境变量DEBUG=true

列表 3.10 QA 环境 Kustomization

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

bases:                        ❶
- ../../base

patchesStrategicMerge:
- debug.yaml                  ❷

images:                       ❸
- name: gitopsbook/sample-app
  newTag: v0.2

❶ bases 引用包含共享配置的“base”目录。

❷ debug.yaml 是对一个 Kustomize 补丁的引用,该补丁将修改 sample-app Deployment 对象并设置 DEBUG 环境变量。

❸ 如果在基础镜像中定义了具有不同标签或镜像仓库的容器镜像,则❸ images 将覆盖这些镜像。此示例使用 v0.2 覆盖了 REPLACEME 镜像标签。

注意,Kustomize 补丁看起来非常类似于实际的 Kubernetes 资源。这是因为它们实际上是它们的完整版本。

列表 3.11 QA 环境 debug 补丁

apiVersion: apps/v1          ❶
kind: Deployment
metadata:
  name: sample-app
spec:
  template:
    spec:
      containers:
      - name: sample-app     ❷
        env:                 ❸
        - name: DEBUG
          value: "true"

❶ apiVersion group(apps)、kind(Deployment)和 name(sample-app)是关键信息,它们告知 Kustomize 此补丁应该应用于基础资源中的哪个资源。

❷ 使用名称字段来识别哪个容器将具有新的环境变量。

❸ 最后,我们在 QA 环境中定义了我们想要的新的 DEBUG 环境变量。

在所有这些完成后,我们运行kustomize build envs/qa。这生成了 QA 环境的最终、渲染的清单。

列表 3.12 Kustomize 构建 envs/qa

$ kustomize build envs/qa
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - command:
        - /app/sample-app
        env:
        - name: DEBUG                       ❶
          value: "true"
        image: gitopsbook/sample-app:v0.2   ❷
        name: sample-app
        ports:
        - containerPort: 8080

❶ 添加了 DEBUG 环境变量。

❷ 将镜像标签设置为 v0.2。

最终的 qa 清单可以使用以下命令安装到 qa 命名空间中的 minikube:

$ kubectl create namespace qa
$ kustomize build envs/qa | kubectl apply -n qa -f -
# kubectl get all -n qa
NAME                              READY   STATUS    RESTARTS   AGE
pod/sample-app-7595985689-46fbj   1/1     Running   0          11s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample-app   1/1     1            1           11s

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-app-7595985689   1         1         1       11s

练习 3.3

在之前的教程中,我们使用 Kustomize 对 QA 和 Prod 环境的 guestbook 镜像标签进行了参数化。

提示:创建一个 replica_count.yaml 补丁文件。

为每个 sample-app 部署所需的副本数量添加额外的参数化。将副本数设置为 QA 的 1 个和 Prod 的 3 个。将 QA 环境部署到 qa 命名空间,将 Prod 环境部署到 prod 命名空间。

练习 3.4

目前,Prod 环境运行的是 sample app 的 v0.1 版本,而 QA 运行的是 v0.2 版本。假设我们已经完成了 QA 的测试。更新 customization.yaml 文件,将版本 v0.2 提升到 Prod 环境中运行。更新 prod 命名空间中的 Prod 环境。

3.3.3 Jsonnet

Jsonnet 是一种语言,而不是真正的工具。此外,它的使用并不特定于 Kubernetes(尽管它是由 Kubernetes 推广的)。最好的方式是将 Jsonnet 视为一个超级 JSON,结合了一种合理的模板化方法。Jsonnet 结合了你希望用 JSON 做的所有事情(注释、文本块、参数、变量、条件、文件导入),而没有你讨厌的 go/Jinja2 模板化中的任何东西,并添加了你甚至不知道需要或想要的功能(函数、面向对象、混入)。它以声明性和密封(代码即数据)的方式完成所有这些。

当我们查看一个基本的 Jsonnet 文件时,它看起来非常类似于 JSON,这是有道理的,因为 Jsonnet 是 JSON 的超集。所有有效的 JSON 都是有效的 Jsonnet。但请注意,在我们的示例中,我们还可以在文档中有注释。如果你在 JSON 中管理配置的时间足够长,你将立即理解这有多么有用!

列表 3.13 基本 Jsonnet

{
   // Look! It's JSON with comments!
   "apiVersion": "apps/v1",
   "kind": "Deployment",
   "metadata": {
      "name": "nginx"
   },
   "spec": {
      "selector": {
         "matchLabels": {
            "app": "nginx"
         }
      },
      "replicas": 2,
      "template": {
         "metadata": {
            "labels": {
               "app": "nginx"
            }
         },
         "spec": {
            "containers": [
               {
                  "name": "nginx",
                  "image": "nginx:1.14.2",
                  "ports": [
                     {
                        "containerPort": 80
                     }
                  ]
               }
            ]
         }
      }
   }
}

继续我们的示例,让我们看看我们如何开始利用简单的 Jsonnet 特性。减少重复并更好地组织代码/配置的最简单方法之一是使用变量。在我们的下一个示例中,我们在 Jsonnet 文件顶部声明了一些变量(名称、版本和副本)并在整个文档中引用这些变量。这允许我们在一个单一、可见的地方进行更改,而无需扫描整个文档以查找所有需要相同更改的其他区域,这在大型文档中尤其容易出错。

列表 3.14 变量

local name = "nginx";
local version = "1.14.2";
local replicas = 2;
{
   "apiVersion": "apps/v1",
   "kind": "Deployment",
   "metadata": {
      "name": name
   },
   "spec": {
      "selector": {
         "matchLabels": {
            "app": name
         }
      },
      "replicas": replicas,
      "template": {
         "metadata": {
            "labels": {
               "app": name
            }
         },
         "spec": {
            "containers": [
               {
                  "name": name,
                  "image": "nginx:" + version,
                  "ports": [
                     {
                        "containerPort": 80
                     }
                  ]
               }
            ]
         }
      }
   }
}

最后,在我们的高级示例中,我们开始利用 Jsonnet 一些独特且强大的特性:函数、参数、引用和条件。下一个示例将开始展示 Jsonnet 的强大之处。

列表 3.15 高级 Jsonnet

function(prod=false) {                      ❶
   "apiVersion": "apps/v1",
   "kind": "Deployment",

   "metadata": {
      "name": "nginx"
   },
   "spec": {
      "selector": {
         "matchLabels": {
            "app": $.metadata.name          ❷
         }
      },
      "replicas": if prod then 10 else 1,   ❸
      "template": {
         "metadata": {
            "labels": {
               "app": $.metadata.name
            }
         },
         "spec": {
            "containers": [
               {
                  "name": $.metadata.name,
                  "image": "nginx:1.14.2",
                  "ports": [
                     {
                        "containerPort": 80
                     }
                  ]
               }
            ]
         }
      }
   }
}

与之前的示例不同,配置被定义为 Jsonnet 函数而不是一个普通的 Jsonnet 对象。这允许配置声明输入并接受来自命令行的参数。prod 是函数的一个布尔参数,默认值为 false。

我们可以在不使用变量的情况下自我引用文档的其他部分。

❸ 副本的数量是基于条件设置的。

练习 3.5

在列表 3.15 中,尝试运行以下两个命令并比较输出:

$ jsonnet advanced.jsonnet

$ jsonnet --tla-code prod=true advanced.jsonnet

Jsonnet 中有更多语言特性,我们甚至还没有触及到其能力的一角。Jsonnet 在 Kubernetes 社区中并没有得到广泛的应用,这很遗憾,因为在这里描述的所有工具中,Jsonnet 无疑是最强大的配置工具,也是为什么有多个衍生工具建立在它之上的原因。解释 Jsonnet 可以做什么本身就是一本书的内容,这就是为什么我们鼓励你阅读 Databricks 如何使用 Jsonnet 与 Kubernetes 结合,以及 Jsonnet 的优秀教程。4

好的:

  • 极其强大——很少会遇到无法用一些简洁优雅的 Jsonnet 片段表达的情况。使用 Jsonnet,你不断地发现新的方法来最大化重用并避免重复。

坏处:

  • 不是 YAML——这可能是由于不熟悉而引起的问题,但大多数人面对非平凡的 Jsonnet 文件时都会经历一定程度的认知负荷。同样地,你需要运行 Helm 模板来验证你的 Helm 图表是否产生了你期望的结果,你也需要类似地运行 jsonnet --yaml-stream guestbook.jsonnet 来验证你的 Jsonnet 是否正确。好消息是,与 Go 模板不同,Go 模板可能会因为一些错误的空白而产生语法上不正确的 YAML,这些错误类型在 Jsonnet 构建过程中会被捕获,并且产生的输出保证是有效的 JSON/YAML。

ksonnet 不要与 Jsonnet 混淆,ksonnet 是一个已废弃的工具,用于创建可以部署到 Kubernetes 集群的应用程序清单。然而,ksonnet 已不再维护,应考虑其他工具。

3.3.4 配置管理总结

与所有事物一样,使用每个工具都有其权衡。表 3.1 展示了这些特定工具在配置管理中我们重视的四个品质方面的比较总结。

表 3.1 功能比较

Helm Kustomize Jsonnet
声明式 公平 优秀 优秀
可读性 优秀 一般
灵活性 优秀 优秀
可维护性 一般 优秀 优秀

注意,本章讨论的工具只是当时在 Kubernetes 社区中最受欢迎的工具。这是一个不断发展的领域,还有许多其他配置管理工具可以考虑。

3.4 持久环境与短暂环境对比

持久环境是指始终可用的环境。例如,生产环境始终需要可用,以便服务不会中断。在持久环境中,资源(内存、CPU、存储)将永久承诺以实现始终在线的可用性。通常,E2E 是内部集成的持久环境,而 Prod 是生产流量的持久环境。

短暂的环境是临时环境,其他服务不依赖于这些环境。短暂的环境也不需要永久性地分配资源。例如,预发布环境用于测试新代码的生产就绪性,测试完成后不需要保留。另一个用例是预览拉取请求的正确性,以确保只有好的代码被合并到主分支。在这种情况下,将创建一个临时环境,包含拉取请求的更改,以便进行测试。一旦所有测试完成,PR 环境将被删除,并且只有当所有测试通过时,PR 更改才允许合并回主分支。

由于持久环境将被其他人使用,持久环境中的缺陷可能会中断其他人,可能需要回滚以恢复正确的功能。使用 GitOps 和 Kubernetes,回滚只是通过 Git 重新应用之前的配置。Kubernetes 将检测清单中的更改,并将环境恢复到之前的状态。

Kubernetes 使得环境回滚变得一致且简单,但对于其他资源如数据库呢?由于用户数据存储在数据库中,我们不能简单地回滚数据库到之前的快照,这会导致用户数据的丢失。与 Kubernetes 中的滚动更新部署类似,新旧版本的代码需要与滚动更新兼容。在数据库的情况下,数据库模式需要向后兼容,以避免回滚期间的中断和用户数据的丢失。在实践中,这意味着只能添加(不能删除)列,并且列定义不能更改。模式更改应与其他变更管理框架(如 Flyway,^(5)) 控制在一起,以便数据库更改也可以遵循 GitOps 流程。

摘要

  • 环境是代码部署和执行特定目的的地方。

  • 每个环境都将拥有自己的访问控制、网络、配置和依赖。

  • 选择环境粒度的因素包括发布独立性、测试边界、访问控制和隔离。

  • Kubernetes 命名空间是实现环境的一种自然结构。

  • 由于命名空间等同于环境,部署到特定环境只是指定了目标命名空间。

  • 可以通过网络策略控制环境间的流量。

  • 预生产和生产应遵循相同的最佳安全实践和操作强度。

  • 高度推荐将 Kubernetes 清单的 Git 仓库与代码的 Git 仓库分开,以便环境更改独立于代码更改。

  • 单个分支与像 Kustomize 这样的工具配合使用进行覆盖时效果很好。

  • 单一仓库对于初创公司来说配置工作效果很好;多仓库对于大型企业来说效果很好。

  • Helm 是一个包管理器。

  • Kustomize 是一个内置的配置管理工具,是 kubectl 的一部分。

  • Jsonnet 是一种用于 JSON 模板的语言。

  • 选择合适的配置管理工具应基于以下标准:声明式、可读性、灵活性和可维护性。

  • 可持久环境总是可供他人使用,而短暂环境则用于短期测试和预览。


1.kubernetes.io/docs/tutorials/stateless-application/guestbook.

2.mng.bz/8N1g.

3.github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/declarative-application-management.md.

4.mng.bz/NYmX.

5.flywaydb.org/.

4 管道

本章涵盖了

  • GitOps CI/CD 管道中的阶段

  • 推送代码、镜像和环境

  • 回滚

  • 合规性管道

本章基于第三章学到的概念,讨论了如何创建管道来构建和测试应用程序代码,然后将其部署到不同的环境。您还将了解不同的推广策略以及如何撤销、重置或回滚应用程序更改。

我们建议您在阅读本章之前阅读第一章、第二章和第三章。

4.1 CI/CD 管道中的阶段

持续集成(CI)是一种软件开发实践,其中所有开发者将代码更改合并到中央仓库(Git)中。使用 CI,每次代码更改(提交)都会触发针对给定仓库的自动构建和测试阶段,并向进行了更改的开发者提供反馈。与传统的 CI 相比,GitOps 的主要区别在于,在构建和测试阶段成功完成后,GitOps 的 CI 管道还会更新应用程序清单以包含新的镜像版本。

持续交付(CD)是自动整个软件发布过程的实践。CD 包括基础设施提供和部署。使 GitOps CD 与传统 CD 不同的地方是使用 GitOps 操作员来监控清单更改并编排部署。只要 CI 构建完成且清单已更新,GitOps 操作员就会负责最终的部署。

注意:请参阅第 2.5 节以了解 GitOps CI/CD 和操作员基础知识。

本章深入探讨了全面的 CI/CD 管道及其在软件开发中的重要性。CI/CD 管道是一系列阶段的集合,每个阶段执行特定任务以实现以下目标:

  • 生产力—在开发周期的早期阶段,就设计、编码风格和质量提供有价值的反馈,而不需要切换上下文。代码审查、单元测试、代码覆盖率、代码分析、集成测试和运行时漏洞检测是设计、质量和安全反馈给开发者的关键阶段。

  • 安全性—检测代码和组件漏洞,这些漏洞是潜在利用的攻击面。漏洞扫描可以检测第三方库中的安全问题。运行时漏洞扫描可以检测代码的运行时安全问题。

  • 缺陷逃逸—减少客户交互失败和昂贵的回滚。新版本通常提供新功能或增强现有功能。如果功能不能提供正确的功能,后果将是客户不满和潜在的收入损失。单元测试在模块级别验证正确性,而功能测试在两个或更多模块之间验证正确性。

  • 可扩展性—在生产发布之前发现可扩展性问题。单元测试和功能测试可以验证功能的正确性,但这些阶段无法检测内存泄漏、线程泄漏或资源竞争等问题。金丝雀发布是一种通过生产流量和依赖关系部署新版本以检测可扩展性问题的方法。

  • 上市时间—更快地将功能交付给客户。使用完全自动化的 CI/CD 管道,无需耗时的人工工作来部署软件。代码可以在通过管道中所有阶段后立即发布。

  • 报告—持续改进的洞察和可审计性的指标。CI/CD 管道执行时间以分钟与小时相比,可能会影响开发者的行为和生产力。持续监控和改进管道执行时间可以显著提高团队生产力。收集和存储构建指标也是许多监管审计的必要条件。请参阅 CI 和 CD 指标发布阶段以获取详细信息。

4.1.1 GitOps 持续集成

图 4.1 展示了基于第二章中 GitOps CI/CD(图 2.9)的全面 CI 管道构建。灰色框是完整 CI 解决方案的新阶段。本节将帮助您根据您的复杂性、成熟度和合规性要求规划并设计相关的阶段。

图片

图 4.1 这些是 GitOps CI 管道中的阶段。白色框来自图 2.9 中的 GitOps CI 管道,灰色框是构建完整 CI 管道的附加阶段。

预构建阶段

以下阶段也称为 静态分析 阶段。它们是在代码构建和打包成 Docker 镜像之前对代码进行手动和自动扫描的组合。

拉取请求/代码审查

所有 CI/CD 管道都应该始终从拉取请求开始,这允许代码审查以确保设计和实现之间的一致性,并捕捉其他潜在错误。如第一章所述,代码审查还有助于分享最佳实践、编码标准和团队凝聚力。

漏洞扫描

开源库可以在不进行定制开发的情况下提供许多功能,但这些库也可能包含漏洞、缺陷和许可问题。集成开源库扫描工具,如 Nexus 漏洞扫描器,可以在开发周期的早期检测已知漏洞和许可问题,并通过升级库或使用替代库来修复问题。

注意:在快速变化的软件行业中,老话“如果没问题,就别修”已经不再适用。每天都会发现开源库中的漏洞,因此尽快升级是谨慎的做法,以避免被利用。在 Intuit,我们大量使用开源软件来加速我们的开发。我们不再进行年度安全审计,而是在 CI 流程中添加了一个漏洞扫描步骤,以便在开发周期中定期检测和解决安全问题。

代码分析

虽然手动代码审查对于设计和实现的一致性非常好,但对于编码标准、重复代码和代码复杂度问题(即代码异味)来说,更适合使用自动化的代码检查或代码分析工具,如 SonarQube。这些工具不是代码审查的替代品,但它们可以更有效地捕捉到一些日常问题。

注意:期望在部署新代码之前解决每一个小问题是不现实的。使用 SonarQube 等工具时,趋势数据也会被报告,这样团队可以看到他们的代码质量是如何随时间变好或变差的,以便在问题恶化之前解决这些问题。

练习 4.1

为了防止你的开源库中已知的安全问题,你需要在 CI/CD 流程中计划哪些阶段?

为了确保实现与设计相匹配,你需要在 CI/CD 流程中计划哪些阶段?

构建阶段

在静态分析之后,是构建代码的时候了。除了构建和创建可部署的工件(即 Docker 镜像)之外,单元测试(模块测试)及其有效性(代码覆盖率)是构建过程中的重要部分。

构建

构建阶段通常在项目源代码的实际编译之前开始下载依赖库。(例如 Python 和 Node.js 这样的脚本语言不需要编译。)对于像 Java、Ruby 和 Go 这样的编译语言,代码会被编译成字节码/机器二进制代码,使用相应的编译器。此外,生成的二进制代码及其依赖库需要被打包成一个可部署单元(例如 Java 中的 jar 或 war 文件)以便部署。

注意:根据我们的经验,构建过程中最耗时的部分是下载依赖项。强烈建议你在构建系统中缓存依赖项以减少构建时间。

单元测试

单元测试是为了验证一小段代码是否按照预期执行。单元测试不应依赖于单元测试之外的代码。单元测试主要关注测试单个单元的功能,并不揭示不同模块交互时出现的问题。在单元测试期间,外部调用通常会被“模拟”以消除依赖性问题并减少测试执行时间。

注意:在单元测试中,模拟对象可以模拟复杂、真实对象的行为,因此当真实对象不切实际或不可能纳入单元测试时非常有用。2 根据我们的经验,模拟是一个必要的投资,并将为团队节省时间(更快的测试执行)和精力(调试不可靠的测试)。

代码覆盖率

代码覆盖率衡量的是自动化单元测试覆盖的代码百分比。代码覆盖率测量简单地确定在测试运行中哪些代码语句被执行,哪些代码语句未被执行。通常,代码覆盖率系统会对源代码进行仪器化,并收集运行时信息以生成关于测试套件代码覆盖率的报告。

代码覆盖率是开发过程中反馈循环的关键部分。随着测试的开发,代码覆盖率突出了代码中可能未得到充分测试的方面,并需要额外的测试。这个循环会一直持续到覆盖率达到某个指定的目标。覆盖率应遵循 80-20 规则,因为提高覆盖率值变得困难,而回报减少。覆盖率测量不能替代彻底的代码审查和编程最佳实践。

注意:仅提高代码覆盖率百分比可能会导致错误的行为,实际上可能会降低质量。代码覆盖率衡量的是正在执行的代码行百分比,但不衡量代码的正确性。带有部分断言的 100%代码覆盖率不会实现单元测试的质量目标。我们的建议是,随着时间的推移,专注于增加单元测试和代码覆盖率,而不是专注于绝对代码覆盖率数字。

Docker 构建

Docker 镜像是 Kubernetes 的部署单元。一旦代码构建完成,你可以通过创建 Dockerfile 并执行docker build命令来创建具有唯一镜像 ID 的 Docker 镜像,用于你的构建工件。Docker 镜像应遵循其独特的命名约定,并且每个版本都应该用唯一的版本号进行标记。此外,你还可以在此阶段运行 Docker 镜像扫描工具,以检测基础镜像和依赖项中潜在的安全漏洞问题。

Docker 标签和 Git 哈希 由于 Git 为每个提交创建一个唯一的哈希值,因此建议使用 Git 哈希来标记 Docker 镜像,而不是创建一个任意的版本号。除了唯一性之外,每个 Docker 镜像都可以通过 Git 哈希轻松回溯到 Git 仓库的历史记录,以确定 Docker 镜像中的确切代码。请参阅第 2.5.2 节以获取更多信息。

Docker 推送

新构建的 Docker 镜像需要发布到 Docker 注册表^(3),以便 Kubernetes 可以编排最终的部署。Docker 注册表是一个无状态的、高度可扩展的服务端应用程序,用于存储和分发 Docker 镜像。对于内部开发,最佳实践是托管一个私有注册表,以便对镜像的存储位置有更紧密的控制。请参阅第六章了解如何最佳地托管安全的私有 Docker 注册表。

练习 4.2

规划所需的构建阶段(s),以便可以测量代码覆盖率指标。

如果镜像带有最新的标签,你能告诉我 Docker 镜像中包含了什么吗?

GitOps CI 阶段

在传统的 CI 中,管道将在构建阶段结束后结束。在 GitOps 中,需要额外的 GitOps 特定阶段来更新清单以实现最终的部署。请参阅图 4.1。

Git 克隆配置仓库

假设你的 Kubernetes 配置存储在一个单独的仓库中,此阶段执行 Git 克隆操作,将 Kubernetes 配置克隆到构建环境,以便后续阶段更新你的清单。

更新清单

一旦你在构建环境中有了清单,你可以使用配置管理工具(如 Kustomize)使用新创建的镜像 ID 更新清单。根据你的部署策略,一个或多个环境特定的清单会更新为新镜像 ID。有关 Kustomize 的更多信息,请参阅第三章。

Git 提交和推送

一旦清单使用新的镜像 ID 更新,最后一步是将清单提交回 Git 仓库。此时 CI 管道完成。你的 GitOps 操作员会检测到清单中的变化,并将更改部署到 Kubernetes 集群。以下是一个使用 Git 命令实现三个阶段的示例。

构建后阶段

在 GitOps CI 的所有工作完成后,还需要额外的阶段来收集指标以实现持续改进和审计报告,并通知团队构建状态。

发布 CI 指标

CI 指标应存储在单独的数据存储中

  • 构建问题——开发团队需要相关数据来对构建失败或单元测试失败的问题进行分类。

  • CI——长的构建时间可能会影响工程团队的行为和生产力。代码覆盖率的减少可能会导致生产缺陷增加。拥有历史构建时间和代码覆盖率指标使团队能够监控趋势,减少构建时间,并增加代码覆盖率。

  • 合规性要求——对于 SOC2 或 PCI 要求,需要维护诸如测试结果、谁进行了发布以及发布了什么等信息,这些信息需要从 14 个月到 7 年不等的时间。

注意:对于大多数构建系统来说,维护超过一年的构建历史记录是昂贵的。一个替代方案是将构建指标导出到外部存储,如 S3,以满足合规性和报告要求。

构建通知

对于 CI/CD 部署,大多数团队更喜欢“无消息即好消息”模型,这意味着如果所有阶段都成功,他们不需要担心构建状态。在构建出现问题时,团队应立即得到通知,以便他们可以获取反馈并纠正问题。此阶段通常通过团队消息或电子邮件系统实现,以便在 CI/CD 管道完成后立即通知团队。

练习 4.3

如果没有构建通知,开发团队应采取哪些步骤来确定构建状态?

80%的代码覆盖率是好是坏?

提示趋势。

如果 CI/CD 管道通常需要一小时来运行,那么开发者在这段时间内可以做哪些其他任务?如果 CI/CD 管道只需要 10 分钟呢?

图 4.2 预构建将涉及代码审查和静态分析。构建完成后,GitOps CI 将更新清单(随后由 GitOps 操作员部署)。

练习 4.4

GitOps 阶段有两个挑战,这个练习将提供解决这些问题的步骤。

  1. 您应该使用哪个 Git 用户来跟踪清单更新和提交?

  2. 你如何处理可能同时更新仓库的并发 CI 构建?

在您开始之前,请将仓库github.com/gitopsbook/resources.git进行分叉。这个练习将假设您的本地计算机是构建系统。

  1. 从 Git 克隆仓库。我们将假设文件夹 chapter-04/exercise4.4 中的 guestbook.yaml 是您的应用程序清单:

    $ git clone https://github.com/<your repo>/resources.git
    
  2. 使用git config指定提交者的用户电子邮件和姓名。根据您的需求,您可以使用服务账户或实际的提交者账户:

    $ git config --global user.email <committerEmail>
    $ git config --global user.name <commmitterName>
    

    注意请参考第 4.2.1 节以创建强大的身份保证。指定的用户也需要存在于您的远程 Git 仓库中。

  3. 假设新的 Docker 镜像具有 Git 标签zzzzzz。我们将使用标签zzzzzz更新清单:

    $ sed -i .bak 's+acme.co.3m/guestbook:.*$*+acme.com/guestbook:zzzzzz+' chapter-04/exercise4.4/guestbook.yaml
    

    注意为了简化,在这个练习中我们将使用 cd 来更新清单。通常,您应该使用像 Kustomize 这样的配置工具来更新镜像 ID。

  4. 接下来,我们将将更改提交到清单:

    $ git commit -am "update container for QAL during build zzzzzz"
    
  5. 由于仓库可能被其他人更新,我们将运行 Git rebase以拉取任何新的提交到我们的本地分支:

    $ git pull --rebase https://<GIT_USERNAME>:<GIT_PASSWORD>@<your repo> master
    
  6. 现在我们准备将更新的清单推回仓库,并让 GitOps 操作员执行其部署魔法:

    $ git push https://<GIT_USERNAME>:<GIT_PASSWORD>@<your repo> master
    Enumerating objects: 9, done.
    Counting objects: 100% (9/9), done.
    Delta compression using up to 16 threads
    Compressing objects: 100% (7/7), done.
    Writing objects: 100% (7/7), 796 bytes | 796.00 KiB/s, done.
    Total 7 (delta 4), reused 0 (delta 0)
    remote: Resolving deltas: 100% (4/4), completed with 2 local objects.
    remote: This repository moved. Please use the new location:
    remote:   https://github.com/gitopsbook/resources.git
    To https://github.com/gitops-k8s/resources
       eb1a692..70c141c  master -> master
    

4.1.2 GitOps 持续交付

图 4.3 展示了基于 GitOps CI/CD(第二章)的综合 CD 管道构建。灰色框是完整 CD 解决方案的新阶段。根据您的复杂性、成熟度和合规性要求,您可以挑选和选择适合您业务的相关阶段。

图 4.3 这是 GitOps CD 管道中的阶段。白色框来自图 2.9 中的 GitOps CI 管道,灰色框是构建完整 CI 管道的附加阶段。

注意图中的阶段表示逻辑顺序。在实践中,GitOps 阶段由 Git 仓库中的清单更改触发,并独立于其他阶段执行。

GitOps CD 阶段

这些是 GitOps 操作员根据清单更改执行的逻辑阶段,以进行部署。

Git clone 配置仓库

GitOps 操作员检测仓库中的更改,并执行 Git clone 以获取 Git 仓库的最新清单。

发现清单

GitOps 操作员还会确定 Kubernetes 中的清单与 Git 仓库的最新清单之间的任何差异。如果没有差异,GitOps 操作员将在此处停止。

Kubectl apply

如果 GitOps 操作员确定 Kubernetes 清单与 Git 仓库清单之间的差异,则 GitOps 操作员将使用kubectl apply命令将新清单应用到 Kubernetes 上。

注意请参阅第二章以获取详细信息。

部署后阶段

部署镜像后,我们可以测试新代码与依赖项和运行时漏洞的端到端。

集成测试

集成测试是一种测试类型,用于检查不同的模块是否正确协同工作。一旦镜像在 QA 环境中部署,集成测试就可以跨多个模块和其他外部系统(如数据库和服务)进行测试。集成测试旨在发现当不同模块交互以执行无法通过单元测试覆盖的高级功能时出现的问题。

注意由于 GitOps 操作员在管道之外处理部署,部署可能在实际测试执行之前未完成。练习 4.6 讨论了使集成测试与 GitOps CD 一起工作的步骤。

运行时漏洞

运行时漏洞传统上通过渗透测试来检测。渗透测试,也称为 pen testing 或道德黑客,是测试计算机系统、网络或 Web 应用程序以寻找攻击者可能利用的安全漏洞的实践。典型的运行时漏洞包括 SQL 注入、命令注入或发布不安全的 cookie。而不是在生产系统中进行渗透测试(这既昂贵又事后的),在执行集成测试时可以使用像 Contrast^(4)这样的代理工具对 QA 环境进行配置,以在开发周期早期检测任何运行时漏洞。

发布 CD 指标

CD 指标应存储在单独的数据存储中,

  • 运行时问题——开发团队需要相关数据来对部署、集成测试失败或运行时漏洞进行分类。

  • 合规性要求——对于 SOC2 或 PCI 要求,需要维护有关测试结果、谁进行了发布以及发布了什么等信息,这些信息可能需要从 14 个月到 7 年不等。

练习 4.5

设计一个可以检测 SQL 注入漏洞的 CD 管道。

提示 SQL 注入是一种运行时漏洞。

练习 4.6

本练习涵盖如何确保更改应用到 Kubernetes 并成功完成部署。我们将使用 frontend-deployment.yaml 作为我们的清单文件。

列表 4.1 frontend-deployment.yaml

apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: frontend
  labels:
    app: guestbook
spec:
  selector:
    matchLabels:
      app: guestbook
      tier: frontend
  replicas: 3
  template:
    metadata:
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google-samples/gb-frontend:v4
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # Using `GET_HOSTS_FROM=dns` requires your cluster to
          # provide a dns service. As of Kubernetes 1.3, DNS is a built-in
          # service launched automatically. However, if the cluster you are
            using
          # does not have a built-in DNS service, you can instead
          # access an environment variable to find the master
          # service's host. To do so, comment out the 'value: dns' line
            above, and
          # uncomment the line below:
          # value: env
        ports:
        - containerPort: 80
© 2020 GitHub, Inc.
  1. 运行 kubectl diff 以确定 frontend-deployment.yaml 清单是否已应用到 Kubernetes。exit status 1 表示清单不在 Kubernetes 中:

    $ kubectl diff -f frontend-deployment.yaml
    diff -u -N /var/folders/s5/v3vpb73d6zv01dhxknw4yyxw0000gp/T/LIVE-057767296/apps.v1.Deployment.gitops.frontend /var/folders/s5/v3vpb73d6zv01dhxknw4yyxw0000gp/T/MERGED-602990303/apps.v1.Deployment.gitops.frontend
    --- /var/folders/s5/v3vpb73d6zv01dhxknw4yyxw0000gp/T/LIVE-057767296/apps.v1.Deployment.gitops.frontend  2020-01-06 14:23:40.000000000 -0800
    +++ /var/folders/s5/v3vpb73d6zv01dhxknw4yyxw0000gp/T/MERGED-602990303/apps.v1.Deployment.gitops.frontend        2020-01-06 14:23:40.000000000 -0800
    @@ -0,0 +1,53 @@
    +apiVersion: apps/v1
    +kind: Deployment
    ...
    +status: {}
    exit status 1
    
  2. 将清单应用到 Kubernetes:

    $ kubectl apply -f frontend-deployment.yaml
    
  3. 重新运行 kubectl diff,你应该看到清单已应用且退出状态为 0。Kubernetes 将在清单更新后开始部署:

    $ kubectl diff -f frontend-deployment.yaml
    
  4. 重复运行 kubectl rollout status,直到部署完全完成:

    $ kubectl rollout status deployment.v1.apps/frontend
    Waiting for deployment "frontend" rollout to finish: 0 of 3 updated replicas are available...
    

在生产中,您将使用一个带有循环和 kubectl rollout status 命令的休眠的脚本来自动化这项工作。

列表 4.2 DeploymentWait.sh

#!/bin/bash
RETRY=0                                                       ❶
STATUS="kubectl rollout status deployment.v1.apps/frontend"   ❷

until $STATUS || [ $RETRY -eq 120 ]; do                       ❸
  $STATUS                                                     ❹
  RETRY=$((RETRY + 1))                                        ❺
  sleep 10                                                    ❻
done

❶ 将 RETRY 变量初始化为 0

❷ 定义 kubectl rollout status 命令

❸ 当 kubectl rollout status 为真或 RETRY 变量等于 120 时退出循环。此示例等待最多 20 分钟(120 x 10 秒)。

❹ 执行 kubectl rollout status 命令

❺ 将 RETRY 变量增加 1

❻ 等待 10 秒

4.2 驱动推广

现在我们已经涵盖了 CI/CD 管道中的所有阶段,我们可以看看 CI/CD 管道如何自动化代码、镜像和环境的推广。自动化环境推广的主要好处是使您的团队能够更快、更可靠地将新代码部署到生产环境中。

4.2.1 代码 vs. 清单 vs. 应用配置

在第三章,我们开始讨论与 GitOps 一起工作时对 Git 策略的考虑。我们讨论了将代码和 Kubernetes 清单保存在单独的仓库中以实现更灵活的部署选择、更好的访问控制和可审计性的好处。我们应该在哪里维护针对特定环境依赖的应用配置,例如数据库连接或分布式缓存?维护环境配置有几种选择:

  • Docker 镜像—所有环境特定的应用配置文件都可以打包在 Docker 镜像中。这种策略最适合快速打包带有所有环境应用配置的遗留应用程序(打包到 Kubernetes 中)。缺点是创建新环境需要完整的构建,并且不能重用现有镜像。

  • ConfigMaps—ConfigMaps 是 Kubernetes 的原生资源,存储在 Kubernetes 的 etcd 数据库中。缺点是如果更新 ConfigMap,Pod 需要重新启动。

  • Config repo—将应用程序配置存储在单独的仓库中可以达到与 ConfigMap 相同的效果。额外的优点是 Pod 可以动态地获取应用程序配置中的更改(例如,在 Java 中使用 Spring Cloud Config)。

注意:给定由代码、清单和应用程序配置组成的 环境,任何仓库更改中的错误都可能导致生产中断。对代码、清单或应用程序配置仓库的任何更改都应遵循严格的拉取请求/代码审查,以确保正确性。

练习 4.7

假设代码、清单和应用程序配置保存在单个仓库中。您需要更新环境中副本的清单,从 X 更新到 Y。您如何仅对 GitOps 部署进行更改提交,而不需要构建另一个镜像?

4.2.2 代码和图像推广

推广被定义为“提升地位或等级的行为或事实。”代码推广意味着代码更改被提交到特性分支,并通过拉取请求与主分支合并(推广)。一旦 CI 构建并发布了一个新镜像,它随后将由 GitOps CD 操作员(第 4.1 节)部署(推广)。

注意 想象你正在构建一个包含加法和减法功能的数学库。你首先会克隆主分支以创建一个名为 addition 的新分支。一旦你完成了加法功能的实现,你将代码提交到 addition 分支,并生成一个拉取请求以合并到主分支。GitOps CI 将创建一个新镜像并更新清单。GitOps 操作员最终将部署新镜像。然后你可以重复此过程以实现减法功能。

代码仓库分支策略对图像推广过程有直接影响。接下来,我们将讨论单分支与多分支策略在图像推广过程中的优缺点。

单分支策略

单分支 策略 也被称为特性分支工作流程.^(5) 在此策略中,主分支是官方项目历史。开发者创建短生命周期的特性分支进行开发。一旦开发者完成特性,通过 PR 流程将更改合并回主分支。当 PR 被批准时,CI 构建将被触发,将新代码打包成新的 Docker 镜像。

使用单分支开发,CI 构建的每个镜像都可以推广到任何环境并用于生产发布。如果您需要回滚,可以使用 Docker 注册表中任何较旧的镜像重新部署。这种策略非常出色,如果您的服务可以独立部署(即微服务)并允许您的团队进行频繁的生产发布,则效果最佳。

图片

图 4.4 在单分支策略中,只有主分支是长期存在的,所有特性分支都是短期存在的。主分支只有一个 CI 构建。

多分支策略

多分支策略通常用于需要紧密协调外部依赖和发布计划的大型项目。多分支策略有很多变体。在本讨论中,我们使用 Gitflow 工作流程^(6)作为示例。在 Gitflow 中,develop 分支包含官方项目历史,master 分支包含最后的发布历史。CI 构建为 develop 分支配置 C。对于功能开发,开发者在功能完成时创建短期功能分支并将更改合并到 develop 分支。

当计划发布时,会从最新开发分支创建一个短期发布的分支,并在该分支中进行测试和错误修复,直到代码准备好进行生产部署。因此,需要配置一个单独的 CI 构建来从发布分支构建新的 Docker 镜像。一旦发布完成,所有更改都将合并到开发和主分支。

与单分支策略不同,只有发布 CI 构建的镜像可以部署到生产环境。所有来自 develop 分支的镜像只能用于预生产测试和集成。如果需要回滚,只能使用从发布分支构建的镜像。如果需要向前推进(或热修复)生产问题,必须从 master 分支创建 hotfix 分支,并为 hotfix 镜像创建单独的 CI 构建。

图片

图 4.5 在多分支策略中,将有多个长期分支,每个长期分支都将有自己的 CI 管道。在这个例子中,长期分支是 develop、master 和 hotfix。

练习 4.8

您的服务需要在特定日期发布一个功能。使用多分支策略,设计一个成功的发布。

使用单分支策略,为特定日期设计一个成功的发布。

提示:功能标志。

4.2.3 环境推广

在本节中,我们将讨论如何将镜像从预生产环境推广到我们的生产环境。拥有多个环境和推广更改的原因是将尽可能多的测试转移到较低的环境(左移),这样我们可以在开发周期的早期检测和纠正错误。

环境推广有两个方面。第一个方面是环境基础设施。正如我们在第三章中讨论的,Kustomize 是推广新镜像到每个环境的首选配置管理工具,GitOps 操作员将完成剩余的工作!

第二个方面是应用程序本身。由于 Docker 镜像是不变的二进制文件,注入特定环境的应用程序配置将配置应用程序以适应特定环境。

在第三章中,我们介绍了质量保证(QA)、端到端(E2E)、阶段和产品(Prod)环境,并讨论了每个环境在开发周期中的独特目的。让我们回顾一下对每个环境都重要的阶段。

质量保证(QA)

QA 环境是第一个运行新镜像以验证代码在执行过程中与外部依赖项正确性的环境。以下阶段对于 QA 环境至关重要:

  • 功能测试

  • 运行时漏洞

  • 发布指标

E2E

E2E 环境主要用于其他应用程序测试现有或预发布功能。E2E 环境应像 Prod 环境一样进行监控和操作,因为 E2E 中断可能会潜在地阻塞其他服务的 CI/CD 管道。对于 E2E 环境,可选的验证阶段(使用功能测试子集进行健全性测试)适用于确保其正确性。

阶段

阶段环境通常将连接到生产依赖项,以确保在生产发布之前所有生产依赖项都已就绪。例如,新版本可能依赖于数据库模式更新或消息队列配置,然后才能部署。使用预发布环境进行测试可以保证所有生产依赖项都是正确的,并避免生产问题。

PROD

金丝雀发布

金丝雀发布^(7)是一种技术,通过在将更改缓慢推出到整个基础设施并使其对所有用户可用之前,先将其推出到一小部分用户,以降低在生产中引入新软件版本的风险。我们将在第五章深入讨论金丝雀发布及其如何在 Kubernetes 中实现。

发布票据

考虑到应用服务的复杂性和分布式特性,发布票据在生产事故发生时对您的生产支持团队至关重要。发布票据将帮助生产事故团队了解是谁部署/更改了什么,以及如果需要的话,应该回滚到什么状态。此外,发布跟踪对于合规性要求是必须的。

4.2.4 将所有内容整合在一起

本章从定义 GitOps CI/CD 管道开始,用于构建 Docker 镜像,验证镜像并将其部署到环境中。然后我们讨论了环境升级,其中每个环境都重要的阶段。图 4.6 展示了完整的 Gitops CI/CD 管道在环境升级方面的示例。

串行或并行 尽管该图将每个阶段描述为串行,但许多现代管道可以支持并行运行阶段。例如,通知和指标发布是互斥的,可以并行执行以减少管道执行时间。

4.3 其他管道

CI/CD 管道主要用于你的“快乐路径”部署,其中你的更改按预期工作,生活很美好,但我们都知道那不是现实。时不时地,生产环境中会出现意外问题,我们需要回滚环境或发布热补丁来减轻问题。在软件即服务(SaaS)中,最高优先级是尽快从生产问题中恢复;在大多数情况下,需要回滚到之前已知良好的状态以实现及时恢复。

对于特定的合规标准,如支付卡行业(PCI),^(8) 生产版本需要第二个人的批准,以确保没有一个人可以发布到生产环境的更改。PCI 还要求进行年度审计,强制报告批准记录。鉴于我们的原始 CI/CD 管道将根据单个 PR 将更改部署到生产环境,我们需要增强我们的管道以支持合规性和可审计性。

图片

图 4.6 一个 CI/CD 管道完成单分支开发的环环相扣。对于多分支开发,在环境提升之前需要额外的阶段来提升分支。

4.3.1 回滚

即使你在 CI/CD 管道中规划了所有审查、分析和测试阶段,消除所有生产问题仍然是不可能的。根据问题的严重程度,你可以选择继续应用修复或回滚到之前已知良好的状态。由于我们的生产环境由清单(包含 Docker 镜像 ID)和环境的配置文件组成,回滚过程可能会回滚应用配置、清单或两个仓库。使用 GitOps,我们的回滚过程再次由 Git 变更控制,GitOps 运营商将负责最终的部署。(如果应用配置仓库也需要回滚,你只需先回滚应用配置中的更改,然后再回滚清单,因为只有清单更改可以触发部署,而不是应用配置更改。)Git Revert 和 Git Reset^(9) 是在 Git 中回滚更改的两种方式。

Git Revert git revert 命令可以被视为一个撤销命令。它不是从项目历史中删除提交,而是找出如何反转由提交引入的更改,并附加一个包含结果逆内容的新的提交。这防止 Git 丢失历史记录,这对于你修订历史的完整性(合规性和可审计性)以及可靠的协作至关重要。请参考图 4.7 顶部的图形说明。

Git 重置 git reset 命令根据其调用方式执行一些操作。它修改索引(所谓的暂存区),或者更改当前分支头指向的提交。此命令可能会更改现有历史记录(通过更改分支引用的提交)。请参考图 4.7 底部的图形说明。由于此命令可以更改历史记录,我们不推荐在合规性和可审计性很重要的情况下使用 git reset

图 4.7 git revert 的工作方式类似于具有历史保留的撤销命令。另一方面,git reset 修改历史记录以重置更改。

图 4.8 是一个回滚管道的示例。该管道将从 git revertgit commit 开始,将清单回滚到之前已知的好状态。从revert提交生成拉取请求后,审批者可以批准并合并 PR 到清单主分支。再次,GitOps 操作员将执行其魔法并根据更新的清单回滚应用程序。

图 4.8 回滚管道涉及将清单回滚到之前的提交,生成一个新的拉取请求,并最终批准该 PR。

练习 4.9

本练习将介绍将镜像 ID 从“zzzzzz”回滚到“yyyyyy”所需的步骤。本练习将使用 git revert 以保留提交历史。在开始之前,请将仓库 github.com/gitopsbook/resources.git 分支。本练习将假设您的本地计算机是构建系统:

  1. 从 Git 克隆仓库。我们将假设文件夹 chapter-04/exercise4.9 中的 guestbook.yaml 是您的应用程序清单:

    $ git clone https://github.com/<your repo>/resources.git
    
  2. 使用 git config 指定提交者用户电子邮件和姓名。根据您的需求,您可以使用服务帐户或实际的提交者帐户:

    $ git config --global user.email <committerEmail>
    $ git config --global user.name <commmitterName>
    

    注意 请参考第六章以创建强大的身份保证。指定的用户还需要存在于您的远程 Git 仓库中。

  3. 让我们回顾 Git 历史:

    $ git log --pretty=oneline
    eb1a692029a9f4e4ae65de8c11135c56ff235722 (HEAD -> master) guestbook with image hash zzzzzz
    95384207cbba2ce46ee2913c7ea51d0f4e958890 guestbook with image hash yyyyyy
    4dcb452a809d99f9a1b5a24bde14116fad9a4ded (upstream/master, upstream/HEAD, origin/master) exercise 4.6 and 4.10
    e62161043d5a3360b89518fa755741c6be7fd2b3 exercise 4.6 and 4.10
    74b172c7703b3b695c79f270d353dc625c4038ba guestbook for exercise 4.4
    ...
    
  4. 从历史记录中,您将看到用于镜像哈希 zzzzzz 的“eb1a692029a9f4e4ae65de8c11135c56ff23 5722”。如果我们回滚此提交,清单将具有镜像哈希 yyyyyy:

    $ git revert eb1a692029a9f4e4ae65de8c11135c56ff235722
    
  5. 现在我们准备通过推送到仓库并让 GitOps 操作员执行其部署魔法来推送清单的回滚:

    $ git push https://<GIT_USERNAME>:<GIT_PASSWORD>@<your repo> master
    

4.3.2 合规性管道

合规性管道本质上需要确保对生产发布的第二人称批准,并记录谁、何时以及发布了什么。在我们的案例中,我们为预生产开发和生产发布创建了单独的 CI/CD 管道。预生产 CI/CD 管道的最后阶段将生成一个 PR 来更新生产清单的最新镜像 ID。当审批者想要将特定镜像发布到生产时,他/她只需简单地批准相应的 PR,GitOps 操作员将更新 Prod 环境。图 4.9 说明了合规性 CI/CD 和生产发布管道的阶段。

练习 4.10

此练习将创建一个包含您的生产清单更新的新分支,并创建一个 pull request 回推到远程仓库以供批准。在您开始之前,请分叉仓库github.com/gitops-k8s/resources.git。此练习将假设您的本地计算机是构建系统:

  1. 安装用于创建 pull request 的hub10 CLI。hub CLI 工具与 GitHub 协作,分叉分支并创建 pull request:

    $ brew install hub
    
  2. 从 Git 克隆仓库。我们将假设文件夹 chapter-04 中的 guestbook.yaml 是您的应用程序清单:

    $ git clone https://github.com/<your repo>/resources.git
    
  3. 使用git config指定提交者的用户电子邮件和姓名。根据您的需求,您可以使用服务账户或实际的提交者账户:

    $ git config --global user.email <committerEmail>
    $ git config --global user.name <commmitterName>
    
    

    图片

    图 4.9 在合规性管道中,有一个从预生产 CI/CD 管道分离的生产管道。在 CI/CD 管道的末尾,有一个阶段用于生成一个包含新镜像 ID 的新 PR 到生产清单仓库。任何获得批准的 PR 都将部署到生产环境中。

    注意:请参阅第六章第 6.2 节,以创建强大的身份保证。用户指定的内容也需要存在于您的远程 Git 仓库中。

  4. 创建一个新的发布分支:

    $ git checkout -b release
    
  5. 假设新的 Docker 镜像具有 Git 标签 zzzzzz。我们将更新清单以包含标签 zzzzzz:

    $ sed -i .bak 's+acme.com/guestbook:.*$*+acme.com/guestbook:zzzzzz+' chapter-04/exercise4.10/guestbook.yaml
    
  6. 接下来,我们将将更改提交到清单:

    $ git commit -am "update container for production during build zzzzzz"
    
  7. 由于仓库可能被其他人更新,我们将运行git rebase以拉取任何新的提交到我们的本地分支:

    $ git pull --rebase https://<GIT_USERNAME>:<GIT_PASSWORD>@<your repo> master
    
  8. 分叉仓库:

    $ hub fork --remote-name=origin
    
  9. 将更改推送到您的新远程仓库:

    $ git push https://<GIT_USERNAME>:<GIT_PASSWORD>@<your repo> release
    
  10. 为您刚刚推送的主题分支打开一个 pull request。这也会为您打开一个编辑器来编辑 pull request 描述。一旦保存描述,此命令将创建 pull request:

    $ hub pull-request -p 
    Branch 'release' set up to track remote branch 'release' from 'upstream'.
    Everything up-to-date
    
  11. 现在,您可以回到您的远程仓库,并在浏览器中审查和批准 pull request。

摘要

  • git rebase可以减轻由于并发管道执行而引起的冲突。

  • 持续运行kubectl rollout status可以确保部署完成并准备好在 GitOps CD 中进行功能测试。

  • 将代码、清单和应用程序配置保存在单独的仓库中将为您提供最佳灵活性,因为基础设施和代码可以独立发展。

  • 单分支策略非常适合小型项目,因为每个 CI 镜像都可以直接升级到生产环境,无需进行分支管理。

  • 多分支策略非常适合具有外部依赖和发布计划的大型项目。缺点是必须维护多个长期分支,并且只能将发布镜像部署到生产环境。

  • 一个完整的 CI/CD 管道将包括环境升级、静态分析、构建、单元/集成测试以及发布构建指标/通知的阶段。

  • 使用 GitOps 回滚生产环境只是将清单回滚到之前的提交(镜像 ID)。

  • GitOps 管道自然支持合规性和可审计性,因为所有更改都以带有批准和历史的拉取请求的形式生成。


  1. en.wikipedia.org/wiki/Code_smell.

2.en.wikipedia.org/wiki/Mock_object.

3.docs.docker.com/registry/.

4.www.contrastsecurity.com/.

5.www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow.

6.www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow.

7.martinfowler.com/bliki/CanaryRelease.html.

8.en.wikipedia.org/wiki/Payment_card_industry.

9.www.atlassian.com/git/tutorials/undoing-changes/git-reset.

10.github.com/github/hub.

5 部署策略

本章涵盖

  • 理解为什么 ReplicaSet 不适合 GitOps

  • 理解为什么 Deployment 是声明式的,并且适合 GitOps

  • 使用原生 Kubernetes 资源和 Argo Rollouts 实现蓝绿部署

  • 使用原生 Kubernetes 资源和 Argo Rollouts 实现金丝雀部署

  • 使用 Argo Rollouts 实现渐进式交付

在前几章中,我们专注于 Kubernetes 资源的初始部署。启动一个新应用可能就像部署一个具有所需 Pod 副本数的 ReplicaSet,并创建一个 Service 将传入流量路由到所需的 Pod。但现在想象一下,你有数百(或数千)客户每秒向你的应用发送数千个请求。你如何安全地部署应用的新版本?如果应用最新版本包含一个关键错误,你如何限制损害?在本章中,你将了解可以使用 Kubernetes 实现的机制和技术,这些机制和技术对于在企业或互联网规模上运行应用至关重要。

我们建议你在阅读本章之前阅读第 1、2 和 3 章。

5.1 部署基础

在 Kubernetes 中,你可以使用仅包含 PodSpec 的清单来部署单个 Pod。假设你想部署一组具有保证可用性的相同 Pod。在这种情况下,你可以定义一个包含 ReplicaSet^(1) 的清单来维护在任何给定时间运行的稳定副本 Pod 集合。ReplicaSet 使用选择器定义,该选择器指定如何识别 Pod、要维护的副本数和 PodSpec。ReplicaSet 通过根据需要创建和删除 Pod 来维护所需的副本数(图 5.1)。

ReplicaSet 不是声明式的 ReplicaSet 不是声明式的,因此不适合 GitOps。第 5.1.1 节将详细解释 ReplicaSet 的工作原理以及为什么它不是声明式的。尽管 ReplicaSet 不是声明式的,但它仍然是一个重要的概念,因为 Deployment 资源使用 ReplicaSet 对象来管理 Pod。

Deployment^(2) 是一个更高级的概念,它利用多个 ReplicaSet(图 5.1)为 Pod 提供声明式更新,以及许多其他有用的功能(第 5.1.2 节)。一旦你在 Deployment 清单中定义了所需状态,Deployment 控制器将持续观察实际状态,并在它们不同的情况下将现有状态更新到所需状态。

图 5.1 部署利用一个或多个 ReplicaSet 为应用提供声明式更新。每个 ReplicaSet 根据 PodSpec 和副本数管理实际的 Pod 数量。

5.1.1 为什么 ReplicaSet 不适合 GitOps

ReplicaSet 清单包括一个选择器,指定如何识别它管理的 Pod,要维护的 Pod 副本数,以及一个 Pod 模板,定义如何创建新的 Pod 以满足所需的副本数。然后 ReplicaSet 控制器将根据清单中指定的所需数量创建和删除 Pod。正如我们之前提到的,ReplicaSet 不是声明式的,我们将通过教程深入理解 ReplicaSet 是如何工作的以及为什么它不是声明式的:

  1. 部署具有两个 Pod 的 ReplicaSet。

  2. 在清单中更新镜像 ID。

  3. 应用更新的清单并观察 ReplicaSet。

  4. 在清单中更新replicas为 3。

  5. 应用更新的清单并观察 ReplicaSet。

如果 ReplicaSet 是声明式的,你应该看到三个具有更新后的镜像 ID 的 Pod。

首先,我们将应用 ReplicaSet.yaml,这将创建两个具有镜像 ID argoproj/rollouts-demo:blue的 Pod 和一个服务:

$ kubectl apply -f ReplicaSet.yaml
replicaset.apps/demo created
service/demo created

图片

图 5.2 应用 ReplicaSet.yaml 将创建两个具有镜像argoproj/rollouts-demo:blue的 Pod。它还将创建一个demo服务,将流量导向 Pod。

列表 5.1  ReplicaSet.yaml

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: demo
  labels:
    app: demo
spec:
  replicas: 2                                ❶
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: argoproj/rollouts-demo:blue   ❷
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  labels:
    app: demo
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: demo

❶ 将副本数量从 2 更新到 3

❷ 将镜像标签从 blue 更新到 green

部署完成后,我们将更新镜像 ID 从blue更新到green并应用更改:

$ sed -i .bak 's/blue/green/g' ReplicaSet.yaml
$ kubectl apply -f ReplicaSet.yaml
replicaset.apps/demo configured
service/demo unchanged

接下来,我们可以使用kubectl diff命令来验证清单已在 Kubernetes 中更新。然后我们可以运行kubectl get Pods并期望看到镜像标签green而不是blue

$ kubectl diff -f ReplicaSet.yaml
$ kubectl get pods -o jsonpath="{.items[*].spec.containers[*].image}"
argoproj/rollouts-demo:blue argoproj/rollouts-demo:blue

尽管已应用更新的清单,但现有的 Pod 并没有更新到green。让我们将replicas数量从 2 更新到 3 并应用清单:

$ sed -i .bak 's/replicas: 2/replicas: 3/g' ReplicaSet.yaml
$ kubectl apply -f ReplicaSet.yaml
replicaset.apps/demo configured
service/demo unchanged
$ kubectl get pods -o jsonpath="{.items[*].spec.containers[*].image}"
argoproj/rollouts-demo:blue argoproj/rollouts-demo:green 
argoproj/rollouts-demo:blue
$ kubectl describe rs demo
Name:         demo
Namespace:    default
Selector:     app=demo
Labels:       app=demo
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"apps/v1","kind":"ReplicaSet","metadata":{"annotations":{},
                 "labels":{"app":"demo"},"name":"demo","namespace":"default"},"spe...
Replicas:     3 current / 3 desired
Pods Status:  3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:  app=demo
  Containers:
   demo:
    Image:        argoproj/rollouts-demo:green
    Port:         8080/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type   Reason           Age  From                   Message
  ----   ------           ---- ----                   -------
  Normal SuccessfulCreate 13m  replicaset-controller  Created pod: demo-gfd8g
  Normal SuccessfulCreate 13m  replicaset-controller  Created pod: demo-gxl6j
  Normal SuccessfulCreate 10m  replicaset-controller  Created pod: demo-vbx9q

图片

图 5.3 应用更改后,你会看到只有两个 Pod 正在运行蓝色镜像。如果 ReplicaSet 是声明式的,所有三个 Pod 都应该是绿色的。

意想不到的是,第三个 Pod 的镜像标签是绿色,但前两个 Pod 仍然是蓝色,因为 ReplicaSet 控制器的工作只是确保运行 Pod 的数量。如果 ReplicaSet 确实是声明式的,那么 ReplicaSet 控制器应该检测到镜像标签/副本数的变化,并将所有三个 Pod 更新为绿色。在下一节中,你将看到 Deployment 是如何工作的以及为什么它是声明式的。

5.1.2 Deployment 与 ReplicaSet 的工作方式

Deployment 是完全声明式的,并且完美地补充了 GitOps。Deployment 通过滚动更新以零停机时间部署服务。让我们通过一个教程来检查 Deployment 是如何使用多个 ReplicaSet 实现滚动更新的。

滚动更新 滚动更新允许 Deployments 通过增量更新 Pod 实例来无停机地更新。如果您的服务是无状态的且向后兼容,滚动更新效果很好。否则,您将不得不考虑其他部署策略,如蓝绿部署,这将在 5.1.3 节中介绍。

让我们想象一个现实生活中的场景,看看这将如何适用。假设你为小企业运行一个支付服务来处理信用卡。该服务需要 24/7 可用,你一直在运行两个 Pod(蓝色)来处理当前流量。你注意到这两个 Pod 正在达到最大容量,因此你决定将容量扩展到三个 Pod(蓝色)以支持增加的流量。接下来,你的产品经理想要添加借记卡支持,因此你需要部署一个包含三个 Pod(绿色)的版本,且无需停机:

  1. 使用 Deployment 部署两个信用卡(蓝色)Pod。

  2. 查看 Deployment 和 ReplicaSet。

  3. replicas 从 2 更改为 3 并应用清单。

  4. 查看 Deployment 和 ReplicaSet。

  5. 更新清单以包含三个信用和借记卡(绿色)Pod。

  6. 在三个 Pod 变为绿色时,查看 Deployment 和 ReplicaSet。

图片

图 5.4 在本教程中,你将最初部署两个蓝色 Pod。然后你将更新/应用清单以包含三个副本。最后,你将更新/应用清单以包含三个绿色 Pod。

让我们从创建初始 Deployment 开始。正如你从列表 5.2 中可以看到,YAML 实际上与列表 5.1 几乎相同,只是第 2 行将 ReplicaSet 更改为 Deployment:

$ kubectl apply -f deployment.yaml
deployment.apps/demo created
service/demo created

列表 5.2 deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  labels:
    app: demo
spec:
  replicas: 2                               ❶
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: argoproj/rollouts-demo:blue   ❷
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  labels:
    app: demo
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  Selector:                                 
    app: demo                                ❸

❶ 初始副本数设置为 2

❷ 初始镜像标签设置为 blue

❸ 服务 demo 初始设置为仅将流量发送到带有标签 app:demo 的 Pod

让我们回顾一下应用 Deployment 清单后创建的内容:

$ kubectl get pods                                                  ❶
NAME                    READY   STATUS              RESTARTS   AGE
demo-8656dbfdc5-97slx   0/1     ContainerCreating   0          7s
demo-8656dbfdc5-sbl6p   1/1     Running             0          7s

$ kubectl get Deployment                                            ❷
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
demo   2/2     2            2           61s

$ kubectl get rs                                                    ❸
NAME              DESIRED   CURRENT   READY   AGE
demo-8656dbfdc5   2         2         2       44s

$ kubectl describe rs demo-8656dbfdc5 |grep Controlled              ❹
Controlled By:  Deployment/demo

$ kubectl describe rs demo-8656dbfdc5 |grep Replicas                ❺
Replicas:       2 current / 2 desired

$ kubectl describe rs demo-8656dbfdc5 |grep Image    #F
    Image:        argoproj/rollouts-demo:blue

❶ 两个正在运行的 Pod

❷ 一个名为 demo 的 demo Deployment

❸ 一个名为 demo-8656dbfdc5 的 ReplicaSet

❹ demo Deployment 创建了名为 demo-8656dbfdc5 的 ReplicaSet。

❺ ReplicaSet demo-8656dbfdc5 使用镜像 id argoproj/rollouts-demo:blue。

如预期,我们创建并控制了一个名为 demo 的部署和一个名为 demo-8656dbfdc5 的 ReplicaSet。ReplicaSet demo-8656dbfdc5 管理着两个带有 blue 图像的 Pod 副本。接下来,我们将更新清单以包含三个副本并查看更改:

$ sed -i .bak 's/replicas: 2/replicas: 3/g' deployment.yaml

$ kubectl apply -f deployment.yaml 
deployment.apps/demo configured
service/demo unchanged

$ kubectl get pods
NAME                    READY   STATUS              RESTARTS   AGE
demo-8656dbfdc5-97slx   1/1     Running   0          98s
demo-8656dbfdc5-sbl6p   1/1     Running   0          98s
demo-8656dbfdc5-vh76b   1/1     Running   0          4s

$ kubectl get Deployment
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
demo   3/3     3            3           109s

$ kubectl get rs
NAME              DESIRED   CURRENT   READY   AGE
demo-8656dbfdc5   3         3         3       109s

$ kubectl describe rs demo-5c5575fb88 |grep Replicas
Replicas:       3 current / 3 desired

$ kubectl describe rs demo-8656dbfdc5 |grep Image
    Image:        argoproj/rollouts-demo:blue

更新后,我们应该看到相同的 Deployment 和 ReplicaSet,现在管理着三个蓝色 Pod。此时,Deployment 的外观与图 5.5 中描述的完全一致。接下来,我们将更新清单以使用绿色镜像并应用更改。由于镜像 id 已更改,Deployment 将创建第二个 ReplicaSet 来部署绿色镜像。

Deployment 和 ReplicaSets Deployment 会为每个镜像 id 创建一个 ReplicaSet,并将副本数设置为 ReplicaSet 中匹配的镜像 id 的期望值。对于所有其他 ReplicaSet,Deployment 将将这些 ReplicaSet 的副本数设置为 0 以终止所有不匹配的镜像 id 的 Pod。

图片

图 5.5 Deployment 使用 ReplicaSet V1 来维护蓝色部署。如果非蓝色镜像发生更改,Deployment 将为新的部署创建 ReplicaSet V2。

在应用更改之前,您可以使用以下命令打开一个新的终端来监控 ReplicaSet 的状态。您应该看到一个包含三个 Pod 的 ReplicaSet(蓝色):

$ kubectl get rs --watch
NAME              DESIRED   CURRENT   READY   AGE
demo-8656dbfdc5   3         3         3       60s

返回原始终端,更新部署并应用更改:

$ sed -i .bak 's/blue/green/g' deployment.yaml
$ kubectl apply -f deployment.yaml 
deployment.apps/demo configured
service/demo unchanged

现在切换到终端,您应该看到 ReplicaSet demo-8656dbfdc5(蓝色)缩小到 0,一个新的 ReplicaSet demo-6b574cb9dd(绿色)扩大到 3:

$ kubectl get rs --watch
NAME              DESIRED   CURRENT   READY   AGE
demo-8656dbfdc5   3         3         3       60s       ❶
demo-6b574cb9dd   1         0         0       0s        ❷
demo-6b574cb9dd   1         0         0       0s
demo-6b574cb9dd   1         1         0       0s
demo-6b574cb9dd   1         1         1       3s
demo-8656dbfdc5   2         3         3       102s
demo-6b574cb9dd   2         1         1       3s
demo-8656dbfdc5   2         3         3       102s
demo-6b574cb9dd   2         1         1       3s
demo-8656dbfdc5   2         2         2       102s
demo-6b574cb9dd   2         2         1       3s
demo-6b574cb9dd   2         2         2       6s
demo-8656dbfdc5   1         2         2       105s
demo-8656dbfdc5   1         2         2       105s
demo-6b574cb9dd   3         2         2       6s
demo-6b574cb9dd   3         2         2       6s
demo-8656dbfdc5   1         1         1       105s
demo-6b574cb9dd   3         3         2       6s
demo-6b574cb9dd   3         3         3       9s        ❸
demo-8656dbfdc5   0         1         1       108s
demo-8656dbfdc5   0         1         1       108s
demo-8656dbfdc5   0         0         0       108s      ❹

❶ 蓝色 ReplicaSet 开始时包含三个 Pod

❷ 绿色 ReplicaSet 增加一个 Pod

❸ 绿色 ReplicaSet 完成,包含三个 Pod

❹ 蓝色 ReplicaSet 完成,没有 Pod。

让我们回顾一下这里发生了什么。Deployment 使用第二个 ReplicaSet,demo-6b574cb9dd,来启动一个绿色的 Pod,并使用第一个 ReplicaSet,demo-8656dbfdc5,来终止一个蓝色的 Pod,如图 5.6 所示。这个过程将重复进行,直到创建所有三个绿色的 Pod,同时终止所有蓝色的 Pod。

图 5.6 部署缩小 ReplicaSet V1 并扩大 ReplicaSet V2。当过程完成后,ReplicaSet V1 将没有 Pod,而 ReplicaSet V2 将包含三个绿色的 Pod。

当我们在讨论 Deployment 时,我们还应该介绍 Deployment 中滚动更新策略的两个重要配置参数:max unavailablemax surge。让我们回顾默认设置以及根据 Kubernetes 文档它们的意义:

$ kubectl describe Deployment demo |grep RollingUpdateStrategy
RollingUpdateStrategy:  25% max unavailable, 25% max surge

Deployment 确保在更新期间只有一定数量的 Pod 处于关闭状态。默认情况下,它确保至少 75%的期望 Pod 数量处于运行状态(最大不可用 25%)。

Deployment 还确保创建的 Pod 数量不超过期望的数量。默认情况下,它确保最多有 125%的期望 Pod 数量处于运行状态(最大激增 25%)。

让我们看看它是如何实际工作的。我们将将镜像 ID 改回蓝色,并将max unavailable配置为 3,max surge也配置为 3:

$ kubectl apply -f deployment2.yaml
deployment.apps/demo configured
service/demo unchanged

现在您可以切换回带有 ReplicaSet 监控的终端:

$ kubectl get rs --watch
NAME              DESIRED   CURRENT   READY   AGE
demo-8656dbfdc5   3         3         3       60s
demo-6b574cb9dd   1         0         0       0s
demo-6b574cb9dd   1         0         0       0s
demo-6b574cb9dd   1         1         0       0s
demo-6b574cb9dd   1         1         1       3s
demo-8656dbfdc5   2         3         3       102s
demo-6b574cb9dd   2         1         1       3s
demo-8656dbfdc5   2         3         3       102s
demo-6b574cb9dd   2         1         1       3s
demo-8656dbfdc5   2         2         2       102s
demo-6b574cb9dd   2         2         1       3s
demo-6b574cb9dd   2         2         2       6s
demo-8656dbfdc5   1         2         2       105s
demo-8656dbfdc5   1         2         2       105s
demo-6b574cb9dd   3         2         2       6s
demo-6b574cb9dd   3         2         2       6s
demo-8656dbfdc5   1         1         1       105s
demo-6b574cb9dd   3         3         2       6s
demo-6b574cb9dd   3         3         3       9s
demo-8656dbfdc5   0         1         1       108s
demo-8656dbfdc5   0         1         1       108s
demo-8656dbfdc5   0         0         0       108s
demo-8656dbfdc5   0         0         0       14m
demo-8656dbfdc5   3         0         0       14m
demo-6b574cb9dd   0         3         3       13m
demo-6b574cb9dd   0         3         3       13m
demo-8656dbfdc5   3         0         0       14m
demo-6b574cb9dd   0         0         0       13m   ❶
demo-8656dbfdc5   3         3         0       14m   ❷
demo-8656dbfdc5   3         3         1       14m
demo-8656dbfdc5   3         3         2       14m
demo-8656dbfdc5   3         3         3       14m

❶ 绿色 ReplicaSet 立即变为零 Pod。

❷ 蓝色 ReplicaSet 一次扩大三个 Pod。

如您从 ReplicaSet 更改状态中看到的,ReplicaSet demo-8656dbfdc5(绿色)立即变为零 Pod,而 ReplicaSet demo-6b574cb9dd(蓝色)立即变为三个,而不是一个一个地。

列表 5.3 deployment2.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  labels:
    app: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3           ❶
      maxUnavailable: 3     ❷
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: argoproj/rollouts-demo:blue
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  labels:
    app: demo
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: demo               ❸

❶ 一次创建最多三个 Pod

❷ 一次终止最多三个 Pod。

❸ 服务 demo 将流量发送到所有带有标签 app:demo 的 Pod。

到目前为止,您可以看到 Deployment 通过利用一个 ReplicaSet 用于蓝色,另一个 ReplicaSet 用于绿色,实现了零停机时间部署。当您在本章的其余部分了解其他部署策略时,您会发现它们都是通过使用两个不同的 ReplicaSet 以类似的方式实现的,以达到预期的目标。

5.1.3 流量路由

在 Kubernetes 中,服务是一个抽象,它定义了一组 Pods 和访问它们的策略。服务针对的 Pods 集合由服务清单中的一个选择器字段确定。然后,服务将流量转发到与选择器指定的标签匹配的 Pods(也参见列表 5.2 和 5.3)。服务执行轮询负载均衡,如果底层 Pods 无状态且向后兼容,则效果很好。如果您需要为您的部署自定义负载均衡,您将需要探索其他路由替代方案。

图 5.7 服务只会将流量路由到具有匹配标签的 Pods。在这个例子中,服务蓝只会将流量路由到带有标签 blue 的 Pods。服务绿只会将流量路由到带有标签 green 的 Pods。

NGINX 入口控制器^(3) 可以用于许多用例,并支持各种平衡和路由规则。入口控制器可以被配置为前端负载均衡器,执行自定义路由,例如 TLS 终止、URL 重写或通过定义自定义规则将流量路由到任意数量的服务。图 5.8 展示了配置了规则的 NGINX 控制器,将 40% 的入站流量发送到服务蓝,60% 的入站流量发送到服务绿。

图 5.8 NGINX 入口控制器可以提供高级流量控制。在这个例子中,NGINX 入口控制器配置了一条规则,将 40% 的流量发送到服务蓝,以及另一条规则将 60% 的流量发送到服务绿。

Istio 网关^(4) 是在 Kubernetes 集群边缘运行的负载均衡器,接收传入或传出的 HTTP/TCP 连接。规范描述了一组应公开的端口、要使用的协议类型以及自定义路由配置。根据自定义配置(图 5.9),Istio 网关将传入流量定向到后端服务(40% 到服务蓝,60% 到服务绿)。

图 5.9 Istio 网关是另一个支持丰富流量路由配置的负载均衡器。在这个例子中,定义了一个自定义配置,将 40% 的流量发送到蓝服务,60% 的流量发送到绿服务。

注意:NGINX 入口控制器和 Istio 网关都是高级主题,超出了本书的范围。请参阅脚注中的链接以获取更多信息。

5.1.4 为其他策略配置 minikube

在本教程的剩余部分,您需要在您的 Kubernetes 集群中启用 NGINX 入口和 Argo Rollouts^(5) 支持。

Argo Rollouts Argo Rollouts 控制器使用 Rollout 自定义资源来为 Kubernetes 提供额外的部署策略,例如蓝绿和金丝雀部署。Rollout 自定义资源提供了与部署资源相同的功能,但增加了额外的部署策略。

使用 minikube,^(6) 你可以通过运行以下命令来启用 NGINX 入口支持:

$ minikube addons enable ingress
?? The 'ingress' addon is enabled

要在您的集群中安装 Argo Rollouts,您需要创建一个 argo-rollouts 命名空间并运行 install.yaml。对于其他环境,请参阅 Argo Rollouts 入门指南:^(7)

$ kubectl create ns argo-rollouts
namespace/argo-rollouts created
$ kubectl apply -n argo-rollouts -f https://raw.githubusercontent.com/argoproj/argo-rollouts/stable/manifests/install.yaml
customresourcedefinition.apiextensions.k8s.io/analysisruns.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/analysistemplates.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/experiments.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/rollouts.argoproj.io created
serviceaccount/argo-rollouts created
role.rbac.authorization.k8s.io/argo-rollouts-role created
clusterrole.rbac.authorization.k8s.io/argo-rollouts-aggregate-to-admin created
clusterrole.rbac.authorization.k8s.io/argo-rollouts-aggregate-to-edit created
clusterrole.rbac.authorization.k8s.io/argo-rollouts-aggregate-to-view created
clusterrole.rbac.authorization.k8s.io/argo-rollouts-clusterrole created
rolebinding.rbac.authorization.k8s.io/argo-rollouts-role-binding created
clusterrolebinding.rbac.authorization.k8s.io/argo-rollouts-clusterrolebinding created
service/argo-rollouts-metrics created
deployment.apps/argo-rollouts created

5.2 蓝绿

正如你在 5.1 节中学到的,部署的滚动更新是一种很好的更新应用程序的方法,因为你的应用程序在部署期间将使用大约相同数量的资源,实现零停机时间和对性能的最小影响。然而,由于向后不兼容或状态性,许多遗留应用程序与滚动更新不兼容。一些应用程序可能还需要立即部署新版本或快速回滚以解决可能出现的问题。

对于这些用例,蓝绿部署将是适当的部署策略。蓝绿部署通过同时完全扩展两个部署来实现这些动机,但只将入站流量定向到这两个部署中的一个。

注意 在本教程中,我们将使用 NGINX Ingress Controller 将 100% 的流量路由到蓝色或绿色部署,因为内置的 Kubernetes 服务^(8) 只操作 iptables^(9) 并不会重置到 Pods 的现有连接,因此不适合蓝绿部署。

图 5.10 初始部署将配置 NGINX Controller 以将所有流量发送到蓝色服务。蓝色服务将反过来将流量发送到蓝色 Pods。

图 5.11 在 NGINX 控制器配置更新后,所有流量都将发送到绿色服务。绿色服务将反过来将流量发送到绿色 Pods。

5.2.1 使用 Deployment 的蓝绿部署

在本教程中,我们将使用原生的 Kubernetes Deployment 和 Service 执行蓝绿部署。

注意 请参考本教程之前 5.1.4 节中如何启用入口和在您的 Kubernetes 集群中安装 Argo Rollouts 的说明。

  1. 创建蓝色部署和服务。

  2. 创建入口以将流量定向到蓝色服务。

  3. 在浏览器中查看应用程序(蓝色)。

  4. 部署绿色部署和服务,并等待所有 Pods 准备就绪。

  5. 更新入口以将流量定向到绿色服务。

  6. 再次在浏览器中查看网页(绿色)。

我们将首先通过应用 blue_deployment.yaml 创建蓝色部署:

$ kubectl apply -f blue_deployment.yaml 
deployment.apps/blue created
service/blue-service created

图 5.12 使用蓝色服务和部署以及 NGINX Ingress Controller 将流量定向到蓝色服务创建初始状态。然后创建绿色服务和部署,并通过更改 NGINX Ingress Controller 的配置将流量定向到绿色服务。

列表 5.4 blue_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: blue
  labels:
    app: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: blue
  template:
    metadata:
      labels:
        app: blue
    spec:
      containers:
      - name: demo
        image: argoproj/rollouts-demo:blue
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: blue-service
  labels:
    app: blue
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: blue
  type: NodePort

现在我们可以公开一个入口控制器,通过应用 blue_ingress.yaml,blue 服务可以通过你的浏览器访问。kubectl get ingress 命令将返回入口控制器的主机名和 IP 地址:

$ kubectl apply -f blue_ingress.yaml 
ingress.extensions/demo-ingress created
configmap/nginx-configuration created
$ kubectl get ingress
NAME           HOSTS       ADDRESS          PORTS   AGE
demo-ingress   demo.info   192.168.99.111   80      60s

注意 NGINX Ingress Controller 将仅拦截自定义规则中定义的主机名的流量。请确保你将 demo.info 及其 IP 地址添加到你的 /etc/hosts。

列表 5.5  blue_ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: demo-ingress
spec:
  rules:
  - host: demo.info                    ❶
    http:
      paths:
      - path: /                        ❷
        backend:
          serviceName: blue-service    ❸
          servicePort: 80               
---
apiVersion: v1                          
kind: ConfigMap                        ❹
metadata:
  name: nginx-configuration
data:
  allow-backend-server-header: "true"  ❺
  use-forwarded-headers: "true"        ❻

❶ 为入口控制器分配主机名 demo.info

❷ 路由所有流量

❸ 将流量路由到 80 端口的蓝色服务

❹ 用于自定义 NGINX 中头部控制的 ConfigMap

❺ 启用从后端返回头部服务器而不是通用的 NGINX 字符串

❻ 将传入的 X-Forwarded-* 头部传递给上游

一旦你创建了入口控制器、蓝色服务和部署,并更新了 /etc/hosts 中的 demo.info 和正确的 IP 地址,你就可以输入 URL demo.info 并看到蓝色服务正在运行。

注意:演示应用程序将继续在后台调用活动服务并显示右侧的最新结果。蓝色(打印中的较深灰色)是运行版本,绿色(打印中的较浅灰色)是新版本。

图片

图 5.13 HTML 页面将在柱状图中每两秒刷新一次,在气泡图中每 100 毫秒刷新一次,以显示蓝色或绿色服务响应。最初,HTML 页面将显示所有蓝色,因为所有流量都流向蓝色部署。

现在我们准备部署新的绿色版本。让我们应用 green_deployment.yaml 来创建绿色服务和部署:

$ kubectl apply -f green_deployment.yaml 
deployment.apps/green created
service/green-service created

列表 5.6  green_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: green
  labels:
    app: green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: green
  template:
    metadata:
      labels:
        app: green
    spec:
      containers:
      - name: green
        image: argoproj/rollouts-demo:green
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: green-service
  labels:
    app: green
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: green
  type: NodePort

绿色服务和部署准备就绪后,我们现在可以更新入口控制器以将流量路由到绿色服务:

$ kubectl apply -f green_ingress.yaml 
ingress.extensions/demo-ingress configured
configmap/nginx-configuration unchanged

列表 5.7  green_ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: demo-ingress
spec:
  rules:
  - host: demo.info                     
    http:
      paths:
      - path: /
        backend:
          serviceName: green-service    ❶
          servicePort: 80               
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-configuration
data:
  allow-backend-server-header: "true"
  use-forwarded-headers: "true"         

❶ 将流量路由到绿色服务而不是蓝色服务

如果你回到你的浏览器,你应该看到服务正在变绿!

图片

图 5.14 绿色部署完成后,将 NGINX Ingress Controller 更新为指向绿色部署后,HTML 页面将开始显示绿色气泡和柱状图。

如果你满意部署,你可以删除蓝色服务及其部署,或者将蓝色部署缩放到 0。

练习 5.1

你会如何以声明式的方式缩小蓝色部署?

练习 5.2

如果你想快速回滚,你应该删除部署还是将其缩放到 0?

5.2.2 使用 Argo Rollouts 的蓝绿部署

蓝绿部署肯定可以在生产中使用本地的 Kubernetes Deployment 通过额外的流程和自动化来实现。更好的方法是使整个蓝绿部署过程完全自动化和声明式;因此,Argo Rollouts 应运而生。

Argo Rollouts 引入了一个名为 Rollout 的新自定义资源,为 Kubernetes 提供了额外的部署策略,如蓝绿、金丝雀(第 5.3 节)和渐进式交付(第 5.4 节)。Rollout 自定义资源提供了与 Deployment 资源相同的功能,并增加了额外的部署策略。在下一教程中,你将看到使用 Argo Rollouts 部署蓝绿是多么简单。

注意:请参考本教程之前的 5.1.4 节,了解如何启用入口并在你的 Kubernetes 集群中安装 Argo Rollouts。

  1. 部署 NGINX 入口控制器。

  2. 使用 Argo Rollouts 部署生产服务和(蓝色)部署。

  3. 更新清单以使用绿色镜像。

  4. 应用更新的清单以部署新的绿色版本。

首先,我们将创建入口控制器、demo-service 和蓝色部署:

$ kubectl apply -f ingress.yaml 
ingress.extensions/demo-ingress created
configmap/nginx-configuration created
$ kubectl apply -f bluegreen_rollout.yaml 
rollout.argoproj.io/demo created
service/demo-service created
$ kubectl get ingress
NAME           HOSTS       ADDRESS          PORTS   AGE
demo-ingress   demo.info   192.168.99.111   80      60s

列表 5.8  ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: demo-ingress
spec:
  rules:
  - host: demo.info
    http:
      paths:
      - path: /
        backend:
          serviceName: demo-service
          servicePort: 80
---
apiVersion: v1
data:
  allow-backend-server-header: "true"
  use-forwarded-headers: "true"
kind: ConfigMap
metadata:
  name: nginx-configuration   

列表 5.9  bluegreen_rollout.yaml

apiVersion: argoproj.io/v1alpha1
kind: Rollout                               ❶
metadata:
  name: demo
  labels:
    app: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: argoproj/rollouts-demo:blue  ❷
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
  Strategy:       
    bluegreen:                              ❸
      autoPromotionEnabled: true            ❹
      activeService: demo-service           ❺
---
apiVersion: v1
kind: Service
metadata:
  name: demo-service
  labels:
    app: demo
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: demo
  type: NodePort

❶ 指定类型为 Rollout 而不是 Deployment

❷ 使用蓝色镜像设置初始部署

❸ 使用蓝绿部署策略而不是滚动更新

❹ 自动更新 demo-service 中的选择器,将所有流量发送到绿色 Pods

❺ 指定 demo-service 为此滚动对象的前端流量

注意:Argo Rollouts 内部将维护一个用于蓝色和一个用于绿色的 ReplicaSet。它还将确保在更新服务选择器以将所有流量发送到绿色之前,绿色部署已完全扩展。(因此,在这种情况下只需要一个服务。)此外,Argo Rollouts 还将等待 30 秒,以确保所有蓝色流量完成并缩小蓝色部署。

图片

图 5.15 Argo Rollouts 与使用一个或多个 ReplicaSet 来满足部署请求的 Deployment 类似。在初始状态下,Argo Rollouts 为蓝色 Pods 创建 ReplicaSet V1。

一旦创建了入口控制器、服务以及更新了 /etc/hosts,你就可以输入 URL demo.info 来查看正在运行的蓝色服务。

注意:NGINX 入口控制器只会拦截自定义规则中定义的主机名的流量。请确保你将 demo.info 及其 IP 地址添加到你的 /etc/hosts 中。

图片

图 5.16 Argo Rollouts 为绿色 Pods 创建 ReplicaSet V2。一旦所有绿色 Pods 都启动并运行,Argo Rollouts 将自动缩小所有蓝色 Pods。

现在我们将更新清单以部署新版本,绿色。一旦你应用了更新的清单,你就可以回到浏览器中,看到所有条形和点都变成了绿色(图 5.16):

$ sed -i .bak 's/demo:blue/demo:green/g' bluegreen_rollout.yaml
$ kubectl apply -f bluegreen_rollout.yaml
deployment.apps/demo configured
service/demo-service unchanged

5.3 金丝雀

金丝雀部署是一种技术,通过在将新软件版本提供给所有用户之前,先对一小部分用户进行短期部署来降低在生产环境中引入新软件版本的风险。金丝雀作为故障的早期指标,可以避免有问题的部署一次性对所有客户造成全面影响。如果一个金丝雀部署失败,其他服务器不会受到影响,你可以简单地终止金丝雀并对问题进行分类。

注意:根据我们的经验,大多数生产事件都是由于系统中的更改,例如新的部署。金丝雀部署是在新版本达到所有用户群体之前测试新发布的另一个机会。

我们的金丝雀示例与蓝绿示例类似。

图 5.17 入口控制器将同时面向蓝色和绿色服务,但在此情况下,90% 的流量将流向蓝色(生产)服务,10% 将流向绿色(金丝雀)服务。由于绿色服务只获得 10% 的流量,因此我们只扩容一个绿色 Pod 以最小化资源使用。

当金丝雀运行并获取生产流量时,我们可以在固定时间段(例如一小时)内监控金丝雀的健康状况(延迟、错误等),以确定是否扩容绿色部署并将所有流量路由到绿色服务,或者在没有问题的情况下将所有流量路由回蓝色服务并终止绿色 Pod。

图 5.18 如果金丝雀 Pod 没有错误,绿色部署将扩容到三个 Pod 并接收 100% 的生产流量。

5.3.1 使用 Deployment 的金丝雀

在本教程中,我们将使用原生的 Kubernetes Deployment 和 Service 执行金丝雀部署。

注意:请在本教程之前参考 5.1.4 节了解如何启用入口并在您的 Kubernetes 集群中安装 Argo Rollouts。

  1. 创建蓝色部署和服务(生产)。

  2. 创建入口以将流量导向蓝色服务。

  3. 在浏览器中查看应用程序(蓝色)。

  4. 部署绿色部署(一个 Pod)和服务,并等待所有 Pod 准备就绪。

  5. 创建金丝雀入口,将 10% 的流量导向绿色服务。

  6. 再次在浏览器中查看网页(10% 绿色且无错误)。

  7. 将绿色部署扩容到三个 Pod。

  8. 更新金丝雀入口以将 100% 的流量发送到绿色服务。

  9. 将蓝色部署缩放到 0。

我们可以通过应用 blue_deployment.yaml(列表 5.4)来创建生产部署:

$ kubectl apply -f blue_deployment.yaml 
deployment.apps/blue created
service/blue-service created

现在,我们可以通过应用 blue_ingress.yaml(列表 5.5)来公开入口控制器,这样我们就可以通过浏览器访问蓝色服务:kubectl get ingress 命令将返回入口控制器的主机名和 IP 地址:

$ kubectl apply -f blue_ingress.yaml 
ingress.extensions/demo-ingress created
configmap/nginx-configuration created
$ kubectl get ingress
NAME           HOSTS       ADDRESS          PORTS   AGE
demo-ingress   demo.info   192.168.99.111   80      60s

注意:NGINX 入口控制器只会拦截自定义规则中定义的主机名的流量。请确保您已将 demo.info 及其 IP 地址添加到您的 /etc/hosts。

一旦创建了入口控制器、蓝色服务和部署,并已更新 /etc/hosts 中的 demo.info 和正确的 IP 地址,你就可以输入 URL demo.info 并看到蓝色服务正在运行。

现在我们已经准备好部署新的绿色版本。让我们应用 green_deployment.yaml 来创建绿色服务和部署:

$ kubectl apply -f green_deployment.yaml 
deployment.apps/green created
service/green-service created

列表 5.10  green_deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: green
  labels:
    app: green
spec:
  replicas: 1         ❶
  selector:
    matchLabels:
      app: green
  template:
    metadata:
      labels:
        app: green
    spec:
      containers:
      - name: green
        image: argoproj/rollouts-demo:green
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: green-service
  labels:
    app: green
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: green
  type: NodePort

❶ 初始绿色部署的 ReplicaSet 设置为 1。

接下来,我们将创建 canary_ingress 以将 10% 的流量路由到金丝雀(绿色)服务:

$ kubectl apply -f canary_ingress.yaml 
ingress.extensions/canary-ingress configured
configmap/nginx-configuration unchanged

列表 5.11  canary_ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: canary-ingress
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"        ❶
    nginx.ingress.kubernetes.io/canary-weight: "10"   ❷
spec:
  rules:
  - host: demo.info
    http:
      paths:
      - path: /
        backend:
          serviceName: green-service
          servicePort: 80
---
apiVersion: v1
data:
  allow-backend-server-header: "true"
  use-forwarded-headers: "true"
kind: ConfigMap
metadata:
  name: nginx-configuration

❶ 告诉 NGINX 入口控制器将其标记为金丝雀,并通过匹配主机和路径将此入口与主入口关联

❷ 将 10% 的流量路由到 green-service

现在,你可以回到浏览器并监控绿色服务。

图片

图 5.19 HTML 页面将在气泡图和柱状图中显示蓝色和绿色的混合,因为 10% 的流量将流向绿色 Pod。

如果你能够看到正确的结果(健康的金丝雀),你就可以完成金丝雀部署(绿色服务)。然后我们将扩展绿色部署,将所有流量发送到绿色服务,并缩减蓝色部署:

$ sed -i .bak 's/replicas: 1/replicas: 3/g' green_deployment.yaml
$ kubectl apply -f green_deployment.yaml
deployment.apps/green configured
service/green-service unchanged
$ sed -i .bak 's/10/100/g' canary_ingress.yaml
$ kubectl apply -f canary_ingress.yaml
ingress.extensions/canary-ingress configured
configmap/nginx-configuration unchanged
$ sed -i .bak 's/replicas: 3/replicas: 0/g' blue_deployment.yaml
$ kubectl apply -f blue_deployment.yaml
deployment.apps/blue configured
service/blue-service unchanged

现在你应该能够看到所有绿色柱子和点,因为 100% 的流量被路由到绿色服务。

注意:在真正的生产环境中,我们将在将 100% 的流量发送到金丝雀服务之前,需要确保所有绿色 Pod 都处于运行状态。可选地,我们可以在绿色部署扩展的同时,逐步增加发送到绿色服务的流量百分比。

图片

图 5.20 如果金丝雀没有错误,绿色部署将扩展,蓝色部署将缩减。在蓝色部署完全缩减后,气泡图和柱状图将显示 100% 绿色。

5.3.2 使用 Argo Rollouts 进行金丝雀部署

如 5.3.1 节中所示,使用金丝雀部署可以帮助早期检测问题,以防止有问题的部署,但将在部署过程中涉及许多额外的步骤。在下一教程中,我们将使用 Argo Rollouts 简化金丝雀部署的过程。

注意:请参考本教程之前的 5.1.4 节,了解如何在 Kubernetes 集群中启用入口并安装 Argo Rollouts。

  1. 创建入口、生产部署和服务(蓝色)。

  2. 在浏览器中查看应用程序(蓝色)。

  3. 使用带有 10% 金丝雀流量的绿色镜像应用清单 60 秒。

  4. 创建将 10% 的流量引导到绿色服务的金丝雀入口。

  5. 在浏览器中再次查看网页(10% 绿色,无错误)。

  6. 等待 60 秒。

  7. 再次在浏览器中查看应用程序(全部绿色)。

首先,我们将创建入口控制器(列表 5.8)、demo-service 和蓝部署(列表 5.12):

$ kubectl apply -f ingress.yaml 
ingress.extensions/demo-ingress created
configmap/nginx-configuration created
$ kubectl apply -f canary_rollout.yaml 
rollout.argoproj.io/demo created
service/demo-service created
$ kubectl get ingress
NAME           HOSTS       ADDRESS          PORTS   AGE
demo-ingress   demo.info   192.168.99.111   80      60s

列表 5.12  canary_rollout.yaml

apiVersion: argoproj.io/v1alpha1
kind: Rollout                                 ❶
metadata:
  name: demo
  labels:
    app: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - name: demo
        image: argoproj/rollouts-demo:blue
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
  strategy:
    canary:                                   ❷
      maxSurge: "25%"
      maxUnavailable: 0
      steps:
      - setWeight: 10                         ❸
      - pause:
          duration: 60                        ❹
---
apiVersion: v1
kind: Service
metadata:
  name: demo-service
  labels:
    app: demo
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: demo
  type: NodePort

❶ 当 Rollout 首次部署时,策略被忽略,并执行常规部署。

❷ 使用金丝雀策略部署

❸ 将 Pod 数量扩展到足以服务 10%的流量。在本例中,Rollout 将扩展一个绿色 Pod 以及三个蓝色 Pod,使得绿色 Pod 获得 25%的流量。Argo Rollouts 可以与服务网格或 NGINX Ingress Controller 配合进行细粒度流量路由。

❹ 等待 60 秒。如果没有错误或用户中断发生,将绿色 Pod 扩展到 100%。

注意对于初始部署(蓝色),Rollout将忽略金丝雀设置并执行常规部署。

一旦创建了 ingress 控制器、服务以及更新了包含 demo.info 和正确 IP 地址的/etc/hosts 文件,您就可以输入 URL demo.info 并看到蓝色服务正在运行。

注意 NGINX Ingress Controller 只会拦截自定义规则中定义的主机名的流量。请确保您已将 demo.info 及其 IP 地址添加到您的/etc/hosts 文件中。

一旦蓝色服务完全启动并运行,我们现在可以更新清单,使用绿色镜像并应用清单:

$ sed -i .bak 's/demo:blue/demo:green/g' canary_rollout.yaml
$ kubectl apply -f canary_rollout.yaml 
rollout.argoproj.io/demo configured
service/demo-service unchanged

一旦金丝雀启动,你应该会看到与第 5.3.1 节中的图 5.19 类似的内容。经过一分钟,绿色 ReplicaSet 将扩展,而蓝色部署将缩减,所有条形和点都将变为绿色(第 5.3.1 节中的图 5.20)。

5.4 渐进式交付

渐进式交付也可以被视为金丝雀部署的完全自动化版本。与在扩展金丝雀部署之前监控固定时间段(例如一小时)不同,渐进式交付将连续监控 Pod 的健康状况,直到完全扩展。

5.4.1 使用 Argo Rollouts 进行渐进式交付

Kubernetes 不提供分析工具来确定新部署的正确性。在本教程中,我们将使用 Argo Rollouts 来实现渐进式交付。Argo Rollouts 使用金丝雀策略以及AnalysisTemplate来实现渐进式交付。

图 5.21 渐进式交付持续收集和分析新 Pod 的健康状况;扩展渐进式部署(绿色);缩减生产部署(蓝色),只要分析确定成功。

注意请参考第 5.1.4 节了解如何在本次教程之前在 Kubernetes 集群中启用 ingress 并安装 Argo Rollouts。

图 5.22 此图展示了完成的渐进式交付,绿色部署完全扩展,蓝色部署缩减。

  1. 创建AnalysisTemplate

  2. 创建 ingress、生产部署和服务(蓝色)。

  3. 创建 ingress 以将流量导向生产服务。

  4. 再次在浏览器中查看应用程序(蓝色)。

  5. 使用 Pass 模板更新并应用包含绿色镜像的清单。

  6. 在浏览器中再次查看网页(绿色)。

  7. 使用 Fail 模板更新并应用包含绿色镜像的清单。

  8. 再次在浏览器中查看应用程序。仍然是蓝色!

首先,我们将为Rollout创建用于收集指标并确定 Pod 健康状况的AnalysisTemplate(列表 5.13)。为了简单起见,我们将创建一个总是返回 0(健康)的AnalysisTemplate pass,以及一个总是返回 1(不健康)的AnalysisTemplate fail。此外,Argo Rollouts 内部维护多个 ReplicaSet,因此不需要多个服务。接下来,我们将创建入口控制器(列表 5.8)、demo-service和蓝色部署(列表 5.14):

$ kubectl apply -f analysis-templates.yaml 
analysistemplate.argoproj.io/pass created
analysistemplate.argoproj.io/fail created
$ kubectl apply -f ingress.yaml 
ingress.extensions/demo-ingress created
configmap/nginx-configuration created
$ kubectl apply -f rollout-with-analysis.yaml 
rollout.argoproj.io/demo created
service/demo-service created
$ kubectl get ingress
NAME           HOSTS       ADDRESS          PORTS   AGE
demo-ingress   demo.info   192.168.99.111   80      60s

注意:对于生产环境,AnalysisTemplate支持 Prometheus、Wavefront 和 Netflix Kayenta,或者可以扩展以支持其他指标存储。

列表 5.13  analysis-templates.yaml

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: pass
spec:
  metrics:
  - name: pass
    interval: 15s                     ❶
    failureLimit: 1
    provider:
      job:
        spec:
          template:
            spec:
              containers:
              - name: sleep
                image: alpine:3.8
                command: [sh, -c]
                args: [exit 0]        ❷
              restartPolicy: Never
          backoffLimit: 0

---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: fail
spec:
  metrics:
  - name: fail
    interval: 15s                   ❸
    failureLimit: 1
    provider:
      job:
        spec:
          template:
            spec:
              containers:
              - name: sleep
                image: alpine:3.8
                command: [sh, -c]
                args: [exit 1]      ❹
              restartPolicy: Never
          backoffLimit: 0

❶ 运行 15 秒

❷ 返回 0(总是通过)

❸ 运行 15 秒

❹ 返回 1(总是失败)

列表 5.14  rollout-with-analysis.yaml

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: demo
spec:
  replicas: 3
  revisionHistoryLimit: 1
  selector:
    matchLabels:
      app: demo
  strategy:
    Canary:                   ❶
      analysis:
        templateName: pass    ❷
      steps:
      - setWeight: 10         ❸
      - pause:
          duration: 20        ❹
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: argoproj/rollouts-demo:blue
        imagePullPolicy: Always
        name: demo
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: demo-service
  labels:
    app: demo
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: demo
  type: NodePort

❶ 使用 Canary 策略部署

❷ 指定 AnalysisTemplate 通过

❸ 扩展足够的 Pods 以服务 10%的流量。在这个例子中,Rollout 将扩展一个绿色 Pod 以及三个蓝色 Pod,导致绿色 Pod 获得 25%的流量。Argo Rollouts 可以与 Service Mesh 或 NGINX Ingress Controller 一起工作,以进行细粒度流量路由。

❹ 在全规模扩展前等待 20 秒

注意:对于初始部署(蓝色),Rollout将忽略Canary设置并执行常规部署。

一旦您创建了入口控制器、服务以及更新了/etc/hosts文件中的 demo.info 和正确的 IP 地址,您就可以输入 URL demo.info 并看到蓝色服务正在运行。

一旦蓝色服务完全启动并运行,我们现在可以更新清单以使用绿色镜像并应用清单。您应该看到蓝色逐步变为绿色,并在 20 秒后完全变为绿色:

$ sed -i .bak 's/demo:blue/demo:green/g' rollout-with-analysis.yaml
$ kubectl apply -f rollout-with-analysis.yaml 
rollout.argoproj.io/demo configured
service/demo-service unchanged

图片

图 5.23 当绿色部署逐步扩展时,如果没有任何错误,气泡图和柱状图都将逐渐变为全绿色。

现在让我们再次部署并回到蓝色图像。这次我们还将切换到“Fail”AnalysisTemplate,该模板将在 15 秒后返回失败状态。我们应该看到蓝色在浏览器中逐步出现,但在 15 秒后变回绿色:

$ sed -i .bak 's/demo:green/demo:blue/g' rollout-with-analysis.yaml
$ sed -i .bak 's/templateName: pass/templateName: fail/g' rollout-with-analysis.yaml
$ kubectl apply -f rollout-with-analysis.yaml 
rollout.argoproj.io/demo configured
service/demo-service unchanged

图片

图 5.24 蓝色部署逐步扩展但返回错误。在失败期间,蓝色部署将被缩减规模,并且气泡图和柱状图都将返回绿色。

注意:演示部署在失败后将标记为已中止,并且只有在中止状态设置为false之后才会再次部署。

从本教程中,你可以看到,如果 AnalysisTemplate 继续根据收集的指标报告成功,Argo Rollouts 会使用金丝雀策略逐步扩展绿色部署。如果 AnalysisTemplate 报告失败,Argo Rollouts 将通过缩小绿色部署和扩大蓝色部署回其原始状态来回滚。参见表 5.1 进行部署比较。

表 5.1 部署策略比较

优点 缺点
部署 内置于 Kubernetes,滚动更新,所需硬件最少 仅适用于向后兼容和无状态的应用程序
蓝绿 与有状态应用程序和非向后兼容的部署一起工作,快速回滚 需要额外的自动化,部署期间需要两倍的硬件
金丝雀 使用生产流量和依赖关系的一部分用户验证新版本 需要额外的自动化,部署过程更长,仅适用于向后兼容和无状态的应用程序
持续交付 逐步将新版本部署到一部分用户,如果指标良好则持续扩展到所有用户;如果指标不佳则自动回滚 需要额外的自动化,需要收集和分析指标,部署过程更长,仅适用于向后兼容和无状态的应用程序

摘要

  • ReplicaSet 不是声明式的,也不适合 GitOps。

  • Deployment 完全声明式,与 GitOps 相辅相成。

  • Deployment 执行滚动更新,最适合无状态和向后兼容的部署。

  • Deployment 可以通过 max surge 来指定新 Pod 的数量,并通过 max unavailable 来限制正在终止的 Pod 的数量。

  • 蓝绿部署适合不兼容回滚的部署或粘性会话服务。

  • 蓝绿部署可以通过利用两个 Deployment(每个都有自定义标签)和更新 Service 中的 selector 来路由 100% 的流量到活动部署来实现。

  • 金丝雀作为故障的早期指标,可以避免有问题的部署一次性对所有客户造成全面影响。

  • 金丝雀部署可以通过利用两个 Deployment 和 NGINX Ingress Controller 逐步调整流量来实现。

  • 持续交付是金丝雀部署的高级版本,使用实时指标来继续或中止部署。

  • Argo Rollouts 是一个开源项目,可以简化蓝绿、金丝雀和持续部署。

  • 每种部署策略都有其优缺点,选择适合你应用程序的正确策略非常重要。


1.kubernetes.io/docs/concepts/workloads/controllers/replicaset/.

2.kubernetes.io/docs/concepts/workloads/controllers/deployment/.

3.kubernetes.github.io/ingress-nginx/user-guide/basic-usage/.

4.istio.io/latest/docs/reference/config/networking/gateway/.

5.github.com/argoproj/argo-rollouts.

6.kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/.

7.argoproj.github.io/argo-rollouts/getting-started/.

8.kubernetes.io/docs/concepts/services-networking/service/.

9.en.wikipedia.org/wiki/Iptables.

6 访问控制和安全

本章涵盖

  • 使用 GitOps 驱动的部署时的攻击区域

  • 确保关键基础设施组件得到保护

  • 选择正确配置管理模式的指南

  • 提升安全性以避免 GitOps 中的安全陷阱

访问控制和安全主题始终是必不可少的,尤其是在部署和基础设施管理方面尤为重要。在这种情况下,攻击面包括像基础设施这样的昂贵事物,像政策和合规性这样的危险事物,以及像包含用户数据的数据存储这样的最重要的事物。现代运维方法使工程团队能够以更快的速度移动,并优化快速迭代。然而,更多的发布也意味着引入漏洞的机会更多,给安全团队带来了新的挑战。依赖于人类操作知识的传统安全流程可能仍然有效,但难以扩展并满足利用 GitOps 和自动化构建及发布基础设施的企业需求。

在阅读本章之前,我们建议您先阅读第一章和第二章。

6.1 访问控制简介

安全主题既关键又复杂。通常,它由安全专家甚至整个专门的安全团队处理。那么,为什么在讨论 GitOps 时还要谈论它呢?GitOps 以同样的方式改变了安全责任,就像它改变了运营责任边界一样。有了 GitOps 和 Kubernetes,工程团队能够通过编写 Kubernetes 访问配置和使用 Git 来强制执行适当的配置更改流程来参与安全。鉴于安全团队不再是瓶颈,它可以卸载一些责任给开发者,并专注于提供安全基础设施。GitOps 促进了安全工程师和 DevOps 工程师之间更紧密和更有效的合作,允许任何影响环境安全性的变更在影响生产之前经过适当的网络安全审查和批准。

6.1.1 什么是访问控制?

为了更好地理解访问控制与 GitOps 结合的细微差别,让我们首先了解什么是访问控制。

访问控制是一种限制对系统或物理或虚拟资源的访问的方式。它规定了谁可以访问受保护资源以及允许执行哪些操作。访问控制由两部分组成:身份验证,确保用户是他们所说的那个人;以及授权,确保他们有权对指定的资源执行请求的操作。无论在哪个领域,访问控制都包括三个主要组件:主体、对象和引用监控器。

访问控制系统最直接的演示是现实世界的一个例子:一个人试图通过门进入大楼。这个人是一个主题,他试图访问对象,即大楼。门是一个引用监控器,只有当试图进入的人有门钥匙时,它才会授权访问请求。

图片

图 6.1 主题是请求访问对象的实体。对象是被访问的实体或资源。引用监控器是控制对受保护对象访问的实体。

练习 6.1

一个电子邮件客户端正在尝试从电子邮件服务器读取电子邮件。你能识别出这个场景中的主题、对象和引用监控器是什么吗?

6.1.2 要保护什么

在端到端安全应用程序交付到 Kubernetes 集群的过程中,许多不同的组件需要被保护。这些包括(但不限于)

  • CI/CD 管道

  • 容器注册库

  • Git 仓库

  • Kubernetes 集群

  • 云服务提供商或数据中心

  • 应用程序本身

  • GitOps 操作员(如果适用)

这些每个组件都有其独特的安全关注点、身份验证机制和基于角色的访问控制(RBAC)模型,并且将根据许多因素和考虑进行不同的配置。由于安全性的强弱取决于最薄弱的环节,所有组件在集群的整体安全性中都扮演着同等重要的角色。

一般而言,安全选择往往是安全和便利之间的权衡。一个可能非常安全的系统可能因为不便而变得无法使用。作为一个操作员,目标是尽可能使用户体验方便,同时不牺牲安全。

影响组件安全性的考虑因素包括

  • 可能的攻击向量

  • 如果组件被攻破的最坏后果

  • 应该允许哪些人访问该服务

  • 不同用户具有的权限(RBAC)

  • 可以采取哪些保护措施来减轻风险

下几节将描述这些组件以及一些独特的安全考虑因素。

CI/CD 管道

CI/CD 构建和部署管道是将新构建的软件交付到 Kubernetes 集群的起点。Jenkins、Circle CI 和 Travis CI 是一些流行的 CI/CD 系统的例子。由于大多数的思考和精力都集中在保护生产环境和生产数据上,安全通常是被事后考虑的。然而,CI/CD 同样是拼图中不可或缺的一部分。这是因为 CI/CD 管道最终控制着新软件如何被推送到环境中。一旦被攻破,它就有能力将 有害 的软件推送到集群中。

构建系统通常配置有足够的凭证来执行其职责。例如,为了发布新的容器镜像,CI/CD 管道可能需要访问容器注册表的凭证。传统上,构建系统也被赋予访问和凭证以访问 Kubernetes 集群,以执行实际的部署。但正如我们将在本章后面看到的那样,随着 GitOps 的出现,直接访问集群不再必要。

能够访问 CI/CD 构建系统的攻击者可以通过多种方式危害安全。例如,可以将管道修改为暴露之前提到的容器注册表或集群凭证。另一个例子是管道可能被劫持,从而将恶意容器部署到集群中,而不是预期的容器。

甚至在某些情况下,恶意行为者可能仅通过使用 CI 系统的标准功能就可能会危害安全。例如,当对代码仓库发起拉取请求时,它将启动一个执行一系列步骤以验证和测试更改的管道。这些步骤的内容通常定义在代码仓库中的一个文件中(例如 Jenkins 的 Jenkinsfile 或 Circle CI 的.circleci/config.yml)。打开新拉取请求的能力通常对公众开放,以便任何人都可以提出对项目的贡献。然而,攻击者可以简单地发起一个修改管道以执行恶意操作的拉取请求。因此,许多 CI 系统集成了防止在不受信任的来源发起 PR 时执行管道的功能。

容器注册表

容器注册表存储了将在集群中部署的容器镜像。由于注册表中的容器镜像有可能在集群中运行,因此注册表的内容以及可以向该注册表推送的用户都需要受到信任。由于任何人都可以向公共注册表(如 DockerHub、Quay.io 和 grc.io)发布镜像,因此企业中阻止从这些不受信任的容器注册表拉取镜像是一项标准的安全措施。相反,所有镜像都将从内部、受信任的注册表拉取,该注册表可以定期扫描存储库中的漏洞。

拥有对受信任容器注册表权限的攻击者可以向注册表推送镜像并覆盖现有的、之前受信任的镜像。例如,假设您的集群已经运行了某个镜像 mycompany/guestbook:v1.0。如果攻击者能够访问注册表,他们可以推送一个新的镜像并覆盖现有的 guestbook:v1.0 标签,改变该镜像的含义为恶意内容。然后,下一次容器启动(可能是由于 Pod 重新调度)时,它将运行受损害的镜像版本。

这种攻击可能不会被检测到,因为从 Kubernetes 和 GitOps 系统的角度来看,一切如预期;实时清单与 Git 中的配置清单相匹配。为了应对这个问题,可以在某些仓库中将镜像标签(或镜像版本)指定为不可变的,这样一旦写入,该镜像标签的含义就永远不会改变。

不可变镜像标签 一些镜像仓库(如 DockerHub)提供了一种使镜像标签不可变的功能。这意味着一旦镜像标签已经存在,就无法覆盖它,从而本质上防止了镜像标签被重复使用。使用此功能通过防止现有已部署的镜像标签被修改来增加额外的安全性。

Git 仓库

在 GitOps 的背景下,Git 仓库定义了将安装到集群中的资源是什么。存储在 Git 仓库中的 Kubernetes 清单最终将出现在集群中。因此,任何可以访问 Git 仓库的人都应该被信任来决定集群的构成,包括像部署(Deployments)、容器镜像、角色(Roles)、角色绑定(RoleBindings)、入口(Ingresses)和网络策略(NetworkPolicies)等。

在最坏的情况下,一个拥有对 Git 仓库完全访问权限的攻击者可以向 Git 仓库推送一个新的提交,更新部署以在集群中运行恶意容器。他们还可能添加一个角色和角色绑定,这可能会赋予部署足够的权限来读取机密信息并泄露敏感数据。

好消息是,由于攻击者需要向仓库推送提交,恶意行为将公然进行,并且可以被审计和追踪。然而,对 Git 仓库的提交和拉取请求访问应该仅限于一小部分人,这些人将有效拥有完整的集群管理权限。

Kubernetes 集群

保护 Kubernetes 集群本身就是一个值得单独成书的话题,因此我们只旨在涵盖与 GitOps 最相关的主题。如您所知,Kubernetes 集群是运行您的应用程序代码的基础设施平台。一个已经获得集群访问权限的攻击者可以说是最坏的情况。因此,Kubernetes 集群对于攻击者来说是一个极具价值的目标,集群的安全性至关重要。

GitOps 为您提供了全新的选择,以决定如何授予用户对集群的访问权限。这一点将在本章的后续部分进行深入探讨,但就目前而言,GitOps 为操作员提供了一种新的方式来提供对集群的访问(例如通过 Git),这与传统的直接授予用户对集群访问的方法(例如使用个性化的 kubeconfig 文件)形成对比。

传统上,在 GitOps 之前,开发者通常需要直接访问 Kubernetes 集群来管理和更改他们的环境。但有了 GitOps,对集群的直接访问就不再是严格必要的,因为环境管理可以通过一个新的媒介,Git 来进行。假设所有开发者对集群的访问都可以通过 Git 进行。在这种情况下,这也意味着运营商可以选择完全关闭对集群的传统直接访问(或者至少是写访问),并强制所有更改都通过 Git 进行。

云提供商或数据中心

虽然在 GitOps 的背景下可能不在考虑范围内,但这对安全性的讨论仍然很重要,即 Kubernetes 集群运行的底层云提供商(如 AWS)或物理数据中心。通常,在 Kubernetes 中运行的应用程序将依赖于云中的一些管理资源或服务,例如数据库、DNS、对象存储(如 S3)、消息队列等。由于开发者和应用程序都需要访问这些资源,运营商需要考虑如何授予用户创建和访问这些云提供商资源的权限。

开发者可能需要访问他们的数据库以执行诸如数据库模式迁移或生成报告之类的操作。虽然 GitOps 本身并不提供针对数据库本身的安全解决方案,但当数据库配置不可避免地开始出现在 Kubernetes 清单中(这些清单是通过 GitOps 管理的)时,GitOps 就会发挥作用。例如,运营商可能采用的一种帮助确保数据库访问安全的机制是在 Kubernetes NetworkPolicy 中的 IP 白名单。由于 NetworkPolicy 是可以通过 Git 管理的标准 Kubernetes 资源,因此 NetworkPolicy 的 内容(IP 白名单)对运营商来说作为一个安全问题变得很重要。

第二个考虑因素是 Kubernetes 资源可以对云提供商资源产生深远的影响。例如,一个被允许创建普通 Kubernetes 服务对象的用户可能会在云提供商中创建许多昂贵的负载均衡器,并无意中向外界暴露服务。因此,集群运营商对 Kubernetes 资源与云提供商资源之间的关系以及允许用户自行管理这些资源的后果有深入理解至关重要。

GitOps 运营商

根据你选择的 GitOps 操作员,保护操作员可能是一个选项,也可能不是。一个基本的 GitOps 操作员,例如我们在第二章中的穷人版基于 CronJob 的 GitOps 操作员示例,由于它不是一个可以公开暴露的服务,也没有任何管理方面,因此没有其他安全影响。另一方面,像 Argo CD、Helm 或 Jenkins X 这样的工具旨在向最终用户公开。因此,它有额外的安全考虑,因为它可能成为攻击的途径。

6.1.3 GitOps 中的访问控制

首先,让我们确定在持续交付(CD)安全模型中的访问控制主体和对象。正如我们已经学到的,对象是需要保护的资源。CD 表面攻击很大,但不可变基础设施和 Kubernetes 将其缩小到只有两件事:Kubernetes 配置和部署工件。

正如你所知道的那样,Kubernetes 配置由一组 Kubernetes 资源表示。资源清单存储在 Git 中,并自动应用到目标 Kubernetes 集群。部署工件是容器镜像。有了这两者,你可以以任何方式塑造你的生产环境,甚至可以在任何时候从头开始重新创建它。

在这个例子中,访问控制主体是工程师和自动化流程,例如 CI 管道。工程师正在利用自动化来持续产生新的容器镜像,并更新 Kubernetes 配置以部署它们。

除非你使用 GitOps,否则 Kubernetes 配置要么是手动更新,要么在持续集成中脚本化。这种方法,有时被称为 CIOps^(1),通常会让安全团队感到紧张。

图片

图 6.2 CIOps 安全模型不安全,因为它允许工程师和 CI 系统访问集群。这里的问题是 CI 系统获得了对集群的控制权,并被允许进行任意的 Kubernetes 配置更改。这大大增加了攻击面,使得保护集群变得困难。

那么 GitOps 是如何改善这种情况的呢?GitOps 统一了从 Git 仓库应用到集群的更改过程。这允许将访问令牌更靠近集群,并将保护集群访问的负担有效地转移到 Git 仓库。

图片

图 6.3 GitOps 安全模型仅限制集群访问 GitOps 操作员。攻击面大大减少,保护集群变得更加简单。

在 Git 仓库中保护配置仍然需要付出努力。它的好处是我们可以使用与保护应用程序源代码相同的工具。Git 托管提供商,如 GitHub 和 GitLab,允许我们为每个更改定义和执行变更过程,如强制审查或静态分析。由于 GitOps 操作者是唯一具有集群访问权限的主题,因此定义工程团队能够部署到集群中的内容以及不能部署的内容变得更加容易,从而显著提高了集群的安全性。

让我们继续学习如何在 Git 仓库中保护 Kubernetes 配置以及如何微调 Kubernetes 访问控制。

6.2 访问限制

如本章开头所述,涉及保护集群的组件有很多,包括 CI/CD 构建系统、容器注册库以及实际的 Kubernetes 集群。每个组件都实现了特定的访问控制机制,以允许或拒绝访问。

6.2.1 Git 仓库访问

Git 是一个完全面向开发者的工具。默认情况下,它被配置得非常容易在任何时候更改任何内容。这种简单性是 Git 在开发者社区中如此受欢迎的原因。

然而,Git 是建立在坚实的加密基础之上的:它使用 Merkle 树作为基本的数据结构。相同的结构被用作区块链的基础.^(2) 因此,Git 可以用作分布式账本,使其成为出色的审计日志存储。

Merkle 树 Merkle 树 是一种树,其中每个叶子节点都标记有数据块的哈希值,每个非叶子节点都标记有其子节点标签的加密哈希值.^(3)

这里是 Git 的工作原理的简要概述。每次开发者想要保存一组更改时,Git 都会计算引入的差异并创建一个包含引入的更改、各种元数据字段(如日期和作者)以及指向表示先前仓库状态的父捆绑包的引用的捆绑包。最后,该捆绑包被哈希处理并存储在仓库中。这个捆绑包被称为提交。该算法基本上与在区块链中使用的算法相同。

图片

图 6.4 每个 Git 提交都引用前一个提交并形成一个类似树的数据结构。所有修改都在 Git 仓库中得到完全跟踪。

哈希用作保证所使用的代码与提交的代码相同,并且没有被篡改。因此,Git 仓库是一个由密码学保护的提交链,可以防止隐藏的修改。由于背后有密码学算法的支持,我们可以安全地信任 Git 实现,因此我们可以将其用作审计日志。

创建部署仓库

让我们创建一个示例部署仓库,然后看看使其准备好进行 GitOps 部署需要什么。为了您的方便,让我们使用在github.com/gitopsbook/sample-app-deployment可用的现有部署仓库。该仓库包含 Kubernetes 服务和一个部署资源的部署清单。部署资源的清单如下所示。

列表 6.1 示例应用部署 (http://mng.bz/ao1z)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - image: gitopsbook/sample-app:v0.1
        name: sample-app
        command:
          - /app/sample-app
        ports:
        - containerPort: 8080

正如之前提到的,Git 是一个分布式版本控制系统。这意味着每个开发者都有一个完整的本地仓库副本,可以完全访问以进行更改。然而,还有一个所有团队成员都用来交换更改的公共仓库。这个公共远程仓库由 GitHub 或 GitLab 等 Git 托管服务托管。托管服务提供一系列安全功能,允许保护仓库免受未授权修改,强制提交作者身份,防止历史覆盖,等等。

作为第一步,导航到 gitopsbook/sample-app-deployment 仓库,并在您的 GitHub 账户中创建一个分支^(4):

https://github.com/gitopsbook/sample-app-deployment

一旦创建了分支,使用以下命令在本地克隆仓库并准备进行更改:

$ git clone https://github.com/<username>/sample-app-deployment.git
Cloning into 'sample-app-deployment'...
remote: Enumerating objects: 14, done.
remote: Total 14 (delta 0), reused 0 (delta 0), pack-reused 14
Receiving objects: 100% (14/14), done.
Resolving deltas: 100% (3/3), done.

尽管仓库是公开的,但这并不意味着拥有 GitHub 账户的每个人都可以在没有适当权限的情况下进行更改。GitHub 确保用户要么是仓库所有者,要么被邀请为协作者.^(5)

练习 6.2

使用 HTTPS URL 克隆仓库,并尝试推送任何更改,而不提供您的 GitHub 用户名和密码:

git clone https://github.com/<username>/sample-app-deployment.git

而不是创建个人仓库,您可能创建一个组织^(6)并使用团队来管理访问权限。这一套访问管理功能非常全面,涵盖了从单个开发者到大型组织的大部分用例。然而,这还不够。

练习 6.3

创建第二个 GitHub 用户,并将该用户邀请为协作者。尝试使用第二个 GitHub 用户的凭据推送任何更改。

执行代码审查流程

无论是通过加密保护还是授权设置都无法防止恶意开发者有意引入或由于糟糕的编码实践不小心引入的漏洞。无论应用程序源代码中的漏洞是有意引入的还是不是,推荐的解决方案是相同的:部署仓库中的所有更改都必须通过 Git 托管提供商强制执行的代码审查流程。

让我们确保对 sample-app-deployment 仓库主分支的任何更改都经过代码审查流程,并且至少由一位审查员批准。启用强制审查流程的步骤在mng.bz/OExn中描述:

  1. 在存储库设置中导航到“分支”部分。

  2. 点击“添加规则”按钮。

  3. 输入所需的分支名称。

  4. 启用“合并前要求拉取请求审查”和“包括管理员”设置。

接下来,让我们尝试进行配置更改并将其推送到主分支:

$ sed -i .bak 's/v0.1/v0.2/' deployment.yaml
$ git commit -am 'Upgrade image version'
$ git push
remote: error: GH006: Protected branch update failed for refs/heads/master.
remote: error: At least 1 approving review is required by reviewers with write access.
To github.com:<username>/sample-app-deployment.git
 ! [remote rejected] master -> master (protected branch hook declined)
error: failed to push some refs to 'github.com:<username>/sample-app-deployment.git'

由于分支受到保护,需要拉取请求和审查,git push 失败。这保证了至少有一个人将审查更改并签署部署。

在移动到下一段之前,不要忘记运行清理。删除保护主分支的规则,并运行以下命令以重置本地更改:

$ git reset HEAD¹ --hard

练习 6.4

在“合并前要求拉取请求审查”部分探索额外的设置。考虑哪种设置组合适合您的项目或组织。

强制执行自动化检查

除了人工判断外,拉取请求还允许我们结合自动化清单分析,这有助于在早期捕捉到安全问题。尽管 Kubernetes 安全工具生态系统仍在发展,但已经有几个选项可用。两个很好的例子是 kubeaudit^(7) 和 kubesec.^(8)。这两个工具都受 Apache 许可证保护,并允许扫描 Kubernetes 清单以查找弱安全参数。

由于我们的存储库是开源的,并由 GitHub 托管,我们可以免费使用 CI 服务,例如 travis-ci.orgcircleci.com!让我们配置自动 kubeaudit 使用,并使用 travis-ci.org 对每个拉取请求进行成功验证:

git add .travis.yml
git commit -am 'Add travis config'
git push

列表 6.2 .travis.yml

language: bash
install:
  - curl -sLf -o kubeaudit.tar.gz  https://github.com/Shopify/kubeaudit/releases/download/v0.7.0/kubeaudit_0.7.0_linux_amd64.tar.gz
  - tar -zxvf kubeaudit.tar.gz
  - chmod +x kubeaudit
script:
  -  ./kubeaudit nonroot -f deployment.yaml &> errors
  - if [ -s errors ] ; then cat errors; exit -1; fi

一旦配置就绪,我们只需在 mng.bz/Gxyq 上启用 CI 集成并创建拉取请求:

$ git checkout -b change1
Switched to a new branch 'change1'

$ sed -i .bak 's/v0.1/v0.2/' deployment.yaml

$ git commit -am 'Upgrade image version'
[change1 c52535a] Upgrade image version
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git push --set-upstream origin change1
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 359 bytes | 359.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote:
remote: Create a pull request for 'change1' on GitHub by visiting:
remote: https://github.com/<username>/sample-app-deployment/pull/new/change1
remote:
To github.com:<username>/sample-app-deployment.git
 * [new branch]      change1 -> change1
Branch 'change1' set up to track remote branch 'change1' from 'origin'.

CI 应在创建 PR 后立即触发,并出现以下错误消息:

time="2019-12-17T09:05:41Z" level=error msg="RunAsNonRoot is not set in ContainerSecurityContext, which results in root user being allowed!  Container=sample-app KubeType=deployment Name=sample-app"

图 6.5 Travis 运行验证部署清单的 CI 作业。验证失败,因为检测到漏洞。

kubeaudit 检测到 Pod 安全上下文缺少防止以 root 用户身份运行容器的 runAsNonRoot 属性。这是一个有效的安全问题。为了修复安全问题,请按以下代码列表更改 Pod 清单。

列表 6.3 示例应用部署 (http://mng.bz/zxXa)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - image: gitopsbook/sample-app:v0.1
        name: sample-app
        command:
          - /app/sample-app
        ports:
        - containerPort: 8080
+     securityContext:
+       runAsNonRoot: true

提交更改并通过推送 change1 分支更新拉取请求:

git commit -am 'Update deployment'
git push upstream change1

拉取请求应通过验证!

练习 6.5

了解 kubeaudit 应用程序提供的附加审计。尝试使用 kubeaudit autofix -f deployment.yaml 命令。

保护提交作者身份

到目前为止,我们的仓库安全托管在 GitHub 上。我们控制哪些 GitHub 账户可以在仓库中做出更改,对每个更改执行代码审查流程,甚至对每个拉取请求运行静态分析。这很好,但仍不够。正如经常发生的那样,社会工程攻击可以绕过所有这些安全关卡。

如果你的老板给你发送了一个拉取请求并要求你立即合并,你会怎么做?在压力之下,一个工程师可能会决定快速查看拉取请求并未经仔细测试就批准它。由于我们的仓库托管在 GitHub 上,我们知道哪个用户创建了提交。不可能代表别人进行提交,对吧?

不幸的是,事实并非如此。Git 并非设计为具有强身份保证的工具。正如我们之前提到的,Git 是一个完全面向开发者的工具。提交的每一部分都在工程师的控制之下,包括提交作者的信息。因此,入侵者可以轻松创建一个提交并将你老板的名字放入提交元数据中。让我们做一个简单的练习来展示这个漏洞。

打开控制台,使用以下命令在 master 分支上创建一个新的提交:

echo '# hacked' >> ./deployment.yaml
git commit --author='Joe Beda <joe.github@bedafamily.com>' -am 'evil commit'
git push upstream master

打开 GitHub 上你仓库的提交历史,并检查最新的提交信息。看,Joe Beda^(9)刚刚更新了我们的 Pod 清单!

图片

图 6.6 GitHub 提交历史包含 Joe Beda 的头像。默认情况下,GitHub 不会执行任何验证,并使用存储在提交元数据中的作者信息。

这看起来相当吓人,但这并不意味着在以后,你需要在批准之前亲自验证每个拉取请求作者的姓名。与其手动验证提交的作者是谁,你可能会利用数字加密签名。

加密工具如 GPG 允许你在提交元数据中注入加密签名。稍后,这个签名可能会被 Git 托管服务或 GitOps 操作员验证。学习 GPG 签名的工作原理可能需要太多时间,但我们确实可以使用它来保护我们的部署仓库。

不幸的是,GPG 配置过程可能很困难。它包括多个步骤,这些步骤可能因操作系统而异。请参考附录 C 中描述的步骤和 GitHub 在线文档^(10)来配置 GPG 密钥。

最后,我们准备好提交并签名。以下命令创建了一个新的更改到部署清单,并使用与你的 GitHub 账户关联的 GPG 密钥进行签名:

echo '# signed change' >> ./deployment.yaml
git add .
git commit -S -am 'good commit'
git push upstream master

现在,GitHub 的提交历史包括基于 GPG 密钥的作者信息,这些信息无法伪造。

GitHub 允许你要求对特定仓库的所有提交都必须进行签名。要求签名的提交设置可以在仓库设置的受保护分支部分找到。

图片

图 6.7 使用密码学签名 Git 可以保护作者身份。GitHub 用户界面可视化了 GPG 验证结果。

除了 Git 托管服务确认之外,您可能还需要配置您的 GitOps 操作员在更新 Kubernetes 集群配置之前自动验证 GPG 签名。幸运的是,一些 GitOps 操作员已经内置了签名验证支持,无需复杂的配置。这一主题将在接下来的章节中介绍。

6.2.2 Kubernetes RBAC

如您所知,GitOps 方法假设 CI 管道没有访问 Kubernetes 集群的权限。唯一具有直接集群访问权限的自动化工具是位于集群内部的 GitOps 操作员。这已经比传统的 DevOps 模型具有很大的优势。然而,这并不意味着 GitOps 应该拥有神级访问权限。我们仍然需要仔细考虑操作员应该获得哪种权限级别。集群内部的操作员,所谓的拉取模型,并不是唯一的选择。您可以考虑将 GitOps 操作员放置在受保护的安全区域内,并通过使用推送模型和管理多个集群使用一个操作员来减少管理开销。每种考虑都有其优缺点。为了做出有意义的决策,您需要对 Kubernetes 访问模型有良好的理解。因此,让我们回顾一下,了解 Kubernetes 内置了哪些安全工具,以及我们如何使用它们。

访问控制类型

有四种著名的访问控制类型:

  • 基于任意访问控制(DAC)——在 DAC 模型中,数据所有者决定访问权限。DAC 是一种基于用户指定的规则分配访问权限的手段。

  • 强制访问控制(MAC)——MAC 是使用非自主模型开发的,其中人们根据信息清分类别授予访问权限。MAC 是一种基于中央权威机构规定的法规分配访问权限的政策。

  • 基于角色的访问控制(RBAC)——RBAC 根据用户的角色授予访问权限,并实现了关键的安全原则,如最小权限和权限分离。因此,试图访问信息的人只能访问他们认为对其角色必要的数据。

  • 基于属性的访问控制(ABAC)——ABAC,也称为基于策略的访问控制,定义了一种访问控制范式,通过使用结合属性的策略来授予用户访问权限。

ABAC 非常灵活,可能是列表中最强大的模型。由于其强大功能,ABAC 最初被选为 Kubernetes 安全模型。然而,后来社区意识到 ABAC 概念及其在 Kubernetes 中的实现方式难以理解和使用。因此,引入了一种基于 RBAC 的新授权机制。2017 年,基于 RBAC 的授权被移至测试版,ABAC 被宣布为过时。目前,RBAC 是 Kubernetes 中首选的授权机制,并建议用于在 Kubernetes 上运行的每个应用程序。

RBAC 模型包括以下三个主要元素:主体、资源和动词。主体代表想要访问资源的用户或进程,动词是对资源可以执行的操作。

这些元素是如何映射到 Kubernetes API 对象的?RBAC 资源由一个常规的 Kubernetes 资源表示,例如 Pod 或 Deployment。为了表示动词,Kubernetes 中引入了两套新的专用资源。动词由 Role 和 RoleBinding 资源表示,而主体由 User 和 ServiceAccount 资源表示。

图 6.8 Kubernetes RoleBinding 授予 Role 中定义的权限给 Users 和 ServiceAccounts。ServiceAccount 为在 Pod 中运行的进程提供身份。

Role 和 RoleBinding

Role 资源旨在连接动词和 Kubernetes 资源。以下代码示例展示了 Role 的一个示例定义。

列表 6.4 示例 Role (http://mng.bz/0mKx)

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sample-role
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list

verbs 部分包含允许的操作列表。因此,该 Role 允许列出与集群相关的配置映射,并获取每个配置映射的详细信息。Role 的优点是它是一个可重用的对象,并且可以用于不同的主体。例如,你可以定义只读的 Role 并将其分配给各种主体,而无需重复 resourcesverbs 定义。

需要知道的是,Role 是命名空间资源,并提供对同一命名空间中定义的资源访问权限.^(11) 这意味着单个 Role 不能提供对多个命名空间或集群级别资源的访问权限。为了提供集群级别的访问权限,你可能可以使用一个称为 ClusterRole 的等效资源。ClusterRole 资源具有与 Role 相当多的字段集,除了命名空间字段外。

RoleBinding 使 Role 能够与主体连接。以下是一个 RoleBinding 的示例定义。

列表 6.5 示例 RoleBinding (http://mng.bz/KMeK)

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sample-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: sample-role
subjects:
- kind: ServiceAccount
  name: sample-service-account

示例 RoleBinding 将名为sample-role的 Role 中定义的一组权限授予名为sample-service-account的 ServiceAccount。类似于 Role,RoleBinding 有一个等效的对象,ClusterRoleBinding,它允许将主体与 ClusterRole 连接起来。

用户和 ServiceAccount

最后,Kubernetes 主体由 ServiceAccounts 和 Users 表示。

基本的 GitOps 操作员 RBAC

在第二章中,我们在实现基本的 GitOps 操作员时配置了 Kubernetes RBAC。让我们利用本章学到的知识来加强操作员权限,并限制其可以部署的内容。

要开始,请确保你已经完成了基本的 GitOps 操作员教程。你可能还记得,我们配置了一个 CronJob,以及 ServiceAccount 和 ClusterRoleBinding 资源。让我们再次查看 ServiceAccount 和 ClusterRoleBinding 的定义,并找出应该更改什么来提高安全性。

列表 6.6 rbac.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitops-serviceaccount
  namespace: gitops

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gitops-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admin
subjects:
- kind: ServiceAccount
  name: gitops-serviceaccount
  namespace: gitops

ClusterRoleBinding 定义了名为 admin 的 ClusterRole 与 GitOps 操作员 CronJob 所使用的 ServiceAccount 之间的联系。admin ClusterRole 在集群中默认存在,并为整个集群提供上帝级别的访问权限。这意味着 GitOps 操作员没有限制,只要在 Git 仓库中定义了资源,就可以部署任何资源。

那么,这个 RBAC 配置中有什么问题呢?问题是,这只有在假设具有 Git 仓库写权限的开发者已经拥有完整的集群访问权限的情况下才是安全的。由于 GitOps 操作员可以创建任何资源,开发者可能会添加额外的角色和角色绑定清单,并授予自己管理员权限。这不是我们想要的,尤其是在多租户环境中。

另一个考虑因素是人为错误。当多个团队使用一个集群时,我们需要确保一个团队不能触及另一个团队的资源。正如你在第三章所学,团队通常通过 Kubernetes Namespaces 相互隔离。因此,将 GitOps 操作员权限限制在一个 Namespace 内是有意义的。

最后,我们希望控制 GitOps 操作员可以管理的 Namespace 级别资源。虽然让开发者管理像 Deployments、ConfigMaps 和 Secrets 这样的资源是完全可以接受的,但有一些资源应该只由集群管理员管理。网络资源的一个很好的例子是 NetworkPolicy。NetworkPolicy 控制允许进入 Namespace 内 Pods 的流量,通常由集群管理员管理。

让我们继续更新操作员的 RBAC 配置。我们需要做出以下更改以确保安全配置:

  • 将 GitOps 操作员权限限制在一个 Namespace 内。

  • 移除安装集群级别资源的权限。

  • 将操作员权限限制在选定的 Namespaced 资源中。

更新的 RBAC 配置在此表示。

列表 6.7 updated-rbac.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitops-serviceaccount
  namespace: gitops

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: gitops-role
  namespace: gitops
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  - configmaps
  verbs:
  - '*'
- apiGroups:
  - "extensions"
  - "apps"
  resources:
  - deployments
  - statefulsets
  verbs:
  - '*'

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gitops-role-binding
  namespace: gitops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: gitops-role
subjects:
- kind: ServiceAccount
  name: gitops-serviceaccount

这里是应用变更的总结:

  • 将 ClusterRoleBinding 替换为 RoleBinding 以确保只有命名空间级别的访问。

  • 我们不是使用内置的 admin 角色,而是使用自定义的命名空间Role

  • 命名空间角色只提供对指定 Kubernetes 资源的访问。这确保了操作员不能修改像 NetworkPolicy 这样的资源。

6.2.3 镜像注册表访问

通过确保 Kubernetes 集群的安全,我们保证集群配置描述了正确的负载,这些负载引用了正确的 Docker 镜像,并最终运行了我们想要的软件。受保护的部署存储库和完全自动化的 GitOps 驱动部署流程提供了可审计性和可观察性。仍然没有保护的最后一块拼图是 Docker 镜像本身。

我们将在最后讨论 Docker 镜像保护,但这绝对不是最不重要的话题。镜像内容最终定义了在集群内部将要执行的二进制文件。所以即使其他一切都很安全,Docker 注册表保护的漏洞会破坏所有其他安全门。

那么,在实践中 Docker 镜像保护意味着什么?我们必须注意以下两个问题:

  • 没有权限,无法更改注册表中的镜像。

  • 镜像被安全地交付到 Kubernetes 集群中。

注册表镜像保护

类似于 Git 仓库,Docker 仓库保护由托管服务提供。最流行的 Docker 仓库托管服务可能是 DockerHub。该服务允许访问成千上万的 Docker 镜像。该服务由 Docker Inc.提供,并且对任何开源项目都是完全免费的。

要获得 DockerHub 的实际操作经验,您需要在 DockerHub 上获取一个账户,创建一个仓库,并推送一个镜像。除非您已经有了账户,否则请导航到hub.docker.com/signup并创建一个账户。作为下一步,您需要创建一个名为 gitops-k8s-security-alpine 的 Docker 仓库,如 DockerHub 文档中所述.^(12) 最后,您准备好验证 DockerHub 是否正在保护仓库,但首先您需要获取一个示例 Docker 镜像。最简单的方法是拉取一个现有的镜像并重命名它。以下命令从官方 DockerHub 仓库拉取 Alpine Linux 镜像并将其重命名为/gitops-k8s-security-alpine,其中是您的 DockerHub 账户名称:

docker pull alpine
docker tag alpine <username>/gitops-k8s-security-alpine:v0.1

下一个命令将镜像推送到 gitops-k8s-security-alpine Docker 注册表:

docker push <username>/gitops-k8s-security-alpine:v0.1

然而,本地的 Docker 客户端没有访问 DockerHub 仓库的凭证,所以push命令应该失败。为了修复错误,运行以下命令并提供您的 DockerHub 账户用户名和密码:

docker login

一旦成功登录,Docker 客户端就知道你是谁,并且可以执行 Docker 的push命令。

确保图像交付安全

将镜像安全地交付到集群中意味着回答问题:“我们是否信任镜像的来源?”信任意味着我们想要确保镜像是由授权的作者创建的,并且在从仓库传输过程中镜像内容没有被篡改。因此,这是保护镜像作者身份的问题。解决方案与保护 Git 提交作者身份的解决方案非常相似:

  • 个人或自动化过程使用数字签名对镜像的内容进行签名。

  • 签名由消费者使用,以验证镜像是由受信任的作者创建的,并且内容没有被篡改。

好消息是,这已经由 Docker 客户端和镜像注册库支持。Docker 的功能名为内容信任,允许对镜像进行签名并将其连同签名一起推送到注册库。消费者可以使用内容信任功能来验证已签名的镜像内容未被更改。

因此,在理想场景下,CI 管道应发布已签名的镜像,并且 Kubernetes 应配置为要求每个在生产环境中运行的镜像都具备有效的签名。坏消息是,截至版本 1.17,Kubernetes 仍然没有提供强制执行镜像签名验证的配置。因此,我们能做的最好的事情就是在修改 Kubernetes 清单之前验证镜像签名。

内容信任配置相当简单。您必须设置 DOCKER_CONTENT_TRUST 环境变量:

export DOCKER_CONTENT_TRUST=1

一旦设置了环境变量,Docker 命令 runpull 应该会验证镜像签名。我们可以通过拉取我们刚刚推送到 gitops-k8s-security-alpine 仓库的未签名镜像来确认这一点:

$ docker pull <username>/gitops-k8s-security-alpine:v0.1
Error: remote trust data does not exist for docker.io/<username>/gitops-k8s-security-alpine: 
       notary.docker.io does not have trust data for docker.io/<username>/gitops-k8s-security-alpine

命令如预期失败,因为 /gitops-k8s-security-alpine:v0.1 镜像未签名。让我们修复它。确保 DOCKER_CONTENT_TRUST 环境变量仍然设置为 1,并使用以下命令创建一个已签名的镜像:

$ docker tag alpine <username>/gitops-k8s-security-alpine:v0.2
$ docker push  <username>/gitops-k8s-security-alpine:v0.2
The push refers to repository [docker.io/<username>/gitops-k8s-security-alpine]
6b27de954cca: Layer already exists
v0.2: digest: sha256:3983cc12fb9dc20a009340149e382a18de6a8261b0ac0e8f5fcdf11f8dd5937e size: 528
Signing and pushing trust metadata
You are about to create a new root signing key passphrase. This passphrase
will be used to protect the most sensitive key in your signing system. Please
choose a long, complex passphrase and be careful to keep the password and the
key file itself secure and backed up. It is highly recommended that you use a
password manager to generate the passphrase and keep it safe. There will be no
way to recover this key. You can find the key in your config directory.
Enter passphrase for new root key with ID cfe0184:
Repeat passphrase for new root key with ID cfe0184:
Enter passphrase for new repository key with ID c7eba93:
Repeat passphrase for new repository key with ID c7eba93:
Finished initializing "docker.io/<username>/gitops-k8s-security-alpine"
Successfully signed docker.io/<username>/gitops-k8s-security-alpine:v0.2

这次,docker push 命令在推送镜像之前对它进行签名。如果您是第一次推送已签名的镜像,Docker 将在 ~/.docker/trust/ 目录中生成密钥,并提示您输入用于根密钥和仓库密钥的密码短语。在提供密码短语后,已签名的镜像将被推送到 Docker Hub。最后,我们可以通过再次运行 docker pull 命令来验证推送的镜像具有正确的签名:

docker pull <username>/gitops-k8s-security-alpine:v0.2

命令成功完成。我们的镜像具有正确的签名,并且 Docker 客户端能够验证它!

6.3 模式

好吧,让我们面对现实。全新的绿色项目并不一定是以一个完美的部署流程开始的。事实上,年轻的项目甚至没有自动部署流程。首席工程师可能是唯一能够部署项目的人,他们可能从他们的笔记本电脑上部署。通常,随着部署所有应用程序服务的耗时越来越多,团队开始添加自动化。随着未经授权访问的潜在成本和损害的增加,该自动化的安全性变得越来越关键。

6.3.1 完全访问

几乎每个新项目的初始安全模型完全基于团队成员之间的信任。每个团队成员都有完全访问权限,部署更改不一定被记录并在以后可供审计。

图片

图 6.9 全访问安全模型假设工程师和 CI 系统都有完全访问 Kubernetes 集群的权限。这种权衡是速度胜过安全。这种模型更适合处于早期阶段的新项目。

在开始时,弱安全性并不一定是一件坏事。完全访问意味着更少的障碍,使团队能够更加灵活并更快地移动。当生产环境中没有重要客户数据时,这是一个专注于速度并塑造项目直到你准备好进入生产的完美机会。但可能更早而不是更晚,你需要实施适当的控制措施,不仅是为了生产中的客户数据,还要确保部署到生产中的代码的完整性。

6.3.2 部署存储库访问

默认情况下禁用开发人员的直接 Kubernetes 访问是一个从安全角度迈出的巨大进步。如果你使用 GitOps,这是最常见的模式。在这个模型中,开发人员仍然可以完全访问部署存储库,但必须依赖 GitOps 操作员将更改推送到 Kubernetes 集群。

图片

图 6.10 GitOps 操作员允许移除集群访问。此时,工程师只需要访问部署存储库。

除了更好的安全性外,此模式还提供了可审计性。假设没有人可以访问 Kubernetes 配置,部署存储库的历史记录包含了所有集群配置更改。

当项目成熟并且团队不断改进部署配置时,手动更新部署存储库仍然感觉非常合适。然而,经过一段时间,每个应用程序发布可能只需要更改图像标签。在这个阶段,维护部署存储库仍然非常有价值,但可能感觉有很多开销。

6.3.3 仅代码访问

代码访问仅模式是部署存储库访问仅方法的逻辑延续。如果部署存储库中的发布更改是可预测的,那么在 CI 管道中可以将其配置更改过程编码化。

图片

图 6.11 代码访问仅模式假设部署存储库和 Kubernetes 集群的更改都是完全自动化的。工程师只需要对代码存储库的访问权限。

这种模式简化了开发过程,并显著减少了手动工作量的需求。它还在几个方面提高了部署的安全性:

  • 开发团队不再需要访问部署存储库。只有专门的自动化账户才有权限向存储库推送。

  • 由于部署存储库的更改是自动化的,因此配置 GPG 签名过程并将其在 CI 管道中自动化的过程要容易得多。

练习 6.6

选择最适合您项目的模式。尽量详细地阐述每种模式的优缺点,并解释您为什么选择该模式。

6.4 安全问题

我们已经学习了如何从最基本的部分开始,保护我们的部署过程端到端,一直到配置更改和新图像的身份保护。最后,让我们学习必须涵盖的重要边缘情况,以确保您集群的安全性。

6.4.1 防止从不受信任的注册表拉取图像

在 6.2.3 节中,我们展示了如何在对公共注册表(如 docker.io)实施安全控制,以确保图像是由授权用户按预期发布的,并且在拉取时没有被篡改。然而,事实是公共注册表超出了您的可见性和控制范围。您必须信任公共注册表维护者遵循安全最佳实践。即使他们确实如此,他们作为公共注册表的事实意味着任何互联网用户都可以向其推送图像。对于一些对安全性要求极高的企业来说,这是不可接受的。

为了解决这个问题,许多企业将维护自己的私有 Docker 图像注册表,以确保可靠性、性能、隐私和安全。在这种情况下,新图像应该推送到私有注册表(如 docker.mycompany.com),而不是公共注册表(如 docker.io)。这可以通过修改 CI 管道,将成功构建的新图像推送到私有注册表来实现。

部署到 Kubernetes 应该也只从私有仓库拉取镜像。但如何强制执行这一规定?如果一位天真无邪的开发者不小心从 docker.io 拉取了病毒或恶意软件感染的镜像怎么办?或者,一个没有权限向私有仓库推送镜像的恶意开发者试图从他们的公共 DockerHub 仓库中侧加载镜像怎么办?当然,使用 GitOps 将确保这些操作被记录在审计跟踪中,因此那些负责的人应该能够被识别。然而,如何从一开始就预防这种情况呢?

这可以通过使用 Open Policy Agent (OPA) 和一个拒绝引用来自禁止镜像仓库的镜像的 admission webhook 来实现。

6.4.2 Git 仓库中的集群级资源

正如您从本章中了解到的,Kubernetes 访问设置是通过 Kubernetes 资源(如 Role 和 ClusterRole)控制的。RBAC 资源管理是 GitOps 运营商的一个完全有效的用途。将应用程序部署的定义与所需的 Kubernetes 访问设置打包在一起是一种常见的做法。然而,存在一个可能被用来提升权限的安全漏洞。因为 Kubernetes 访问设置由资源管理,这些资源可以被放入部署仓库并由 GitOps 运营商交付。入侵者可能会创建一个 ClusterRole 并授予服务账户权限,该服务账户后来可能被用作后门。

防止权限提升的实用规则是限制 GitOps 运营商的权限。如果利用 GitOps 运营商的开发团队不应该管理 ClusterRoles,那么 GitOps 运营商就不应该拥有那个权限。如果 GitOps 运营商被多个团队共享,运营商应该适当配置并执行特定于团队的网络安全检查。

练习 6.7

参考穷人的 GitOps 运营商教程。审查 RBAC 配置并检查它是否允许安全权限提升攻击。

摘要

  • 由于工程师和 CI 系统的访问,传统的 CI/Ops 安全模型具有广泛的攻击面。GitOps 安全模型显著减少了集群的攻击面,因为只有 GitOps 运营商可以访问集群。

  • Git 的底层数据结构使用 Merkle 树,它提供了一个具有加密保护的树状结构,以提供防篡改的提交日志。

  • 除了 Git 的数据结构安全优势外,使用拉取请求进行的代码审查过程以及使用 kubeaudit 和 kubesec 等工具进行的自动化检查可以检测到清单中的安全漏洞。

  • Git 本身并不保护提交作者的标识。使用 GPG 可以通过在提交元数据中注入数字加密签名来保证提交作者的认证性。

  • RBAC 是在 Kubernetes 中实现访问控制的推荐方式。用户和 GitOps 运营商的访问控制都可以通过 RBAC 进行配置。

  • 与 Git 类似,所有 Docker 镜像都应该使用数字签名进行签名,以使用内容信任功能验证其真实性。

  • 新项目可以从完全访问权限(集群、部署仓库和代码仓库)开始,以便工程师最初专注于开发速度。随着项目的成熟和准备进行初始生产发布,集群和部署仓库的访问权限应受到限制,以强调安全性而不是速度。


1.mng.bz/MXB7.

2.en.wikipedia.org/wiki/Blockchain.

3.en.wikipedia.org/wiki/Merkle_tree.

4.mng.bz/goBl.

5.mng.bz/e51z.

6.mng.bz/pVPG.

7.github.com/Shopify/kubeaudit.

8.kubesec.io/.

9.Joe Beda (www.linkedin.com/in/jbeda) 是 Kubernetes 的主要创始人之一。

10.help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account.

11.mng.bz/9MDl.

12.docs.docker.com/docker-hub/repos.

7 秘密

本章涵盖了

  • Kubernetes Secrets

  • GitOps 管理秘密的策略

  • 管理秘密的工具

Kubernetes 提供了一种机制,允许用户在称为 Secret 的受保护资源对象中存储少量敏感信息。Secret 是您想要严格控制访问权限的任何内容。您可能希望存储在 Secret 中的常见数据包括用户名和密码凭证、API 密钥、SSH 密钥和 TLS 证书。在本章中,您将了解在使用 GitOps 系统时不同的秘密管理策略。您还将简要介绍一些可用于存储和管理秘密的不同工具。

我们建议您在阅读本章之前先阅读第一章和第二章。

7.1 Kubernetes Secrets

一个简单的 Kubernetes Secret 是一个由三部分信息组成的数据结构:

  • 秘密的名称

  • 秘密的类型(可选)

  • 字段名到敏感数据的映射,以 Base64 编码

一个基本的秘密看起来如下。

列表 7.1 example-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: login-credentials
type: Opaque                  ❶
data:
  username: YWRtaW4=          ❷
  password: UEA1NXcwcmQ=      ❸

❶ 秘密的类型,用于简化对秘密数据的程序化处理

❷ 字符串“admin”Base64 编码

❸ 字符串“P@55w0rd”Base64 编码

当第一次查看秘密的值时,您可能会错误地认为秘密值是用加密保护的,因为字段对人类不可读,并且不以纯文本形式呈现。但您是错误的,并且重要的是要理解

  • 秘密值是 Base64 编码的。

  • Base64 编码与加密不同。

  • 应将查看视为与纯文本相同。

Base64 编码 Base64 是一种编码算法,它允许您将任何字符转换成一个由拉丁字母、数字、加号和斜杠组成的字母表。它允许二进制数据以 ASCII 字符串格式表示。Base64 编码提供加密。

Kubernetes 对数据进行 Base64 编码的原因是它允许秘密存储二进制数据。这对于将证书等存储为秘密非常重要。如果没有 Base64 编码,将无法将二进制配置作为秘密存储。

7.1.1 为什么使用秘密?

在 Kubernetes 中使用秘密是可选的,但比其他技术(如直接在 Pod 规范中放置敏感值或在构建时将值烘焙到容器镜像中)更方便、更灵活、更安全。就像 ConfigMaps 一样,秘密允许将应用程序的配置与构建工件分离。

7.1.2 如何使用秘密

Kubernetes Secrets,就像 ConfigMaps 一样,可以用几种方式使用:

  • 作为 Pod 中挂载的文件

  • 作为 Pod 中的环境变量

  • Kubernetes API 访问

图片

图 7.1 使用机密卷传递敏感信息,如密码,到 Pod。机密卷由 tmpfs(一个基于 RAM 的文件系统)支持,因此它们永远不会写入非易失性存储。

Pod 中的文件卷挂载机密信息

利用机密的第一种技术是将它们挂载到 Pod 中作为卷。为此,你首先声明以下内容。

列表 7.2 secret-volume.yaml

apiVersion: v1
kind: Pod
metadata:
  name: secret-volume-pod
spec:
  Volumes:                      ❶
  - name: foo
    secret:
      secretName: mysecret
  containers:
  - name: mycontainer
    image: redis
    volumeMounts:               ❷
    - name: foo
      mountPath: /etc/foo
      readOnly: true

❶ 在 Pod 中声明了一个类型为 Secret 的卷,具有任意名称。

❷ 需要机密信息的容器指定了挂载机密数据卷的路径。

当将机密(或 ConfigMap)投影到 Pod 作为文件卷时,底层机密的变化最终会更新 Pod 中挂载的文件。这为应用程序提供了重新配置自己或热重载的机会,而无需重启容器/Pod。

将机密作为环境变量使用

利用 Kubernetes 机密的第二种方式是将它们设置为环境变量。

列表 7.3 secret-environment-variable.yaml

apiVersion: v1
kind: Pod
metadata:
  name: secret-env-pod
spec:
  containers:
  - name: mycontainer
    image: redis
    env:
    - name: SECRET_USERNAME
      valueFrom:
        secretKeyRef:
          name: mysecret       ❶
          key: username        ❷
    - name: SECRET_PASSWORD
      valueFrom:
        secretKeyRef:
          name: mysecret       ❶
          key: password        ❷

❶ 机密的名称

❷ 机密数据映射的键

将机密作为环境变量暴露给容器,虽然方便,但可能不是消费机密的最佳方式,因为它比将其作为卷挂载文件消费的安全性更低。当机密被设置为环境变量时,容器中的所有进程(包括子进程)都将继承操作系统环境并能够读取环境变量值,从而读取机密数据。例如,一个分叉的 shell 脚本可以通过运行env实用程序来读取环境变量。

机密环境变量的缺点 使用机密作为环境变量的第二个缺点是,与投影到卷中的机密不同,如果容器启动后机密被更新,机密环境变量的值将不会更新。需要容器或 Pod 重启才能注意到变化。

使用 K8s API 中的机密

最后,Kubernetes 机密也可以直接从 Kubernetes API 中检索。假设你有一个以下带有密码字段的机密。

列表 7.4 secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
type: Opaque
data:
  password: UEA1NXcwcmQ=

要检索机密,Pod 本身可以直接从 Kubernetes 中检索机密值,例如,通过使用kubectl命令或 REST API 调用。以下kubectl命令检索名为my-secret的机密,对密码字段进行 Base64 解码,并将纯文本值打印到标准输出:

$ kubectl get secret my-secret -o=jsonpath='{.data.password}' | base64 --decode
P@55w0rd

这种技术要求 Pod 有权限检索机密。

机密类型

机密类型字段是数据包含在机密中的类型的指示。它主要用于软件程序识别它们可能感兴趣的相关的机密,以及安全地做出关于机密内部可用的字段设置的假设。

以下表格描述了内置的 Kubernetes 机密类型,以及每种类型所需的字段。

表 7.1 内置机密类型

类型 描述 必需字段
Opaque 默认类型。包含任意用户定义的数据。
kubernetes.io/service-account-token 包含一个标识 Kubernetes API 中服务账户的令牌。 data["token"]
kubernetes.io/dockercfg 包含一个序列化的 ~/.dockercfg 文件。 data[".dockercfg"]
kubernetes.io/dockerconfigjson 包含一个序列化的 ~/.docker/config.json 文件。 data[".dockerconfigjson"]
kubernetes.io/basic-auth 包含基本的用户名/密码凭证。 data["username"]data["password"]
kubernetes.io/ssh-auth 包含用于身份验证所需的私有 SSH 密钥。 data["ssh-privatekey"]
kubernetes.io/tls 包含 TLS 私钥和证书。 data["tls.key"]data["tls.crt"]

7.2 GitOps 和密钥

Kubernetes GitOps 实践者不可避免地会遇到相同的问题:虽然用户在 Git 中存储配置时感到非常舒适,但当涉及到敏感数据时,由于安全顾虑,他们不愿意在 Git 中存储这些数据。Git 被设计为一个协作工具,使得多人团队可以轻松访问代码并查看彼此的更改。但正是这些相同的属性使得使用 Git 来存储密钥成为一种极其危险的做法。有许多担忧和原因说明为什么在 Git 中存储密钥是不合适的,我们将在下面进行说明。

7.2.1 没有加密

如我们之前所学的,Kubernetes 并不对 Secret 的内容进行加密,值的 Base64 编码应被视为与纯文本相同。此外,Git 本身也不提供任何形式的内置加密。因此,当在 Git 仓库中存储密钥时,密钥对任何有权访问 Git 仓库的人来说都是公开的。

7.2.2 分布式 Git 仓库

使用 GitOps,你和你的同事将本地克隆 Git 仓库到你的笔记本电脑和工作站,目的是管理应用程序的配置。但这样做的同时,你也会在许多系统中传播和分发密钥,而没有足够的审计或跟踪。如果其中任何系统遭到破坏(被黑客攻击或甚至物理丢失),有人将能够访问你所有的密钥。

7.2.3 没有细粒度(文件级)访问控制

Git 不提供对 Git 仓库子路径或子文件的读取保护。换句话说,不可能限制对某些文件的访问,而不限制对其他文件的访问。在处理密钥时,理想情况下应根据需要了解情况来授予读取访问权限。例如,如果你有一个需要部分访问 Git 仓库的临时工作人员,你希望尽可能少地给予该用户访问内容。不幸的是,Git 不提供任何实现此目的的设施,在授予仓库权限时是一个全有或全无的决定。

7.2.4 不安全存储

Git 从来没有打算用作 Secret 管理系统。因此,它没有将标准安全功能(如静态加密)设计到系统中。因此,如果 Git 服务器被入侵,它可能会泄露它所管理的所有存储库的 Secrets,使其成为攻击的主要目标。

Git 提供商功能 虽然 Git 本身不提供静态加密等安全功能,但 Git 提供商通常会在 Git 之上提供这些功能。例如,GitHub 声称会对存储库进行静态加密。但此功能可能因提供商而异。

7.2.5 完整提交历史

一旦 Secret 被添加到 Git 提交历史中,就很难删除。如果 Secret 被检查到 Git 中,然后后来被删除,那么在 Secret 被删除之前的存储库历史中的早期点仍然可以检索到该 Secret。即使 Secret 被加密,当用于加密 Secret 的密钥后来被轮换,并且使用新密钥重新加密 Secret 时,使用旧密钥加密的 Secret 仍然存在于存储库历史中。

7.3 Secrets 管理策略

在 GitOps 中处理 Secrets 有许多不同的策略,这些策略在灵活性、可管理性和安全性方面存在权衡。在介绍实现这些策略的工具之前,我们首先从概念层面概述一些策略。

7.3.1 在 Git 中存储 Secrets

GitOps 和 Secrets 的第一策略就是根本不制定策略。换句话说,你只需像管理其他 Kubernetes 资源一样,将 Secrets 提交到 Git 并接受其安全后果。

你可能会想,“将我的 Secrets 存储在 Git 中有什么不好?”即使你有私有 GitHub 存储库,只有你的团队成员可以访问,你也可能希望允许第三方访问 Git 仓库——CI/CD 系统、安全扫描器、静态分析等。通过向这些第三方软件系统提供你的 Secrets Git 仓库,你实际上是在将你的 Secrets 交托给他们。

因此,在实践中,唯一真正可以接受的将 Secrets 以原样存储在 Git 中的场景是当 Secrets 不包含任何真正敏感数据时,例如开发和测试环境。

7.3.2 将 Secrets 集成到容器镜像中

一种可能想到的简单策略是直接将敏感数据集成到容器镜像中,以避免在 Git 中存储 Secrets。在这种方法中,Secret 数据作为 Docker 构建过程的一部分直接复制到容器镜像中。

图 7.2 将 Secret 集成到容器镜像中。Docker 构建过程将敏感数据(例如,通过将敏感文件复制到容器中)集成到镜像中。没有使用任何 Secret 存储库(Kubernetes 或外部),但容器注册库变得敏感,因为它实际上是一个 Secret 存储库。

一个简单的 Dockerfile,将秘密烘焙到镜像中可能看起来像这样。

列表 7.5 带有秘密的 Dockerfile

FROM scratch

COPY ./my-app /my-app
COPY ./credentials.txt /credentials.txt

ENTRYPOINT [“/my-app”]

这种方法的优点是它消除了 Git 和甚至 Kubernetes 本身。实际上,由于秘密数据被烘焙到容器镜像中,该镜像可以在任何地方运行,而不仅仅是 Kubernetes,并且无需任何配置即可工作。

然而,将敏感数据直接烘焙到容器镜像中有一些非常严重的缺点,这应该自动将其排除为可行的选项。第一个问题是容器镜像本身现在变得敏感。由于敏感数据被烘焙到镜像中,现在任何或任何可以访问容器镜像的人或事物(例如通过 docker pull),现在可以轻易地复制和检索秘密。

另一个问题是因为秘密被烘焙到镜像中,秘密数据的更新非常繁琐。每当需要轮换凭证时,都需要完全重建容器镜像。

第三个问题是容器镜像不够灵活,无法适应需要使用不同秘密数据集运行相同镜像的情况。假设你有三个环境,这个容器镜像将被部署到这些环境中:

  • 开发环境

  • 测试环境

  • 生产环境

每个这些环境都需要一组不同的凭证,因为它们连接到三个不同的数据库。将秘密数据烘焙到容器镜像中的方法在这里不起作用,因为它只能选择将其中一个数据库凭证烘焙到镜像中。

7.3.3 非带管理

处理 GitOps 中秘密的另一种方法是完全在 GitOps 之外管理秘密。采用这种方法,除了 Kubernetes 秘密之外的所有内容都将定义在 Git 中并通过 GitOps 部署,但将使用某种其他机制来部署秘密,即使它是手工的。

例如,用户可以将他们的秘密存储在数据库中,云提供商管理的秘密存储中,甚至是在他们本地工作站上的文本文件中。当部署时,用户将手动运行 kubectl apply 将秘密部署到集群中,然后让 GitOps 运营商部署其他所有内容。

图片

图 7.3 在非带管理中,GitOps 用于部署常规资源。但使用某种其他机制(如手动 kubectl apply)来部署秘密。

这种方法的明显缺点是,你需要有两种不同的机制来将资源部署到集群中:一种是通过 GitOps 部署常规 Kubernetes 资源,另一种专门用于部署秘密。

7.3.4 外部秘密管理系统

在 GitOps 中处理密钥的另一种策略是使用除 Kubernetes 之外的外部密钥管理系统。在这种策略中,而不是使用 Kubernetes 的本地功能将密钥存储和加载到容器中,应用程序容器本身在运行时、使用点动态检索密钥值。

存在着各种密钥管理系统,但最流行和最广泛使用的是 HashiCorp Vault,这是我们讨论外部密钥管理系统时将主要关注的工具。各个云服务提供商也提供了自己的密钥管理服务,例如 AWS Secrets Manager、Google Cloud Secret Manager 和 Microsoft Azure Key Vault。这些工具在功能和特性集上可能有所不同,但基本原则是相同的,并且应该适用于所有情况。

图 7.4 从外部密钥存储检索密钥。在这种方法中,敏感数据不是作为 Kubernetes 密钥存储的。相反,它存储在外部系统中,该系统将在运行时由容器检索(例如通过 API 调用)。

通过选择使用外部密钥管理系统(如 Vault)来管理您的密钥,您实际上是在做出一个决定,即不使用 Kubernetes 密钥。这是因为当使用这种策略时,您依赖于外部密钥管理系统来存储和检索您的密钥,而不是 Kubernetes。一个重要的后果是,您也无法利用 Kubernetes 密钥提供的一些便利,例如从密钥设置环境变量的值或将密钥映射为卷中的文件。

当使用外部密钥存储时,应用程序有责任安全地从存储中检索密钥。例如,当应用程序启动时,它可以在运行时动态地从密钥存储中检索密钥值,而不是使用 Kubernetes 机制(环境变量、卷挂载等)。这把保管密钥的责任转移给了必须安全检索密钥的应用程序开发人员和外部密钥存储的管理员。

这种技术的另一个后果是,由于密钥是在单独的数据库中管理的,因此您无法像 Git 中管理的配置那样拥有密钥更改的历史/记录。这甚至可能影响您以可预测的方式回滚的能力。例如,在回滚过程中,应用上一个 Git 提交的清单可能不足以完成。您还必须同时回滚密钥到 Git 提交时的先前值。根据所使用的密钥存储,这可能在最好情况下不方便,在最坏情况下甚至不可能。

7.3.5 在 Git 中加密密钥

由于 Git 被认为不适合存储纯文本 Secrets,一种策略是加密敏感数据,使其在 Git 中存储是安全的,然后在接近使用点的地方解密加密数据。执行解密的操作者必须拥有解密加密 Secret 所需的密钥。这可能是由应用程序本身、填充应用程序使用的卷的 init 容器,或者是一个控制器,以无缝地处理这些任务。

图片

图 7.5 Secrets 与其他 Kubernetes 资源一起在 Git 中加密并安全存储。在运行时,应用程序可以在使用之前解密内容。

一个流行的工具,有助于在 Git 中加密 Secrets 的技术是 Bitnami SealedSecrets,我们将在本章后面详细讨论。

Git 中加密 Secrets 的挑战在于,仍然涉及一个最后的 Secret,那就是用于加密这些 Secrets 的加密密钥。如果没有对加密密钥的充分保护,这种技术就毫无意义,因为任何能够访问加密密钥的人现在都有能力解密并获取清单中的敏感数据。

7.3.6 策略比较

在 Kubernetes 中管理 Secrets 有许多不同的方法,每种方法都有其权衡。在决定适合您需求的解决方案和/或工具之前,请考虑以下优点和缺点。

表 7.2 GitOps Secrets 管理策略

类型 优点 缺点
存储在 Git 中 简单方便 Secrets 和配置在同一个地方(Git)管理 完全不安全
将其嵌入到镜像中 简单方便 容器镜像敏感。轮换 Secrets 需要重新构建镜像。镜像不可移植。Secrets 不能在 Pod 之间共享。
离线管理 仍然能够利用原生 Kubernetes Secrets 功能(如卷挂载和环境变量) 部署 Secrets 和配置修改到 Secrets 的过程不同,Git 历史记录中未记录,可能会影响回滚的能力。

7.4 工具

在 Kubernetes 生态系统内外,已经出现了许多项目来帮助用户解决 Secrets 的问题。所有这些项目都使用之前讨论过的 Secret 管理策略之一。在本节中,我们将介绍一些更受欢迎的工具,这些工具可以通过 GitOps 方法补充 Kubernetes 环境。

7.4.1 HashiCorp Vault

HashiCorp 的 Vault 是一个专为以安全方式存储和管理 Secrets 而构建的开源工具。Vault 提供命令行界面(CLI)、用户界面(UI)以及用于以编程方式访问 Secret 数据的 API。Vault 不仅限于 Kubernetes,并且作为独立的 Secret 管理系统而广受欢迎。

Vault 安装和设置

安装和运行 Vault 有许多方法。但如果你是 Vault 的新手,推荐且最简单的方法是使用由 HashiCorp 维护的官方 Helm 图表安装 Vault。为了简化我们的教程,我们将以开发模式安装 Vault,这种模式适用于实验、开发和测试。此外,该命令还安装了 Vault Agent Sidecar Injector,我们将在下一节中介绍并使用:

# NOTE: requires Helm v3.0+

$ helm repo add hashicorp https://helm.releases.hashicorp.com

$ helm install vault hashicorp/vault \
    --set server.dev.enabled=true \
    --set injector.enabled=true

非 Kubernetes 安装注意:在 Kubernetes 环境中运行 Vault 并非必需。Vault 是一个通用的密钥管理系统,适用于 Kubernetes 以外的应用程序和平台。许多企业选择为公司运行一个集中管理的 Vault 实例,因此单个 Vault 实例可以为多个 Kubernetes 集群和虚拟机提供服务,同时也可以被来自企业网络和工作站的开发人员和运维人员访问。

Vault CLI 可以从www.vaultproject.io/downloads下载,或者(对于 macOS)通过使用brew软件包管理器:

$ brew install vault

安装完成后,Vault 可以通过标准端口转发访问,并访问 http://localhost:8200 的 UI:

# Run the following from a different terminal, or background it with ‘&’
$ kubectl port-forward vault-0 8200

$ export VAULT_ADDR=http://localhost:8200

# In dev mode, the token is the word: root
$ vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                root
token_accessor       o4SQvGgg4ywEv0wnGgqHhK1h
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

$ vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.4.0
Cluster Name    vault-cluster-23e9c708
Cluster ID      543c058a-a9d4-e838-e270-33f7e93814f2
HA Enabled      false

Vault 使用

一旦 Vault 安装到您的集群中,就是时候在 Vault 中存储您的第一个密钥了:

$ vault kv put secret/hello foo=world
Key              Value
---              -----
created_time     2020-05-15T12:36:21.956834623Z
deletion_time    n/a
destroyed        false
version          1

要检索密钥,运行vault kv get命令:

$ vault kv get secret/hello
====== Metadata ======
Key              Value
---              -----
created_time     2020-05-15T12:36:21.956834623Z
deletion_time    n/a
destroyed        false
version          1

=== Data ===
Key    Value
---    -----
foo    world

默认情况下,vault kv get将以表格格式打印密钥。虽然这种格式以易于阅读的方式呈现,并且非常适合人类阅读,但它不太容易通过自动化解析,也不太容易被应用程序消费。为了帮助解决这个问题,Vault 提供了一些额外的输出格式化方法和提取密钥特定字段的方法:

$ vault kv get -field foo secret/hello
world

$ vault kv get -format json secret/hello
{
  "request_id": "825d85e4-8e8b-eab0-6afb-f6c63856b82c",
  "lease_id": "",
  "lease_duration": 0,
  "renewable": false,
  "data": {
    "data": {
      "foo": "world"
    },
    "metadata": {
      "created_time": "2020-05-15T12:36:21.956834623Z",
      "deletion_time": "",
      "destroyed": false,
      "version": 1
    }
  },
  "warnings": null
}

这使得 Vault CLI 在启动脚本中使用变得容易,这可能

  1. 运行vault kv get命令以检索密钥的值。

  2. 将密钥值设置为环境变量或文件。

  3. 启动主应用程序,现在它可以从环境变量或文件中读取密钥。

这样的启动脚本可能看起来如下所示。

列表 7.6 vault-startup.sh

#!/bin/sh

export VAULT_TOKEN=your-vault-token
export VAULT_ADDR=https://your-vault-address.com:8200
export HELLO_SECRET=$(vault kv get -field foo secret/hello)./guestbook

要将其与 Kubernetes 应用程序集成,此启动脚本将用作容器的入口点,用启动脚本替换正常的应用程序命令,启动脚本会在密钥被检索并设置为环境变量之后启动应用程序之后

关于这种方法需要注意的一点是,vault kv get 命令本身需要权限来访问 Vault。因此,为了使此脚本工作,vault kv get 需要安全地与 Vault 服务器通信,通常使用 Vault 令牌。另一种说法是,您仍然需要一个机密来获取更多机密。这提出了一个“先有鸡还是先有蛋”的问题,即您现在需要以某种方式安全地配置和存储用于检索应用程序机密的 Vault 机密。解决方案在于 Kubernetes-Vault 集成,我们将在下一节中介绍。

7.4.2 Vault Agent 侧边注入器

由于 Vault 的流行,已经创建了众多 Vault 和 Kubernetes 集成,以使其更容易使用。由 HashiCorp 开发并支持的官方 Kubernetes 集成是 Vault Agent 侧边注入器。

如前节所述,为了从 Vault 中检索机密,使用了专门的脚本,该脚本在启动应用程序之前执行了一些先决步骤。这包括检索和准备应用程序的机密。这种方法存在一些问题:

  • 尽管以安全的方式检索了应用程序机密,但该技术仍需要处理保护用于访问应用程序机密的 Vault 机密。

  • 容器需要是 Vault 意识的,也就是说,容器需要使用一个专门的脚本构建,该脚本了解如何检索特定的 Vault 机密并将其传递给应用程序。

为了解决这个问题,HashiCorp 开发了 Vault Agent 侧边注入器,以通用方式解决了这两个问题。Vault Agent 侧边注入器自动修改以特定方式注释的 Pods,并安全地检索注释的机密引用(应用程序机密)并将这些值渲染到应用程序容器可访问的共享卷中。通过将机密渲染到共享卷中,Pod 内的容器可以在不了解 Vault 的情况下消费 Vault 机密。

工作原理

Vault Agent 注入器修改 Pod 规范以包含 Vault Agent 容器,这些容器将 Vault 机密填充到应用程序可访问的共享内存卷中。为了实现这一点,您使用 Kubernetes 中的一个功能,称为修改性准入网关。

修改性准入网关修改性准入网关是扩展 Kubernetes API 服务器以添加额外功能的方式之一。修改性网关作为 HTTP 回调实现,它拦截准入请求(创建、更新、补丁请求)并以某种方式修改对象。

图 7.6 解释了 Vault Agent 注入器的工作原理。

图 7.6 正常创建了一个 Pod,但它具有 Vault Agent 侧边注入器可以理解的特殊注释。根据这些注释,包含所需机密的目录将被挂载到容器中,以便应用程序使用。

采用这种方法涉及的一系列步骤如下:

  1. 将工作负载资源(Deployment、Job、ReplicaSet 等)部署到集群中。这最终会创建一个 Kubernetes Pod。

  2. 随着 Pod 的创建,Kubernetes API 服务器调用 Vault Agent Sidecar Injector 的修改 webhook 调用。Vault Agent Sidecar Injector 通过向 Pod 注入一个 init 容器(以及可选的 sidecar)来修改 Pod。

  3. 当 Vault Agent Init 容器运行时,它会安全地与 Vault 通信以检索 Secret。

  4. Secret 被写入共享内存卷,该卷由 init 容器和应用容器共享。

  5. 当应用容器运行时,它现在能够从共享内存卷中检索 Secret。

Vault Agent Sidecar Injector 安装和设置

在本章前面的部分,我们描述了如何使用官方 Helm 图表安装 Vault。此图表还包括 Agent Sidecar Injector。以下重复了这些说明。请注意,示例假设您的当前 kubectl 上下文指向默认命名空间:

# NOTE: requires Helm v3.0+

$ helm repo add hashicorp https://helm.releases.hashicorp.com

$ helm install vault hashicorp/vault \
    --set server.dev.enabled=true \
    --set injector.enabled=true

使用

当应用程序希望从 Vault 中检索其 Secrets 时,Pod 规范至少需要包含以下 Vault 代理注解。

列表 7.7 vault-agent-inject-annotations.yaml

annotations:
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/agent-inject-secret-hello.txt: secret/hello
  vault.hashicorp.com/role: app

分解来看,这些注解传达了几个信息点:

  • 注解键 vault.hashicorp.com/agent-inject: "true" 通知 Vault Agent Sidecar Injector,对于这个 Pod 应该进行 Vault 秘密注入。

  • 注解值 secret/hello 指示要将哪个 Vault 秘密键注入到 Pod 中。

  • 注解 vault.hashicorp.com/agent-inject-secret-hello.txt 的后缀 hello.txt 指示在共享内存卷中应该在一个名为 hello.txt 的文件下填充 Secret,最终路径为 /vault/secrets/hello.txt。

  • 来自 vault.hashicorp.com/role 的注解值指示在检索 Secret 时应使用哪个 Vault 角色。

现在我们用一个真实示例来尝试。要运行本教程中的所有 Vault 命令,您首先需要获取 Vault 内部的控制台访问权限。运行 kubectl exec 以访问 Vault 服务器的交互式控制台:

$ kubectl exec -it vault-0 -- /bin/sh
/ $

如果您还没有这样做,请遵循之前关于在 Vault 中创建第一个名为“hello”的 Secret 的指南:

$ vault kv put secret/hello foo=world
Key              Value
---              -----
created_time     2020-05-15T12:36:21.956834623Z
deletion_time    n/a
destroyed        false
version          1

接下来,我们需要配置 Vault 以允许 Kubernetes Pods 进行身份验证并检索 Secrets。为此,运行以下 Vault 命令以启用 Kubernetes auth 方法:

$ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/

$ vault write auth/kubernetes/config \
    token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
    kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
    kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Success! Data written to: auth/kubernetes/config

这两个命令配置 Vault 使用 Kubernetes 认证方法,使用服务账户令牌、Kubernetes 主机的位置及其证书。

接下来,我们定义一个名为“app”的策略以及一个名为“app”的角色,该角色将具有对“hello”Secret 的读取权限:

# Create a policy "app" which will have read privileges to the "secret/hello" secret
$ vault policy write app - <<EOF
path "secret/hello" {
  capabilities = ["read"]
}
EOF

# Grants a pod in the "default" namespace using the "default" service account
# privileges to read the "hello" secret
$ vault write auth/kubernetes/role/app \
    bound_service_account_names=default \
    bound_service_account_namespaces=default \
    policies=app \
    ttl=24h

现在是时候部署一个 Pod,该 Pod 将自动获取我们注入的 Vault Secret。应用以下 Deployment 清单,其中包含我们在 Pod 上描述的 Vault 注解。

列表 7.8 vault-agent-inject-example.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vault-agent-inject-example
spec:
  selector:
    matchLabels:
      app: vault-agent-inject-example
  template:
    metadata:
      labels:
        app: vault-agent-inject-example
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-inject-secret-hello.txt: secret/hello
        vault.hashicorp.com/role: app
    spec:
      containers:
      - name: debian
        image: debian:latest
        command: [sleep, infinity]

当部署运行时,我们可以访问 Pod 的控制台并验证 Pod 确实已经将 Secret 挂载在其中:

$ kubectl exec deploy/vault-agent-inject-example -it -c debian -- bash
root@vault-agent-inject-example-5c48967c97-hgzds:/# cat /vault/secrets/hello.txt
data: map[foo:world]
metadata: map[created_time:2020-10-14T17:58:34.5584858Z deletion_time: destroyed:false version:1]

正如你所见,使用 Vault Agent Sidecar Injector 是将 Vault Secrets 无缝安全地注入到你的 Pods 中的最简单方法之一。

7.4.3 Sealed Secrets

Sealed Secrets,由 Bitnami 提供,是 GitOps Secret 问题的另一种解决方案,并恰当地描述了问题为“我可以管理我在 Git 中的所有 K8s 配置,除了 Secrets。”虽然不是唯一的工具,但目前 Sealed Secrets 是那些更愿意在 Git 中加密他们的 Secrets 的团队中最受欢迎和最广泛使用的工具。这允许包括 Secrets 在内的一切都可以在 Git 中完全和彻底地管理。

Sealed Secrets 采用加密敏感数据以安全存储在 Git 中的策略,并在集群内部解密。使其独特的是,它提供了一个控制器和命令行界面,有助于自动化此过程。

它是如何工作的

Sealed Secrets 由以下内容组成:

  • 一个新的 CustomResourceDefinition,称为 SealedSecret,它将生成一个正常的 Secret

  • 一个在集群中运行的控制器,负责解密 SealedSecret,并生成一个包含解密数据的正常 Kubernetes Secret

  • 一个命令行工具,kubeseal,它将敏感数据加密到 SealedSecret 中,以在 Git 中安全存储

当用户希望使用 Git 管理一个 Secret 时,他们将通过 kubeseal CLI 将 Secret 封装或加密为 SealedSecret 自定义资源,并将其存储在 Git 中,与其他应用程序资源(Deployments、ConfigMaps 等)一起。SealedSecret 的部署就像任何其他 Kubernetes 资源一样。

当一个 SealedSecret 部署时,sealed-secrets-controller 将解密数据并生成一个具有相同名称的正常 Kubernetes Secret。此时,与 SealedSecret 和正常 Kubernetes Secret 的体验没有区别,因为常规 Kubernetes Secret 可供 Pods 使用。

图片

图 7.7 用户将 Secret 加密为 SealedSecret 并存储在 Git 中。Sealed Secrets 控制器解密 SealedSecret,并制定一个相应的 Kubernetes Secret,供 Pod 使用常规 Kubernetes 功能。

安装

CRD 和控制器:

$ kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.12.4/controller.yaml

Kubeseal CLI:

使用

要使用 SealedSecrets,你首先创建一个常规 Kubernetes Secret,就像你通常使用你喜欢的技术一样,并将其放置在某个本地文件路径。在这个简单的例子中,我们将使用 kubectl create secret 命令创建一个密码 Secret。--dry-run 标志用于将值打印到 stdout,然后重定向到临时文件。我们将其存储在临时位置,因为包含未加密数据的 Secret 应该被丢弃,不应持久化在 Git(或任何其他地方)。

$ kubectl create secret generic my-password --from-literal=password=Pa55Word1 --dry-run -o yaml > /tmp/my-password.yaml

不要使用 kubectl create secret --from-literal 在前面的例子中,使用 --from-literal 只是为了演示和练习目的,它永远不应该与任何敏感数据一起使用。这是因为你的 shell 将最近运行的命令记录到历史文件中,以便方便检索。如果你希望使用 kubectl 生成 Secret,请考虑使用 --from-file 代替。

前一个命令将生成以下临时 Kubernetes Secret 文件。

列表 7.9 my-password.yaml

apiVersion: v1
kind: Secret
metadata:
  name: my-password
data:
  password: UGE1NVdvcmQx

下一步是使用 kubeseal CLI 对临时 Secret 进行密封或加密。以下命令基于我们刚刚创建的临时 Secret 文件创建一个 SealedSecret 对象:

$ kubeseal -o yaml </tmp/my-password.yaml > my-sealed-password.yaml

这将生成以下 SealedSecret 资源,可以安全地存储在 Git 中。

列表 7.10 my-sealed-password.yaml

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: my-password
  namespace: default
spec:
  encryptedData:
    password: AgAF7r6v4LG/JPU7TiOi77bhd1NJ9ua9gldvzNw7wBKK2LLJyndSR8GShF3f1zRY+cNM0iOGTkcaFrNRCG/CMrLiwNltQv1gZKqryFugjcp7tiM0dwmmi4M0aIeqRfXq3+vL/Mmdc/xEsK/FtuKOg18rWoG/wEhvNhtvXu1t4kXHTSVL5xa4KmYD8Hn8p8CNZrGATLfy6rIlZsydM9DoB1nSFDsfG5kHlE++RbyXxd6Y6vckK1DPl6oqI5GidnrEQlQmkhEr+h/YuUrajAxMFNZpqzs9yaTkURdc0xDp2w
     MiycBooEn7eRzTt2aTohO4q9rgoiWwjztCyXdOCyCt+eisoG0QsqC697PiQV35IFuNbkpty
     FUU04nfMtxYfb2aZEHfVt8/j3xl9JlqKQ16zy9g0jhj1QLxhBjmRK9EyqTxqVGRTfrHaHqqz
     7mzSy/x2H6lkfBBVFLWSvwOFkYD82wdQRfTYBF5Uu/cnjeB2Uob8JkM91nEtXhLWAwtl2K5
     w0LYyUd3qOaNEEXgyv+dN/4pTHK1V+LF6IHNDOFau8QVNmqJrxrXv8yEnRGzBYg60J99Kl9
     vhp8pfbHAYfn2Tb9o8WxWjWD0YAc+pAuFAGjUmJKEJmaPr0vUo0k67BlXj77LVuHPH6
     Ei6JxGYOZA0B2WElmOwILHzDl7unWXnI+Q7Hmk2TEYSeEo81x+I9mLd8D6EpunG2lFndo=
  template:
    metadata:
      creationTimestamp: null
      name: my-password
      namespace: default

SealedSecret 现在可以存储在与你的其他应用程序清单一起,并像正常资源一样部署,无需任何特殊处理。

无集群访问权限密封 Secret 默认情况下,kubeseal 将使用 sealed-secrets-controller 的证书加密 Secret。为此,它需要访问 Kubernetes 集群以直接从 sealed-secrets-controller 获取证书。可以在不直接访问集群的情况下离线密封 Secret。或者,可以使用不同的方式提供证书,通过使用 kubeseal--cert 标志,该标志允许你指定证书的本地路径或甚至 URL。

当你比较原始 Kubernetes Secret 和 SealedSecret 时,你可能已经注意到,SealedSecret 在元数据中指定了命名空间,而原始 Secret 没有指定。这意味着 SealedSecret 比原始 Secret 的可移植性要低得多,因为它不能在不同的命名空间中部署。这实际上是 SealedSecret 的一个安全特性,被称为 严格范围。在严格范围内,SealedSecret 以一种方式加密 Secret,使其只能用于加密的命名空间,并且与完全相同的 Secret 名称(在我们的例子中是 my-password)。这个特性防止了攻击者将 SealedSecret 部署到不同的命名空间或名称,该攻击者有权访问,以查看 Secret 的敏感数据。

在非多租户环境中,严格的范围可以被放宽,使得相同的 SealedSecret 可以在集群中的任何命名空间中使用。为此,你可以在密封过程中指定 --scope cluster-wide 标志:

$ kubeseal -o yaml --scope cluster-wide </tmp/my-password.yaml > my-cluster-sealed-password.yaml

这将生成一个略微不同的集群范围 SealedSecret,现在它不再包含命名空间。

列表 7.11 my-clusterwide-sealed-password.yaml

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  annotations:
    sealedsecrets.bitnami.com/cluster-wide: "true"
  creationTimestamp: null
  name: my-password
spec:
  encryptedData:
    password: AgBjcpeaU2SKqOTMQ2SxYnxoJgy19PR7uzi1qrP5e3PqCPRi7yWD6TvozJE2r9O
     rey0zLL0/yTuIHn0Z5S7FBQT6p7FA19FGxcCu+Xdd1p/purofibL5xR8Zfk/VxEAH2RSVPS
     UGdMwpMRqhKFrsK2rZujjrDjOdC/7zTRgueSMJ6RTIWSCctXZ5htaWIBvN3nUJKGAWsrG/cF1xA6pPANE6eZTjyX3+pEQ3YPmPqkc4chseU/aUqk3fXN5tEcwuLWFXFkihN5hMnhKUH8
     CePk7IWB/BXATxLNYlGRzrcYAoXZOyYGkUlw24yVMl0AbpmlmqYiCdlnQMEhTilc9iyKKT3
     ASplH+T/WMr7DdKcDpbTcgL0wI0EeBtUXV2zBWdNWquVA6oPCJmo4TruiBtLDZjeu6xj9fV
     tlZD/HETGLgeDuBSw/BN7fUqi6GuRObFMiZUhoN4ynm2jNHTe0bVDV6QOidbTvy6FcPjHV
     qjwKsLu2jN/TYhLTkbzHjL9Or2dZX8gI/BrmMOtoRDzSK2C4T9KqyAxipRgYkSH9cImdT9
     ChCPA9jIQUZRZGMS48Yg/SDRvA/d+QaGdMhhbhtmApWPWMaA/0+adxnPcoKBnVtuzAlPla
     YN64JCBzyJkKDVutm/wvMYtoZ95vMnLDG1d/b9CmYobAyeuz9AGZ5UeZWoZ32DMMhc5kXecR/FsnfMWeCaHiT+6423nJU=
  template:
    metadata:
      annotations:
        sealedsecrets.bitnami.com/cluster-wide: "true"
      creationTimestamp: null
      name: my-password

集群范围内的 SealedSecret 作用域的一个后果是,SealedSecret 现在可以在集群的任何命名空间中部署和解密。这意味着任何拥有集群中单个命名空间权限的人都可以简单地在自己的命名空间中部署 SealedSecret,以便查看敏感数据。

使用 SealedSecrets 的一个挑战是,用于加密 Secrets 的加密密钥对于每个集群都是不同的。当sealed-secrets-controller将普通 Kubernetes Secret 转换为 SealedSecret 并加密时,该 SealedSecret 对象仅对签名控制器有效,其他地方无效。这意味着每个集群在 Git 中都有一个不同的 SealedSecret 对象。如果您只处理单个集群,这个挑战可能不会成为问题。然而,如果同一个 Secret 需要部署到更多集群,那么这就会成为一个配置管理问题,因为需要为每个环境生成 SealedSecret。

虽然可以为多个集群使用相同的加密密钥,但这会带来不同的挑战:它变得难以在所有集群中安全地分发、管理和保护该密钥。加密密钥将分布到许多位置,提供了更多机会使其被破坏,最终允许攻击者访问每个集群中的每个 Secret。

7.4.4 Kustomize Secret 生成插件

使用 Kustomize 管理 Kubernetes 配置的用户可以访问一个独特的功能,即 Kustomize 插件,这些插件可以用于检索 Secrets。Kustomize 的插件功能允许 Kustomize 在构建过程中调用用户定义的逻辑来生成或转换 Kubernetes 资源。插件非常强大,可以编写为从外部源检索 Secrets,例如数据库、RPC 调用,甚至外部 Secret 存储库,如 Vault。该插件甚至可以编写为执行加密数据的解密并将其转换为 Kubernetes Secret。重要的是要记住,Kustomize 插件提供了一种非常灵活的机制来生成 Secrets,并且可以根据您的需求实现任何逻辑。

图 7.8 与将 Secret 存储在 Git 中不同,生成或检索该 Secret 的配方(作为 Kustomize Secret 生成器)被存储。这种方法意味着渲染后的 Secret 将在渲染后立即应用于集群(Kustomize 构建)。

它是如何工作的

正如我们在前面的章节中学到的,Kustomize 是一个配置管理工具,通常不涉及管理或检索 Secrets。但是,通过使用 Kustomize 的插件功能,可以在 kustomize build 命令中注入一些用户定义的逻辑来生成 Kubernetes 清单。由于 kustomize build 常常是渲染清单实际部署之前发生的最后一步,因此这是一个在部署之前安全检索 Secret 的完美机会,并最终避免在 Git 中存储 Secrets。

Kustomize 有两种类型的插件:exec 插件和 Go 插件。exec 插件简单地说是一个可执行文件,它接受单个命令行参数:插件 YAML 配置文件的路径。还支持 Go 插件,这些插件是用 Golang 编写的,但开发起来更复杂。在接下来的练习中,我们将编写一个 exec 插件,因为它更容易编写和理解。

在这个练习中,我们将实现一个 Kustomize Secret 检索插件,该插件将通过密钥名称“检索”特定的 Secret,并从中生成 Kubernetes Secret。这里的“检索”一词加了引号,因为在现实中,这个例子将仅仅假装检索一个 Secret,并使用硬编码的值。

要使用 Kustomize 生成器插件,我们只需在 kustomization.yaml 中引用插件配置。

列表 7.12 kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

generators:
- my-password.yaml

引用的插件配置 YAML 的内容特定于插件。Kustomize 插件规范中包含的内容没有标准。在这个练习中,我们的插件规范非常简单,只包含两件必要的信息:

  • 创建 Kubernetes Secret 的名称(我们将使用与插件配置名称相同的名称)

  • 要从外部 Secret 存储中检索的密钥(插件将假装获取)

列表 7.13 my-password.yaml

apiVersion: gitopsbook    ❶
kind: SecretRetriever
metadata:
  name: my-password       ❷
spec:
  keyName: foo            ❸

apiVersionkind 字段被 Kustomize 用于发现要运行哪个插件。

❷ 在这个例子中,插件将选择使用配置名称作为生成的 K8s Secret 名称。然而,Kustomize 插件可以自由忽略 metadata.name

keyName 将是“从外部存储检索”的密钥。

最后,我们到达实际的插件实现,我们将以 shell 脚本的形式编写它。此插件接受插件配置的路径,解析出 Secret 名称和密钥以生成最终要部署的 Kubernetes Secret。

列表 7.14 gitopsbook/secretretriever/SecretRetriever

#!/bin/bash

config=$(cat $1)                                                    ❶

secretName=$(echo "$config" | grep "name:" | awk -F: '{print $2}')  ❷

keyName=$(echo "$config" | grep "keyName:" | awk -F: '{print $2}')  ❸

password="Pa55w0rd!"                                                ❹

base64password=$(echo -n $password | base64)                        ❺

echo "                                                              ❻
kind: Secret
apiVersion: v1
metadata:
  name: $secretName
data:
  $keyName: $base64password
"

❶ Kustomize 插件的第一参数是插件配置文件的路径,该路径在 kustomization.yaml 中被引用。这一行只是简单地获取其内容。

❷ 解析插件配置,并使用配置的名称作为生成的 K8s Secret 的名称。

❸ 从插件配置中解析 keyName,并将其用作 K8s Secret 中的密钥。

❹ 为了演示目的,我们使用一个硬编码的值。这通常会被替换为检索和/或解密秘密的逻辑。

❺ Kubernetes 秘密需要 Base64 编码。

❻ 将最终的 Kubernetes 秘密打印到标准输出。

此示例也可以从 GitOps 资源 Git 仓库中运行:

$ git clone https://github.com/gitopsbook/resources

$ cd resources/chapter-07/kustomize-secret-plugin

$ export KUSTOMIZE_PLUGIN_HOME=$(pwd)/plugin

$ kustomize build ./config --enable_alpha_plugins
apiVersion: v1
data:
  foo: UGE1NXcwcmQh
kind: Secret
metadata:
  name: my-password

使用 Kustomize 插件,可以选择几乎任何技术来生成秘密,包括本章中提到的所有不同策略。这包括通过引用检索秘密、在 Git 中解密加密的秘密、访问秘密管理系统等。选项留给用户决定哪种策略对他们的情况最有意义。

摘要

  • Kubernetes 秘密是简单的数据结构,允许将应用程序的配置与构建工件分离。

  • Kubernetes 秘密可以通过多种方式被 Pod 使用,包括卷挂载、环境变量或直接从 Kubernetes API 中检索。

  • 由于缺乏加密和路径级访问控制,Git 不适合用于秘密。

  • 将烘焙秘密嵌入容器意味着容器本身也是敏感的,配置与构建工件之间没有分离。

  • 离线秘密管理允许使用原生 Kubernetes 功能,但会导致管理/部署秘密和配置的不同机制。

  • 外部秘密管理提供了灵活性,但失去了使用 Kubernetes 原生秘密功能的可能性。

  • HashiCorp Vault 是一个安全的外部秘密存储库,可以使用 brew 安装。Vault 还提供了一个 CLI 工具 vault 来管理存储中的秘密。Pod 启动时可以使用 CLI 和脚本从外部存储库检索秘密。

  • Vault Agent Sidecar 注入器可以在不使用 CLI 和脚本的情况下自动化将秘密注入到 Pod 中。

  • Sealed Secrets 是一个用于保护 Kubernetes 秘密数据的自定义资源定义 (CRD)。Sealed Secrets 可以通过应用 Sealed Secrets 清单安装到集群中。Sealed Secrets 随附一个 CLI 工具 kubeseal,用于加密 Kubernetes 秘密中的数据。

  • Kustomize 秘密生成插件允许用户在构建过程中定义逻辑以注入秘密。

8 可观察性

本章涵盖

  • 将 GitOps 与可观察性联系起来

  • 为集群操作员提供 Kubernetes 的可观察性

  • 通过 Kubernetes 的可观察性启用 GitOps

  • 通过 GitOps 提高系统的可观察性

  • 使用工具和技术确保您的云原生应用程序也具有可观察性

可观察性对于正确管理系统、确定系统是否正常工作以及决定需要做出哪些更改以修复、更改或改进其行为(例如如何控制系统)至关重要。可观察性一直是云原生社区的一个研究热点,许多项目和产品被开发出来以允许系统和应用程序的可观察性。云原生计算基金会最近成立了一个专门致力于可观察性的特别兴趣小组(SIG)1。

在本章中,我们将讨论在 GitOps 和 Kubernetes 环境下的可观察性。正如之前提到的,GitOps 是通过 GitOps 操作员(或控制器)或服务实现的,它必须管理和控制 Kubernetes 集群。GitOps 的关键功能是将系统的期望状态(存储在 Git 中)与系统的当前实际状态进行比较,并执行必要的操作以使两者收敛。这意味着 GitOps 依赖于 Kubernetes 和应用程序的可观察性来完成其工作。但 GitOps 也是一个必须提供可观察性的系统。我们将探讨 GitOps 可观察性的两个方面。

我们建议您在阅读本章之前先阅读第一章和第二章。

8.1 什么是可观察性?

可观察性是一个系统能力,就像可靠性、可伸缩性或安全性一样,必须在系统设计、编码和测试期间设计和实现到系统中。在本节中,我们将探讨 GitOps 和 Kubernetes 为集群提供可观察性的各种方式。例如,最近部署到集群中的应用程序版本是什么?谁部署了它?一个月前为该应用程序配置了多少个副本?应用程序性能的下降是否可以与对应用程序或 Kubernetes 配置的更改相关联?

在管理一个系统时,重点是控制该系统并应用某种方式改进系统的更改,无论是通过增加功能、提高性能、提高稳定性还是其他有益的更改。但您如何知道如何控制系统以及要做出哪些更改?一旦应用了更改,您如何知道它们是否改进了系统而没有使其变得更糟?

记得回到前面的章节。我们之前讨论了 GitOps 如何将系统的期望状态存储在 Git 中的声明性格式中。GitOps 操作员(或服务)通过改变(控制)系统的运行状态来匹配系统的期望状态。GitOps 操作员必须能够观察被管理的系统,在我们的案例中是 Kubernetes 和在 Kubernetes 上运行的应用程序。更重要的是,GitOps 操作员本身也必须提供可观察性,以便最终用户可以控制 GitOps。

好的,但在实际操作中,这究竟意味着什么呢?

如前所述,可观察性是一个涵盖多个方面的系统功能。这些方面中的每一个都必须设计和构建到系统中。让我们简要地检查这三个方面:事件记录、指标和跟踪。

图 8.1 可观察性由三个主要方面组成:事件记录、指标和跟踪。这些方面结合在一起,提供了操作洞察力,使得正确管理系统成为可能。

8.1.1 事件记录

大多数开发者都熟悉日志的概念。随着代码的执行,可以输出日志消息来指示重要事件、错误或更改。每个事件日志都有时间戳,并且是特定系统组件内部操作的不可变记录。当发生罕见或不可预测的故障时,事件日志可能提供细粒度的上下文,指示出了什么问题。

图 8.2 事件日志有时间戳,并提供特定系统组件内部操作的不可变记录。

在调试表现不当的应用程序时,通常的第一步是查看应用程序的日志以获取线索。日志是开发者观察和调试系统和应用程序的无价工具。日志和记录的保留也可能需要符合适用的行业标准。

对于 Kubernetes 来说,可观察性的一个基本方面是显示集群中所有各种 Pods 的日志输出。应用程序将有关其运行状态的调试信息输出到 stdout(标准输出),由 Kubernetes 捕获并保存到 Pod/容器运行的 Kubernetes 节点上的文件。可以使用 kubectl logs <pod_name> 命令显示特定 Pod 的日志。

为了说明日志、指标和跟踪的各个方面,我们将使用一个名为 Hot ROD 的共享出行应用程序作为示例.^(2) 让我们在 minikube 集群中启动该应用程序,以便我们可以查看其日志。以下是应用程序部署的清单。

列表 8.1 Hot ROD 应用程序部署 (http://mng.bz/vzPJ)

apiVersion: v1
kind: List
items:
  - apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: hotrod
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: hotrod
      template:
        metadata:
          labels:
            app: hotrod
        spec:
          containers:
            - args: ["-m", "prometheus", "all"]
              image: jaegertracing/example-hotrod:latest
              name: hotrod
              ports:
                - name: frontend
                  containerPort: 8080
  - apiVersion: v1
    kind: Service
    metadata:
      name: hotrod-frontend
    spec:
      type: LoadBalancer
      ports:
        - port: 80
          targetPort: 8080
      selector:
        app: hotrod

使用以下命令部署 Hot ROD 应用程序:

$ minikube start
$ kubectl apply -f hotrod-app.yaml
deployment.apps/hotrod created
service/hotrod-frontend created

现在我们来看看 Pod 的日志消息:

$ kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
hotrod-59c88cd9c7-dxd55   1/1     Running   0          6m4s

$ kubectl logs hotrod-59c88cd9c7-dxd55
2020-07-10T02:19:56.940Z  INFO  cobra@v0.0.3/command.go:792  Using Prometheus
                                                             as metrics
                                                             backend
2020-07-10T02:19:56.940Z  INFO  cobra@v0.0.3/command.go:762  Starting all
                                                             services
2020-07-10T02:19:57.048Z  INFO  route/server.go:57     Starting {"service": "route", "address": "http://0.0.0.0:8083"}
2020-07-10T02:19:57.049Z  INFO  frontend/server.go:67  Starting {"service": "frontend", "address": "http://0.0.0.0:8080"}
2020-07-10T02:19:57.153Z  INFO  customer/server.go:55  Starting {"service": "customer", "address": "http://0.0.0.0:8081"}

输出告诉我们,每个微服务(路由前端客户)都处于“启动”状态。但在此阶段,日志中并没有太多信息。而且,可能并不完全清楚每个微服务是否成功启动。

使用minikube service hotrod-frontend命令在您的工作站上为hotrod-frontend服务创建隧道,并在网页浏览器中打开 URL:

$ minikube service hotrod-frontend
|-----------|-----------------|-------------|-------------------------|
| NAMESPACE |      NAME       | TARGET PORT |           URL           |
|-----------|-----------------|-------------|-------------------------|
| default   | hotrod-frontend |          80 | http://172.17.0.2:31725 |
|-----------|-----------------|-------------|-------------------------|
🏃   Starting tunnel for service hotrod-frontend.
|-----------|-----------------|-------------|------------------------|
| NAMESPACE |      NAME       | TARGET PORT |          URL           |
|-----------|-----------------|-------------|------------------------|
| default   | hotrod-frontend |             | http://127.0.0.1:53457 |
|-----------|-----------------|-------------|------------------------|
🎉     Opening service default/hotrod-frontend in default browser...

这将打开一个网页浏览器到应用程序。当它打开时,点击每个按钮以模拟为每位客户请求一次乘车。

图 8.3 Hot ROD 示例应用程序的截图,该应用程序模拟了一个拼车系统。点击页面顶部的按钮将启动一个调用多个微服务的过程,以将客户与司机和路线匹配起来。

现在,在另一个终端窗口中,让我们查看应用程序的日志:

$ kubectl logs hotrod-59c88cd9c7-dxd55
:
2020-07-10T03:02:13.012Z  INFO  frontend/server.go:81  HTTP request received             {"service": 
                                                                                          "frontend", "method": "GET", "url": "/dispatch?customer=567&nonse=0.6850439192313089"}
2020-07-10T03:02:13.012Z   INFO   customer/client.go:54    Getting customer              {"service": 
                                                                                          "frontend", "component": "customer_client", "customer_id": "567"}
http://0.0.0.0:8081/customer?customer=567
2020-07-10T03:02:13.015Z   INFO   customer/server.go:67    HTTP request received         {"service": 
                                                                                          "customer", "method": "GET", "url": "/customer?customer=567"}
2020-07-10T03:02:13.015Z   INFO   customer/database.go:73  Loading customer              {"service": 
                                                                                          "customer", "component": "mysql", "customer_id": "567"}
2020-07-10T03:02:13.299Z   INFO   frontend/best_eta.go:77  Found customer                {"service": 
                                                                                          "frontend", "customer": {"ID":"567","Name":"Amazing Coffee Roasters","Location":"211,653"}}
2020-07-10T03:02:13.300Z   INFO   driver/client.go:58      Finding nearest drivers       {"service": 
                                                                                          "frontend", "component": "driver_client", "location": "211,653"}
2020-07-10T03:02:13.301Z   INFO   driver/server.go:73      Searching for nearby drivers  {"service": 
                                                                                          "driver", "location": "211,653"}
2020-07-10T03:02:13.324Z   INFO   driver/redis.go:67       Found drivers                 {"service": 
                                                                                          "driver", "drivers": ["T732907C", "T791395C", "T705718C", 
                                                                                          "T724516C", "T782991C", "T703350C", "T771654C", "T724823C", 
                                                                                          "T718650C", "T785041C"]}
:

日志记录非常灵活,它还可以用来推断关于应用程序的大量信息。例如,您可以在日志片段的第一行看到HTTP 请求 接收,这表明一个前端服务请求。您还可以看到与加载客户信息、定位最近的司机等相关日志消息。每个日志消息上都有一个时间戳,这样您就可以通过从结束时间减去开始时间来计算特定请求所需的时间量。您还可以计算在给定时间间隔内处理了多少个请求。要进行此类大规模日志分析,您需要集群级别的日志记录^(3)以及像 Elasticsearch 加 Kibana^(4)或 Splunk.^(5)这样的中央日志后端。

在 Hot ROD 应用程序中点击更多按钮。我们可以通过计算前端服务收到的HTTP 请求接收消息的数量来确定请求数量:

$ kubectl logs hotrod-59c88cd9c7-sdktk | grep -e "received" | grep frontend | wc -l
       7

从这个输出中,我们可以看到自 Pod 启动以来已经收到了七个前端请求。

然而,尽管日志记录既关键又灵活,但它有时并不是观察系统某些方面的最佳工具。日志是可观察性的一个非常低级方面。使用日志消息来推导出诸如处理请求数量、每秒请求数等指标可能相当昂贵,并且可能无法提供您所需的所有信息。此外,如果没有对代码的深入理解,仅通过解析日志消息来确定系统在任何给定时刻的状态可能相当棘手,甚至不可能。通常,日志消息来自系统的不同线程和子进程,必须将它们相互关联以跟踪系统的当前状态。因此,虽然日志很重要,但 Pod 日志只是刚刚触及 Kubernetes 可观察性能力的表面。

在下一节中,我们将看到如何使用指标来观察系统的属性,而不是进行低级日志解析。

练习 8.1

使用 kubectl logs 命令显示 Hot ROD Pod 的日志消息,并查找任何错误消息(提示:使用 grep 查找字符串 ERROR)。如果有,你看到了哪些类型的错误?

8.1.2 指标

可观测性的另一个关键方面是衡量系统或应用程序性能和操作的指标。在基本层面上,指标是一组键值对,提供了关于系统操作的详细信息。你可以将指标视为系统每个组件的可观察属性。一些适用于所有组件的核心资源指标包括 CPU、内存、磁盘和网络利用率。其他指标可能特定于应用程序,例如遇到特定类型错误的数量或等待处理的队列中项的数量。

图 8.4 指标是一组键值对,提供了关于系统操作的详细信息。

Kubernetes 使用一个名为 metrics-server 的可选组件提供基本指标。可以通过运行以下命令在 minikube 中启用 metrics-server

$ minikube addons enable metrics-server
🌟  The 'metrics-server' addon is enabled

一旦启用 metrics-server 并等待几分钟以收集足够的指标,你就可以使用 kubectl top nodeskubectl top pods 命令访问 metrics-server数据

列表 8.2 kubectl top nodeskubectl top pods 的输出

$ kubectl top nodes
NAME       CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
minikube   211m         5%     805Mi           40%

$ kubectl top pods --all-namespaces
NAMESPACE     NAME                               CPU(cores)   MEMORY(bytes)
default       hotrod-59c88cd9c7-sdktk            0m           4Mi
kube-system   coredns-66bff467f8-gk4zp           4m           8Mi
kube-system   coredns-66bff467f8-qqxdv           4m           20Mi
kube-system   etcd-minikube                      28m          44Mi
kube-system   kube-apiserver-minikube            62m          260Mi
kube-system   kube-controller-manager-minikube   26m          63Mi
kube-system   kube-proxy-vgzw2                   0m           22Mi
kube-system   kube-scheduler-minikube            5m           12Mi
kube-system   metrics-server-7bc6d75975-lc5h2    0m           9Mi
kube-system   storage-provisioner                0m           35Mi

此输出显示了节点(minikube)和运行 Pods 的 CPU 和内存利用率。

除了所有 Pods 都有的通用 CPU 和内存利用率之外,应用程序可以通过公开一个返回键值对形式指标列表的 HTTP metrics endpoint 来提供自己的指标。让我们看看我们在上一节中使用的 Hot ROD 应用程序指标端点。

在另一个终端中,使用 kubectl port-forward 命令将你的工作站上的一个端口转发到 Hot ROD 的指标端点,该端点在 Pod 的 8083 端口上公开:

$ kubectl port-forward hotrod-59c88cd9c7-dxd55 8083
Forwarding from 127.0.0.1:8083 -> 8083
Forwarding from [::1]:8083 -> 8083

一旦建立端口转发连接,请在您的网页浏览器中打开 http://localhost:8083/metrics,或者从命令行运行 curl http://localhost:8083/metrics

列表 8.3 Hot ROD 指标端点的输出

$ curl http://localhost:8083/metrics
# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 6.6081e-05
go_gc_duration_seconds{quantile="0.25"} 8.1335e-05
go_gc_duration_seconds{quantile="0.5"} 0.000141919
go_gc_duration_seconds{quantile="0.75"} 0.000197202
go_gc_duration_seconds{quantile="1"} 0.000371112
go_gc_duration_seconds_sum 0.000993336
go_gc_duration_seconds_count 6
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 26
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.14.4"} 1
:
:

指标本身提供了在特定时间点系统组件性能和操作的快照。通常,指标会定期收集并存储在时间序列数据库中,以观察指标的历史趋势。在 Kubernetes 中,这通常由一个名为 Prometheus 的 Cloud Native Computing Foundation (CNCF) 开源项目来完成(prometheus.io)。

图 8.5 单个 Prometheus 部署可以抓取集群中节点和 Pods 的指标端点。指标以可配置的间隔抓取并存储在时间序列数据库中。

如本章前面所述,一些指标可以通过检查日志消息来推断。然而,让系统或应用程序直接测量其指标并提供程序性访问以查询指标值更为高效。

练习 8.2

查找不同 HTTP 请求的数量。(提示:搜索名为http_requests的指标)。应用程序处理了多少GET /dispatchGET /customerGET /route请求?您如何从应用程序的日志中获取类似的信息?

8.1.3 跟踪

通常,分布式跟踪数据需要一个特定于应用程序的代理,该代理知道如何收集被跟踪代码的详细执行路径。分布式跟踪框架捕获系统内部运行的详细数据,从初始终端用户请求开始,通过可能数十(数百?)次调用到不同的

微服务和其他外部依赖项,可能托管在另一个系统上。而指标通常提供特定系统中的应用程序的聚合视图,而跟踪数据通常提供单个请求执行流程的详细情况,可能跨越多个服务和系统。在微服务时代,一个“应用程序”可能利用来自数十或数百个服务的功能,跨越多个操作边界,这一点尤为重要。如前文第 8.1.1 节所述,Hot ROD 应用程序由四个不同的微服务(前端、客户、司机和路线)和两个模拟存储后端(MySql 和 Redis)组成。

图 8.6 跟踪捕获了系统内部运行的详细数据。

为了说明这一点,让我们看看一个流行的跟踪框架 Jaeger,以及第 8.1.1 节和第 8.1.2 节中的示例 Hot ROD 应用程序。首先,在 minikube 集群上安装 Jaeger,并使用以下命令验证它是否成功运行:

$ kubectl apply -f https://raw.githubusercontent.com/gitopsbook/resources/master/chapter-08/jaeger/jaeger-all-in-one.yaml
deployment.apps/jaeger created
service/jaeger-query created
service/jaeger-collector created
service/jaeger-agent created
service/zipkin created

$ kubectl get pods -l app=jaeger
NAME                     READY   STATUS    RESTARTS   AGE
jaeger-f696549b6-f7c9h   1/1     Running   0          2m33s

现在 Jaeger 正在运行,我们需要更新 Hot ROD 应用程序部署,以便将跟踪数据发送到 Jaeger。这可以通过简单地向hotrod容器添加JAEGER_AGENT_HOST环境变量来完成,指示在前一步骤中由 jaeger-all-in-one.yaml 部署的jaeger-agent服务:

$ diff --context=4 hotrod-app.yaml hotrod-app-jaeger.yaml
*** hotrod-app.yaml             2020-07-20 17:57:07.000000000 -0700
--- hotrod-app-jaeger.yaml      2020-07-20 17:57:07.000000000 -0700
***************
*** 22,29 ****
--- 22,32 ----
                  #- "--fix-disable-db-conn-mutex"
                  - "all"
                image: jaegertracing/example-hotrod:latest
                name: hotrod
+               env:
+                 - name: JAEGER_AGENT_HOST
+                   value: "jaeger-agent"
                ports:
                  - name: frontend
                    containerPort: 8080
                  - name: customer
***************
*** 41,45 ****
          - port: 80
            targetPort: 8080
        selector:
          app: hotrod
!       type: LoadBalancer
\ No newline at end of file
--- 44,48 ----
          - port: 80
            targetPort: 8080
        selector:
          app: hotrod
!       type: LoadBalancer

由于我们已经配置了hotrod-app向 Jaeger 发送数据,我们需要通过打开hotrod-app UI 并点击几个按钮来生成一些跟踪数据,就像我们在事件日志部分所做的那样。

使用minikube service hotrod-frontend命令在您的工作站上为hotrod-frontend服务创建隧道,并在网络浏览器中打开 URL:

$ minikube service hotrod-frontend
|-----------|-----------------|-------------|------------------------|
| NAMESPACE |      NAME       | TARGET PORT |          URL           |
|-----------|-----------------|-------------|------------------------|
| default   | hotrod-frontend |             | http://127.0.0.1:52560 |
|-----------|-----------------|-------------|------------------------|
🎉 Opening service default/hotrod-frontend in default browser...

这将打开一个网络浏览器到应用程序。当它打开时,点击每个按钮以模拟为每位客户请求一次乘车。

现在我们应该有一些跟踪数据,通过运行minikube service jaeger-query来打开 Jaeger UI:

$ minikube service jaeger-query
|-----------|--------------|-------------|-----------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|--------------|-------------|-----------------------------|
| default   | jaeger-query | query-http  | http://192.168.99.120:30274 |
|-----------|--------------|-------------|-----------------------------|
🎉 Opening service default/jaeger-query in default browser...

这将在你的默认浏览器中打开 Jaeger UI。或者,你也可以打开之前输出中列出的 URL(例如 http://127.0.0.1:51831)。当你完成本章的 Jaeger 练习后,你可以按 Ctrl-C 关闭到jaeger-query服务的隧道。

从 Jaeger UI 中,你可以选择服务“前端”和操作“HTTP GET /dispatch”,然后点击查找跟踪以获取所有GET /dispatch调用图跟踪的列表。

图片

图 8.7 Jaeger 搜索标签显示在过去一小时中前端服务发生的GET /dispatch请求。右上角的图表显示了每个请求随时间的变化持续时间,每个圆圈的大小代表每个请求中的跨度数量。右下角列出了所有请求,并且可以点击每一行以获取更多详细信息。

从那里,你可以选择要检查的跟踪。以下屏幕截图显示了 Jaeger UI 中前端GET /dispatch请求的调用图。

图片

图 8.8 在这个GET /dispatch跟踪的详细视图下,Jaeger 显示了从原始请求发起的所有调用跨度。这个例子展示了诸如 SQL SELECT调用的持续时间以及 Redis GetDriver调用返回错误等详细信息。

如图 8.8 所示,关于GET /dispatch请求处理有很多有价值的信息。从这里,你可以看到正在调用的代码的分解,它的响应(成功或失败),以及它花费了多长时间。例如,在屏幕截图中,这个请求使用的 SQL 查询耗时 930.37 毫秒。这是否合理?应用程序开发者可以进行更多测试并深入挖掘,看看这个查询是否可以优化,或者代码的哪个区域可以通过额外的优化受益。

再次强调,正如之前提到的,开发者可以在他们的代码中添加日志语句,以便在应用程序日志中拥有“跟踪数据”,但这是一种成本高且效率低的方法。使用合适的跟踪框架要理想得多,并且从长远来看会提供更好的结果。

如你所想,跟踪数据可能相当大,并且可能无法为每个单独的请求收集数据。跟踪数据通常以配置的速率进行采样,并且可能比应用程序日志或指标有更短的保留期。

跟踪与 GitOps 有什么关系?老实说,关系不大。但跟踪是可观察性的一个基本且不断发展的部分,有许多新工具和服务的发展有助于提供、管理和分析分布式跟踪数据,因此了解它在整体可观察性系统中的位置很重要。也有可能在未来,跟踪工具(如 OpenTelemetry)将通过扩展到覆盖指标和日志来用于更多可观察性的方面。

练习 8.3

使用 Jaeger 分布式跟踪平台在 Hot ROD 中识别性能瓶颈.^(6) 要这样做,请在浏览器窗口中打开 Hot ROD UI。在 Hot ROD UI 中点击几个客户(例如日本甜点和惊人的咖啡烘焙师)以在应用程序中安排行程。实际上,尽情点击不同的客户按钮。

一旦你对应用程序进行了一些操作,请按照本节前面所述打开 Jaeger UI。使用搜索功能查找各种服务和请求的跟踪记录以回答以下问题。你可能需要将“限制结果”更改为更大的值,比如 200:

  1. 你是否有包含错误的跟踪记录?如果有,是哪个组件导致了错误?

  2. 当你缓慢或非常快速地点击客户按钮时,你是否注意到请求延迟有任何差异?你是否有一些超过 1000 毫秒的调度请求?

  3. 使用 Jaeger 搜索根据 Hot ROD 应用程序使用驾驶员 ID(粗体显示)的最长延迟跟踪记录。提示:使用“标签”过滤器并搜索“driver=T123456C”,其中“T123456C”是您最长延迟请求的驾驶员 ID。

  4. 应用程序在哪里花费的时间最多?哪个跨度是最长的?

  5. 长跨度日志中说了什么?提示:日志消息以Waiting for...开头。

  6. 根据你在上一个问题中发现的内容,什么可能影响了 Hot ROD 应用程序的性能?请检查以下链接中的代码:

    mng.bz/QmRw

练习 8.4

通过向 hotrod 容器添加 --fix-disable-db-conn-mutex 参数并更新部署来解决 Hot ROD 应用程序的性能瓶颈。(提示:查看 GitHub 上的 hotrod-app-jaeger.yaml 文件,并取消注释相应的行。)这模拟了在代码中修复数据库锁竞争:

  1. 通过添加 --fix-disable-db-conn-mutex 参数更新 hotrod 容器。

  2. 重新部署更新的 hotrod-app-jaeger.yaml 文件。

  3. 测试 hotrod UI。你是否注意到每个调度的延迟有差异?你是否能让调度请求超过 1000 毫秒?

  4. 查看 Jaeger UI。你是否注意到跟踪记录的差异?添加 --fix-disable-db-conn-mutex 参数后有什么不同?

要深入了解 Jaeger 和 Hot ROD 示例应用程序,请参阅 README 页面顶部的博客和视频链接.^(7)

8.1.4 可视化

所讨论的可观察性的各个方面实际上都是关于系统提供自身数据。这累积了大量的数据,可能难以理解。可观察性的最后一个方面是可视化工具,它有助于将所有这些可观察性数据转换为信息和洞察。

许多工具提供了可观察性数据的可视化。在上一节中,我们讨论了 Jaeger,它提供了跟踪数据的可视化。但现在,让我们看看另一个提供 Kubernetes 集群当前运行状态可视化的工具,即 K8s 仪表板。

您可以使用以下命令启用 K8s 仪表板 minikube 插件^(8):

$ minikube stop                         ❶
✋   Stopping "minikube" in docker ...
🛑   Powering off "minikube" via SSH ...
🛑   Node "minikube" stopped.
$ minikube addons enable dashboard      ❷
🌟   The 'dashboard' addon is enabled

❶ 如果 minikube 已经在运行,则停止它

❷ 启用仪表板插件

一旦启用,您就可以启动 minikube 集群并显示仪表板:

$ minikube start
😁  minikube v1.11.0 on Darwin 10.15.5
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
✨  minikube 1.12.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.12.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.18.3 on Docker 19.03.2 ...
     ▪  kubeadm.pod-network-cidr=10.244.0.0/16
🔎  Verifying Kubernetes components...
🌟  Enabled addons: dashboard, default-storageclass, metrics-server, storage-provisioner   ❶
🏄  Done! kubectl is now configured to use "minikube"
$ minikube dashboard

❶ 仪表板插件与 minikube 一起启动。

命令minikube dashboard将打开一个类似于图 8.9 的浏览器窗口。

图 8.9 Kubernetes 仪表板的概览页面显示了集群上当前运行的工作负载的 CPU 和内存使用情况,以及每种类型工作负载的摘要图表(部署、Pod 和 ReplicaSet)。页面右下角的面板显示了每个工作负载GET /dispatch的列表,并且可以点击每一行以获取更多详细信息。

Kubernetes 仪表板提供了对 Kubernetes 集群许多不同方面的可视化,包括您的部署、Pod 和 ReplicaSet 的状态。我们可以看到bigpod部署存在问题。看到这一点后,Kubernetes 管理员应该采取行动,将此部署恢复到健康状态。

练习 8.5

在 minikube 上安装仪表板插件。打开仪表板 UI,探索可用的不同面板,并执行以下操作:

  1. 切换到所有命名空间视图。minikube 上运行的总 Pod 数量是多少?

  2. 选择 Pod 面板。通过点击 Pod 面板右上角的过滤器图标,过滤 Pod 列表以包含名称中带有“dns”的 Pod。通过点击该 Pod 的动作图标并选择删除来删除一个与 DNS 相关的 Pod。会发生什么?

  3. 在仪表板 UI 的右上角点击加号图标(+),然后选择“从表单创建”。使用nginx镜像创建一个新的包含三个 Pods 的 NGINX 部署。尝试使用“从输入创建”和“从文件创建”选项,也许可以使用前面章节中的代码列表。

8.1.5 可观察性在 GitOps 中的重要性

好的,现在您已经知道了什么是可观察性,以及它通常是一件好事,但您可能想知道为什么一本关于 GitOps 的书会专门用一整章来介绍可观察性。可观察性跟 GitOps 有什么关系?

正如第二章所讨论的,使用 Kubernetes 的声明式配置可以精确地定义系统的期望状态。但您如何知道系统的运行状态是否与期望状态一致?您如何知道特定的部署是否成功?更广泛地说,您如何判断系统是否按预期工作?这些问题是 GitOps 和可观察性应该帮助解决的问题。

Kubernetes 中有几个可观察性的方面对于 GitOps 系统良好运行和回答关于系统的关键问题至关重要:

  • 应用健康状态—应用是否正常运行?如果应用的新版本正在使用 GitOps 进行部署,系统是否“更好”于之前?

  • 应用同步状态—应用的运行状态是否与在部署 Git 仓库中定义的期望状态相同?

  • 配置漂移—应用配置是否在声明式 GitOps 系统之外(如手动或命令式)进行了更改?

  • GitOps 变更日志—最近对 GitOps 系统做了哪些更改?谁做的,出于什么原因?

本章的其余部分将涵盖 Kubernetes 系统和应用的可观察性如何使 GitOps 系统能够回答这些问题,以及 GitOps 系统如何反过来提供可观察性功能。

8.2 应用健康状态

可观察性与 GitOps 的第一和最重要的关联方式是观察应用健康状态。在本章开头,我们讨论了运营一个系统实际上就是管理该系统,使其整体随着时间的推移不断改进和变得更好,而不是变得更糟。GitOps 在系统期望状态(假设比当前状态“更好”)提交到 Git 仓库,然后 GitOps 操作员将此期望状态应用到系统中,使其成为当前状态的地方至关重要。

例如,想象一下,当在本章前面讨论的 Hot ROD 应用最初部署时,它有相对较小的运行需求。然而,随着时间的推移,随着应用变得越来越受欢迎,数据集增长,分配给 Pod 的内存不再足够。Pod 会定期耗尽内存并被终止(称为“OOMKilled”的事件)。Hot ROD 应用的应用健康状态会显示它定期崩溃和重启。系统操作员可以检查 Hot ROD 应用的 Git 部署仓库中的更改,以增加其请求的内存。然后 GitOps 操作员将应用该更改,增加运行中的 Hot ROD 应用的内存。

也许我们可以就此结束。毕竟,有人对系统进行了更改,GitOps 应该只是按照指示行事。但假设提交的更改实际上使事情变得更糟呢?假设在部署最新更改后,应用重新启动但开始返回比之前更多的错误?或者更糟的是,假设它根本无法重新启动?在这个例子中,如果操作员为 Hot ROD 应用增加了过多的内存,导致集群上运行的其他应用耗尽内存怎么办?

使用 GitOps,我们至少希望检测到那些在部署后系统“变得更糟”的条件,并且至少提醒用户他们可能应该考虑回滚最近的变化。这只有在系统和应用程序具有 GitOps 操作员可以观察到的强大可观察性特征的情况下才可能实现。

8.2.1 资源状态

在基本层面上,Kubernetes 的一个关键特性是它如何通过声明性配置同时维护所需的配置和活动状态,从而使其内部状态可观察。这允许在任何时候检查每个 Kubernetes 资源,以查看资源是否处于所需状态。每个组件或资源在任何给定时间都将处于特定的操作状态。例如,资源可能处于INITIALIZEDNOT READY状态。它也可能处于PENDINGRUNNINGCOMPLETED状态。通常,资源的状态与其类型特定。

应用程序健康的第一方面是确定与应用程序相关的所有 Kubernetes 资源都处于良好状态。例如,所有 Pod 是否都已成功调度,并且它们是否处于Running状态?让我们看看如何确定这一点。

Kubernetes 为每个 Pod 提供了额外的信息,以指示 Pod 是否健康。让我们在etcd Pod 上运行kubectl describe命令:

$ kubectl describe pod etcd-minikube -n kube-system
Name:                 etcd-minikube
Namespace:            kube-system
Priority:             2000000000
Priority Class Name:  system-cluster-critical
Node:                 minikube/192.168.99.103
Start Time:           Mon, 10 Feb 2020 22:54:14 -0800
:
Status:               Running
IP:                   192.168.99.103
IPs:
  IP:           192.168.99.103
Controlled By:  Node/minikube
Containers:
  etcd:
    :
    Command:
      :
    State:          Running
      Started:      Mon, 10 Feb 2020 22:54:05 -0800
    Ready:          True
    Restart Count:  0
    Liveness:       http-get http://127.0.0.1:2381/health delay=15s timeout=15s period=10s #success=1 #failure=8
    Environment:    <none>
    Mounts:
      :
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  :
QoS Class:         BestEffort
Node-Selectors:    <none>
Tolerations:       :NoExecute
Events:            <none>

这个输出已被截断以节省空间,但请仔细查看它并与您 minikube 的输出进行比较。您是否看到了 Pod 的任何可能有助于可观察性的属性?最上面一个非常重要(且明显)的一个是Status: Running,这表示 Pod 所处的阶段。Pod 阶段是对 Pod 在其生命周期中位置的简单、高级总结。条件数组、原因和消息字段以及单个容器状态数组包含了关于 Pod 状态的更多详细信息。

表 8.1 Pod 阶段

阶段值 描述
Pending Kubernetes 系统已接受 Pod,但一个或多个容器镜像尚未创建。这包括在调度之前的时间以及通过网络下载镜像所花费的时间,这可能需要一段时间。
Running Pod 已被绑定到节点,并且所有容器都已创建。至少有一个容器仍在运行或正在启动或重启过程中。
Succeeded Pod 中的所有容器都已成功终止,并且不会被重新启动。
Failed Pod 中的所有容器都已完成,并且至少有一个容器已失败终止。容器要么以非零状态退出,要么被系统终止。
Unknown 由于某种原因,无法获取 Pod 状态,通常是由于与 Pod 的主机通信错误。

这就是可观察性:Pod 的内部状态可以轻松查询,因此可以做出关于控制系统的决策。对于 GitOps 来说,如果部署了应用程序的新版本,查看由此产生的新 Pod 的状态以确保其成功是非常重要的。

但 Pod 状态(阶段)只是一个总结,要了解 Pod 为什么处于特定状态,你需要查看 Conditions。Pod 有四个条件,可以是 TrueFalseUnknown

表 8.2 Pod 条件

阶段值 描述
PodScheduled Pod 已成功调度到集群中的某个节点。
Initialized 所有 init 容器都已成功启动。
ContainersReady Pod 中的所有容器都已就绪。
Ready Pod 可以提供服务,应该被添加到所有匹配服务的负载均衡池中。

练习 8.6

使用 kubectl describe 命令显示在 kube-system 命名空间中运行的其它 Pods 的信息。

Pod 可能进入不良状态的一个基本例子是提交一个请求比集群中可用的资源更多的资源的 Pod。这将导致 Pod 进入 Pending 状态。可以通过运行 kubectl describe pod <pod_name> 来“观察”Pod 的状态。

列表 8.4 http://mng.bz/yYZG

apiVersion: v1
kind: Pod
metadata:
  name: bigpod
spec:
  containers:
    - command:
        - /app/sample-app
      image: gitopsbook/sample-app:v0.1
      name: sample-app
      ports:
        - containerPort: 8080
      resources:
        requests:
          memory: "999Gi"    ❶
          cpu: "99"          ❷

❶ 请求了不可能的内存量

❷ 请求了不可能数量的 CPU 核心

如果你应用此 YAML 并检查 Pod 状态,你会注意到 Pod 是 Pending 状态。当你运行 kubectl describe 时,你会看到 Pod 处于 Pending 状态,因为 minikube 集群无法满足 999 GB RAM 或 99 个 CPU 的请求:

$ kubectl apply -f bigpod.yaml
deployment.apps/bigpod created

$ kubectl get pod
NAME                      READY   STATUS    RESTARTS   AGE
bigpod-7848f56795-hnpjx   0/1     Pending   0          5m41s

$ kubectl describe pod bigpod-7848f56795-hnpjx
Name:           bigpod-7848f56795-hnpjx
Namespace:      default
Priority:       0
Node:           <none>
Labels:         app=bigpod
                pod-template-hash=7848f56795
Annotations:    <none>
Status:         Pending                                                  ❶
IP:
IPs:            <none>
Controlled By:  ReplicaSet/bigpod-7848f56795
Containers:
  bigpod:
    Image:      gitopsbook/sample-app:v0.1
    Port:       8080/TCP
    Host Port:  0/TCP
    Command:
      /app/sample-app
    Requests:
      cpu:        99
      memory:     999Gi
    Environment:  <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-8dzwz (ro)
Conditions:
  Type           Status
  PodScheduled   False                                                   ❷

Volumes:
  default-token-8dzwz:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-8dzwz
    Optional:    false
QoS Class:       Burstable
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason            Age        From               Message       ❸
  ----     ------            ----       ----               -------
  Warning  FailedScheduling  <unknown>  default-scheduler  0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory.
  Warning  FailedScheduling  <unknown>  default-scheduler  0/1 nodes are available: 1 Insufficient cpu, 1 Insufficient memory.

❶ Pod 状态是 Pending。

❷ PodScheduled 条件为 False。

❸ 没有足够的内存或 CPU 的节点可以调度 Pod。

练习 8.7

使用更合理的 CPU 和内存请求更新 bigpod.yaml,并重新部署 Pod。(提示:将 CPU 更改为 99m,内存更改为 999Ki。)在更新的 Pod 上运行 kubectl describe,并将输出与更改前的输出进行比较。更新的 Pod 的 StatusConditionsEvents 是什么?

8.2.2 准备就绪和活跃性

如果你仔细查看表 8.2 中列出的 Pod Conditions,可能会发现一些不同之处。Ready 状态声称“Pod 可以提供服务。”它是如何知道的?如果 Pod 需要执行一些初始化怎么办?Kubernetes 如何知道 Pod 已经就绪?答案是 Pod 本身会根据其自身的应用特定逻辑通知 Kubernetes 它已经就绪。

Kubernetes 使用就绪探针来决定何时 Pod 可用于接受流量。Pod 中的每个容器都可以指定一个就绪探针,形式为命令或 HTTP 请求,以指示容器何时为 Ready。容器需要提供关于其内部状态的可观察性。一旦所有 Pod 容器都 Ready,则 Pod 本身被认为是 Ready,可以被添加到匹配服务的负载均衡器中,并开始处理请求。

同样,每个容器可以指定一个存活探针,以指示容器是否存活,例如,是否处于某种死锁状态。Kubernetes 使用存活探针来确定何时重启进入不良状态的容器。

因此,这里再次提到了 Kubernetes 内置的可观察性的一个方面。应用程序 Pod 通过就绪和存活探针提供其内部状态的可视性,以便 Kubernetes 系统可以决定如何控制它们。应用程序开发者必须正确实现这些探针,以便应用程序提供其操作的正确可观察性。

图 8.10 Kubernetes 使用就绪和存活探针来确定哪些 Pod 可用于接受流量。Pod 1 处于 Running 状态,就绪和存活探针都通过。Pod 2 处于 Pending 状态,尽管存活探针通过,但就绪探针未通过,因为 Pod 正在启动。Pod 3 通过就绪探针但未通过存活探针,这意味着它可能很快会被 kubelet 重启。

练习 8.8

创建一个 Pod 规范,使用初始化容器创建一个文件并配置应用程序容器的存活和就绪探针,以期望该文件存在。创建 Pod 后,观察其行为。进入 Pod 删除文件并观察其行为。

8.2.3 应用程序监控和警报

除了状态和就绪/存活之外,应用程序通常有一些关键的指标可以用来确定它们的整体健康状况。这是运营监控和警报的基础:监视一组指标,并在它们偏离允许值时触发警报。但应该监控哪些指标,以及允许的值是什么?

幸运的是,已经对这个主题进行了大量研究,并在其他书籍和文章中得到了很好的覆盖。Rob Ewaschuk 将“四个黄金信号”描述为在高级别上最重要的指标。这提供了一个有用的框架来思考指标:^(9)

  • 延迟—处理请求所需的时间

  • 流量—对系统需求的衡量

  • 错误—未成功请求的比率

  • 饱和度—你的服务“多满”

图 8.11 四个“黄金信号”,延迟、流量、错误和饱和度,是衡量系统整体健康状况的关键指标。每个指标都衡量系统的一个特定操作方面。系统中的任何给定问题都可能通过负面影响一个或多个“黄金信号”指标来表现出来。

布伦丹·格雷格提出了用于描述系统资源性能(如基础设施,例如 Kubernetes 节点)的 USE 方法^(10)。

  • 利用率—资源忙于服务工作的平均时间

  • 饱和度—资源额外工作量的程度,通常排队

  • 错误—错误事件的计数

对于请求驱动的应用程序(如微服务),汤姆·威尔基定义了 RED 方法^(11)。

  • 速率—服务每秒处理的请求数量

  • 错误—每秒失败的请求数量

  • 持续时间—每个请求所需时间的分布

虽然通过指标确定应用程序健康状况的更深入讨论超出了本书的范围,但我们强烈推荐阅读这里总结的三个相关脚注参考文献。

一旦你确定了评估应用程序健康状况的指标,你需要监控它们,并在它们超出允许值时生成警报。在传统的运营环境中,这通常由一个人类操作员盯着仪表板来完成,但可能由一个自动化的系统来完成。如果监控检测到问题,就会发出警报,触发值班工程师检查系统。值班工程师分析系统并确定解决警报的正确行动方案。这可能包括停止向服务器群集推出新版本,或者甚至回滚到上一个版本。

所有这些都需要时间,并延迟了系统恢复到最佳运行状态。如果 GitOps 系统能帮助改善这种情况呢?

考虑所有 Pod 都成功启动的情况。所有就绪性检查都成功了,但一旦应用程序开始处理,处理每个请求所需的时间突然增加了两倍(RED 方法的持续时间)。可能是因为最近的代码更改引入了一个性能错误,这正在降低应用程序的性能。

理想情况下,这样的性能问题应该在预生产测试期间被发现。如果没有,GitOps 运营商和部署机制是否可以自动停止或回滚部署,如果特定的黄金信号指标突然恶化并偏离既定的基线?

这在 8.3 节中作为高级可观测性用例讨论的一部分进行了更详细的说明。

8.3 GitOps 可观测性

经常管理员会根据观察到的应用程序健康特征更改 GitOps 中定义的系统配置。如果一个 Pod 由于内存不足条件而卡在CrashLoopBackoff状态,Pod 的清单可能被更新以请求更多内存。如果内存泄漏导致应用程序中的内存不足条件,也许 Pod 的镜像将被更新为包含修复内存泄漏的镜像。也许应用程序的黄金信号表明它正在达到饱和状态,无法处理负载,因此 Pod 清单可能被更新以请求更多 CPU,或者 Pod 副本的数量增加以水平扩展应用程序。

这些都是基于应用程序的可观察性而采取的 GitOps 操作。但 GitOps 过程本身呢?GitOps 部署的可观察特征是什么?

8.3.1 GitOps 指标

如果 GitOps 操作员或服务是一个应用程序,它的黄金信号是什么?让我们考虑每个领域,以了解 GitOps 操作员提供的某些可观察性特征:

  • 延迟

    • 部署系统并使其运行状态与期望状态匹配所需的时间
  • 流量

    • 部署频率

    • 正在进行的部署数量

  • 错误

    • 失败的部署数量以及当前处于失败状态的部署数量

    • 系统运行状态与期望状态不匹配的离线部署数量

  • 饱和度

    • 部署排队等待处理的时间长度

图片

图 8.12 GitOps 的四个黄金信号表示 GitOps 持续部署系统的健康状况。GitOps 操作员/服务的问题可能会通过负面影响一个或多个这些黄金信号指标来体现。

这些指标的实现方式以及它们如何被公开将因每个 GitOps 工具而异。这将在本书的第三部分中更详细地介绍。

8.3.2 应用程序同步状态

GitOps 操作员必须提供的最重要状态是 Git 仓库中应用程序的期望状态是否与当前应用程序状态(同步)相同,或者不同(不同步)。如果应用程序不同步,用户应被提醒可能需要进行部署(或重新部署)。

但是什么原因导致应用程序变得不同步?这是 GitOps 的常规操作的一部分;用户将更改提交到系统的期望状态。在更改提交的那一刻,应用程序的当前状态与期望状态不匹配。

让我们考虑第二章 2.5.1 节中描述的基本 GitOps 操作员是如何工作的。在那个例子中,CronJob 定期运行基本 GitOps 操作员,以便检出仓库,并将仓库中包含的清单自动应用到运行系统中。

在这个 GitOps 运算符的基本示例中,为了简化示例,完全回避了应用程序同步状态的问题。基本的 GitOps 运算符假设在每次计划执行(或轮询间隔)时应用程序都处于不同步状态,需要部署。这种简单的方法不适合实际生产使用,因为用户无法看到是否存在需要部署的更改,以及这些更改是什么。这也可能给 GitOps 操作、Git 服务器和 Kubernetes API 服务器增加不必要的额外负载。

样本应用程序

让我们通过几个不同的部署场景来运行我们的 sample-app,以探索其他应用程序同步状态方面。sample-app 是一个简单的 Go 应用程序,它返回一个 HTTP 响应消息“Kubernetes ♡ <input>”。

首先,登录到 GitHub 并分叉 github.com/gitopsbook/sample-app-deployment 仓库。如果您之前已分叉此仓库,建议删除旧的分叉并重新分叉,以确保从一个没有任何先前练习更改的干净仓库开始。

在分叉后,使用以下命令克隆您的分叉仓库:

$ git clone git@github.com:<username>/sample-app-deployment.git
$ cd sample-app-deployment

让我们手动将 sample-app 部署到 minikube:

$ minikube start
😁   minikube v1.11.0 on Darwin 10.14.6
✨   Automatically selected the hyperkit driver
👍   Starting control plane node minikube in cluster minikube
🔥   Creating hyperkit VM (CPUs=2, Memory=2200MB, Disk=20000MB) ...
🐳   Preparing Kubernetes v1.16.10 on Docker 19.03.8 ...
🔎   Verifying Kubernetes components...
🌟   Enabled addons: default-storageclass, storage-provisioner
🏄   Done! kubectl is now configured to use "minikube"
$ kubectl create ns sample-app
namespace/sample-app created
$ kubectl apply -f . -n sample-app
deployment.apps/sample-app created
service/sample-app created

注意:在撰写本文时,Kubernetes v1.18.3 使用 kubectl diff 命令报告了额外的差异。如果在完成以下练习时遇到此问题,您可以使用以下命令以较旧版本的 Kubernetes 启动 minikube:minikube start --kubernetes-version=1.16.10

检测差异

现在,sample-app 已成功部署,让我们对部署做一些更改。让我们在 deployment.yaml 文件中将 sample-app 的副本数量增加到 3。使用以下命令更改 Deployment 资源中的副本数:

$ sed -i '' 's/replicas: .*/replicas: 3/' deployment.yaml

或者,您可以使用您喜欢的文本编辑器将 deployment.yaml 文件中的 replicas: 1 更改为 replicas: 3。一旦对 deployment.yaml 进行了更改,运行以下命令以查看 Git 分支仓库中的未提交差异:

$ git diff
diff --git a/deployment.yaml b/deployment.yaml
index 5fc3833..ed2398a 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -3,7 +3,7 @@ kind: Deployment
 metadata:
   name: sample-app
 spec:
-  replicas: 1
+  replicas: 3
   revisionHistoryLimit: 3
   selector:
     matchLabels:

最后,使用 git commitgit push 将更改推送到远程 Git 分支仓库:

$ git commit -am "update replica count"
[master 5a03ca3] update replica count
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 353 bytes | 353.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:tekenstam/sample-app-deployment.git
   09d6663..5a03ca3  master -> master

现在,您已将更改提交到 sample-app 部署仓库,由于部署的当前状态仍然只有三个副本,sample-app 的 GitOps 同步状态处于不同步状态。让我们确认这一点:

$ kubectl get deployment sample-app -n sample-app
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
sample-app   1/1     1            1           3m27s

从此命令中,您可以看到 sample-app 部署有 1/1READY 副本。但有没有更好的方法来比较表示所需状态的部署仓库与运行应用程序的实际状态?幸运的是,Kubernetes 提供了检测差异的工具。

kubectl diff

Kubernetes 提供了 kubectl diff 命令,它接受一个文件或目录作为输入,并显示那些文件中定义的资源与 Kubernetes 集群中同名的当前资源之间的差异。如果我们对现有的部署仓库运行 kubectl diff,我们会看到以下内容:

$ kubectl diff -f . -n sample-app
@@ -6,7 +6,7 @@
   creationTimestamp: "2020-06-01T04:17:28Z"
-  generation: 2
+  generation: 3
   name: sample-app
   namespace: sample-app
   resourceVersion: "2291"
@@ -14,7 +14,7 @@
   uid: eda45dca-ff29-444c-a6fc-5134302bcd81
 spec:
   progressDeadlineSeconds: 600
-  replicas: 1
+  replicas: 3
   revisionHistoryLimit: 3
   selector:
     matchLabels:

从这个输出中,我们可以看到 kubectl diff 正确地识别出 replicas1 更改为 3

虽然这是一个基本的例子,用于说明这个观点,但同样的技术可以识别多个不同资源之间的更广泛的变化。这赋予了 GitOps 运营商或服务确定 Git 中包含所需状态的部署仓库是否与 Kubernetes 集群的当前实时状态不同步的能力。更重要的是,kubectl diff 输出提供了如果部署仓库同步,将应用到集群中的更改的预览。这是 GitOps 可观察性的一个关键特性。

练习 8.9

sample-app 部署仓库进行分支。按照 sample-app-deployment README.md 中的说明将 sample-app 部署到您的 minikube 集群。现在将 sample-app 服务的 type: 改为 LoadBalancer。运行 kubectl diff -f . -n sample-app命令。您是否看到了任何意外的更改?为什么?使用kubectl apply -f . -n sample-app 应用更改。现在您应该可以通过命令 minikube service sample-app -n sample-app来看到sample-app` 网页。

kubediff

在前面的章节中,我们介绍了如何使用 git diff 来查看 Git 仓库中修订之间的差异,以及如何使用 kubectl diff 来查看 Git 仓库与实时 Kubernetes 集群之间的差异。在两种情况下,diff 工具都提供了非常原始的差异视图,输出差异前后的行以提供上下文。而且 kubectl diff 也可能报告系统管理(如 generation)的差异,这些差异与 GitOps 用例不相关。如果有一个工具能给出每个资源特定属性差异的简洁报告,那岂不是很好?事实上,Weaveworks^(12) 的人们已经发布了一个名为 kubediff^(13) 的开源工具,它正是这样做的。

以下是针对我们的 sample-app 部署仓库运行 kubediff 的输出:

$ kubediff --namespace sample-app .
## sample-app/sample-app (Deployment.v1.apps)

.spec.replicas: '3' != '1'

kubediff 还可以输出 JSON 结构化的输出,这使得它更容易被程序化使用。以下是使用 JSON 输出运行的相同命令:

$ kubediff --namespace sample-app . --json
{
  "./deployment.yaml": [
    ".spec.replicas: '3' != '1'"
  ]
}

练习 8.10

sample-app-deployment 仓库运行 kubediff。如果您的环境中尚未安装,您首先需要安装 Python 和 pip,然后按照 kubediff README 中的说明运行 pip install -r requirements.txt

8.3.3 配置漂移

但应用程序如何才能与 Git 仓库中定义的期望状态不同步呢?可能是用户直接修改了正在运行的应用程序(例如,通过在 Deployment 资源上执行kubectl edit),而没有将期望的更改提交到 Git 仓库。我们称这种情况为配置漂移。

在使用 GitOps 管理系统时,这通常是一个大忌;你应该避免在 GitOps 之外直接修改系统。例如,如果你的 Pod 资源不足,你可能只是简单地执行一个kubectl edit命令来增加副本数量以增加容量。

这种情况有时会发生。当这种情况发生时,GitOps 操作员需要“观察”当前状态,检测与期望状态的差异,并向用户指示应用程序不同步。一个特别激进的 GitOps 操作员可能会自动重新部署之前部署的最后配置,从而覆盖手动更改。

使用 minikube 和我们在上一节中使用的sample-app-deployment仓库,运行kubectl apply -f . -n sample-app以确保当前内容已部署到 Kubernetes。现在运行kubectl diff -f . -n sample-app;你应该看不到任何差异。

现在,让我们通过运行以下命令来模拟在 GitOps 系统之外对应用程序部署进行更改:

$ kubectl scale deployment --replicas=4 sample-app -n sample-app
deployment.apps/sample-app scaled

现在,如果我们重新运行kubectl diff命令,我们会看到应用程序不同步,我们已经经历了配置漂移:

$ kubectl diff -f . -n sample-app
@@ -6,7 +6,7 @@
   creationTimestamp: "2020-06-01T04:17:28Z"
-  generation: 6
+  generation: 7
   name: sample-app
   namespace: sample-app
   resourceVersion: "16468"
@@ -14,7 +14,7 @@
   uid: eda45dca-ff29-444c-a6fc-5134302bcd81
 spec:
   progressDeadlineSeconds: 600
-  replicas: 4
+  replicas: 3
   revisionHistoryLimit: 3
   selector:
     matchLabels:

或者,如果你运行kubediff,你会看到以下内容:

$ kubediff --namespace sample-app .
## sample-app/sample-app (Deployment.v1.apps)

.spec.replicas: '3' != '4'

配置漂移与应用程序不同步非常相似。事实上,正如你所看到的,效果是相同的;当前配置的实时状态与在 Git 部署仓库中定义的期望配置不同。区别在于,通常当 Git 部署仓库中提交了尚未部署的新版本时,应用程序会不同步。相比之下,配置漂移发生在配置更改在 GitOps 系统之外进行时。

通常情况下,当遇到配置漂移时,必须发生以下两种情况之一。一些系统会将配置漂移视为错误状态,并允许启动自愈过程以同步系统回到声明的状态。其他系统可能会检测到这种漂移,并允许手动更改被整合回保存在 Git 中的声明状态(例如双向同步)。然而,我们的观点是双向同步并不可取,因为它允许并鼓励手动更改集群,绕过了 GitOps 提供作为其核心优势之一的安保和审查流程。

练习 8.11

sample-app-deployment 中运行命令 kubectl delete -f . -n sample-app。哎呀,你刚刚删除了你的应用程序!运行 kubectl diff -f . -n sample-app。你看到了什么差异?你如何将应用程序恢复到运行状态?提示:这应该很容易。

8.3.4 GitOps 变更日志

在本章前面,我们讨论了事件日志是可观察性的一个关键方面。对于 GitOps 来说,应用程序部署的“事件日志”主要是由部署仓库的提交历史组成的。由于所有对应用程序部署的更改都是通过更改表示应用程序期望状态的文件来进行的,通过观察提交、拉取请求批准和合并请求,我们可以了解集群中发生了哪些更改。

例如,在 sample-app-deployment 仓库上运行 git log 命令会显示自创建以来对这个仓库所做的所有提交:

$ git log --no-merges
commit ce920168912a7f3a6cdd57d47e630ac09aebc4e1 (origin/tekenstam-patch-2)
Author: Todd Ekenstam <tekenstam@gmail.com>
Date:   Mon Nov 9 13:59:25 2020 -0800

    Reduce Replica count back to 1

commit 8613d1b14c75e32ae04f3b4c0470812e1bdec01c (origin/tekenstam-patch-1)
Author: Todd Ekenstam <tekenstam@gmail.com>
Date:   Mon Nov 9 13:58:26 2020 -0800

    Update Replica count to 3

commit 09d6663dcfa0f39b1a47c66a88f0225a1c3380bc
Author: tekenstam <tekenstam@gmail.com>
Date:   Wed Feb 5 22:14:35 2020 -0800

    Update deployment.yaml

commit 461ac41630bfa3eee40a8d01dbcd2a5cd032b8f1
Author: Todd Ekenstam <Todd_Ekenstam@intuit.com>
Date:   Wed Feb 5 21:51:03 2020 -0800

    Update sample-app image to gitopsbook/sample-app:cc52a36

commit 99bb7e779d960f23d5d941d94a7c4c4a6047bb22
Author: Alexander Matyushentsev <amatyushentsev@gmail.com>
Date:   Sun Jan 26 22:01:20 2020 -0800

    Initial commit

从这个输出中,我们可以看到 Alex 在 1 月 26 日创建了该仓库的第一个提交。最近的提交是由 Todd 完成的,根据提交标题,将副本数量减少到 1。我们可以通过运行以下命令来查看提交的实际差异:

$ git show ce920168912a7f3a6cdd57d47e630ac09aebc4e1
commit ce920168912a7f3a6cdd57d47e630ac09aebc4e1 (origin/tekenstam-patch-2)
Author: Todd Ekenstam <tekenstam@gmail.com>
Date:   Mon Nov 9 13:59:25 2020 -0800

    Reduce Replica count back to 1

diff --git a/deployment.yaml b/deployment.yaml
index ed2398a..5fc3833 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -3,7 +3,7 @@ kind: Deployment
 metadata:
   name: sample-app
 spec:
-  replicas: 3
+  replicas: 1
   revisionHistoryLimit: 3
   selector:
     matchLabels:

从这里我们可以看到,行 replicas: 3 被更改为 replicas: 1

相同的信息也可在 GitHub UI 中找到。

图片

图 8.13 通过审查部署仓库的 GitHub 提交历史,可以看到应用程序部署随时间所做的所有更改。

图片

图 8.14 可以检查单个提交的详细信息。

为你集群中部署的每个应用程序拥有一个部署审计日志对于大规模管理它们至关重要。如果只有一个人更改集群,如果出现问题,那个人很可能会知道他们可能做了什么更改导致了问题。但是,如果你有多个团队,可能在不同的地理位置和时间区域,能够回答“谁最后部署了应用程序,他们做了什么更改?”这个问题是至关重要的。

正如我们在前面的章节中学到的,GitOps 仓库是一系列对表示系统期望状态的仓库文件的更改、添加和删除的集合。对仓库的每一次修改都称为一个提交。就像应用程序日志有助于提供代码中发生的历史记录一样,Git 提供了一个日志,提供了仓库更改的历史记录。我们可以检查 Git 日志来观察仓库随时间发生的变化。

对于 GitOps 来说,查看部署存储库的日志与查看应用日志一样重要,因为 GitOps 存储库是系统期望状态的真相来源。检查存储库的日志显示我们何时以及为什么(如果提交注释足够描述性)对系统的期望状态进行了更改,以及谁进行了更改并批准了这些更改。

这是 GitOps 向用户提供可观察性的一个关键方面。虽然 GitOps 操作员或服务也可能提供详细说明其执行的日志,但部署存储库的 Git 日志通常能给你一个很好的理解,了解系统中发生了哪些变化。

练习 8.12

使用你在这章中一直使用的sample-app-deployment存储库的相同 Git 分叉,运行git log命令。检查输出。你能通过本章的各个部分追踪你的过程吗?你看到作者对这个存储库的任何早期提交吗?

摘要

  • 可观察性的方面可以通过监控事件日志、指标和跟踪来衡量。

  • 数据收集器,如 Logstash、Fluentd 或 Scribe,可以收集应用输出(事件),并将日志消息存储在集中式数据存储中,以供后续分析。

  • 使用kubectl logs观察应用输出。

  • Prometheus 从节点和 Pod 收集指标,以提供所有系统组件性能和操作的快照。

  • 使用 Jaeger(Open Tracing)来监控分布式调用,以获得系统洞察,如错误和延迟。

  • 应用健康状态:应用是否正常运行?如果应用的新版本正在使用 GitOps 进行部署,系统是否“更好”于之前?

  • 使用kubectl describe来监控应用健康状态。

  • 应用同步状态:应用的运行状态是否与部署 Git 存储库中定义的期望状态相同?

  • 配置漂移:应用配置是否在声明式 GitOps 系统之外(例如手动或强制)被更改?

  • 使用kubectl diffkubediff检测应用同步状态和配置漂移

  • GitOps 变更日志:最近对 GitOps 系统做了哪些更改?谁做的,为什么?


1.github.com/cncf/sig-observability.

2.mng.bz/4Z6a.

3.kubernetes.io/docs/concepts/cluster-administration/logging/.

4.www.elastic.co/what-is/elk-stack.

5.www.splunk.com/.

6.github.com/jaegertracing/jaeger/tree/master/examples/hotrod.

7.mng.bz/XdqG.

8.minikube.sigs.k8s.io/docs/tasks/addons/.

9.landing.google.com/sre/sre-book/chapters/monitoring-distributed-systems/.

10.www.brendangregg.com/usemethod.html.

11.mng.bz/MX97.

12.www.weave.works/.

13.github.com/weaveworks/kubediff.

第三部分. 工具

本部分介绍了几个企业级 GitOps 工具,这些工具可以简化并自动化您的 GitOps 流程。

第九章描述了 Argo CD 的动机和设计。它还讨论了如何使用 Argo CD 部署应用程序及其企业功能。

第十章描述了 Jenkins X 的动机和设计。它还讨论了如何将应用程序部署和推广到各种环境。

第十一章描述了 Flux 的动机和设计。它还讨论了如何使用 Flux 部署应用程序并设置多租户。

9 Argo CD

本章涵盖

  • 什么是 Argo CD?

  • 使用 Argo CD 部署应用程序

  • 使用 Argo CD 企业功能

在本章中,您将学习如何使用 Argo CD GitOps 运营商将参考示例应用程序部署到 Kubernetes。您还将了解 Argo CD 如何解决企业考虑因素,例如单点登录 (SSO) 和访问控制。

我们建议您在阅读本章之前阅读第 1、2、3 和 5 章。

9.1 什么是 Argo CD?

Argo CD 是一个开源的 GitOps 运营商,用于 Kubernetes.^(1) 该项目是 Argo 家族的一部分,是一套用于在 Kubernetes 上运行和管理作业和应用程序的云原生工具集。与 Argo Workflows、Rollouts 和 Events 一起,Argo CD 专注于应用程序交付用例,并简化了三种计算模式的结合:服务、工作流和基于事件的处理。在 2020 年,Argo CD 被云原生计算基金会 (CNCF) 批准为孵化级托管项目。

CNCF 云原生计算基金会是一个 Linux 基金会项目,它托管着全球技术基础设施的关键组件。

Argo CD 背后的公司是 Intuit,它是 TurboTax 和 QuickBooks 的创造者。在 2018 年初,Intuit 决定采用 Kubernetes 来加速云迁移。当时,市场上已经存在几个针对 Kubernetes 的成功的持续部署工具,但它们都没有完全满足 Intuit 的需求。因此,而不是采用现有的解决方案,公司决定投资一个新的项目,并开始开发 Argo CD。Intuit 的需求有什么特别之处?这个问题的答案解释了 Argo CD 与其他 Kubernetes CD 工具的不同之处,以及其主要项目用例。

9.1.1 主要用例

GitOps 方法论的重要性以及将基础设施表示为代码的好处是不容置疑的。然而,企业规模的需求需要额外的要求。Intuit 是一家基于云的软件即服务公司。拥有大约 5,000 名开发者,该公司成功地在本地和云端运行了数百个微服务。鉴于这种规模,期望每个团队都运行自己的 Kubernetes 集群是不合理的。相反,决定由一个集中式平台团队运行和维护整个公司的多租户集群。同时,最终用户应该有自由和必要的工具来管理这些集群中的工作负载。这些考虑因素在决定使用 GitOps 的基础上定义了以下额外的要求。

作为服务提供

如果您试图将数百个微服务迁移到 Kubernetes,一个简单的入门流程至关重要。与其要求每个团队安装、配置和维护部署操作员,不如由集中团队提供。随着数千名新用户的加入,SSO 集成至关重要。该服务必须与各种 SSO 提供商集成,而不是引入自己的用户管理。

启用多租户和多集群管理

在多租户环境中,用户需要一个有效且灵活的访问控制系统。Kubernetes 有一个出色的内置基于角色的访问控制系统,但当您必须处理数百个集群时,这还不够。持续部署工具应在多个集群上提供访问控制,并无缝集成到现有的 SSO 提供商中。

启用可观察性

最后,但同样重要的是,持续部署工具应向开发者提供有关管理应用程序状态的了解。这假设有一个用户友好的界面,可以快速回答以下问题:

  • 应用程序配置是否与 Git 中定义的配置同步?

  • 究竟是什么不匹配?

  • 应用程序是否已启动并运行?

  • 究竟是什么出了问题?

该公司需要为企业规模准备好的 GitOps 操作员。团队评估了几个 GitOps 操作员,但没有一个满足所有要求,因此决定实施 Argo CD。

练习 9.1

反思您组织的需要,并将它们与 Argo CD 专注于的使用案例进行比较。尝试决定 Argo CD 是否解决了您团队面临的痛点。

9.1.2 核心概念

为了有效地使用 Argo CD,我们应该了解两个基本概念:应用程序和项目。让我们首先更详细地看看应用程序。

应用程序

应用程序提供 Kubernetes 资源的逻辑分组,并定义资源清单的源和目标。

图片

图 9.1 Argo CD 应用程序的主要属性是源和目标。源指定资源清单在 Git 仓库中的位置。目标指定资源应在 Kubernetes 集群中的何处创建。

应用程序源包括仓库 URL 和仓库内的目录。通常,仓库包含多个目录,每个应用程序环境一个,例如 QA 和 Prod。此类仓库的示例目录结构如下所示:

├──  prod
│   └──  deployment.yaml
└──  qa
 └── deployment.yaml

每个目录不一定包含纯 YAML 文件。Argo CD 不强制使用任何配置管理工具,而是提供对各种配置管理工具的一级支持。因此,目录可能也包含与 Helm 图表定义一起的 YAML 文件以及 Kustomize 遮罩。

应用程序目标定义了资源必须部署的位置,包括目标 Kubernetes 集群的 API 服务器 URL 以及集群命名空间名称。API 服务器 URL 识别所有应用程序清单必须部署的集群。无法在多个集群之间部署应用程序清单,但不同的应用程序可能被部署到不同的集群中。命名空间名称用于识别所有命名空间级别应用程序资源的目标命名空间。

因此,Argo CD 应用程序代表在 Kubernetes 集群中部署的环境,并将其连接到 Git 仓库中存储的期望状态。

练习 9.2

考虑您组织中实际部署的服务,并为您列表中的应用程序制定一个 Argo CD 应用程序列表。定义一个应用程序的源仓库 URL、目录和目标集群以及命名空间。

9.1.3 同步和健康状态

除了源和目标之外,Argo CD 应用程序还有两个更重要的属性:同步和健康状态。

同步状态回答观察到的应用程序资源状态是否与存储在 Git 仓库中的资源状态不同。偏差计算背后的逻辑等同于 kubectl diff 命令的逻辑。同步状态的值可能是同步或不同步。同步状态表示每个应用程序资源都被找到,并且完全匹配预期的资源状态。不同步状态表示至少有一个资源状态与预期状态不匹配,或者在目标集群中未找到。

应用程序的健康状态汇总了构成应用程序的每个资源的观察到的健康状态。对于每个 Kubernetes 资源类型,健康评估逻辑不同,并导致以下值之一:

  • 健康——例如,如果所需的 Pod 数量正在运行,并且每个 Pod 成功通过了就绪和存活探针,则认为 Kubernetes 部署是健康的。

  • 进行中——表示尚未健康但预计将达到健康状态的资源。如果 Deployment 尚未健康但尚未指定由 progressingDeadlineSeconds2 字段指定的超时时间,则认为它是进行中的。

  • 退化——健康状态的相反。例如,一个 Deployment 在预期超时内无法达到健康状态。

  • 缺失——表示存储在 Git 中但未部署到目标集群的资源。

汇总的应用程序状态是每个应用程序资源的最差状态。健康状态是最好的,依次下降到进行中、退化、缺失(最差)。因此,如果所有应用程序资源都是健康的,而只有一个资源是退化的,则汇总状态也是退化的。

练习 9.3

考虑一个由两个 Deployment 组成的应用程序。关于资源的以下信息是已知的:

  • 部署 1 有一个与 Git 仓库中存储的镜像不匹配的镜像。由于部署progressingDeadlineSeconds设置为 10 分钟,所有部署 Pod 已经失败启动数小时。

  • 部署 2 没有完全匹配预期的状态,并且所有 Pod 都在运行。

应用程序的同步和健康状态是什么?

健康和同步状态回答了关于应用程序最重要的两个问题:

  • 我的应用程序是否在运行?

  • 我是否运行了预期的内容?

项目

Argo CD 应用程序提供了一种非常灵活的方式来独立管理不同的应用程序。这种功能为团队提供了关于每项基础设施的非常有用的见解,并大大提高了生产力。然而,这还不足以支持具有不同访问级别的多个团队:

  • 应用程序的混合列表造成了混淆,这可能导致人为错误的可能性。

  • 不同的团队有不同的访问级别。正如第六章所述,个人可能会使用 GitOps 操作员来提升自己的权限以获得完整的集群访问权限。

这些问题的解决方案是为每个团队创建一个单独的 Argo CD 实例。这不是一个完美的解决方案,因为单独的实例意味着管理开销。为了避免管理开销,Argo CD 引入了项目抽象。图 9.2 说明了应用程序和项目之间的关系。

项目为应用程序提供了一个逻辑分组,隔离了团队之间的联系,并允许在每个项目中进行细粒度的访问控制调整。

图片

图 9.2 展示了应用程序和项目之间的关系。项目为应用程序提供了一个逻辑分组,隔离了团队之间的联系,并允许在多租户环境中使用 Argo CD。

除了分离应用程序集之外,项目还提供以下功能集:

  • 限制项目应用程序可能使用的 Kubernetes 集群和 Git 存储库

  • 限制项目内每个应用程序可以部署的 Kubernetes 资源

练习 9.4

尝试列出你组织中的项目列表。使用项目,你可以限制用户可以部署的资源类型、源存储库以及项目内可用的目标集群。你会为你的项目配置哪些限制?

9.1.4 架构

初看起来,GitOps 操作员的实现并不太复杂。从理论上讲,你所需要做的就是克隆包含清单的 Git 仓库,并使用kubectl diffkubectl apply来检测和处理配置漂移。这在你尝试为多个团队自动化此过程并同时管理数十个集群的配置时是正确的。逻辑上,这个过程分为三个阶段,每个阶段都有其自身的挑战:

  • 获取资源清单。

  • 检测并修复偏差。

  • 将结果呈现给最终用户。

每个阶段消耗不同的资源,每个阶段的实现必须以不同的方式扩展。每个阶段都有一个单独的 Argo CD 组件负责。

图 9.3

图 9.3 Argo CD 由三个主要组件组成,实现了 GitOps 协调周期阶段。argocd-repo-server 从 Git 中检索清单。argocd-application-controller 将 Git 中的清单与 Kubernetes 集群中的资源进行比较。argocd-api-server 向用户展示协调结果。

让我们逐一了解每个阶段及其对应的 Argo CD 组件实现细节。

检索资源清单

Argo CD 中的清单生成由 argocd-repo-server 组件实现。这个阶段带来了一系列挑战。

生成清单需要您下载 Git 仓库内容并生成可用的清单 YAML 文件。首先,每次需要检索预期的资源清单时都下载整个仓库内容是非常耗时的。Argo CD 通过在本地磁盘上缓存仓库内容并使用 git fetch 命令仅从远程 Git 仓库下载最近更改来解决此问题。下一个挑战与内存使用有关。在现实生活中,资源清单很少以纯 YAML 文件的形式存储。在大多数情况下,开发者更喜欢使用像 Helm 或 Kustomize 这样的配置管理工具。每次工具调用都会导致内存使用量的峰值。为了处理内存使用问题,Argo CD 允许用户限制并行生成清单的数量,并增加 argocd-repo-server 实例的数量以提高性能。

图 9.4

图 9.4 argocd-repo-server 在本地存储中缓存克隆的仓库,并封装与生成最终资源清单所需的配置管理工具的交互。

检测并修复偏差

协调阶段由 argocd-application-controller 组件实现。控制器加载实时 Kubernetes 集群状态,将其与 argocd-repo-server 提供的预期清单进行比较,并修补偏离的资源。这个阶段可能是最具挑战性的。为了正确检测偏差,GitOps 运营商需要了解集群中的每个资源,并比较和更新数千个资源。

图 9.5

图 9.5 argocd-application-controller 执行资源协调。控制器利用 argocd-repo-server 组件检索预期的清单,并将清单与轻量级的内存中 Kubernetes 集群状态缓存进行比较。

控制器维护每个管理集群的轻量级缓存,并在后台使用 Kubernetes 监视 API 更新它。这使得控制器能够在几秒钟内对应用程序进行协调,并赋予其扩展和管理数十个集群的能力。每次协调后,控制器都会收集每个应用程序资源的详尽信息,包括同步和健康状态。控制器将这些信息保存到 Redis 集群中,以便稍后向最终用户展示。

向最终用户展示结果

最后,必须将协调结果展示给最终用户。这项任务由 argocd-server 组件执行。虽然繁重的工作已经由 argocd-repo-serverargocd-application-controller 完成,但这个最后阶段具有最高的弹性要求。argocd-server 是一个无状态的 Web 应用程序,它加载关于协调结果的信息并驱动 Web 用户界面。

架构设计使得 Argo CD 能够以最小的维护开销为大型企业提供 GitOps 操作。

练习 9.5

哪些组件服务于用户请求并需要多个副本以提高弹性?哪些组件可能需要大量内存以进行扩展?

9.2 部署您的第一个应用程序

虽然 Argo CD 是一个企业级、复杂的分布式系统,但它仍然轻量级,可以轻松运行在 minikube 上。安装过程简单,包括几个简单的步骤。请参阅附录 B 了解有关如何安装 Argo CD 的更多信息,或遵循官方 Argo CD 指令.^(3)

9.2.1 部署第一个应用程序

一旦 Argo CD 启动,我们就准备好部署我们的第一个应用程序。正如之前提到的,要部署 Argo CD 应用程序,我们需要指定包含部署清单的 Git 仓库,并针对 Kubernetes 集群和命名空间。为了进行此练习创建 Git 仓库,请打开以下 GitHub 仓库并创建一个仓库 fork:^(4)

https://github.com/gitopsbook/sample-app-deployment

Argo CD 可以部署到外部集群,也可以部署到它安装的同一集群。让我们使用第二种选项,将我们的应用程序部署到 minikube 集群的默认命名空间。

重置您的分支 在您之前章节的工作中已经 fork 了部署仓库吗?请确保重置更改以获得最佳体验。最简单的方法是删除之前 fork 的仓库并再次 fork。

应用程序可能通过 Web 用户界面、CLI 或甚至通过 REST 或 gRPC API 以编程方式创建。由于我们已安装并配置了 Argo CD CLI,让我们使用它来部署应用程序。请执行以下命令以创建应用程序:

$ argocd app create sample-app \                                 ❶
  --repo https://github.com/<username>/sample-app-deployment \   ❷
  --path . \                                                     ❸
  --dest-server https://kubernetes.default.svc \                 ❹
  --dest-namespace default                                       ❺

❶ 唯一的应用程序名称

❷ Git 仓库 URL

❸ Git 仓库内的目录路径

❹ Kubernetes API 服务器 URL。https://kubernetes.default.svc/是每个 Kubernetes 集群中可用的 API 服务器 URL。

❺ Kubernetes 命名空间名称

一旦应用创建,我们就可以使用 Argo CD CLI 获取应用状态信息。使用以下命令获取sample-app应用状态信息:

argocd app get sample-app                          ❶
Name:               sample-app
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://<host>:<port>/applications/sample-app
Repo:               https://github.com/<username>/sample-app-deployment
Target:
Path:               .
SyncWindow:         Sync Allowed
Sync Policy:        <none>
Sync Status:        OutOfSync from  (09d6663)      ❷
Health Status:      Missing                        ❸

GROUP  KIND        NAMESPACE  NAME        STATUS     HEALTH   HOOK  MESSAGE
       Service     default    sample-app  OutOfSync  Missing
apps   Deployment  default    sample-app  OutOfSync  Missing

❶ 返回应用状态信息的 CLI 命令

❷ 应用同步状态,回答应用状态是否与预期状态匹配

❸ 应用聚合健康状态

如我们从命令输出中可以看到,应用已出现同步错误且不健康。默认情况下,如果 Argo CD 检测到偏差,它不会将 Git 仓库中定义的资源推送到集群中。除了高级摘要外,我们还可以看到每个应用资源的详细信息。Argo CD 检测到应用应该有一个 Deployment 和一个 Service,但这两个资源都缺失。要部署资源,我们需要配置使用同步策略的自动应用同步^(5)或手动触发同步。要触发同步并部署资源,请使用以下命令:

$ argocd app sync sample-app                                                                          ❶
TIMESTAMP                  GROUP    KIND        NAMESPACE   NAME    STATUS    HEALTH   HOOK  MESSAGE  ❷
2020-03-17T23:16:50-07:00  Service  default     sample-app  OutOfSync Missing
2020-03-17T23:16:50-07:00  apps     Deployment  default     sample-app  OutOfSync  Missing

Name:               sample-app
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://<host>:<port>/applications/sample-app
Repo:               https://github.com/<username>/sample-app-deployment
Target:
Path:               .
SyncWindow:         Sync Allowed
Sync Policy:        <none>
Sync Status:        OutOfSync from  (09d6663)
Health Status:      Missing

Operation:          Sync
Sync Revision:      09d6663dcfa0f39b1a47c66a88f0225a1c3380bc
Phase:              Succeeded
Start:              2020-03-17 23:17:12 -0700 PDT
Finished:           2020-03-17 23:17:21 -0700 PDT
Duration:           9s
Message:            successfully synced (all tasks run)

GROUP  KIND        NAMESPACE  NAME        STATUS   HEALTH   HOOK  MESSAGE                             ❸
       Service     default    sample-app  Synced   Healthy        service/sample-app created
apps   Deployment  default    sample-app  Synced   Progressing    deployment .apps/sample-app created

❶ 触发应用同步的 CLI 命令

❷ 同步操作前的初始应用状态

❸ 同步完成后最终的 应用状态

一旦触发同步,Argo CD 将 Git 中存储的清单推送到 Kubernetes 集群,然后重新评估应用状态。当同步完成时,最终的应用状态将打印到控制台。sample-app应用成功同步,每个结果都与预期状态相匹配。

9.2.2 使用用户界面检查应用

除了 CLI 和 API 之外,Argo CD 还提供了一个用户友好的 Web 界面。使用 Web 界面,您可能会看到跨多个集群部署的所有应用的高级视图以及每个应用资源的非常详细的信息。打开 https://: URL 以在 Argo CD 用户界面中查看应用列表。

图 9.6 显示可用 Argo CD 应用的列表页面。页面提供了关于每个应用的高级信息,例如同步和健康状态。

应用列表页面提供了所有已部署应用的高级信息,包括健康和同步状态。使用此页面,您可以快速查找是否有任何应用出现降级或配置漂移。用户界面是为大型企业设计的,能够处理数百个应用。您可以使用搜索和各种过滤器快速找到所需的应用。

练习 9.6

尝试调整过滤器和平页面视图设置,以了解应用列表页面中其他哪些功能可用。

应用详情页面

关于应用程序的附加信息可在应用程序详情页面找到。通过点击“sample app”应用程序磁贴导航到应用程序详情页面。

应用程序详情页面可视化应用程序资源层次结构,并提供有关同步和健康状态的额外详细信息。让我们更仔细地查看应用程序资源树并了解它提供了哪些功能。

资源树的根元素是应用程序本身。下一级包括管理资源。管理资源是 Git 中定义并由 Argo CD 明确控制的资源。正如我们在第二章中学到的,Kubernetes 控制器通常利用委派并创建子资源来委派工作。第三级及更深层级代表这样的资源。这提供了关于每个应用程序元素的完整信息,使得应用程序详情页面成为一个极其强大的 Kubernetes 仪表板。

图片

图 9.7 应用程序详情页面提供了关于应用程序资源层次结构以及每个资源的详细信息。

除了这些信息外,用户界面允许对每个资源执行各种操作。可以删除任何资源,通过运行同步操作重新创建它,使用内置的 YAML 编辑器更新资源定义,甚至运行特定于资源的操作,如 Deployment 重启。

练习 9.7

继续使用应用程序详情页面来检查你的应用程序。尝试找到查看资源清单、定位 Pod 和查看实时日志的方法。

9.3 深入了解 Argo CD 功能

到目前为止,我们已经学习了如何使用 Argo CD 部署新应用程序以及如何使用 CLI 和用户界面获取详细的应用程序信息。接下来,让我们学习如何使用 GitOps 和 Argo CD 部署新应用程序版本。

9.3.1 GitOps 驱动的部署

为了执行 GitOps 部署,我们需要更新资源清单并让 GitOps 操作员将更改推送到 Kubernetes 集群。作为第一步,使用以下命令克隆 Deployment 仓库:

$ git clone git@github.com:<username>/sample-app-deployment.git
$ cd sample-app-deployment

接下来,使用以下命令更改 Deployment 资源的形象版本:

$ sed -i '' 's/sample-app:v.*/sample-app:v0.2/' deployment.yaml

使用 git diff 命令确保你的 Git 仓库有预期的更改:

$ git diff                                                                                                                                                      
diff --git a/deployment.yaml b/deployment.yaml
index 5fc3833..397d058 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -16,7 +16,7 @@ spec:
       containers:
       - command:
         - /app/sample-app
-        image: gitopsbook/sample-app:v0.1
+        image: gitopsbook/sample-app:v0.2
         name: sample-app
         ports:
         - containerPort: 8080

最后,使用 git commitgit push 将更改推送到远程 Git 仓库:

$ git commit -am "update deployment image"
$ git push

让我们使用 Argo CD CLI 来确保 Argo CD 正确检测到 Git 中的清单更改并触发了同步过程以将更改推送到 Kubernetes 集群:

$ argocd app diff sample-app --refresh
===== apps/Deployment default/sample-app ======
21c21
<         image: gitopsbook/sample-app:v0.1
---
>         image: gitopsbook/sample-app:v0.2

练习 9.8

打开 Argo CD UI 并使用应用程序详情页面来检查应用程序同步状态并检查管理资源状态。

使用 argocd sync 命令触发同步过程:

$ argocd app sync sample-app

太好了,你刚刚使用 Argo CD 执行了 GitOps 部署!

9.3.2 资源钩子

资源清单同步只是基本用例。在现实生活中,我们通常需要在实际部署前后执行额外的步骤。例如,设置维护页面,在新版本部署前执行数据库迁移,最后移除维护页面。

传统上,这些部署步骤是在 CI 管道中编写的。然而,这又需要从 CI 服务器访问生产环境,这涉及到安全风险。为了解决这个问题,Argo CD 提供了一种名为资源钩子的功能。这些钩子允许在同步过程中在 Kubernetes 集群内部运行自定义脚本,通常打包成一个 Pod 或一个 Job。

钩子是一个存储在 Git 仓库中并带有argocd.argoproj.io/hook注解的 Kubernetes 资源清单。注解值包含一个逗号分隔的列表,表示钩子应该执行的阶段。以下阶段被支持:

  • 预同步—在应用清单之前执行

  • 同步—在所有预同步钩子完成且成功执行后执行,同时执行清单的应用

  • 跳过—指示 Argo CD 跳过清单的应用

  • 同步后—在所有同步钩子完成且成功执行、成功应用以及所有资源处于健康状态后执行

  • 同步失败—在同步操作失败时执行

钩子在集群内部执行,因此无需从 CI 管道访问集群。指定同步阶段的能力提供了必要的灵活性,并允许解决大多数现实生活中的部署用例的机制。

图片

图 9.8 同步过程包括三个主要阶段。预同步阶段用于执行准备任务,如数据库迁移。同步阶段包括应用资源同步。最后,同步后阶段运行后处理任务,如发送电子邮件通知。

是时候看看钩子功能在实际中的应用了!将钩子定义添加到示例应用部署仓库中,并将更改推送到远程仓库:

$ git add pre-sync.yaml
$ git commit -am 'Add pre-sync hook'
$ git push

列表 9.1 http://mng.bz/go7l

apiVersion: batch/v1
kind: Job
metadata:
  name: before
  annotations:
    argocd.argoproj.io/hook: PreSync
spec:
  template:
    spec:
      containers:
      - name: sleep
        image: alpine:latest
        command: ["echo", "pre-sync"]
      restartPolicy: Never
  backoffLimit: 0

Argo CD 用户界面比 CLI 提供了更好的动态过程可视化。让我们使用它来更好地理解钩子是如何工作的。使用以下命令打开 Argo CD UI:

$ minikube service argocd-server -n argocd --url

导航到sample-app详情页面,并使用同步按钮触发同步过程。同步过程在图 9.9 中展示。

图片

图 9.9 应用详情页面允许用户触发同步,以及查看同步进度的详细信息,包括同步钩子。

一旦开始同步,应用程序详情页面在右上角显示实时进程状态。状态包括操作开始时间和持续时间。您可以通过点击同步状态图标查看包含同步钩子结果的详细信息的同步状态面板。

钩子以常规资源清单的形式存储在 Git 仓库中,并在应用程序资源树中以常规资源的形式可视化。您可以看到“删除前”作业的实时状态,并使用 Argo CD 用户界面检查子 Pod。

除了阶段之外,您还可以自定义钩子删除策略。删除策略允许自动化钩子资源的删除,这将为您节省大量的手动工作。

练习 9.9

在 Argo CD 文档中阅读更多详细信息^(6) 并更改“删除前”作业的策略。使用 Argo CD 用户界面来观察不同的删除策略如何影响钩子行为。同步应用程序并观察 Argo CD 如何创建和删除钩子资源。

9.3.3 部署后验证

资源钩子允许封装应用程序同步逻辑,因此我们不必使用脚本和持续集成工具。然而,某些此类用例自然属于持续集成流程,并且仍然更倾向于使用像 Jenkins 这样的工具。

其中一个用例是部署后验证。这里的挑战在于 GitOps 部署本质上是异步的。在将提交推送到 Git 仓库之后,我们仍然需要确保更改已传播到 Kubernetes 集群。即使在更改已传播之后,启动测试也不是安全的。在大多数情况下,Kubernetes 资源更新也不是瞬时的。例如,Deployment 资源更新会触发滚动更新过程。滚动更新可能需要几分钟,如果新应用程序版本存在问题,甚至可能失败。因此,如果您开始测试过早,可能会测试之前部署的应用程序版本。

Argo CD 通过提供帮助监控应用程序状态的工具,使这个问题变得微不足道。argocd app wait 命令监控应用程序,在应用程序达到同步和健康状态后退出。一旦命令退出,您就可以假设所有更改都已成功部署,并且可以安全地开始部署后验证。argocd app wait 命令通常与 argocd app sync 一起使用。使用以下命令同步您的应用程序,并等待更改完全部署,应用程序准备就绪进行测试:

$ argocd app sync sample-app && argocd app wait sample-app

9.4 企业功能

Argo CD 非常轻量级,并且使用起来非常简单。同时,它对于大型企业具有良好的可扩展性,能够满足多个团队的需求。企业功能可以根据需要进行配置。如果您正在为您的组织部署 Argo CD,那么第一个问题就是如何配置最终用户并有效地管理访问控制。

9.4.1 单点登录

Argo CD 没有引入自己的用户管理系统,而是提供了与多个 SSO 服务的集成。该列表包括 Okta、Google OAuth、Azure AD 以及更多。

SSO SSO 是一种会话和用户身份验证服务,允许用户使用一组登录凭证访问多个应用程序。

SSO 集成非常出色,因为它可以为您节省大量的管理开销,并且最终用户无需记住另一组登录凭证。存在几种用于交换身份验证和授权数据的开放标准。其中最受欢迎的是 SAML、OAuth 和 OpenID Connect (OIDC)。在这三者中,SAML 和 OIDC 最符合典型企业的需求,可以用来实现 SSO。Argo CD 决定采用 OIDC,因为它具有强大的功能和简单的特性。

配置 OIDC 集成所需的步骤数量取决于您的 OIDC 提供者。Argo CD 社区已经为 Okta 和 Azure AD 等流行的 OIDC 提供者贡献了许多说明。在 OIDC 提供者端完成配置后,您需要将相应的配置添加到argocd-cm ConfigMap 中。以下是一个示例 Okta 配置片段:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  url: https://<myargocdhost>                          ❶
  oidc.config: |                                       ❷
    name: Okta
    issuer: https://yourorganization.oktapreview.com
    clientID: <your client id>
    clientSecret: <your client secret>
    requestedScopes: ["openid", "profile", "email", "groups"]
    requestedIDTokenClaims: {"groups": {"essential": true}}

❶ 面向外部的基 URL Argo CD URL

❷ 包含 Okta 应用程序客户端 ID 和密钥的 OIDC 配置

如果您的组织没有 OIDC 兼容的 SSO 服务怎么办?在这种情况下,您可以使用一个联邦 OIDC 提供者,Dex,^(7),它默认包含在 Argo CD 中。Dex 充当其他身份提供者的代理,并允许与 SAML、LDAP 提供者或 GitHub 和 Active Directory 等服务建立集成。

GitHub 通常是一个非常吸引人的选择,尤其是如果它已经被您的组织中的开发者使用。此外,在 GitHub 中配置的组织和团队自然符合组织集群访问所需的访问控制模型。正如您很快就会学到的那样,使用 GitHub 团队成员资格来建模 Argo CD 访问非常简单。让我们使用 GitHub 来增强我们的 Argo CD 安装并启用 SSO 集成。

首先,我们需要创建一个 GitHub OAuth 应用程序。导航到github.com/settings/applications/new并配置应用程序设置,如图 9.10 所示。

图 9.10

图 9.10 新的 GitHub OAuth 应用程序设置包括应用程序名称和描述、主页 URL,最重要的是,授权回调 URL。

指定您选择的应用程序名称和与 Argo CD 网页用户界面 URL 匹配的首页 URL。最重要的应用程序设置是回调 URL。回调 URL 的值是 Argo CD 网页用户界面 URL 加上 /api/dex/callback 路径。使用 minikube 的示例 URL 可能是 http://192.168.64.2:32638/api/dex/callback。

创建应用程序后,您将被重定向到 OAuth 应用程序设置页面。复制应用程序客户端 ID 和客户端密钥。这些值将用于配置 Argo CD 设置。

图 9.11 GitHub OAuth 应用程序设置页面显示客户端 ID 和客户端密钥值,这些值是配置 SSO 集成所必需的。

在 argocd-cm.yaml 文件中将占位符值替换为您的环境值。

列表 9.2 http://mng.bz/pV1G

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  labels:
    app.kubernetes.io/name: argocd-cm
    app.kubernetes.io/part-of: argocd
data:
  url: https://<minikube-host>:<minikube-port>   ❶

  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: <client-id>                  ❷
          clientSecret: <client-secret>          ❸
          loadAllGroups: true

❶ 面向外部的基 URL Argo CD URL

❷ GitHub OAuth 应用程序客户端 ID

❸ GitHub OAuth 应用程序客户端密钥

使用 kubectl apply 命令更新 Argo CD ConfigMap:

$ kubectl apply -f ./argocd-cm.yaml -n argocd

您已经准备就绪!在浏览器中打开 Argo CD 用户界面并使用“通过 GitHub 登录”按钮。

9.4.2 访问控制

您可能会注意到,使用 GitHub SSO 集成成功登录后,应用程序列表页面为空。如果您尝试创建新的应用程序,您将看到“权限拒绝”错误。这种行为是预期的,因为我们尚未为新 SSO 用户授予任何权限。为了向用户提供适当的访问权限,我们需要更新 Argo CD 访问控制设置。

Argo CD 提供了一个灵活的基于角色的访问控制(RBAC)系统,其实施基于 Casbin,^(8) 一个强大的开源访问控制库。Casbin 提供了一个非常坚实的基础,并允许配置各种访问控制规则。

RBAC Argo CD 设置是通过 argocd-rbac-cm ConfigMap 配置的。为了快速深入了解配置细节,让我们更新 ConfigMap 字段,然后一起查看每个更改。

在 argocd-rbac-cm.yaml 文件中将 <username> 占位符替换为您的 GitHub 账户用户名。

列表 9.3 http://mng.bz/OEPn

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  labels:
    app.kubernetes.io/name: argocd-rbac-cm
    app.kubernetes.io/part-of: argocd
data:

  policy.csv: |                                      ❶
    p, role:developer, applications, *, */*, allow                   
    g, role:developer, role:readonly                                 

    g, <username>, role:developer                                    

  scopes: '[groups, preferred_username]'             ❷

❶ policy.csv 包含基于角色的访问规则。

❷ 范围设置指定了用于推断用户组的 JWT 声明。

使用 kubectl apply 命令应用 RBAC 更改:

$ kubectl apply -f ./argocd-rbac-cm.yaml -n argocd

此配置中的 policy.csv 字段定义了一个名为 role:developer 的角色,该角色对 Argo CD 应用程序拥有完全权限,并且对 Argo CD 系统设置拥有只读权限。该角色授予任何属于名称与您的 GitHub 账户用户名匹配的组的用户。一旦应用更改,刷新应用程序列表页面并尝试同步 sample-app 应用程序。

我们介绍了一些新的术语。让我们回顾一下角色、组和声明是什么,以及它们是如何协同工作的。

角色

角色允许或拒绝对 Argo CD 对象上的特定主题执行一组动作。角色以以下形式定义

p, subject, resource, action, object, effect

其中

  • p表示 RBAC 策略行。

  • subject是一个组。

  • resource是 Argo CD 资源类型之一。Argo CD 支持以下资源:"clusters""projects""applications""repositories""certificates""accounts"

  • action是对资源可能执行的动作名称。所有 Argo CD 资源都支持以下动作:"get""create""update""delete""*"值匹配任何动作。

  • object是一个标识特定资源实例的模式。"*"值匹配任何实例。

  • effect定义了角色是授予还是拒绝动作。

此示例中的role:developer角色允许对任何 Argo CD 应用程序执行任何动作:

p, role:developer, applications, *, */*, allow

组提供识别一组用户的能力,并与 OIDC 集成协同工作。在成功执行 OIDC 身份验证后,最终用户会收到一个 JWT 令牌,该令牌验证用户身份并提供存储在令牌断言中的额外元数据。

JWT 令牌 JWT 令牌是一个互联网标准,用于创建基于 JSON 的访问令牌,该令牌断言一些断言。^(9)

令牌随每个 Argo CD 请求提供。Argo CD 从配置的令牌断言列表中提取用户所属的组列表,并使用它来验证用户权限。

以下是由 Dex 生成的令牌断言示例:

{
  "iss": "https://192.168.64.2:32638/api/dex",
  "sub": "CgY0MjY0MzcSBmdpdGh1Yg",
  "aud": "argo-cd",
  "exp": 1585646367,
  "iat": 1585559967,
  "at_hash": "rAz6dDHslBWvU6PiWj_o9g",
  "email": "AMatyushentsev@gmail.com",
  "email_verified": true,
  "groups": [
    "gitopsbook"
  ],
  "name": "Alexander Matyushentsev",
  "preferred_username": "alexmt"
}

令牌包含两个可能对授权有用的断言:

  • groups包括用户所属的 GitHub 组织和团队列表。

  • preferred_username是 GitHub 账户用户名。

默认情况下,Argo CD 使用groups从 JWT 令牌中检索用户组。我们已通过scopes设置添加了preferred_username断言,以便可以通过名称识别 GitHub 用户。

练习 9.10

更新argocd-rbac-cm ConfigMap,以便根据用户的电子邮件提供对 GitHub 用户的管理员访问权限。

注意:本章涵盖了 Argo CD 的重要基础,并为您进一步学习做好准备。探索 Argo CD 文档,了解差异逻辑定制、微调配置管理工具、高级安全功能(如身份验证令牌)等。项目不断进化,并在每个版本中添加新功能。查看 Argo CD 博客以了解最新变化,并在 Argoproj 的 Slack 频道中不要犹豫提问。

9.4.3 声明式管理

正如你可能已经注意到的,Argo CD 提供了大量的配置设置。RBAC 策略、SSO 设置、应用程序和项目——所有这些都需要有人来管理。好消息是你可以利用 GitOps 并使用 Argo CD 来自动管理自己!

所有 Argo CD 设置都持久化在 Kubernetes 资源中。SSO 和 RBAC 设置存储在 ConfigMap 中,而应用程序和项目存储在自定义资源中,因此您可以将这些资源清单存储在 Git 仓库中,并配置 Argo CD 使用它作为真相来源。这项技术非常强大,允许我们管理配置设置,以及无缝升级 Argo CD 版本。

作为第一步,让我们演示如何将我们刚刚强制执行的 SSO 和 RBAC 更改转换为声明性配置。为此,我们需要创建一个 Git 仓库,用于存储每个 Argo CD 组件的清单定义。您可以直接从github.com/gitopsbook/resources仓库中的代码列表作为起点。导航到仓库的 GitHub URL,创建您个人的分叉,以便您可以存储特定于您环境的设置。

需要的清单文件位于第九章目录中,我们应该首先查看的文件如下所示。

列表 9.4 http://mng.bz/YqRN

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml                      ❶

patchesStrategicMerge:     
- argocd-cm.yaml                       ❷
- argocd-rbac-cm.yaml                  ❸
- argocd-server.yaml                   ❹

❶ 包含默认 Argo CD 清单的远程文件 URL

❷ 包含 argocd-cm ConfigMap 修改的文件路径

❸ 包含 argocd-rbac-cm ConfigMap 修改的文件路径

❹ 包含 argocd-server 服务修改的文件路径

kustomization.yaml 文件包含对默认 Argo CD 清单和具有特定环境更改的文件的引用。

下一步是将特定环境的更改移动到 Git 中,并将它们推送到远程 Git 仓库。克隆分叉的 Git 仓库:

$ git clone git@github.com:<USERNAME>/resources.git

重复第 9.4.1 节和第 9.4.2 节中描述的 argocd-cm.yaml 和 argocd-rbac-cm.yaml 文件的更改。在 argocd-cm.yaml 中的 ConfigMap 清单中添加 SSO 配置。更新 argocd-rbac-cm.yaml 文件中的 RBAC 策略。一旦文件更新,提交并将更改推回远程仓库:

$ git commit -am  "Update Argo CD configuration"
$ git push

最困难的部分已经完成!Argo CD 配置更改不受版本控制,可以使用 GitOps 方法进行管理。最后一步是创建一个 Argo CD 应用程序,将基于 Kustomize 的清单从您的 Git 仓库部署到argocd命名空间:

$ argocd app create argocd \                                                                                                                                                              
--repo https://github.com/<USERNAME>/resources.git \
--path chapter-09 \
--dest-server https://kubernetes.default.svc \
--dest-namespace argocd \
--sync-policy auto
application 'argocd' created

一旦创建应用程序,Argo CD 应该检测已部署的资源并可视化检测到的偏差。

那么,如何管理应用程序和项目呢?两者都由 Kubernetes 自定义资源表示,也可能使用 GitOps 进行管理。下一条清单中的清单代表了我们之前在章节中手动创建的sample-app Argo CD 应用程序的声明性定义。为了开始以声明性方式管理sample-app,将 sample-app.yaml 添加到 kustomization.yaml 的资源部分,并将更改推回您的仓库分叉。

列表 9.5 http://mng.bz/Gx9q

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: sample-app
spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  source:
    path: .
    repoURL: https://github.com/<username>/sample-app-deployment

如您所见,您不必在声明式和管理式管理风格之间做出选择。Argo CD 支持同时使用这两种风格,以便某些设置使用 GitOps 管理,而某些设置使用命令式命令管理。

摘要

  • Argo CD 考虑到企业需求而设计,可以作为集中式服务提供给大型企业,以支持多租户和多集群。

  • 作为持续部署工具,Argo CD 还提供了 Git、目标 Kubernetes 集群和运行状态之间的详细差异,以实现可观察性。

  • Argo CD 自动化部署的三个阶段:

    • 获取资源清单。

    • 检测并修复偏差。

    • 将结果呈现给最终用户。

  • Argo CD 提供了用于配置应用程序部署的 CLI,并且可以通过脚本集成到 CI 解决方案中。

  • Argo CD 的 CLI 和 Web 界面可用于检查应用程序的同步和健康状态。

  • Argo CD 提供资源钩子,以启用对部署生命周期的额外自定义。

  • Argo CD 还提供支持以确保部署完成和应用程序就绪。

  • Argo CD 支持企业级 SSO 和 RBAC 集成,以实现单点登录和访问控制。


  1. argoproj.github.io/projects/argo-cd.

2.mng.bz/aomz.

3.argoproj.github.io/argo-cd/getting_started/.

4.help.github.com/en/github/getting-started-with-github/fork-a-repo.

5.argoproj.github.io/argo-cd/user-guide/auto_sync/.

6.mng.bz/e5Ez.

7.github.com/dexidp/dex.

8.github.com/casbin/casbin.

9.en.wikipedia.org/wiki/JSON_Web_Token.

10 Jenkins X

本章涵盖

  • 什么是 Jenkins X?

  • 安装 Jenkins X

  • 将项目导入 Jenkins X

  • 在 Jenkins X 中将发布版本推送到生产环境

本章由 Viktor Farcic 和 Oscar Medina 贡献。

在本章中,你将学习如何使用 Jenkins X 将我们的参考示例应用程序部署到 Kubernetes。你还将了解 Prow、Jenkins X 管道操作员和 Tekton 如何协同工作以构建 CI/CD 管道。

我们建议你在阅读本章之前先阅读第 1、2、3 和 5 章。

10.1 什么是 Jenkins X?

要理解 Jenkins X 的复杂性和内部工作原理,我们需要了解 Kubernetes。然而,我们不需要了解 Kubernetes 就可以使用 Jenkins X。这是项目的主要贡献之一。Jenkins X 允许我们利用 Kubernetes 的力量,而无需花费无数的时间学习 Kubernetes 不断增长的“正确做事”的列表。Jenkins X1 是一个开源工具,它将复杂的过程简化为可以快速采用的概念,而无需花费数月时间试图弄清楚“正确做事的方式”。它通过移除和简化由 Kubernetes 及其生态系统的整体复杂性引起的一些问题来帮助用户。如果你确实是一个 Kubernetes 大师,你会欣赏 Jenkins X 所投入的所有努力。如果你不是,你将能够直接进入并利用 Kubernetes 的力量,而不会因为 Kubernetes 的复杂性而感到沮丧。在第 10.2 节中,我们将详细讨论 Jenkins X 的模式和工具。

注意:Jenkins X 是一个由 CloudBees 提供企业支持的免费、开源工具。2

现在,大多数软件供应商都在构建下一代软件以使其成为 Kubernetes 原生的,或者至少在 Kubernetes 内运行得更好。一个完整的生态系统正在出现,并将 Kubernetes 视为一个空白画布。因此,每天都有新的工具被添加,而且越来越明显,Kubernetes 提供了几乎无限的可能性。然而,这也带来了更高的复杂性。选择使用哪些工具比以往任何时候都更难。我们将如何开发我们的应用程序?我们将如何管理不同的环境?我们将如何打包我们的应用程序?我们将应用哪种流程来管理应用程序的生命周期?等等。组装一个包含所有工具和流程的 Kubernetes 集群需要时间,而学习如何使用我们所组装的就像是一个永无止境的故事。Jenkins X 的目标就是移除这些以及其他障碍。

Jenkins X 有自己的观点。它定义了软件开发生命周期的许多方面,并为我们做出决策。它告诉我们该做什么以及如何做。它就像一个度假时的导游,告诉你去哪里,看什么,何时拍照,何时休息。同时,它也是灵活的,允许高级用户调整它以满足他们的需求。

Jenkins X 的真正力量在于流程、工具的选择以及将一切包裹成一个易于学习和使用的整体粘合剂。我们(在软件行业工作的人)倾向于不断重新发明轮子。我们花费无数小时试图弄清楚如何更快地开发我们的应用程序,以及如何拥有尽可能接近生产环境的本地环境。我们投入时间寻找能够让我们更有效地打包和部署应用程序的工具。我们设计构成持续交付管道的步骤。我们编写脚本来自动化重复性任务。然而,我们无法摆脱这种感觉,我们可能正在重新发明别人已经完成的事情。Jenkins X 设计来帮助我们做出这些决定,并帮助我们选择适合工作的正确工具。它是一系列行业最佳实践的集合。在某些情况下,Jenkins X 是定义这些实践的人,而在其他情况下,它帮助我们采用其他人定义的实践。

如果我们即将开始一个新的项目,Jenkins X 将创建结构和所需的文件。如果我们需要一个包含所有工具、已安装和配置好的 Kubernetes 集群,Jenkins X 会这样做。如果我们需要创建 Git 仓库、设置 webhooks^(3) 和创建持续交付管道,我们只需要执行一个单一的 jx 命令。Jenkins X 所做的事情列表非常庞大,并且每天都在增长。

Jenkins 与 Jenkins X 如果你对 Jenkins 很熟悉,你需要清除你可能已经拥有的任何 Jenkins 经验。当然,Jenkins 是存在的,但它只是整个包的一部分。Jenkins X 与“传统 Jenkins”非常不同。差异如此之大,你唯一接受它的方式就是忘记你所知道的 Jenkins 知识,从头开始。

10.2 探索 Prow、Jenkins X 管道操作员和 Tekton

Jenkins X 的无服务器版本,或者有些人称之为 Jenkins X 新一代,是试图重新定义我们在 Kubernetes 集群内如何进行持续交付和 GitOps。它是通过将相当多的工具组合成一个易于使用的捆绑包来做到这一点的。因此,大多数人不需要理解各个部分独立工作或如何全部集成的复杂性。相反,许多人只需将更改推送到 Git,然后让系统完成剩余的工作。但是,总有那些想了解引擎盖下发生什么的人。为了满足那些渴望洞察的人,我们将探索在无服务器 Jenkins X 平台上涉及的过程和组件。了解由 Git webhook 启动的事件流将让我们了解解决方案是如何工作的,并有助于我们后来在深入研究每个新组件时。

一切从向 Git 仓库的推送开始,这反过来会向集群发送 webhook 请求。与传统 Jenkins 设置的不同之处在于,没有 Jenkins 来接受这些请求。相反,我们有 Prow.^(4) 它做很多事情,但在 webhook 的上下文中,它的任务是接收请求并决定下一步做什么。这些请求不仅限于推送事件,还包括我们可以通过拉取请求注释指定的斜杠命令(例如/approve)。

Prow 由几个不同的组件(deck、hook、crier、tide 等)组成。然而,我们不会深入探讨每个组件的角色。现在,重要的是要注意,Prow 是集群的入口点。它接收由 Git 动作(如推送)或通过评论中的斜杠命令生成的 Git 请求。

在收到请求后,Prow 可能会做很多事情。如果请求来自 Git 评论中的命令,它可能会重新运行测试、合并拉取请求、分配给某个人,或者执行许多其他与 Git 相关的操作。如果 webhook 通知它有新的推送,它将向 Jenkins X 管道操作员发送请求,确保运行与定义的管道对应的构建。最后,Prow 还将构建状态报告回 Git。

图片

图 10.1 工程师将代码和配置推送到 Git。Prow 钩子监听 Git webhook 并分发到插件。

这些功能并不是 Prow 可能执行的所有动作类型,但到目前为止,你可能已经抓住了大概。Prow 负责 Git 和集群内部进程之间的通信。

图片

图 10.2 当 Prow 钩子收到 Git webhook 的请求时,它将其转发到 Jenkins X 管道操作员。

操作员的角色是从启动过程的仓库中获取 jenkins-x.yml 文件,并将其转换为 Tekton 任务和管道。它们反过来定义了在向 Git 推送更改时应执行的整体管道。

Tekton Tekton 是一个 Kubernetes 原生开源框架 CI/CD 系统.^(5)

图片

图 10.3 管道操作员简化了我们的持续交付流程的定义,而 Tekton 为每个项目/仓库定义管道时做繁重的工作。

Tekton 是一个非常底层的解决方案,并不打算直接使用。编写 Tekton 定义可能会非常痛苦且复杂。管道操作员通过易于学习和使用的 YAML 格式简化了定义管道的过程。列表 10.1 是基础管道将提供的内容示例。

注意:正如您将在第 10.3 节中发现的那样,您项目的管道文件将被称为 jenkins-x.yml,其中包含单行“buildPack: go”,以引用以下管道文件。如果您想了解更多关于管道如何工作,请参阅 Jenkins X 文档.^(6)

列表 10.1 http://mng.bz/zx0a

extends:
  import: classic
  file: go/pipeline.yaml
pipelines:
  pullRequest:
    build:
      steps:
      - sh: export VERSION=$PREVIEW_VERSION && skaffold build -f skaffold.yaml
        name: container-build
    postBuild:
      steps:
      - sh: jx step post build --image $DOCKER_REGISTRY/$ORG/$APP_NAME:$PREVIEW_VERSION
        name: post-build
    promote:
      steps:
      - dir: /home/jenkins/go/src/REPLACE_ME_GIT_PROVIDER/REPLACE_ME_ORG/REPLACE_ME_APP_NAME/charts/preview
        steps:
        - sh: make preview
          name: make-preview
        - sh: jx preview --app $APP_NAME --dir ../..
          name: jx-preview

  release:
    build:
      steps:
      - sh: export VERSION=`cat VERSION` && skaffold build -f skaffold.yaml
        name: container-build
      - sh: jx step post build --image $DOCKER_REGISTRY/$ORG/$APP_NAME:\$(cat VERSION)
        name: post-build
    promote:
      steps:
      - dir: /home/jenkins/go/src/REPLACE_ME_GIT_PROVIDER/REPLACE_ME_ORG/REPLACE_ME_APP_NAME/charts/REPLACE_ME_APP_NAME
        steps:
        - sh: jx step changelog --version v\$(cat ../../VERSION)
          name: changelog
        - comment: release the helm chart
          name: helm-release
          sh: jx step helm release
        - comment: promote through all 'Auto' promotion Environments
          sh: jx promote -b --all-auto --timeout 1h --version \$(cat ../../VERSION)
          name: jx-promote

Tekton 为每个推送到相关分支(主分支、PRs)的构建创建一个 PipelineRun。它执行所有我们需要验证推送的步骤。它运行测试,将二进制文件存储在注册表中(Docker 注册表、Nexus 和 ChartMuseum),并将发布部署到临时(PR)或永久阶段或生产环境。

完整流程可以在图 10.4 中看到。

图 10.4 事件完整流程从 PR 开始,经过 Prow 的 webhook,再到管道操作员,最后到 Tekton。Jenkins X 将为每个构建/提交执行管道并部署应用程序。

练习 10.1

哪个组件将接收 Git webhook 请求?哪个组件将协调部署?

10.3 将项目导入 Jenkins X

您可以看到我们如何使用 Jenkins X 快速入门来快速推进新应用程序的开发和持续交付。然而,您的公司可能不是昨天成立的。这意味着您已经有一些应用程序,并且您可能希望将它们迁移到 Jenkins X。

从 Jenkins X 的角度来看,导入现有项目相对简单。我们只需执行 jx import 命令,Jenkins X 就会施展其魔法。它会创建我们需要的文件。如果我们还没有 skaffold.yml,它将为我们生成。如果我们不创建 Helm 图表,它也会创建。没有 Dockerfile?没问题。我们也会得到它。从未为该项目编写过 Jenkins 管道?再次,这不是问题。我们将得到一个自动生成的 jenkins-x.yml 文件。Jenkins X 将重用我们已有的东西,并创建我们缺少的东西。

导入过程不仅限于创建缺失的文件并将它们推送到 Git。它还会在 Jenkins 中创建一个作业,在 GitHub 中创建钩子,以及许多其他事情。

注意:有关如何安装 Jenkins X 的更多信息,请参阅附录 B。

10.3.1 导入项目

我们将导入存储在 gitopsbook/sample-app 仓库中的应用程序。我们将用它作为测试导入过程以及解决可能遇到的问题的试验品。

但是,在我们导入仓库之前,您必须先分叉代码。否则,由于您还不是该特定仓库的协作者(尚不是),您将无法推送更改:

$ open "https://github.com/gitopsbook/sample-app"

确保您已登录,并点击位于右上角的 Fork 按钮。按照屏幕上的说明操作。

接下来,您需要克隆您刚刚分叉的仓库:

$ GH_USER=[...]                                          ❶
$ git clone https://github.com/$GH_USER/sample-app.git
$ cd sample-app

❶ 在执行后续命令之前,请将 [...] 替换为您的 GitHub 用户名。

现在你应该已经在你分叉的仓库的主分支中有了预期的代码。你可以自由地查看我们有什么,只需在浏览器中打开仓库即可。幸运的是,有一个jx命令可以做到这一点:

$ jx repo --batch-mode

在我们将项目导入到 Jenkins X 之前,让我们快速探索一下项目的文件:

$ ls -1
Dockerfile
Makefile
README.md
main.go

如你所见,那个仓库中几乎什么都没有,只有 Go^(7)代码 (*.go)。

这个项目是我们可能想要导入到 Jenkins X 中的可能项目范围的一个极端。它只有应用程序的代码。有一个Dockerfile。然而,没有 Helm 图表或构建二进制的脚本,也没有运行测试的机制,并且肯定没有定义应用程序持续交付管道的jenkins-x.yml文件。这里只有代码,几乎没有其他东西。

这种情况可能并不适用于你。也许你确实有运行测试或构建代码的脚本。或者,你可能已经是 Kubernetes 的重度用户,并且已经有一个 Helm 图表。你可能还有其他文件。我们稍后会讨论这些情况。现在,我们将专注于没有任何东西,只有应用程序代码的情况。

让我们看看当我们尝试将那个仓库导入到 Jenkins X 中会发生什么:

$ jx import
intuitdep954b9:sample-app byuen$ jx import
WARNING: No username defined for the current Git server!
? github.com username: billyy                                         ❶
To be able to create a repository on github.com we need an API Token
Please click this URL and generate a token                            ❷
https://github.com/settings/tokens/new?scopes=repo, read:user,read:org,user:email,write:repo_hook,delete_repo

Then COPY the token and enter it below:

? API Token: ****************************************
performing pack detection in folder /Users/byuen/git/sample-app
--> Draft detected Go (48.306595%)
selected pack: /Users/byuen/.jx/draft/packs/github.com/jenkins-x-buildpacks/jenkins-x-kubernetes/packs/go
replacing placeholders in directory /Users/byuen/git/sample-app
app name: sample-app, git server: github.com, org: billyy, Docker registry org: hazel-charter-283301
skipping directory "/Users/byuen/git/sample-app/.git"
Draft pack go added
? Would you like to define a different preview Namespace? No          ❸
Pushed Git repository to https://github.com/billyy/sample-app.git
Creating GitHub webhook for billyy/sample-app for url http://hook-jx.34.74.32.142.nip.io/hook
Created pull request: https://github.com/billyy/environment-cluster-1-dev/pull/1
Added label updatebot to pull request https://github.com/billyy/environment-cluster-1-dev/pull/1
created pull request https://github.com/billyy/environment-cluster-1-dev/pull/1 on the development 
    git repository https://github.com/billyy/environment-cluster-1-dev.git
regenerated Prow configuration
PipelineActivity for billyy-sample-app-master-1
upserted PipelineResource meta-billyy-sample-app-master-cdxm7 
    for the git repository https://github.com/billyy/sample-app.git
upserted Task meta-billyy-sample-app-master-cdxm7-meta-pipeline-1
upserted Pipeline meta-billyy-sample-app-master-cdxm7-1
created PipelineRun meta-billyy-sample-app-master-cdxm7-1
created PipelineStructure meta-billyy-sample-app-master-cdxm7-1

Watch pipeline activity via:    jx get activity -f sample-app -w
Browse the pipeline log via:    jx get build logs billyy/sample-app/master
You can list the pipelines via: jx get pipelines
When the pipeline is complete:  jx get applications

For more help on available commands see: https://jenkins-x.io/developing/browsing/

Note that your first pipeline may take a few minutes to start while the necessary images get downloaded!

❶ GitHub 用户名

❷ 生成新的令牌

❸ 预览命名空间默认为“否”

从输出中我们可以看到,Jenkins X 检测到项目 100%是用 Go 编写的,因此选择了 Go 构建包。它将构建包应用到本地仓库,并将更改推送到 GitHub。此外,它还创建了一个 Jenkins 项目以及一个 GitHub webhook,每当我们将更改推送到所选分支之一时,它都会触发构建。这些分支默认为主分支、develop、PR-.和 feature.。我们可以通过添加--branches标志来更改模式。但是,就我们的目的而言,以及许多其他情况,这些分支正是我们所需要的。

图片

图 10.5 jx import 添加的文件

现在,让我们再次查看本地仓库副本中的文件:

$ ls -1
Dockerfile
Makefile
OWNERS
OWNERS_ALIASES
README.md
charts
jenkins-x.yml
main.go
skaffold.yaml
watch.sh

我们可以看到,通过导入过程,项目添加了相当多的新文件。我们有一个Dockerfile,它将被用来构建容器镜像,我们还有一个 jenkins-x.yml,它定义了我们管道的所有步骤。

我们还得到了一个 Makefile,它定义了构建、测试和安装应用程序的目标。还有一个包含用于打包、安装和升级我们应用程序的 Helm 格式的文件的 charts 目录。我们还得到了 watch.sh,它监控构建更改并调用 skaffold.yaml。skaffold.yaml 包含构建和发布容器镜像的指令。还有一些其他新文件(例如:OWNERS)被添加到混合中。

现在项目已经在 Jenkins X 中,我们应该将其视为一项活动,并观察第一次构建的实际操作。您已经知道我们可以限制 Jenkins X 活动的检索范围到特定的项目,并且可以使用--watch来监视进度。

注意:在继续教程的其余部分之前,请等待jx promotePullRequest完成。如果过程超过 60 分钟,拉取请求将显示“失败”状态。如果您检查 GitHub,PR 将在jx promote完成后仍然合并。

$ jx get activities --filter sample-app --watch
STEP                                                         STARTED AGO DURATION STATUS
billyy/sample-app/master #1                                  11h31m0s  1h4m20s Succeeded Version: 0.0.1
 meta pipeline                                                1h25m2s      31s Succeeded 
    Credential Initializer                                    1h25m2s       0s Succeeded 
    Working Dir Initializer                                   1h25m2s       1s Succeeded 
    Place Tools                                               1h25m1s       1s Succeeded 
    Git Source Meta Billyy Sample App Master R Xnfl4 Vrvtm    1h25m0s       8s Succeeded https://github.com/billyy/sample-app.git
    Setup Builder Home                                       1h24m52s       0s Succeeded 
    Git Merge                                                1h24m52s       1s Succeeded 
    Merge Pull Refs                                          1h24m51s       1s Succeeded 
    Create Effective Pipeline                                1h24m50s       7s Succeeded 
    Create Tekton Crds                                       1h24m43s      12s Succeeded 
  from build pack                                            1h23m49s 1h13m39s Succeeded 
    Credential Initializer                                   1h23m49s       2s Succeeded 
    Working Dir Initializer                                  1h23m47s       2s Succeeded 
    Place Tools                                              1h23m45s       4s Succeeded 
    Git Source Billyy Sample App Master Releas 658x6 Nzdbp   1h23m41s      21s Succeeded https://github.com/billyy/sample-app.git
    Setup Builder Home                                       1h23m20s       2s Succeeded 
    Git Merge                                                1h23m18s      11s Succeeded 
    Setup Jx Git Credentials                                  1h23m7s      12s Succeeded 
    Build Make Build                                         1h22m55s       1s Succeeded 
    Build Container Build                                    1h22m54s   11m56s Succeeded 
    Build Post Build                                         1h10m58s       4s Succeeded 
    Promote Changelog                                        1h10m54s       6s Succeeded 
    Promote Helm Release                                     1h10m48s       8s Succeeded 
    Promote Jx Promote                                       1h10m40s  1h0m30s Succeeded 
  Promote: staging                                           1h10m31s          Running 
    PullRequest                                              1h10m31s  1h0m21s Failed  PullRequest: https://github.com/billyy/environment-cluster-1-staging/pull/1

图片

图 10.6 Jenkins X 将生成一个拉取请求以添加我们应用程序的新版本。这就是 GitOps 在发挥作用!

管道活动为您提供了关于管道阶段和步骤的大量详细信息。然而,其中最重要的细节之一是合并到预发布环境的 PR。这告诉 Jenkins X 将我们的应用的新版本添加到 env/requirements.yaml 文件中。这就是 GitOps 在发挥作用!

到目前为止,Jenkins X 创建了它需要的文件,它创建了一个 GitHub webhook,它创建了一个管道,并将更改推送到 GitHub。因此,我们得到了第一次构建,从外观上看,它是成功的。但让我们再次检查一切是否正常。

通过点击活动输出中的链接,在浏览器中打开 PullRequest 链接,如图 10.5 所示。

到目前为止,一切顺利。sample-app作业向环境-cluster-1-staging 仓库创建了一个拉取请求。因此,该仓库的 webhook 应该已经启动了一个管道活动,结果应该是在预发布环境中的应用程序的新版本。我们暂时不会深入这个过程。现在,只需注意应用程序应该正在运行,我们很快就会检查这一点。

我们需要确认应用程序确实正在运行的信息在预发布环境中运行的应用程序列表中。我们将在稍后探索环境。现在,只需运行以下命令:

$ jx get applications
APPLICATION STAGING PODS URL
sample-app  0.0.1        http://sample-app-jx-staging.34.74.32.142.nip.io

我们可以在 URL 列中看到我们的应用程序应该通过哪个地址访问。复制它,并在随后的命令中使用它代替[...]:

$ STAGING_ADDR=[...]               ❶
$ curl "$STAGING_ADDR/demo/hello"
Kubernetes ♡ Golang!

❶ 预发布 URL 地址

输出显示Kubernetes ♡ Golang!,从而确认应用程序正在运行,并且我们可以访问它。

在我们继续之前,我们将离开 sample-app 目录。至少从应用程序生命周期角度来看,我们已经达到了最终阶段。

注意:有关完整的应用程序生命周期参考,请参阅第四章(管道)中的图 4.6。

实际上,我们跳过了创建拉取请求,这恰好是 Jenkins X 最重要功能之一。尽管如此,我们没有足够的空间来涵盖所有 Jenkins X 功能,所以我们将把 PR 和其他功能留给你自己探索(PR 可以从 jx get activities 命令的输出中找到)。现在,我们将通过探索推广到生产的过程来关注应用生命周期的最后阶段。我们已经涵盖了以下内容:

  1. 我们看到了如何导入现有项目以及如何创建一个新项目。

  2. 我们看到了如何开发构建包,这将简化那些现有构建包未涵盖或与之偏离的应用类型的流程。

  3. 一旦我们将我们的应用添加到 Jenkins X 中,我们就探索了它是如何通过环境(如预发布和生产环境)实现 GitOps 流程的。

  4. 然后我们进入了应用开发阶段,并探讨了 DevPods 如何帮助我们设置一个个人专用的应用环境,这简化了“传统”的设置,迫使我们花费无数小时在笔记本电脑上设置它,同时,也避免了共享开发环境的陷阱。

  5. 一旦一个功能、变更或错误修复的开发完成,我们就创建了一个拉取请求,执行了自动验证,并将发布候选版本部署到了一个针对 PR 的特定预览环境中,以便我们可以手动检查它。一旦我们对所做的更改感到满意,我们就将其合并到主分支,这导致了部署到设置为接收自动推广的环境(如预发布环境)以及另一轮测试。现在,我们已经对我们的更改感到舒适,剩下要做的就是将我们的发布推广到生产环境。

需要注意的关键点是,将应用推广到生产并不是一个技术决策。当我们到达软件开发生命周期的最后一步时,我们应该已经知道发布是按预期工作的。我们已经收集了所有需要做出上线决策的信息。因此,这个选择与业务相关。“我们希望用户何时看到新版本?”我们知道每个通过管道所有步骤的发布都是生产就绪的,但我们不知道何时将其发布给用户。但是,在我们讨论何时将某个东西发布到生产之前,我们应该决定由谁来执行这个操作。执行者将决定何时是正确的时间。是一个人批准拉取请求,还是由机器来处理?

商业、市场和管理工作可能是由负责将内容升级到生产环境的决策者。在这种情况下,我们不能在代码合并到主分支时(就像在预览环境中一样)启动这个过程,这意味着我们需要一个机制通过命令手动启动这个过程。如果执行命令过于复杂和令人困惑,添加一个按钮应该很简单(我们将在稍后通过 UI 探索这一点)。也可能存在没有人决定将内容升级到生产环境的情况。相反,我们可以自动将每个更改升级到主分支。在这两种情况下,启动升级的命令是相同的。唯一的区别在于执行它的行为者。是我们(人类)还是 Jenkins X(机器)?

目前,我们的生产环境设置为手动升级。因此,我们正在采用完全自动化的持续交付,只需要一个手动操作就可以将版本升级到生产环境。剩下要做的就是点击一个按钮,或者在我们这个案例中,执行一个单独的命令。我们本来可以将升级生产的步骤添加到 Jenkinsfile 中,那样的话,我们就是在实践持续部署(而不是持续交付)。这将导致每次合并或推送到主分支时都会进行部署。但是,我们今天不实践持续部署,我们将坚持当前的设置,并跳到最后一个持续交付的阶段。我们将把我们的最新版本升级到生产环境。

10.3.2 将发布升级到生产环境

现在我们觉得我们的新版本已经准备好投入生产,我们可以将其升级到生产环境。但在我们这样做之前,我们将检查是否已经在生产环境中运行了某些内容:

$ jx get applications --env production
APPLICATION
sample-app

那么,关于预览环境呢?我们必须确保我们的 sample-app 应用程序的发布版本在那里运行。让我们再次检查:

$ jx get applications --env staging
APPLICATION STAGING PODS URL
sample-app  0.0.1   1/1  http://sample-app-jx-staging.34.74.32.142.nip.io

对于我们正在尝试做的事情,重要的信息是 STAGING 列中显示的版本。

图片

图 10.7 jx promote 命令将在生产环境中创建一个新的分支并部署到预览环境。在命令执行结束时,新版本将被升级到生产环境。

现在我们可以将 sample-app 的特定版本升级到生产环境:

$ VERSION=[...]                                                     ❶
$ jx promote sample-app --version $VERSION --env production --batch-mode

❶ 在执行以下命令之前,请确保将 [...] 替换为上一个命令输出中的 STAGING 列中的版本。

升级过程可能需要一分钟左右才能完成。您可以使用以下命令再次运行以监控状态:

$ jx get activities
...
Promote: production                                          4m3s    1m36s  Succeeded
    PullRequest                                              4m3s    1m34s  Succeeded
    PullRequest: https://github.com/billyy/environment-cluster-1-production/pull/2 Merge SHA: 33b48c58b3332d3abc2b0c4dcaba8d7ddc33c4b3
    Update                                                  2m29s       2s  Succeeded
    Promoted                                                2m29s       2s  Succeeded
    Application is at: http://sample-app-jx-production.34.74.32.142.nip.io

我们刚才执行的命令将在生产环境(environment-pisco-sour-production)中创建一个新的分支。接下来,它将遵循与之前所做任何事情相同的基于拉取请求的实践。它将创建一个拉取请求,并等待 Jenkins X 构建完成并成功。你可能会看到错误信息表明它无法查询拉取请求。这是正常的。该过程是异步的,jx会定期查询系统,直到收到确认拉取请求已成功处理的信

一旦处理完拉取请求,它将被合并到主分支,这将启动另一个 Jenkins X 构建。它将运行我们在仓库的Jenkinsfile中定义的所有步骤。默认情况下,这些步骤仅将发布版部署到生产环境,但我们可以添加额外的验证,例如集成或其他类型的测试。一旦合并到主分支的构建完成,我们将在生产环境中运行发布版,最终输出将表明合并状态检查全部通过,所以升级成功了!

手动推广(生产)的过程与我们在自动化推广(预发布)中经历的过程相同。唯一的区别是执行推广的人。自动化的推广是由应用程序管道将更改推送到 Git 中触发的。另一方面,手动推广是由我们(人类)触发的。

接下来,我们将通过检索该环境中所有应用程序来确认发布版确实已部署到生产环境:

$ jx get applications --env production
APPLICATION PRODUCTION PODS URL
sample-app  0.0.1           http://sample-app-jx-production.35.185.219.24.nip.io

在我们的情况下,输出状态表明生产环境中只有一个应用程序(sample-app)正在运行,并且版本号为 0.0.1。

为了安全起见,我们将向运行在生产环境中的应用程序的发布版发送请求:

$ PROD_ADDR=[...]                 ❶
$ curl "$PROD_ADDR/demo/hello"
Kubernetes ♡ Golang!                                

❶ 在执行以下命令之前,请确保将 [...] 替换为上一条命令输出的 URL 列。

摘要

  • Jenkins X 定义了过程、工具的选择以及将一切封装成一个易于学习和使用的统一单元的粘合剂。

  • Prow 以政策执行和自动 PR 合并的形式提供 GitHub 自动化。

  • 管道操作员用于编排和简化我们持续交付过程的定义。

  • Tekton 是一个 Kubernetes 原生开源框架,用于创建持续集成和交付(CI/CD)系统。

  • 要将项目导入 Jenkins X,你只需执行jx import命令,它将为你的仓库添加所有必要的文件,并创建管道和环境。

  • 要将发布版推广到生产环境,你可以简单地执行jx promote命令,该命令将生成 PR 以添加新发布版,部署到预览环境进行测试,并将(部署)到生产环境。


1.jenkins-x.io/.

2.www.cloudbees.com/.

3.developer.github.com/webhooks/

4.github.com/kubernetes/test-infra/tree/master/prow

5.cloud.google.com/tekton

6.jenkins-x.io/docs/reference/components/build-packs/

7.golang.org/

11 Flux

本章涵盖

  • 什么是 Flux?

  • 使用 Flux 部署应用程序

  • 使用 Flux 设置多租户

在本章中,您将学习如何使用 Flux GitOps 操作员将我们的参考示例应用程序部署到 Kubernetes。您还将了解 Flux 如何作为多租户解决方案的一部分使用。

我们建议您在阅读本章之前阅读第 1、2、3 和 5 章。

11.1 什么是 Flux?

Flux 是一个开源项目,它实现了基于 GitOps 的 Kubernetes 持续部署。该项目始于 2016 年 Weaveworks,三年后加入了 CNCF 沙箱。

值得注意的是,Weaveworks 是提出GitOps术语的公司。该公司还制定了 Kubernetes 的 GitOps 最佳实践,并为 GitOps 的推广做出了大量贡献。Flux 的发展历程说明了 GitOps 理念如何基于实践经验随着时间的推移演变到当前的形式。

Flux 项目旨在自动化容器镜像交付到 Kubernetes,并填补持续集成和持续部署过程之间的差距。项目介绍博客中描述的工作流程专注于 Docker 注册扫描、计算最新镜像版本并将其提升到生产集群。经过多次迭代,Flux 团队意识到了以 Git 为中心的方法的所有优势。在发布 v1.0 版本之前,项目架构被重新设计,使用 Git 作为事实来源,并制定了 GitOps 工作流程的主要阶段。

11.1.1 Flux 做什么

Flux 专注于将清单自动交付到 Kubernetes 集群。该项目可能是本书中描述的操作员中最少有偏见的 GitOps 操作。Flux 不在 Kubernetes 之上引入任何额外的层,例如应用程序或其自己的访问控制系统。单个 Flux 实例管理一个 Kubernetes 集群,并要求用户维护一个代表集群状态的 Git 仓库。Flux 通常在管理的集群内部运行,并依赖于 Kubernetes RBAC。这种方法显著简化了 Flux 配置,并有助于降低学习曲线。

RBAC Kubernetes 支持基于角色的访问控制(RBAC),它允许容器绑定到赋予它们操作各种资源权限的角色。

Flux 的简单性也使得它几乎无需维护,并且易于集成到集群引导过程中,因为不需要新的组件或管理员权限。使用 Flux 命令行界面,Flux 部署可以轻松地集成到集群配置脚本中,以实现集群自动创建。

Flux 不仅限于集群引导。它作为应用程序的持续部署工具被成功使用。在多租户环境中,每个团队都可以安装一个具有有限访问权限的 Flux 实例,并使用它来管理单个命名空间。这完全赋予了团队管理应用程序命名空间中资源的能力,并且仍然 100%安全,因为 Flux 访问由 Kubernetes RBAC 管理。

项目的简单性带来了优点和缺点,不同团队有不同的看法。最重要的考虑因素之一是 Flux 必须由 Kubernetes 最终用户配置和维护。这意味着团队获得了更多权力,但也承担了更多责任。另一种方法,即 Argo CD 所采用的方法,是作为一项服务提供 GitOps 功能。

11.1.2 Docker 仓库扫描

除了 GitOps 的核心功能外,该项目还提供了一项显著的功能。Flux 能够扫描 Docker 仓库,并在新标签推送到仓库时自动更新部署仓库中的镜像。尽管这项功能不是 GitOps 的核心功能,但它简化了开发者的工作,提高了生产力。让我们考虑在没有自动化部署仓库更新的开发者工作流程。

图片

图 11.1 开发者使用持续集成工具手动推送新镜像,然后使用新镜像的标签更新部署 Git 仓库。Flux 注意到 Git 中的清单变更并将其传播到 Kubernetes 集群。

开发团队经常抱怨第二步,因为它需要手动操作,所以他们试图自动化它。通常的解决方案是使用 CI 流水线自动化清单更新。CI 方法解决了问题,但需要脚本编写,可能不够稳定。

Flux 走得更远一步,自动化了部署仓库的更新。你不必使用 CI 系统和脚本,可以配置 Flux 在每次新镜像推送到 Docker 仓库时自动更新部署仓库。自动化 Docker 仓库扫描的开发者工作流程在图 11.2 中表示。

图 11.2 当启用自动化仓库更新时,Flux 完全控制部署仓库和 Kubernetes 集群管理。

图片

开发者的唯一责任是进行代码更改,并让 CI 系统将更新的 Docker 镜像推送到仓库。如果镜像标签遵循语义版本控制约定,自动化的部署仓库管理特别有用。

语义版本控制 语义版本控制^(2) 是一种使用三部分版本号(主版本、次版本和修订版)来指定兼容性的正式约定。

Flux 允许配置利用语义版本约定进行图像标签过滤。典型用例是自动化次要和补丁版本,这些版本应该是安全且向后兼容的,而主要版本则需要手动部署。

与使用持续集成管道相比,Docker 仓库扫描功能的明显好处是你不需要花费时间在你的管道中实现仓库更新步骤。然而,这种便利性伴随着更多的责任。将部署仓库更新纳入持续集成管道提供了完全的控制权,并允许我们在将镜像推送到 Docker 仓库后运行更多测试。如果启用了 Flux Docker 仓库扫描,你必须确保在将其推送到 Docker 仓库之前对镜像进行了充分的测试,以避免意外部署到生产环境。

练习 11.1

考虑 Docker 仓库监控功能的优缺点,并尝试决定它是否适合你的团队。

11.1.3 架构

Flux 只由两个组件组成:Flux 守护进程和键值存储 Memcached.^(3)

Memcached Memcached 是一个开源的、高性能的、分布式内存对象缓存系统。

Flux 架构在图 11.3 中展示。

图片

图 11.3 Flux 守护进程是负责 Flux 大多数功能的主要组件。它克隆 Git 仓库,生成清单,将更改传播到 Kubernetes 集群,并扫描 Docker 仓库。

在任何时候,Flux 守护进程只能有一个副本运行。然而,这并不是一个问题,因为即使守护进程在部署过程中崩溃,它也会快速且幂等地重新启动并继续部署过程。

Memcached 的主要目的是支持 Docker 仓库扫描。Flux 使用它来存储每个 Docker 图像的可用图像版本列表。Memcached 部署是一个可选组件,除非你想使用 Docker 仓库扫描功能,否则不需要。要移除它,只需在安装步骤中使用 --registry-disable-scanning 标志。

练习 11.2

你应该检查哪个组件的日志来排查部署问题?

11.2 简单应用部署

我们已经对 Flux 了解了很多,现在是时候看到它在实际中的应用了。首先,我们需要让它运行起来。Flux 安装包括两个步骤:安装 Flux CLI 和配置集群中的守护进程。使用附录 B 学习如何安装 fluxctl 并准备部署你的第一个应用。

11.2.1 部署第一个应用

fluxctlminikube 应用程序是启动使用 Flux 管理 Kubernetes 资源所需的唯一两个组件。下一步是准备包含 Kubernetes 清单的 Git 仓库。我们示例应用的清单可在以下链接找到:

github.com/gitopsbook/sample-app-deployment

请继续创建一个仓库分支^(4) 作为第一步。Flux 需要部署仓库的写入权限以自动更新清单中的图像标签。

重置您的分支 在您之前章节工作时,您是否已经为部署仓库创建了分支?请确保撤销更改以获得最佳体验。最简单的方法是删除之前创建的分支仓库,然后再次创建。

使用 fluxctl 安装和配置 Flux 守护进程:

$ kubectl create ns flux
$ export GHUSER="YOURUSER"
$ fluxctl install \
--git-user=${GHUSER} \
--git-email=${GHUSER}@users.noreply.github.com \
--git-url=git@github.com:${GHUSER}/sample-app-deployment.git \
--git-path=. \
--namespace=flux | kubectl apply -f -

此命令创建 Flux 守护进程并将其配置为从您的 Git 仓库部署清单。请确保使用以下命令运行 Flux 守护进程:

$ kubectl rollout status deploy flux -n flux

作为本教程的一部分,我们将尝试自动仓库更新功能,因此我们需要给予 Flux 仓库写入访问权限。向 GitHub 仓库提供写入访问权限的方便且安全的方式是使用部署密钥。

部署密钥 部署密钥是存储在您的服务器上并授予对单个 GitHub 仓库访问权限的 SSH 密钥。

无需手动生成新的 SSH 密钥。Flux 在首次启动时生成密钥,并使用它来访问部署仓库。运行以下命令以获取生成的 SSH 密钥:

$ fluxctl identity --k8s-fwd-ns flux

导航到 github.com/<username>/sample-app-deployment/settings/keys /新,并使用 fluxctl 身份命令的输出创建一个新的部署密钥。请确保勾选“允许写入访问”复选框以提供对仓库的写入访问权限。

配置已完成!当您阅读此内容时,Flux 应该正在克隆仓库并部署清单。请继续检查 Flux 守护进程日志以确认。

您在日志中看到 kubectl apply 吗?

$ kubectl logs deploy/flux -n flux -f
caller=sync.go:73 component=daemon info="trying to sync git changes to the cluster" 
    old=6df71c4af912e2fc6f5fec5d911ac6ad0cd4529a new=1c51492fb70d9bdd2381ff2f4f4dc51240dfe118
caller=sync.go:539 method=Sync cmd=apply args= count=2
caller=sync.go:605 method=Sync cmd="kubectl apply -f -" took=1.224619981s err=null 
    output="service/sample-app configured\ndeployment.apps/sample-app configured"
caller=daemon.go:701 component=daemon event="Sync: 1c51492, default:service/sample-app" logupstream=false

太好了,这意味着 Flux 成功执行了部署。接下来,运行以下命令以确认已创建样本应用程序部署资源:

$ kubectl get deploy sample-app -n default

恭喜!您已成功使用 Flux 部署了您的第一个应用程序。

11.2.2 观察应用程序状态

阅读 Flux 守护进程的日志并不是获取 Flux 管理的资源信息的唯一方法。fluxctl CLI 提供了一组命令,允许我们获取有关集群资源的详细信息。我们应该尝试的第一个命令是 fluxctl list-workloads。该命令打印有关集群中管理 Pods 的所有 Kubernetes 资源的信息。运行以下命令以输出有关 sample-app 部署的信息:

$ fluxctl list-workloads --k8s-fwd-ns flux
WORKLOAD               CONTAINER   IMAGE                       RELEASE
deployment/sample-app  sample-app  gitopsbook/sample-app:v0.1  ready

如您从输出中看到的,Flux 正在管理一个部署,该部署使用 gitopsbook/sample-app 图像的 v0.1 版本创建了一个 sample-app 容器。

除了当前图像的信息外,Flux 还扫描了 Docker 仓库并收集了所有可用的图像标签。运行以下命令以打印发现的图像标签列表:

$ fluxctl list-images --k8s-fwd-ns flux -w default:deployment/sample-app
WORKLOAD              CONTAINER  IMAGE                  CREATED
deployment/sample-app sample-app gitopsbook/sample-app
                                           |   v0.2     27 Jan 20 05:46 UTC
                                           '-> v0.1     27 Jan 20 05:35 UTC

从命令输出中,我们可以看到 Flux 正确地发现了两个可用的镜像版本。此外,Flux 将 v0.2 识别为较新版本,并准备好升级我们的部署,如果我们配置了自动升级。让我们继续这样做。

11.2.3 升级部署镜像

默认情况下,Flux 不会升级资源镜像版本,除非资源具有 fluxcd.io/automated: 'true' 注解。这个注解告诉 Flux 资源镜像是自动管理的,并且一旦将新版本推送到 Docker 仓库,镜像就应该升级。以下列表包含应用了注解的 sample-app 部署清单。

列表 11.1 deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  annotations:
    fluxcd.io/automated: 'true'                ❶
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - command:
        - /app/sample-app
        image: gitopsbook/sample-app:v0.1      ❷
        name: sample-app
        ports:
        - containerPort: 8080

❶ 启用自动化管理的注解

❷ 部署镜像标签

添加注解的一种方法是通过手动编辑 deployment.yaml 文件并将其提交到部署存储库。在下一个协调周期中,Flux 应该会检测到注解并启用自动化管理。fluxctl 提供了方便的命令 automatedeautomate,可以为你添加或删除注解。运行以下命令来自动化 sample-app 部署管理:

$ fluxctl automate --k8s-fwd-ns flux -w default:deployment/sample-app
WORKLOAD                       STATUS   UPDATES
default:deployment/sample-app  success
Commit pushed:          <commit-sha>

该命令更新清单并将更改推送到 Git 存储库。如果你使用 GitHub 检查存储库历史记录,你会看到两个提交,第一个提交更新了部署注解,第二个更新了镜像版本。

最后,让我们使用 fluxctllist-workloads 命令来验证部署状态:

$ fluxctl list-workloads --k8s-fwd-ns flux
WORKLOAD               CONTAINER   IMAGE                       RELEASE  
deployment/sample-app  sample-app  gitopsbook/sample-app:v0.2  ready    

部署镜像已成功更新为使用 gitopsbook/sample-app 镜像的 v0.2 版本。别忘了将 Flux 执行的更改拉取到本地 Git 仓库中:

$ git pull

11.2.4 使用 Kustomize 生成清单

在部署存储库中管理纯 YAML 文件并不是一个很难的任务,但在现实生活中也不是非常实用。正如我们在前面的章节中学到的,维护应用程序的基础清单集并使用 Kustomize 或 Helm 等工具生成环境特定的清单是一种常见的做法。与配置管理工具的集成解决了这个问题,Flux 通过生成器实现了这一功能。让我们学习生成器是什么以及如何使用它们。

与为所选的配置管理工具集提供一级支持不同,Flux 提供了配置清单生成过程的能力,并能够与任何配置管理工具集成。生成器是一个命令,它在 Flux 守护进程内部调用配置管理工具,生成最终的 YAML 文件。生成器配置在存储在部署清单存储库中的名为 .flux.yaml 的文件中。

让我们深入探讨这个特性,并通过一个真实示例了解配置细节。首先,我们需要在 Flux 部署中启用清单生成。这是通过 Flux 守护进程的--manifest-generation CLI 标志来完成的。运行以下命令,使用 JSON 补丁将标志注入到 Flux 部署中:

kubectl patch deploy flux --type json -n flux -p \
'[{ "op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--manifest-generation"}]'

JSON 补丁 JSON 补丁^(5) 是描述对 JSON 文档更改的格式。补丁文档是一个操作序列列表,这些操作应用于 JSON 对象,允许进行添加、删除和替换等更改。

一旦 Flux 配置更新,就是时候将 Kustomize 引入我们的部署仓库并开始利用它了。使用以下代码添加 kustomization.yaml 文件。

列表 11.2 kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:                       ❶
- deployment.yaml
- service.yaml

images:
- name: gitopsbook/sample-app    ❷
  newTag: v0.1

❶ 包括资源清单在内的清单列表

❷ 更改镜像标签的转换器

下一步是配置一个使用 Kustomize 的生成器。将以下.flux.yaml 文件添加到sample-app-deployment仓库中:

列表 11.3 flux.yaml

version: 1
patchUpdated:
  generators:                    ❶
  - command: kustomize build .   ❷
  patchFile: flux-patch.yaml     ❸

❶ 生成器列表

❷ 利用 Kustomize 生成清单的生成器命令

❸ 存储清单修改的文件名;对于自动镜像更新是必需的

配置已完成。继续将更改推送到部署仓库:

$ git add -A
$ git commit -am "Introduce kustomize" && git push

让我们再次查看.flux.yaml,并详细了解配置了什么。生成器部分配置 Flux 使用 Kustomize 进行清单生成。您可以在本地运行完全相同的命令来验证 Kustomize 是否生成了预期的 YAML 清单。

patchFile属性是什么?这是一个更新器配置。为了演示它是如何工作的,让我们使用以下命令触发 Flux 的发布:

$ kubectl patch deploy sample-app -p '[{ "op": "add", "path": "/spec/template/spec/containers/0/image", "value": "gitopsbook/sample-app:v0.1"}]' --type json -n default

$ fluxctl sync --k8s-fwd-ns flux

我们已将sample-app部署降级回v0.1版本,并要求 Flux 修复它。sync命令启动了协调循环,一旦完成,应该更新镜像标签并将更改推回 Git 仓库。由于现在清单是通过 Kustomize 生成的,Flux 不再知道哪个文件需要更新。patchFile属性指定了部署仓库中存储镜像标签更新的文件路径。该文件包含自动应用到生成器输出的 JSON 合并补丁。

JSON 合并补丁 JSON 合并补丁是一个描述对目标 JSON 进行更改的 JSON 文档,它包含目标文档的节点,在应用补丁后,这些节点应该不同。

生成的合并补丁包括管理资源镜像的更改。在同步过程中,Flux 生成并推送到 Git 仓库的文件包含合并补丁,并实时应用到生成的 YAML 清单上。

不要忘记将 Flux 执行的改变拉取到本地 Git 仓库中:

$ git pull

11.2.5 使用 GPG 保护部署

Flux 是一个非常实用的工具,专注于现实生活中的用例。部署更改验证就是其中之一。正如我们在第六章中学到的,部署存储库中的提交应该使用 GPG 密钥签署和验证,以确保提交的作者身份,防止未经授权的更改被推送到集群。

将 GPG 验证集成到持续集成管道中的典型方法是将 GPG 验证集成到持续集成管道中。Flux 提供了这种集成,无需额外操作,这节省了时间并提供了更稳健的实现。了解该功能如何操作的最佳方式是尝试它。

首先,我们需要一个有效的 GPG 密钥,它可以用来签署和验证 Git 提交。如果您已经完成了第六章的教程,那么您已经有一个 GPG 密钥并且可以签署提交。否则,使用附录 C 中描述的步骤创建 GPC 密钥。配置 GPG 密钥后,我们需要使其对 Flux 可用并启用提交验证。

为了验证提交,Flux 需要访问我们信任的哪个 GPG 密钥。密钥可以通过 ConfigMap 进行配置。使用以下命令创建 ConfigMap 并将您的公钥存储在其中:

$ kubectl create configmap flux-gpg-public-keys -n flux --from-literal=author.asc="$(gpg --export --armor <ID>)"

下一步是更新 Flux 部署以启用提交验证。更新以下列表中所示的 flux-deployment-patch.yaml 文件中的用户名。

列表 11.4 flux-deployment-patch.yaml

spec:
  template:
    spec:
      volumes:
      - name: gpg-public-keys                                 ❶
        configMap:
          name: flux-gpg-public-keys
          defaultMode: 0400
      containers:
      - name: flux
        volumeMounts:                                         ❷
        - name: gpg-public-keys
          mountPath: /root/gpg-public-keys
          readOnly: true
        args:
        - --memcached-service=
        - --ssh-keygen-dir=/var/fluxd/keygen
        - --git-url=git@github.com:<USERNAME>/sample-app-deployment.git
        - --git-branch=master
        - --git-path=.
        - --git-label=flux
        - --git-email=<USERNAME>@users.noreply.github.com
        - --manifest-generation=true
        - --listen-metrics=:3031
        - --git-gpg-key-import=/root/gpg-public-keys          ❸
        - --git-verify-signatures                             ❹
        - --git-verify-signatures-mode=first-parent           ❺

❶ 使用 ConfigMap 作为数据源的 Kubernetes 卷

❷ 将 ConfigMap 密钥存储在 /root/gpg-public-keys 目录的卷挂载

❸ --git-gpg-key-import 参数指定了受信任 GPG 密钥的位置。

❹ --git-verify-signatures 参数启用提交验证。

❺ --git-verify-signatures-modes=first-parent 参数允许在仓库历史中存在未签署的提交。

使用以下命令应用 Flux 部署修改:

$ kubectl patch deploy flux -n flux -p "$(cat ./flux-deployment-patch.yaml)"

提交验证现在已启用。为了证明它正在工作,尝试使用 fluxctl sync 命令触发同步:

$ fluxctl sync --k8s-fwd-ns flux
Synchronizing with ssh://git@github.com/<USERNAME>/sample-app-deployment.git
Failed to complete sync job
Error: verifying tag flux: no signature found, full output:
 error: no signature found

Run 'fluxctl sync --help' for usage.

命令如预期失败,因为部署存储库中最新的提交未签署。让我们继续修复它。首先使用此命令创建一个空的已签署 Git 提交,然后再次同步:

$ git commit --allow-empty -S -m "verified commit"
$ git push

下一步是签署由 Flux 维护的同步标签:

$ git tag --force --local-user=<GPG-KEY_ID> -a -m "Sync pointer" flux HEAD
$ git push --tags --force

仓库已成功同步。最后,使用 fluxctl sync 命令确认验证配置正确:

fluxctl sync --k8s-fwd-ns flux
Synchronizing with ssh://git@github.com/<USERNAME>/sample-app-deployment.git
Revision of master to apply is f20ac6e
Waiting for f20ac6e to be applied ...
Done.

11.3 使用 Flux 的多租户

Flux 是一个强大且灵活的工具,但它没有专门为多租户构建的功能。所以问题是,我们能否在一个拥有多个团队的大型组织中使用它?答案是绝对可以。Flux 赞同“Git PUSH 全部”的理念,并依靠 GitOps 来管理在多租户集群中部署的多个 Flux 实例。

在多租户集群中,集群用户只有有限的命名空间访问权限,不能创建新的命名空间或任何其他集群级资源。每个团队拥有自己的命名空间资源,并独立于其他团队进行操作。在这种情况下,强迫每个人都使用单个 Git 仓库并依赖基础设施团队审查每个配置更改是没有意义的。另一方面,基础设施团队负责集群的整体健康,需要工具来管理集群服务。应用团队仍然可以依赖 Flux 来管理应用资源。基础设施团队使用 Flux 提供命名空间以及配置了适当命名空间级访问权限的多个 Flux 实例。图 11.4 展示了这一概念。

图 11.4 集群有一个“控制平面”命名空间和一个由基础设施团队管理的集中式集群 Git 仓库。集中式仓库包含表示集中式 Flux 部署和特定团队命名空间以及 Flux 配置的清单。

├── infra
│      └── flux                   ❶
├── cluster
│      ├── team1
│      │      ├── ...
│      │      ├── namespace.yaml     ❷
│      │      └── flux.yaml          ❸
│      └── team2
│              ├── ...
│              ├── namespace.yaml
│              └── flux.yaml

❶ 集中式 Flux 部署,为特定团队提供命名空间

❷ 特定团队的命名空间清单

❸ 特定团队的 Flux 部署

应用团队可以通过向集中式仓库创建拉取请求并添加命名空间和 Flux 清单来自主加入。一旦拉取请求被合并,中央 Flux 就会创建命名空间并为特定团队提供 Flux,确保正确的 RBAC 设置。

特定团队的 Flux 实例配置为从由应用团队管理的单独 Git 仓库中拉取清单。这意味着应用团队完全独立,无需涉及基础设施团队来更新其命名空间内的资源。

摘要

  • Flux 易于安装和维护,因为 Flux 不需要新的组件,并使用 Kubernetes RBAC 进行访问控制。

  • Flux 可以通过自动仓库更新配置来自动部署新镜像。

  • 由于 Flux 直接与 Git 或 Docker 注册库接口,Flux 消除了在 CI 管道中部署时进行自定义集成的需求。

  • Flux 附带 CLI 工具fluxctl,用于 Flux 的安装和部署应用。

  • Flux 不自带清单生成工具,但可以通过简单的配置轻松集成 Kustomize 等工具。

  • Flux 可以通过简单的配置轻松集成 GPG 以实现安全的部署。

  • Flux 可以通过集中提供命名空间(带有访问控制和命名空间特定的 Flux 实例)进行多租户配置。


1.www.weave.works/.

2.semver.org/.

3.memcached.org/.

4.help.github.com/en/github/getting-started-with-github/fork-a-repo.

5.jsonpatch.com/.

封底内页

图片

附录 A. 设置测试 Kubernetes 集群

一个完全具备生产能力的 Kubernetes 集群是一个非常复杂的系统,由多个组件组成,这些组件必须根据你的特定需求进行安装和配置。如何在生产中部署和维护 Kubernetes 远远超出了本书的焦点,并在其他地方进行了介绍。

幸运的是,有几个项目可以处理配置复杂性,并允许使用单个 CLI 命令在本地运行 Kubernetes。在笔记本电脑上本地运行 Kubernetes 对于熟悉 Kubernetes 并为你完成本书中的练习做好准备非常有用。尽可能多的练习都将利用在笔记本电脑上运行的 minikube 集群。然而,如果你更喜欢使用自己运行的集群(无论是在云服务提供商上还是在本地),练习同样可以在那里进行。

minikube Minikube 是 Kubernetes 社区维护的官方工具,可以在你的笔记本电脑上的虚拟机内部创建单个节点的 Kubernetes 集群。它支持 macOS、Linux 和 Windows。除了实际运行集群外,minikube 还提供简化访问 Kubernetes 内部服务、卷管理等功能。

你可以考虑使用的其他项目

  • Docker for desktop—如果你在你的笔记本电脑上使用 Docker,你可能已经安装了 Kubernetes!从版本 18.6.0 开始,Windows 和 Docker Desktop for Mac 都自带了捆绑的 Kubernetes 二进制文件和开发者生产力功能。

  • K3s—正如其名所示,K3s 是一个轻量级的 Kubernetes 部署。根据其作者的描述,K3s 比八少五,所以 K8s 减去五就是 K3s。除了有趣的名字外,K3s 确实非常轻量级、快速,如果你需要将 Kubernetes 作为 CI 作业的一部分或在资源有限的硬件上运行,它是一个很好的选择。安装说明可在k3s.io.找到。

  • Kind—另一个由 Kubernetes 社区开发的工具,kind 是为 Kubernetes v1.11+兼容性测试而开发的。安装说明可在kind.sigs.k8s.io/找到。

虽然这些工具简化了 Kubernetes 的部署到一个 CLI 命令,并提供了很好的体验,但 minikube 仍然是开始使用 Kubernetes 的最安全选择。得益于虚拟化支持的所有平台,以及一套出色的开发者生产力功能,这是一个适合初学者和专家的出色工具。本书中的所有练习和示例都依赖于 minikube。

A.1 与 Kubernetes 一起工作的先决条件

与 Kubernetes 一起工作所需的工具和实用程序。

除了 Kubernetes 本身,你还需要安装 kubectl。kubectl 是一个命令行工具,允许与 Kubernetes 控制平面交互,并几乎可以执行与 Kubernetes 相关的任何操作。

A.1.1 配置 kubectl

要开始,请按照 kubernetes.io/docs/tasks/tools/install-kubectl/ 中的说明安装 minikube。如果你是 macOS 或 Linux 用户,你可以使用 Homebrew 软件包管理器和以下命令一步完成安装过程:

$ brew install kubectl

Homebrew Homebrew 是一个免费的开源软件包管理系统,它简化了在 Apple 的 macOS 操作系统和 Linux 上的软件安装。更多信息可在 brew.sh/ 找到。

A.2 安装 minikube 和创建集群

Minikube 是一个应用程序,允许你在桌面或笔记本电脑上运行单个节点的 Kubernetes 集群。安装说明可在 kubernetes.io/docs/tasks/tools/install-minikube/ 找到。

本书中的大多数(但并非全部)练习都可以使用本地 minikube 集群完成。

A.2.1 配置 minikube

下一步是安装并启动 minikube 集群。安装过程在 minikube.sigs.k8s.io/docs/start/ 中描述。minikube 软件包也通过 Homebrew 提供:

$ brew install minikube

如果到目前为止一切顺利,我们就准备好启动 minikube 并配置我们的第一个部署:

$ minikube start 
minikube/default)
😁  minikube v1.1.1 on darwin (amd64)
🔥  Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
🐳  Configuring environment for Kubernetes v1.14.3 on Docker 18.09.6
🚜  Pulling images ...
🚀  Launching Kubernetes ...
⏳  Verifying: apiserver proxy etcd scheduler controller dns
🏄  Done! kubectl is now configured to use "minikube"

A.3 在 GCP 中创建 GKE 集群

Google Cloud Platform (GCP) 提供了 Google Kubernetes Engine (GKE) 作为其免费层的一部分:

cloud.google.com/free/

你可以创建一个 Kubernetes GKE 集群来运行本书中的练习:

cloud.google.com/kubernetes-engine/

请记住,尽管 GKE 本身是免费的,但你可能会因为 Kubernetes 创建的其他 GCP 资源而被收费。建议你在完成每个练习后删除测试集群,以避免意外费用。

A.4 在 AWS 中创建 EKS 集群

Amazon Web Services (AWS) 提供了一个名为 Elastic Kubernetes Service (EKS) 的托管 Kubernetes 服务。你可以创建一个免费的 AWS 账户并创建一个 EKS 集群来运行本书中的练习。然而,尽管相对便宜,EKS 并非免费服务(在撰写本文时每小时收费 0.20 美元),你可能会因为 Kubernetes 创建的其他资源而被收费。建议你在完成每个练习后删除测试集群,以避免意外费用。

Weaveworks 有一个名为 eksctl 的工具,允许你轻松地在 AWS 账户中创建 EKS Kubernetes 集群:

github.com/weaveworks/eksctl/blob/master/README.md

附录 B. 设置 GitOps 工具

本附录将逐步说明设置第三部分教程所需工具的步骤。

B.1 安装 Argo CD

Argo CD 支持多种安装方法。您可能使用基于 Kustomize 的官方安装、社区维护的 Helm 图表^(1),甚至 Argo CD 操作员^(2) 来管理您的 Argo CD 部署。最简单的安装方法只需要使用单个 YAML 文件。请继续使用以下命令将 Argo CD 安装到您的 minikube 集群中:

$ kubectl create namespace argocd
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

此命令使用适用于大多数用户的默认设置安装所有 Argo CD 组件。出于安全原因,Argo CD UI 和 API 默认情况下不会在集群外部暴露。在 minikube 上完全打开访问权限是绝对安全的。

通过在单独的终端中运行以下命令,在您的 minikube 集群中启用负载均衡器访问^(3):

$ minikube tunnel

使用以下命令打开对 argocd-server 服务的访问并获取访问 URL:

$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'

Argo CD 提供了基于 Web 的用户界面和命令行界面 (CLI)。为了简化说明,我们将在此教程中使用 CLI 工具。让我们继续安装 CLI 工具。您可以使用以下命令在 Mac 上安装 Argo CD CLI,或者遵循官方的入门指南^(4) 在您的平台上安装 CLI:

$ brew tap argoproj/tap
$ brew install argoproj/tap/argocd

一旦安装了 Argo CD,它就有一个预配置的管理员用户。初始管理员密码是自动生成的,是 Argo CD API 服务器 Pod 名称,可以使用此命令检索:

$ kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2

使用以下命令获取 Argo CD 服务器 URL 并使用 Argo CD CLI 更新生成的密码:

$ argocd login <ARGOCD_SERVER-HOSTNAME>:<PORT>
$ argocd account update-password

<ARGOCD_SERVER-HOSTNAME>:<PORT> 是 minikube API 和服务端口,应从 Argo CD URL 获取。您可以使用以下命令检索 URL:

minikube service argocd-server -n argocd --url

命令返回 HTTP 服务 URL。请确保删除 http://,并仅使用主机名和端口号使用 Argo CD CLI 登录。

最后,登录到 Argo CD 用户界面。请在浏览器中打开 Argo CD URL,并使用管理员用户名和您的密码登录。您现在可以开始了!

B.2 安装 Jenkins X

Jenkins X CLI 依赖于 kubectl ^(5) 和 Helm^(6),并将尽力安装这些工具。然而,我们笔记本电脑上可能存在的所有可能排列组合接近无限,因此您最好自己安装这些工具。

注意:在撰写本文时,2021 年 2 月,Jenkins X 还不支持 Helm v3+。请确保您正在使用 Helm CLI v2+。

B.2.1 前提条件

您可以使用(几乎)任何 Kubernetes 集群,但它需要是公开可访问的。这样做的主要原因在于 GitHub 触发器。Jenkins X 严重依赖于 GitOps 原则。大部分事件将由 GitHub webhook 触发。如果您的集群无法从 GitHub 访问,您将无法触发这些事件,并且您将难以跟随示例进行操作。

现在,这提出了两个重大问题。您可能更喜欢在本地使用 minikube 或 Docker for Desktop 进行练习,但这两个工具都无法从您的笔记本电脑外部访问。您可能有一个无法从外部访问的企业集群。在这些情况下,我们建议您使用 AWS、GCP 或其他地方的服务。最后,我们将使用命令hub执行一些 GitHub 操作。如果您还没有安装,请安装它。

注意请参考附录 A 以获取有关配置 AWS 或 GCP Kubernetes 集群的更多信息。

为了您的方便,我们将使用的所有工具列表如下:

  • Git

  • kubectl

  • Helm^(7)

  • AWS CLI

  • eksctl^(8)(如果使用 AWS EKS)

  • gcloud(如果使用 Google GKE)

  • hub^(9)

现在,让我们安装 Jenkins X CLI:

$ brew tap jenkins-x/jx
$ brew install jx

B.2.2 在 Kubernetes 集群中安装 Jenkins X

我们如何以比我们通常安装软件更好的方式安装 Jenkins X?Jenkins X 的配置应该被定义为代码并存储在 Git 仓库中,这正是社区为我们创建的。它维护一个 GitHub 仓库,其中包含 Jenkins X 平台定义的结构,以及一个将安装它的管道,以及一个我们可以用它来调整以满足特定需求的 requirements 文件。

注意您还可以参考 Jenkins X 网站^(10)来了解如何在您的 Kubernetes 集群中设置 Jenkins X。

让我们看看这个仓库:

$ open "https://github.com/jenkins-x/jenkins-x-boot-config.git"

一旦您在浏览器中看到仓库,您首先需要在您的 GitHub 账户下创建一个分支。我们稍后会探索仓库中的文件。

接下来,我们将定义一个变量CLUSTER_NAME,它将,正如您所猜到的,保存我们刚刚创建的集群的名称。在随后的命令中,请将[...]的第一个出现替换为集群名称,第二个替换为您的 GitHub 用户:

$ export CLUSTER_NAME=[...]
$ export GH_USER=[...]

在我们分支 boot 仓库并且知道我们的集群名称后,我们可以使用一个合适的名称克隆仓库,该名称将反映我们即将安装的 Jenkins X 的命名方案:

$ git clone \
    https://github.com/$GH_USER/jenkins-x-boot-config.git \
    environment-$CLUSTER_NAME-dev

包含(几乎)所有可用于自定义设置的参数的关键文件是 jx-requirements.yml。让我们看看它:

$ cd environment-$CLUSTER_NAME-dev
$ cat jx-requirements.yml
cluster:
  clusterName: ""
  environmentGitOwner: ""
  project: ""
  provider: gke
  zone: ""
gitops: true
environments:
- key: dev
- key: staging
- key: production
ingress:
  domain: ""
  externalDNS: false
  tls:
    email: ""
    enabled: false
    production: false
kaniko: true
secretStorage: local
storage:
  logs:
    enabled: false
    url: ""
  reports:
    enabled: false
    url: ""
  repository:
    enabled: false
    url: ""
versionStream:
  ref: "master"
  url: https://github.com/jenkins-x/jenkins-x-versions.git
webhook: prow

正如您所看到的,该文件包含的值格式类似于与 Helm 图表一起使用的 requirements.yaml 文件。它被分为几个部分。

首先,有一组值定义了我们的集群。您应该能够通过查看其中的变量来了解它代表什么。您可能不会花费超过几分钟的时间就能看出,我们至少需要更改其中的一些值,所以这就是我们接下来要做的。

在您最喜欢的编辑器中打开 jx-requirements.yml 并更改以下值:

  • cluster.clusterName 设置为您的集群名称。它应该与环境变量 CLUSTER_NAME 的名称相同。如果您已经忘记了,请执行 echo $CLUSTER_NAME

  • cluster.environmentGitOwner 设置为您的 GitHub 用户。它应该与之前声明的环境变量 $GH_USER 相同。

  • 仅当您的 Kubernetes 集群运行在 GKE 上时,将 cluster.project 设置为您的 GKE 项目名称。否则,保持该值不变(为空)。

  • cluster.provider 设置为 gkeeks 或任何其他提供者,如果您决定您很勇敢并想尝试目前不受支持的平台。或者,自本章编写以来,事情可能已经改变,并且您的提供者现在确实受到支持。

  • cluster.zone 设置为您的集群正在运行的区域。如果您正在运行区域集群(您应该这样做),则值应该是区域,而不是区域。例如,如果您使用我们的 Gist 创建了 GKE 集群,则值应该是 us-east1-b。类似地,EKS 的值是 us-east-1

我们已经完成了 cluster 部分,下一个是 gitops 值。它指导系统如何处理引导过程。将其更改为 false 没有意义,所以我们将保持它不变(true)。

下一个部分包含我们已经熟悉的环境的列表。键是后缀,最终名称将是 environment- 与集群名称的组合,后面跟着键。我们将保持它们不变。

ingress 部分定义了与集群外部访问相关的参数(域名、TLS 等)。

kaniko 值应该是显而易见的。当设置为 true 时,系统将使用 kaniko 而不是,比如说,Docker 来构建容器镜像。这是一个更好的选择,因为 Docker 不能在容器中运行,因此具有重大的安全风险(挂载套接字是邪恶的),并且它破坏了 Kubernetes 调度程序,因为它绕过了其 API。无论如何,kaniko 是使用 Tekton 构建容器镜像时唯一受支持的方式,所以我们将保持它不变(true)。

接下来,secretStorage当前设置为local。整个平台将在该仓库中定义,除了机密信息(如密码)。将它们推送到 Git 将是幼稚的,因此 Jenkins X 可以在不同的位置存储机密信息。如果你将其更改为local,那么该位置就是你的笔记本电脑。虽然这比 Git 仓库要好,但你可能可以想象为什么这不是正确的解决方案。将机密信息本地化会复杂化合作(它们只存在于你的笔记本电脑上),是易变的,并且仅比 Git 稍微安全一些。机密信息的一个更好的地方是 HashiCorp Vault。它是 Kubernetes(及其之外)中最常用的机密管理解决方案,Jenkins X 默认支持它。如果你已经设置了 Vault,可以将secretStorage的值设置为vault。否则,你可以保留默认值local

secretStorage值下方是整个部分,它定义了日志、报告和仓库的存储。如果启用,这些工件将存储在网络驱动器上。正如你所知道的那样,容器和节点是短暂的,如果我们想要保留任何这些,我们需要将它们存储在其他地方。这并不一定意味着网络驱动器是最好的地方,但这是默认的设置。稍后,你可能选择更改这一点,比如将日志发送到中央数据库,如 Elasticsearch、Papertrail、Cloudwatch、Stackdriver 等。

目前,我们将保持简单,并为所有三种类型的工件启用网络存储:

  • storage.logs.enabled的值设置为true

  • storage.reports.enabled的值设置为true

  • storage.repository.enabled的值设置为true

versionStream部分定义了包含 Jenkins X 所使用的所有包(图表)版本的仓库。你可能选择分叉该仓库并自行控制版本。在你跳入这样做之前,请注意 Jenkins X 的版本控制相当复杂,因为涉及许多包。除非你有很好的理由接管 Jenkins X 的版本控制并且准备好维护它,否则请保持现状。

正如你所知道的那样,Prow 仅支持 GitHub。如果你的 Git 提供商不是 GitHub,那么 Prow 就不适用。作为一个替代方案,你可以在 Jenkins 中设置它,但这也不是正确的解决方案。鉴于未来在 Tekton,Jenkins(没有 X)将不会得到长期支持。它仅在第一代 Jenkins X 中被使用,因为它是一个好的起点,并且几乎支持我们所能想象的一切。但社区已经接受 Tekton 作为唯一的管道引擎,这意味着静态 Jenkins 正在逐渐消失,并且它主要被用作那些习惯于“传统”Jenkins 的人的过渡解决方案。

所以,如果你不使用 GitHub,Prow 又不是一个选择,而 Jenkins 的日子又屈指可数,你该怎么办?更复杂的是,Prow 也将在未来的某个时刻(或者过去,取决于你阅读这篇文章的时间)被弃用。它将被 Lighthouse 取代,至少在开始时,它将提供与 Prow 相似的功能。与 Prow 相比,它的主要优势是 Lighthouse 将(或已经)支持所有主要的 Git 提供商(例如 GitHub、GitHub Enterprise、Bitbucket Server、Bitbucket Cloud、GitLab 等等)。在某个时刻,webhook的默认值将是lighthouse。但是,在撰写本文时(2021 年 2 月),情况并非如此,因为 Lighthouse 尚未稳定且尚未准备好投入生产。它很快就会准备好。无论如何,我们目前将继续使用 Prow 作为我们的 webhook。

只有在使用 EKS 时才执行以下命令。它们将添加与 Vault 相关的附加信息,即具有足够权限与之交互的 IAM 用户。确保用你的具有足够权限的 IAM 用户(总是作为管理员)替换[...]

$ export IAM_USER=[...] # such as jx-boot
echo "vault:
  aws:
    autoCreate: true
    iamUserName: \"$IAM_USER\"" \
    | tee -a jx-requirements.yml

只有在使用 EKS 时才执行以下命令。jx-requirements.yaml 文件包含一个区域条目,对于 AWS,我们需要一个区域。此命令将替换一个为另一个:

$ cat jx-requirements.yml \
    | sed -e \
    's@zone@region@g' \
    | tee jx-requirements.yml

让我们看看现在的 jx-requirements.yml 文件看起来如何:

$ cat jx-requirements.yml
cluster:
  clusterName: "jx-boot"
  environmentGitOwner: "vfarcic"
  project: "devops-26"
  provider: gke
  zone: "us-east1"
gitops: true
environments:
- key: dev
- key: staging
- key: production
ingress:
  domain: ""
  externalDNS: false
  tls:
    email: ""
   enabled: false
    production: false
kaniko: true
secretStorage: vault
storage:
  logs:
    enabled: true
    url: ""
  reports:
    enabled: true
    url: ""
  repository:
    enabled: true
    url: ""
versionStream:
  ref: "master"
  url: https://github.com/jenkins-x/jenkins-x-versions.git
webhook: prow

现在,你可能担心我们遗漏了一些值。例如,我们没有指定域名。这意味着我们的集群将无法从外部访问吗?我们也没有指定存储的 URL。在这种情况下,Jenkins X 会忽略它吗?

事实是,我们只指定了我们知道的事情。例如,如果你使用我们的 Gist 创建了集群,那么就没有 Ingress,因此也就没有它本应创建的外部负载均衡器。结果,我们还不知道可以通过哪个 IP 地址访问集群,也无法生成.nip.io 域名。同样,我们也没有创建存储。如果我们创建了,我们就可以在 URL 字段中输入地址。

这些只是未知因素的一小部分。我们指定了我们知道的内容,我们将让 Jenkins X 的boot来找出未知因素。或者,更准确地说,我们将让boot创建缺少的资源,从而将未知因素转化为已知因素。

让我们安装 Jenkins X:

$ jx boot

现在我们需要回答很多问题。过去,我们试图通过指定所有答案作为我们执行的命令的参数来避免回答问题。这样,我们就有了记录在案的方法来做事情,而这些事情最终并没有进入 Git 仓库。其他人可以通过运行相同的命令来重现我们所做的一切。然而,这一次,我们不需要避免提问,因为我们将要做的所有事情都将存储在 Git 仓库中。

第一个输入要求输入逗号分隔的 Git 提供者用户名列表,这些用户名是开发环境存储库的审批者。这将创建一个用户列表,这些用户可以批准由 Jenkins X boot管理的开发存储库的拉取请求。现在,输入你的 GitHub 用户名并按 Enter 键。

我们可以看到,过了一会儿,我们收到了两条警告,指出 Vault 和 webhooks 没有启用 TLS。如果我们指定了一个“真实”的域名,boot将安装 Let’s Encrypt 并生成证书。但由于我们无法确定你手头是否有域名,我们没有指定它,因此我们不会得到证书。虽然这在生产环境中是不可接受的,但作为一个练习来说,这完全没问题。

由于那些警告,boot正在询问我们是否希望继续。输入y并按 Enter 键继续。

由于 Jenkins X 每天都会创建多个版本,所以你很可能没有jx的最新版本。如果是这种情况,boot会询问你是否想要升级到jx版本。按 Enter 键使用默认答案Y。结果,boot将升级 CLI,但会终止管道。这没关系。没有造成损害。我们只需要重复这个过程,但这次使用jx的最新版本:

$ jx boot

流程再次开始。我们将跳过对jx boot前几个问题的注释,并继续不使用 TLS。答案与之前相同(两种情况下都是y)。

接下来的一组问题与日志、报告和存储库的长期存储有关。对于所有三个问题都按 Enter 键,boot将创建具有自动生成的唯一名称的存储桶。

从现在开始,这个过程将创建机密并安装 CRDs(CustomResourceDefinitions),这些 CRDs 提供特定于 Jenkins X 的自定义资源。然后,它将安装 NGINX Ingress Controller(除非你的集群已经有一个),并将域名设置为.nip.io,因为我们没有指定一个。进一步来说,它将安装cert-manager,这将提供 Let’s Encrypt 证书。或者,更准确地说,如果指定了域名,它将提供证书。无论如何,它已经安装好了,以防我们改变主意,选择通过更改域名和稍后启用 TLS 来更新平台。

接下来是 Vault。boot将安装它并尝试用机密填充它。但由于它还不知道它们,这个过程将再次询问我们。这一组中的第一个问题是管理员用户名。请随意按 Enter 键接受默认值 admin。之后是管理员密码。输入你想要使用的任何密码(我们今天不需要它)。

此过程需要知道如何访问我们的 GitHub 仓库,因此它会询问我们的 Git 用户名、电子邮件地址和令牌。您可以使用您的 GitHub 用户名和电子邮件回答前两个问题。至于令牌,^(11),您需要在 GitHub 中创建一个新的,并授予完整的仓库访问权限。最后,与 Secrets 相关的下一个问题是 HMAC 令牌。请随意按 Enter 键,过程将为您创建它。

最后一个问题。您想配置一个外部的 Docker 仓库吗?按 Enter 键使用默认答案(N),boot 将在集群内部创建它,或者在大多数云服务提供商的情况下,使用作为服务提供的仓库。在 GKE 的情况下,那就是 GCR;对于 EKS,那就是 ECR。在任何情况下,如果不配置外部 Docker 仓库,boot 将使用对特定提供商最有意义的选项:

? Jenkins X Admin Username admin
? Jenkins X Admin Password [? for help] ********
? The Git user that will perform git operations inside a pipeline. It should be a user within the Git organisation/own? Pipeline bot Git username vfarcic
? Pipeline bot Git email address vfarcic@gmail.com
? A token for the Git user that will perform git operations inside a pipeline. This includes environment repository creation, and so this token should have full repository permissions. To create a token go to https://github.com/settings/tokens/new?scopes=repo,read:user,read:org, user:email,write:repo_hook,delete_repo then enter a name, click Generate token, and copy and paste the token into this prompt.
? Pipeline bot Git token ****************************************
Generated token bb65edc3f137e598c55a17f90bac549b80fefbcaf, to use it press enter.
This is the only time you will be shown it so remember to save it
? HMAC token, used to validate incoming webhooks. Press enter to use the generated token [? for help] 
? Do you want to configure non default Docker Registry? No

剩余的过程将安装和配置平台的全部组件。我们不会详细介绍它们,因为它们与我们之前使用的相同。重要的是,系统将在一段时间后完全运行。

最后一步将验证安装。在过程的最后一步,您可能会看到一些警告。不要惊慌。boot 可能有些不耐烦。随着时间的推移,您会看到正在运行的 Pods 数量增加,而挂起的 Pods 数量减少,直到所有 Pods 都在运行。

就这些了。Jenkins X 现在已经启动并运行。我们已经将平台的完整定义(除了 Secrets)存储在 Git 仓库中:

verifying the Jenkins X installation in namespace jx
verifying pods
Checking pod statuses
POD                                          STATUS
jenkins-x-chartmuseum-774f8b95b-bdxfh        Running
jenkins-x-controllerbuild-66cbf7b74-twkbp    Running
jenkins-x-controllerrole-7d76b8f449-5f5xx    Running
jenkins-x-gcactivities-1594872000-w6gns      Succeeded
jenkins-x-gcpods-1594872000-m7kgq            Succeeded
jenkins-x-heapster-679ff46bf4-94w5f          Running
jenkins-x-nexus-555999cf9c-s8hnn             Running
lighthouse-foghorn-599b6c9c87-bvpct          Running
lighthouse-gc-jobs-1594872000-wllsp          Succeeded
lighthouse-keeper-7c47467555-c87bz           Running
lighthouse-webhooks-679cc6bbbd-fxw7z         Running
lighthouse-webhooks-679cc6bbbd-zl4bw         Running
tekton-pipelines-controller-5c4d79bb75-75hvj Running
Verifying the git config
Verifying username billyy at git server github at https://github.com
Found 2 organisations in git server https://github.com: IntuitDeveloper, intuit
Validated pipeline user billyy on git server https://github.com
Git tokens seem to be setup correctly
Installation is currently looking: GOOD
Using namespace 'jx' from context named 'gke_hazel-charter-283301_us-east1-b_cluster-1' on server 'https://34.73.66.41'.

B.3 安装 Flux

Flux 由一个 CLI 客户端和运行在托管 Kubernetes 集群内部的守护进程组成。本节将解释如何安装 Flux CLI。守护进程的安装需要您指定带有访问凭证的 Git 仓库,这部分内容在第十一章中介绍。

B.3.1 安装 CLI 客户端

Flux 分发包括名为 fluxctl 的 CLI 客户端。fluxctl 自动化 Flux 守护进程的安装,并允许您获取由 Flux 守护进程控制的 Kubernetes 资源的信息。

使用以下命令之一在 Mac、Linux 和 Windows 上安装 fluxctl。

macOS:

brew install fluxctl

Linux:

sudo snap install fluxctl

Windows:

choco install fluxctl

在官方安装说明中查找有关 fluxctl 安装细节的更多信息:docs.fluxcd.io/en/latest/references/fluxctl/


1.github.com/argoproj/argo-helm/tree/master/charts/argo-cd.

2.github.com/argoproj-labs/argocd-operator.

3.minikube.sigs.k8s.io/docs/handbook/accessing/#loadbalancer-access.

4.argoproj.github.io/argo-cd/cli_installation/#download-with-curl.

5.kubernetes.io/docs/tasks/tools/install-kubectl/.

6.docs.helm.sh/using_helm/#installing-helm.

7.docs.helm.sh/using_helm/#installing-helm.

8.github.com/weaveworks/eksctl.

9.hub.github.com/.

10.jenkins-x.io/docs/.

11.docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token.

附录 C. 配置 GPG 密钥

GPG,或 GNU 隐私守护者,是一种公钥加密实现。GPG 允许在各方之间安全地传输信息,并可用于验证消息来源的真实性。以下是为设置 GPG 密钥的步骤:

  1. 首先,我们需要安装 GPG 命令行工具。无论您的操作系统是什么,这个过程可能需要一些时间。macOS 用户可能使用 brew 软件包管理器通过以下命令安装 GPG:

    brew install gpg
    
  2. 下一步是生成一个将用于签名和验证提交的 GPG 密钥。使用以下命令生成密钥。在提示时,按 Enter 键接受默认密钥设置。在输入用户身份信息时,请确保使用您 GitHub 账户的已验证电子邮件:

    gpg --full-generate-key
    
  3. 查找生成的密钥 ID,并使用该 ID 访问 GPG 密钥主体:

    gpg --list-secret-keys --keyid-format LONG
    gpg --armor --export <ID>
    

    在本例中,GPG 密钥 ID 为 3AA5C34371567BD2:

    gpg --list-secret-keys --keyid-format LONG
    /Users/hubot/.gnupg/secring.gpg
    ------------------------------------
    sec   4096R/3AA5C34371567BD2 2016-03-10 [expires: 2017-03-10]
    uid                          Hubot 
    ssb   4096R/42B317FD4BA89E7A 2016-03-10
    
  4. 使用 gpg --export 的输出,并按照 mng.bz/0mlx 中描述的步骤将密钥添加到您的 GitHub 账户。

  5. 配置 Git 使用生成的 GPG 密钥:

    git config --global user.signingkey <ID>
    
posted @ 2025-11-14 20:39  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报