流水线即代码-全-

流水线即代码(全)

原文:Pipeline as Code

译者:飞龙

协议:CC BY-NC-SA 4.0

前置内容

前言

十年前,我编写了我的第一个 makefile 来自动化 C++应用程序的测试、构建和部署。三年后,在担任顾问期间,我遇到了 Jenkins 和 Docker,并发现了如何通过 CI/CD 原则将我的自动化技能提升到下一个层次。

CI/CD 的美丽之处在于,它只是记录你正在做的事情的一种严谨方式。它并没有从根本上改变你做事的方式,但它鼓励你记录开发过程中的每个步骤,使你和你团队能够在以后大规模地重现整个工作流程。在接下来的几个月里,我开始写博客文章,做演讲,并为 CI/CD 相关的工具做出贡献。

然而,对我而言,设置 CI/CD 工作流程一直是一个非常手动的过程。它是通过图形界面定义一系列针对各种管道任务的单独作业来完成的。每个作业都是通过网页表单配置的——填写文本框、从下拉列表中选择条目等等。然后,一系列作业被串联起来,每个作业触发下一个,形成一个管道。这使得故障排除体验成为噩梦,在失败的情况下回滚到最后已知配置是一项繁琐的操作。

几年后,pipeline-as-code实践作为更大规模的“as code”运动的一部分出现,该运动还包括基础设施即代码。我终于可以配置构建、测试和部署,这些都在可追踪的代码中,并存储在集中的 Git 仓库中。所有之前的痛苦都得到了缓解。

当我从软件工程师、技术领导者和高级 DevOps 经理转变为现在作为 CTO 共同领导我的第一个初创公司时,我成为了 pipeline as code 的粉丝和信徒。pipeline as code 成为我参与的每个项目的重要组成部分。

我有机会参与不同类型的架构——从单体到微服务,再到无服务器应用程序——为大规模应用程序构建和维护 CI/CD 管道。在这个过程中,我积累了在“持续一切”的旅程中遵循的技巧和最佳实践。

分享这种经验的想法触发了这本书的诞生。对于许多团队来说,实现 pipeline as code 具有挑战性,因为它们需要使用许多相互协作的工具和流程。学习曲线需要大量的时间和精力,导致人们怀疑这是否值得。这本书是一本手册,介绍了如何从头开始构建 CI/CD 管道,使用最广泛采用的 CI 解决方案:Jenkins。我希望结果能帮助你接受构建 CI/CD 管道的新范式。

致谢

首先,我要感谢我的妻子,Mounia。你一直支持我,在我努力完成这项工作时,你总是耐心地倾听,并总是让我相信我能完成它。我爱你。

接下来,我想感谢我的编辑 Karen Miller。感谢您与我合作,感谢您在疫情期间遇到困难时保持耐心。您对本书质量的承诺使它对每一位读者都变得更好。还要感谢所有与我一起参与本书制作和推广的其他人:Deirdre Hiam,我的项目编辑;Sharon Wilkey,我的校对编辑;Keri Hales,我的校对员;以及 Mihaela Batinić,我的审稿编辑。这确实是一个团队的努力。

最后,我想感谢我的家人,包括我的父母和兄弟,感谢他们在每次聚会中倾听我谈论这本书时所展现出的内在力量。

致所有审稿人:Alain Lompo、Alex Koutmos、Andrea Carlo Granata、Andres Damian Sacco、Björn Neuhaus、Clifford Thurber、Conor Redmond、Giridharan Kesavan、Gustavo Filipe Ramos Gomes、Iain Campbell、Jerome Meyer、John Guthrie、Kosmas Chatzimichalis、Maciej Drożdżowski、Matthias Busch、Michal Rutka、Michele Adduci、Miguel Montalvo、Naga Pavan Kumar Tikkisetty、Ryan Huber、Satej Kumar Sahu、Simeon Leyzerzon、Simon Seyag、Steve Atchue、Tahir Awan、Theo Despoudis、Ubaldo Pescatore、Vishal Singh 和 Werner Dijkerman,你们的建议帮助使这本书变得更好。

关于本书

代码即管道 被设计成通过实际示例进行动手实践。它将教会您 Jenkins 的方方面面,并成为您构建云原生应用的稳固 CI/CD 管道的最佳伴侣。

适合阅读本书的人群

代码即管道 是为所有希望提高 CI/CD 技能的 DevOps 和云实践者设计的。

本书组织结构

本书分为四个部分,共涵盖 14 章。

第一部分将带您了解基本的 CI/CD 原则,并讨论 Jenkins 如何帮助实现这些原则:

  • 第一章概述了持续集成、部署和交付实践,并讨论了 Jenkins 如何帮助您拥抱这些 DevOps 实践。

  • 第二章介绍了代码化管道方法以及如何使用 Jenkins 实现,还涵盖了声明式和脚本 Jenkins 管道之间的区别。

第二部分涵盖了如何使用基础设施即代码方法在云上部署自愈 Jenkins 集群:

  • 第三章深入探讨了 Jenkins 分布式构建架构,并提供了 AWS 上的完整示例。

  • 第四章介绍了使用 HashiCorp Packer 的不可变基础设施方法,包括如何烘焙一个包含所有必需依赖项的 Jenkins 机器镜像,以便直接运行 Jenkins 集群。

  • 第五章演示了如何使用 HashiCorp Terraform 在 AWS 上部署安全且可扩展的 Jenkins 集群。

  • 第六章详细描述了在不同云服务提供商上部署 Jenkins 集群的过程,包括 GCP、Azure 和 DigitalOcean。

第三部分专注于从头开始构建云原生应用的 CI/CD 管道,包括在 Swarm 或 Kubernetes 中运行的 Docker 化微服务和无服务器应用:

  • 第七章为构建容器化微服务的 CI 工作流程奠定了基础。它涵盖了如何在 Jenkins 上定义多分支管道以及如何在推送事件上触发管道。

  • 第八章演示了如何在 Docker 容器内运行自动化测试。描述了各种测试,包括使用无头 Chrome 的 UI 测试、代码覆盖率、使用 SonarQube 的静态代码分析以及安全分析。

  • 第九章涵盖了在 CI 管道内构建 Docker 镜像、管理它们的版本以及扫描安全漏洞。它还讨论了如何使用 Jenkins 自动化 GitHub 拉取请求的审查。

  • 第十章介绍了将 Docker 化应用部署到 Docker Swarm 的过程,并使用 Jenkins 进行演示。它展示了如何维护多个运行时环境以及如何实现持续部署和交付。

  • 第十一章深入探讨了使用 Jenkins 管道自动化 Kubernetes 上应用程序部署的方法,包括如何打包和版本化 Helm 图表以及运行部署后测试。它还演示了 Jenkins X 的使用方法以及它与 Jenkins 的比较。

  • 第十二章介绍了如何为基于无服务器的应用构建 CI/CD 管道,以及如何管理多个 Lambda 部署环境。

第四部分涵盖了轻松维护、扩展和监控在生产中运行的 Jenkins 集群:

  • 第十三章探讨了如何使用 Prometheus、Grafana 和 Slack 构建交互式仪表板,以持续监控 Jenkins 的异常和性能问题。它还涵盖了如何将 Jenkins 日志流式传输到基于 ELK 堆栈的集中日志平台。

  • 第十四章介绍了如何使用细粒度的 RBAC 机制来保护 Jenkins 作业。它还探讨了如何备份、恢复和迁移 Jenkins 作业和插件。

关于代码

这本书提供了一种动手实践的经验,其中包含了许多代码示例。这些代码示例遍布全文,并以单独的代码列表形式出现。代码以固定宽度字体的形式呈现,就像这样,所以当你看到它时就会知道。

书中使用的所有源代码均可在 Manning 网站(www.manning.com/books/pipeline-as-code)或我的 GitHub 仓库(github.com/mlabouardy/pipeline-as-code-with-jenkins)上找到。这个仓库是我倾注心血的成果,我感谢所有捕捉到错误、进行性能改进和帮助文档工作的人。一切都非常适合贡献!

liveBook 讨论论坛

购买 Pipeline as Code 包括免费访问由 Manning Publications 运营的私人网络论坛,您可以在论坛上对书籍发表评论、提出技术问题,并从作者和其他用户那里获得帮助。要访问论坛,请访问 livebook.manning.com/#!/book/pipeline-as-code/discussion。您还可以在 livebook.manning.com/#!/discussion 了解更多关于 Manning 的论坛和行为准则。

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

其他在线资源

需要更多帮助?

  • 查看我的博客 (labouardy.com/),我在那里定期分享关于 Jenkins 的最新消息以及构建 CI/CD 工作流的最佳实践。

  • 一份每周 DevOps 新闻通讯 (devopsbulletin.com) 可以帮助您了解 pipeline-as-code 空间的最新奇事。

  • StackOverflow 上的 Jenkins 标签 (stackoverflow.com/questions/tagged/jenkins) 是一个提问和帮助他人的绝佳地方。

关于作者

Mohamed LabouardyCrew.work 的 CTO 和联合创始人,同时也是 DevSecOps 传教士。他是 Komiser.io 的创始人,也是多本关于无服务器和分布式应用的书籍的作者。他喜欢为开源项目做贡献,并经常参加会议演讲。您也可以在 Twitter 上找到他 (@mlabouardy)。

关于封面插图

Pipeline as Code 封面上的插图标题为“Bohémien de prague”,或称布拉格的波希米亚人。这幅插图取自雅克·格拉塞·德·圣索沃尔(1757–1810)的作品集,名为 Costumes de Différents Pays,于 1797 年在法国出版。每一幅插图都是手工精心绘制和着色的。格拉塞·德·圣索沃尔收藏的丰富多样性生动地提醒我们,200 年前世界的城镇和地区在文化上有多么不同。人们彼此孤立,说着不同的方言和语言。在街道或乡村,仅凭他们的服饰就能轻易识别他们居住的地方以及他们的职业或社会地位。

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

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

第一部分. Jenkins 入门

本书的第一部分将带您了解 DevOps 的基本概念。您将学习 CI/CD 实践以及它们如何让您一次集成小块代码并减轻技术债务。之后,我将介绍构建 CI/CD 管道的新方法——管道即代码,以及如何使用 Jenkins 实现它。最后,我将通过介绍 GitFlow 分支模型来为设计良好的 CI/CD 工作流程奠定基础。

1 什么是 CI/CD?

本章涵盖

  • 组织从单体应用向云原生应用演变的路径

  • 实施 CI/CD 实践对云原生架构的挑战

  • 持续集成、部署和交付概述

  • 如何通过像 Jenkins 这样的 CI/CD 工具为进行持续一切旅程的组织带来商业价值

软件开发和运维最近经历了几次范式转变。这些转变为行业提供了构建和部署应用的创新方法。更重要的是,两次重大的范式转变巩固了开发、部署和管理可扩展应用的能力:云原生架构和 DevOps。

云原生架构 随着云的采用而出现,云服务提供商如亚马逊网络服务(AWS)、谷歌云平台(GCP)和微软 Azure 拥有基础设施的所有权。开源工具如 Kubernetes、Docker 和 Istio 提供了横向扩展能力,让开发者能够构建和运行现代可扩展应用,无需担心底层基础设施。因此,运营开销降低,应用的开发速度提高。

DevOps 桥梁了开发者和运维团队之间的鸿沟,通过协作、自动化工具以及迭代和敏捷开发和部署恢复了和谐。

结合这两种重大且强大的方法,组织现在有能力创建具有高度团队协作和信息共享的、可扩展、稳健和可靠的应用程序。然而,为了构建、测试和安全地部署云原生应用,必须在云原生方式下实施两个基本的 DevOps 实践:持续集成(CI)和持续部署/交付(CD)。

本书的第一部分将带您了解云原生应用的演变。您将了解 CI/CD 的主要原则以及自动化如何通过 代码即管道 方法实现这些原则。本章奠定了基础。它介绍了 DevOps 和云原生方法的基本原则,以及选择用于实现 CI/CD 管道的工具。

1.1 走向云原生

在探索云原生应用的基本特性和 CI/CD 实践如何有助于为开发者标准化反馈循环以及实现快速产品迭代之前,我们将介绍软件开发模型所经历的变革以及每个模型所面临的挑战,从单体方法开始。

1.1.1 单体

在过去,组织通常以 单体 方式构建他们的软件:所有功能都打包在一个单一的项目中,并在运行一个进程的单个服务器上部署。这种架构存在许多缺点和限制:

  • 开发速度—在现有应用程序上添加新功能几乎是不可能的。应用程序模块紧密耦合,并且大多数情况下没有文档。因此,添加新功能通常很慢、成本高昂,并且在与分布式团队中的多个开发者合作时,在大型代码库上工作需要额外的同步。此外,由于应用程序的大型代码库,发布周期可能需要数月甚至数年。这种延迟使公司面临被新竞争对手超越的风险,并最终损害公司的利润。

  • 可维护性—单体架构中的模块通常紧密耦合,这使得它们难以维护和测试。此外,升级到新技术仅限于用于开发应用程序的框架(没有多语言编程)。

  • 可扩展性和弹性—应用程序在设计时没有考虑到可扩展性,如果流量增加,应用程序可能会面临停机。单体应用程序作为一个单一单元运行,并使用单一编程语言和单一技术栈进行开发。因此,为了实现部分水平扩展,整个应用程序都需要进行扩展(服务器资源的低效使用)。

  • 成本效益—从长远来看,应用程序的维护成本很高(例如,寻找经验丰富的 COBOL 开发者既耗时又昂贵)。

在 2000 年代后期,许多网络巨头(包括 Facebook、Netflix、Twitter 和 Amazon)带着创新的想法、积极的策略和“快速行动”的方法进入科技界,这导致了他们平台的指数级增长。这些公司引入了一种新的架构模式,今天被称为微服务。那么,微服务架构究竟是什么呢?

1.1.2 微服务

James Lewis 和 Martin Fowler 在 2014 年如下定义了微服务架构:

简而言之,微服务架构是将单个应用程序作为一系列小型服务集来开发的方法,每个服务都在自己的进程中运行,并通过轻量级机制(通常是 HTTP 资源 API)进行通信。这些服务围绕业务能力构建,并且可以由完全自动化的部署机器独立部署。对这些服务的集中式管理最少,这些服务可能使用不同的编程语言,并使用不同的数据存储技术

这种架构使用“分而治之”的相同技术来解决应用程序的复杂性。应用程序被分割成更小、独立且可组合的服务/片段,每个服务负责应用程序的特定功能或任务(围绕业务能力组织)。

这些微服务通过应用程序编程接口(API)进行通信,通常是通过 HTTP 或 HTTP/2(例如 gRPC、RESTful API、Google Protocol Buffers 或 Apache Thrift),或者通过消息代理(如 Apache ActiveMQ 或 Kafka)。每个微服务都可以在不同的编程语言和不同的操作系统平台上实现。

与微服务架构相比,单体架构意味着代码的组件被设计成作为一个统一的整体协同工作,共享相同的服务器资源(内存、CPU、磁盘等)。图 1.1 展示了单体架构与微服务架构之间的差异。

图 1.1 比较单体架构与微服务架构

微服务架构是面向服务架构(SOA)的扩展。这两种架构都依赖于服务作为主要组件,但在服务特性方面存在很大差异:

  • 粒度——微服务架构中的服务组件通常是单一用途的服务,只做一件事。在 SOA 中,服务组件的大小可以从小的应用服务到非常大的企业服务不等。

  • 共享——SOA 增强了组件共享,而微服务架构试图通过边界上下文(松散耦合的服务或模块)和最小依赖来最小化共享。

  • 通信——微服务依赖于轻量级协议,如 HTTP/REST 和简单消息传递,而 SOA 架构依赖于企业服务总线(ESB)进行通信;SOA 的早期版本使用面向对象的协议进行通信,例如分布式组件对象模型(DCOM)和对象请求代理(ORBs)。后期版本则使用消息服务,如 Java 消息服务(JMS)或高级消息队列协议(AMQP)。

  • 部署——SOA 服务部署到应用服务器(如 IBM WebSphere Application Server、WildFly、Apache Tomcat)和虚拟机。另一方面,微服务部署在容器中。这使得微服务比 SOA 更加灵活和轻量。

注意:关于微服务架构的更多细节,我推荐阅读 Morgan Bruce 和 Paulo A. Perreira 合著的《Microservices in Action》(Manning,2018)。它涵盖了微服务的定义,以及个人或专业团队如何对其进行组合,单体应用与微服务之间的持续比较,以及部署微服务时需要考虑的事项。

微服务的优势说服了一些大型企业玩家,如亚马逊、Netflix 和 Uber,采用这种方法。跟随他们的脚步,其他公司也在同一方向努力:从单体架构向灵活的基于微服务的架构演变。

但是什么让它如此特别呢?与更传统的单体设计结构相比,微服务架构具有以下优势:

  • 可伸缩性—作为微服务构建的应用程序可以被分解成多个组件,这样每个组件都可以独立部署和扩展,而不会中断服务。对于无状态的微服务,使用 Docker 或 Kubernetes 可以在几秒钟内提供水平扩展。

  • 容错性—如果一个微服务失败,由于组件松散耦合,其他微服务将继续工作。单个微服务可以很容易地被新的一个所替代,而不会影响整个系统。因此,微服务架构的现代化可以逐步进行,而单体架构的现代化可能会导致服务中断。

  • 开发速度—微服务可以用不同的语言(多语言编程)编写,并使用不同的数据库或操作系统环境。例如,如果一个微服务是 CPU 密集型的,它可以由像 Golang 或 C++这样高度生产力的语言实现,而其他组件可以由像 JavaScript 或 Python 这样的轻量级编程语言实现。因此,公司可以轻松地雇佣更多的开发者并扩展开发。此外,由于微服务是自治的,开发者可以独立开发和部署服务,而不会相互干扰代码(避免组织内部的同步地狱),也不必等待一个团队完成其工作后再开始自己的工作。结果,团队的生产力提高,供应商或技术堆栈锁定减少。

  • 持续一切—微服务架构与敏捷软件开发相结合,可以实现持续交付。在微服务应用程序中的软件发布周期变得非常小,许多功能可以通过 CI/CD 管道每天发布,使用像 Jenkins 这样的开源 CI 工具。

总结来说,微服务使解决大问题变得更容易,提高了生产力,提供了选择技术的灵活性,非常适合跨职能团队。同时,在分布式云环境中运行微服务可能对组织来说是一个严峻的挑战。以下是与微服务设计相关的潜在痛点:

  • 复杂性—由于涉及的服务数量增加,比单体应用程序增加了复杂性。因此,需要巨大的努力、同步和自动化来处理服务间通信、监控、测试和部署。

  • 运营开销—部署基于微服务的应用程序可能很复杂。它需要在多个服务之间进行大量协调。每个服务都必须与其自己的运行时环境和资源隔离。因此,传统的部署解决方案,如虚拟化,不能使用,必须用容器化解决方案,如 Docker,来替代。

  • 同步——微服务需要寻求采用它们的组织进行文化变革。拥有多个开发团队在不同的服务上工作,需要巨大的努力来确保沟通、协调和自动化流程到位。像敏捷和 DevOps 实践这样的文化是强制性的,以承担基于微服务的应用程序。

注意:虽然 Docker 没有学习曲线,但在处理在机器或节点集群中部署微服务时,它很快就会变成一场噩梦。

大多数这些缺点都通过使用 AWS 提供的云计算服务以及开源工具(尤其是 Kubernetes)的兴起得到了解决。它带来了一种完全新的基础设施管理方法,并使应用程序能够以分布式的方式进行架构设计。因此,在 2014 年出现了一种新的软件架构风格:云原生应用程序。

1.1.3 云原生

云原生计算基金会(Cloud Native Computing Foundation,CNCF),是一个成立于 2015 年的 Linux 基金会项目,旨在帮助推进容器技术,它将云原生定义为如下:

云原生技术使组织能够在现代、动态的环境中构建和运行可扩展的应用程序,如公共、私有和混合云。容器、服务网格、微服务、不可变基础设施和声明式 API 是这种方法的例证。这些技术使系统松散耦合,具有弹性、可管理和可观察性。结合强大的自动化,它们允许工程师频繁且可预测地做出高影响的变化,而工作量最小化

云原生是构建应用程序为微服务并在容器化和动态编排平台上运行的一种范例,这些平台充分利用了云计算模型的优势。这些应用程序使用最适合其功能的语言和框架进行开发。它们被设计为松散耦合的系统,针对云规模和性能优化,使用托管服务,并利用持续交付来实现可靠性和更快的上市时间。

总体目标是提高速度、可扩展性和最终,利润率。图 1.2 展示了云原生应用程序的一个示例。

图 1.2 云原生应用程序概述

云原生应用程序以轻量级容器打包,并作为微服务高效部署。它们使用轻量级 API 来公开其功能,并使用二进制和非二进制协议进行内部通信。更进一步,应用程序通过具有持续交付工作流程的敏捷 DevOps 流程在弹性云基础设施上管理。

注意:Docker 已成为容器技术的标准。它彻底改变了我们思考开发微服务的方式,并使我们能够轻松地在本地、本地或云中部署微服务。

Kubernetes (kubernetes.io/) 是运行作为云原生应用的工作负载的首选平台之一。它是一个开源的容器编排平台,最初由谷歌开发。它确保了容器化应用的自动化部署、扩展和管理的高端功能。这种构建和部署应用的新范式带来了许多好处:

  • 无运营开销—开发者可以专注于开发功能和增加商业价值,而不是处理基础设施的配置和管理。

  • 安全合规性—由于应用程序的各个部分是隔离的,因此需要简化的安全监控。一个容器中的安全问题可能发生,而不会影响应用程序的其他区域。

  • 自动扩展—容器可以被部署到不同可用区或多个隔离数据中心(区域)的服务器集群中。因此,云原生应用可以通过在高峰使用期间动态扩展或缩减资源来利用云的弹性,而无需采购和配置物理服务器。此外,通过采用云服务,企业可以在几分钟内实现全球化,降低适应成本,增加收入,同时无需担心可扩展性。

  • 开发速度—由于每个容器代表一小块功能,因此应用程序架构易于理解,并且易于开发者修改,因此他们可以帮助新团队成员快速变得高效。此外,采用云原生技术和实践使公司能够内部创建软件,使业务人员能够与 IT 人员紧密合作,跟上竞争对手,并向客户提供更好的服务。

  • 弹性—云原生微服务允许在细粒度级别上进行故障。它们通过在每个服务之间提供足够的隔离来实现这一点,并提供多种可能提高组件可用性和弹性的设计模式,例如断路器 (martinfowler.com/bliki/CircuitBreaker.html)、节流 (www.redhat.com/architect/pros-and-cons-throttling) 和重试模式。像 Netflix 这样的公司使用它来开发一种名为 混沌工程 的新方法,以构建一个弹性的流媒体平台。

图 1.3 展示了单体架构、微服务架构和云原生架构之间的差异。

图 1.3 单体架构、微服务架构和云原生架构

总结来说,云原生架构允许你在分布式应用上动态扩展并支持大量用户、事件和请求。云原生架构在实际应用中的真实例子是无服务器模型。

1.1.4 无服务器

无服务器计算模型始于 2014 年的 AWS Lambda。在这个架构中,开发者可以编写成本效益高的应用程序,而无需配置或维护复杂的基础设施。

云服务提供商将客户的代码部署到完全托管、短暂、时间限制的容器中,这些容器仅在函数调用期间存在。因此,企业可以增长,而客户无需担心水平扩展或维护复杂的基础设施。

注意:无服务器并不意味着“无运维”。你只是将系统管理员外包给了无服务器服务。你仍然需要处理监控、部署和安全问题。

基于无服务器架构构建的应用程序最终可能看起来像图 1.4。

图 1.4 无服务器应用程序的示例

与维护一个专门的容器或实例来托管你的静态 Web 应用程序相比,你可以结合使用 Amazon Simple Storage Service (S3)存储桶,以更低的成本获得可伸缩性。来自网站的 HTTP 请求将通过 Amazon API Gateway HTTP 端点,触发正确的 AWS Lambda 函数来处理应用程序逻辑并将数据持久化到完全托管的数据库服务,如 DynamoDB。对于特定的用例,采用无服务器架构可能有几个原因:

  • 减少运维开销——基础设施由云服务提供商管理,这减少了开销并提高了开发速度。操作系统更新由函数即服务(FaaS)提供商负责,补丁修复也由他们完成。这导致上市时间缩短,软件发布速度加快,并消除了系统管理员的需求。

  • 水平自动扩展——函数成为扩展的单位,导致小型、松散耦合、无状态的组件,从长远来看,这些组件可以扩展成可伸缩的应用程序。此外,扩展机制转移到云服务提供商,它决定如何有效地使用其基础设施来服务客户端请求。

  • 成本优化——你只需为使用的计算时间和资源付费。因此,你不需要为闲置资源付费,这显著降低了基础设施成本。

  • 多语言支持——另一个好处是能够根据用例选择不同的语言运行时。应用程序的一部分可以是用 Java 编写的,而另一部分可以用 Python 编写;只要工作能完成,这并不重要。

注意:在采用无服务器架构时,一个主要担忧是供应商锁定。尽管你应该首先考虑开发速度和效率,但根据你的用例选择供应商是很重要的。

云原生架构通常正在获得广泛的应用,但许多团队的学习曲线很陡峭。此外,转向云原生架构对于许多组织来说可能是一把双刃剑,而在完全采用云原生方法时可能面临的挑战之一是 CI/CD。

但这些实践意味着什么?当您构建云原生应用程序时,它们如何应用?

1.2 定义持续集成

持续集成(CI)是指在共享和集中的代码仓库中,在将更改和功能集成到中央仓库(如 GitHub、Bitbucket 或 GitLab)之前,通过复杂的管道指导所有更改和功能。一个经典的 CI 管道如下:

  1. 每当发生代码提交时触发构建

  2. 运行单元测试和所有预集成测试(质量和安全测试)

  3. 构建工件(例如,Docker 镜像、zip 文件、机器学习训练模型)

  4. 运行验收测试并将结果推送到一个工件管理存储库(如 Docker Registry、Amazon S3 存储桶、Sonatype 的 Nexus 或 JFrog Artifactory)

图 1.5 显示了容器化应用程序的 CI 管道示例。

图片

图 1.5 云原生应用程序的基本 CI 工作流程

基本上,持续集成(CI)会自动监控每个开发者做出的提交,并启动自动化测试。自动化测试是 CI/CD 管道的组成部分。没有自动化测试,CI/CD 管道将缺少质量检查,这对于应用程序的发布非常重要。

您可以实施各种类型的测试以确保您的软件满足所有初始要求。以下是最著名的几种:

  • 单元测试—这些测试源代码的每一部分。它们包括测试单个函数和方法。您还可以输出测试覆盖率并验证您是否满足代码覆盖率要求。

  • 质量测试—检查代码是否格式良好,遵循最佳实践,并且没有严重的编码错误。这也被称为静态代码分析,因为它通过寻找可能产生错误的代码模式来帮助产生高质量的代码。

  • 安全测试—检查源代码以发现常见的安全漏洞和常见的安全缺陷(例如,泄露的用户名和密码)。

  • UI 测试—通过系统模拟用户行为,以确保应用程序在所有支持的浏览器(包括 Google Chrome、Mozilla Firefox 和 Microsoft Internet Explorer)和平台(如 Windows、Linux 和 macOS)上都能正确工作,并且提供用户故事中承诺的功能。

  • 集成测试—检查应用程序使用的服务或组件是否协同工作且不存在缺陷。例如,集成测试可能会测试应用程序与数据库的交互。

手动执行所有这些测试可能会耗时且效率低下。因此,您应该始终使用适合您应用程序要求的测试框架,以可重复和可靠的方式在规模上执行这些测试。

注意第八章介绍了如何使用 Jenkins 和 Headless Chrome 运行自动化测试,以及如何集成 SonarQube 进行代码分析。

一旦测试成功,应用程序将被编译和打包,并在远程仓库中生成和版本化可发布的工件。

1.3 定义持续部署

持续 部署(CD)是持续集成的扩展。每个通过您持续集成管道所有阶段的更改都会自动发布到您的预发布/生产环境。

在这样的过程中,无需决定部署什么以及何时部署。管道将自动部署成功通过管道的任何构建组件/包。图 1.6 展示了在 Kubernetes 中运行的微服务的典型 CI/CD 管道。

图 1.6 云原生应用的基本 CI/CD 工作流程

这个 CI 工作流程有四个步骤,CD 管道是部署到 Kubernetes(步骤 5)。然而,纯持续部署方法并不总是适合所有人。

例如,许多客户可能不会欣赏每周几次新版本落入他们手中,他们更喜欢更可预测和透明的发布周期。商业和营销考虑因素也可能在何时实际部署新版本时发挥作用。

虽然持续部署可能不适合每家公司,但持续交付是 DevOps 实践的绝对要求。只有当您持续交付代码时,您才能真正有信心,您的更改在按下“启动”按钮后的几分钟内将为您的客户提供价值,并且您实际上可以在业务准备好时随时按下该按钮。

1.4 定义持续交付

持续 交付(CD)与持续部署类似,但在将发布部署到生产之前需要人工干预或业务决策。图 1.7 展示了 CI/CD 实践之间的关系。

图 1.7 持续部署成熟度模型

注意:在复杂的 CI/CD 工作流程中,可能存在监控和优化阶段。这一步骤包括收集和分析指标和反馈,以消除风险和浪费并优化发布时间。

1.5 接受 CI/CD 实践

CI/CD 和持续交付可以通过每日构建为云原生应用带来更多敏捷性,从而导致以下结果:

  • 在早期阶段检测异常(降低风险)以及通过单元和功能测试最小化技术债务。根据 Atlassian (www.atlassian.com/software-development/practices) 的数据,75% 的开发团队在发布时都会遇到与虫子、缺陷或延迟相关的问题。

  • 构建用户真正需要的特性。这通常会导致更好的用户交互和关于发布特性的更快反馈,这有助于产品团队专注于最受欢迎的特性并构建高质量的产品。

  • 拥有一个可用于生产的软件包。这是加速上市时间的一个极好方法。

  • 通过质量测试和压力测试提高产品质量和可靠性,并通过更好的可见性跟踪项目状态和健康状况。

  • 通过每次迭代构建高质量产品的同时,从反馈中驱动创新。

然而,从手动到高度自动化的部署过程的转变可能需要几个月的时间。因此,公司需要迭代地采用 CI/CD,如图 1.8 所示。

图片

图 1.8 向组织引入 CI/CD

你应该始终优先考虑 CI/CD 中的步骤。首先,自动化源代码编译的过程。理想情况下,你将每天开发新功能并修复多个错误。手动这个过程可能需要几分钟到几个小时。此外,你应该在 UI 测试之前优先进行功能测试,因为它经常发生变化,因此需要频繁的管道更改。所以确保将 CI/CD 步骤分解成更小的部分,并分批自动化,以最大限度地利用你的资源。

另一个担忧是 CI/CD 的复杂性将会增加,从处理单一应用程序到数十个微服务(多个管道)。因此,适应你的 CI/CD 工具和流程是必要的,以保持同步。

此外,你需要有一个清晰的产品路线图,并有一个经过验证的开发成功记录。你的最终客户应该能够消费持续的产品变化。因此,使用 CI/CD 需要高度的纪律性,对质量的奉献精神,以及一个学习曲线(新的技能集)。如果你无法处理这些,立即停止考虑 CI/CD。

因此,转向 CI/CD 不应是一个孤立的决定,仅由 DevOps 团队做出。CI/CD 的成功推广必须是整个组织的决定,并且只有当整个组织都同意时才能做出。

尽管你需要考虑一些担忧,但 CI/CD 的好处几乎总是超过挑战。为了实现云原生应用的全部承诺,你必须实施最适合你独特业务目标的 CI/CD 实践。

在这本书中,我们将探讨为大多数采用的云原生架构构建 CI/CD 管道的一些真实世界用例,例如同时使用 Docker Swarm 和 Kubernetes 的 Docker 化微服务,以及基于 Lambda 的无服务器应用程序。我们还将介绍如何以更少的维护麻烦来管理和扩展 CI 工具,以帮助你提高部署速度。但首先,是什么让现代 CI 工具变得独特,我们将使用哪一个?

注意:虽然单体架构可能不再流行,但许多公司仍然拥有单体旗舰产品,并且仍然可以从一个精心设计的 CI/CD 解决方案中受益巨大。所以书中的大部分例子也可以应用于单体应用的现代化。

1.6 使用基本的 CI/CD 工具

现在有许多优秀的 CI 工具。一些已经存在很长时间,而另一些则是相对较新的。说一个现代 CI 工具必须是快速、用户友好和灵活的有点多余,因为这些是我们已经期望的功能。CI 工具可以分为以下三个主要类别:

1.6.1 选择 CI/CD 工具

图 1.9 展示了当今市场上最受欢迎的 CI/CD 工具。这些工具是成熟的,具备您项目所需的基本功能。

图 1.9 2021 年最受欢迎的 CI/CD 工具

有许多优秀的 CI 工具可供选择,因此您需要根据以下因素选择最佳工具:

  • 团队经验和技能—虽然许多工具使用配置 YAML 文件来声明 CI/CD 管道,但它们可能需要一些系统管理员技能来设置和提供所需的基础设施以运行 CI/CD 平台。此外,维护底层基础设施可能会引起很多麻烦,并成为您公司增长瓶颈,一旦您的项目代码库变得更大(扩展能力),因为您需要维护跨多个节点或服务器的分布式 CI/CD 复杂管道。

  • 目标平台—考虑您的应用程序或项目运行在哪个操作系统上(一些 CI 工具不支持 macOS 和 ARM 架构),以及使用自托管基础设施或云提供商。

  • 编程语言和架构—大多数 CI 工具支持最前沿的语言,包括 Java、Ruby、Python、PHP 和 JavaScript。然而,一些工具如 TeamCity 提供了更好的 Java 和 .NET 项目的集成和支持。同样,Bamboo 作为 Atlassian 的作品,具有对 Jira 和 Bitbucket 的原生支持。此外,部署解决方案可以是选择适合您项目的正确 CI 工具的一个因素。像 Drone (www.drone.io) 和 GitLab CI (docs.gitlab.com/ee/ci/) 这样的工具提供了集成的 Docker 注册表和原生 Docker 支持。

1.6.2 介绍 Jenkins

尽管没有单个工具能够满足每个项目的需求,但在本书中,我们将大量依赖 Jenkins。它被认为是市场上最受欢迎的 CI 工具之一,拥有超过一百万用户。它是用 Java 编写的,使其成为一个跨平台(Windows、Linux 和 macOS)的持续集成工具。

最初是 Hudson 项目的组成部分,在 Sun Microsystems 被甲骨文公司收购后,由于商标冲突,社区和代码库分开了。Hudson 最初于 2005 年发布,而 Jenkins 的首次发布是在 2011 年。

注意:如果您愿意为他人维护和更新解决方案支付一些额外费用,托管 SaaS 平台可能是有益的。当企业需要比 Jenkins 提供的更好的 UI 并且缺乏基础设施技能时,往往会选择这种选项。但自托管解决方案(如 Jenkins)的主要好处是,您对自己的数据安全和作业流程有更多的控制和灵活性。

一套丰富的插件集使 Jenkins 能够支持任何类型的语言或技术,例如 Docker、Maven、Git、Mercurial 和 AWS。作为一个开源项目,它使得开发者可以通过创建自定义插件来定制和扩展它。以下是 Jenkins 的关键特性:

  • 可通过庞大的社区贡献的插件资源(超过 1,400 个插件)进行扩展。

  • 一款免费和开源的工具,以及 CloudBees 提供的付费企业版(www.cloudbees.com/jenkins),提供快速的客户支持。

  • 拥有一个活跃的社区,帮助开发者减少构建工作 CI/CD 工作流程的时间。

  • 可以通过用户界面或命令行轻松配置,在本地或云端部署。

  • 支持具有内置并行机制的主从架构的分布式构建。

  • 一个强大且灵活的工具,可以完全控制工作流程,满足每个 CI/CD 需求。

  • 在许多平台上运行,并支持广泛的工具和框架。

  • 支持容器作为构建代理,适用于计划使用 Docker 的团队。

  • 与 GitHub、GitLab、Bitbucket 以及大多数源代码管理(SCM)系统和 Apache Subversion(SVN)无缝集成。

  • 灵活的用户管理,用户角色分配,将用户分类到不同的组中,不同的用户认证方式(包括 LDAP、GitHub OAuth 和 Active Directory)。

  • 由于 Jenkins 管道工作流程,CI 流程可以使用存储库内的文件或 Jenkins 网页 UI 中的文本字段使用 Groovy 语言定义。

注意:如果您只想测试特定平台上的一个小型应用程序,您不需要运行 Jenkins 服务器的复杂性。

Jenkins 的另一个关键特性是 代码化流水线。我们将使用这种方法来创建 Jenkins 作业。使用这种方法的好处是,我们的整个 Jenkins 作业配置可以与应用程序源代码的其他部分一起创建、更新和版本控制。

有助于注意的是,Jenkins 必须托管在服务器上,因此它通常需要具有基础设施技能的人的关注。你不能只是设置好然后期望它自行运行;系统需要频繁的更新和维护。大多数团队进入的主要障碍是初始设置、拖延或之前设置失败的尝试。人们往往知道它是好的,但许多团队却忽视了它,转而去做更紧急的编码工作。也许你的团队中有人试图部署 Jenkins,但没有成功维护它。也许这种浪费的努力给你的老板留下了不好的印象。

人们不实施 Jenkins 的原因通常非常实际。这就是为什么,在这本书中,我们将使用基础设施即代码的神奇力量,结合像 Terraform 和 Packer 这样的开源工具,在大多数流行的公共云提供商(如 AWS、GCP 和微软 Azure)上从无到有地设置我们的整个 CI 基础设施。

在这本书中,我们还将解决另一个问题,那就是如何编写测试。编写测试是大多数开发者想要做的事情,但往往没有时间去做。可以理解的是,编写实际的应用程序对于业务来说通常是优先级更高的任务。此外,测试会出错,这意味着当被测试的功能发生变化时,它需要更新。如果功能没有更新,它就停止提供价值。我们将介绍如何在 CI/CD 管道中运行各种类型的测试,以及如何集成外部代码分析工具。

总结来说,为云原生架构实施 CI/CD 需要文化和心态的转变,尤其是从管理层来说。管理者必须为这项“低效的工作”留出时间。

尽管如此,短暂的牺牲时间对于整个公司来说会带来长期的好处。使用 Jenkins,你的代码变得更容易维护,更少的错误会悄悄进入生产环境。你的团队变得更加紧密,构建所需的时间更少。你的业务可以更快地发货,并跟上客户不断变化的需求(通过更快地发货代码,组织可以迅速响应变化并保持产品在市场上)。

CI/CD 不是一个开销,而是一种投资。实施的投资回报率(ROI)可以通过节省的时间、避免的错误以及更容易交付给客户的更高品质的产品来衡量。

摘要

  • 云原生架构正在改变格局,迫使组织思考新的模型和新的交付方法。

  • 持续集成、交付和部署是旨在帮助提高开发速度和发布经过良好测试、可用的产品的实践。

  • 选择合适的 CI/CD 工具对于云原生应用的长期成功至关重要,应基于平台复杂性、集成、学习曲线、定价和工作时间效率来考虑。

  • Jenkins 可以利用团队当前的流程,最大限度地利用自动化功能,并创建一个稳固的 CI/CD 流水线。

2 使用 Jenkins 的管道作为代码

本章涵盖

  • 管道作为代码如何与 Jenkins 一起工作

  • Jenkinsfile 结构和语法的概述

  • Blue Ocean 的新 Jenkins 用户体验介绍

  • 声明式与脚本式 Jenkins 管道

  • 在 Jenkins 项目中集成 GitFlow 模型

  • 在编写复杂的 CI/CD 管道 Jenkinsfile 时的生产力和效率技巧

毫无疑问,云计算对公司在构建、扩展和维护技术产品的方式产生了重大影响。只需点击几个按钮就能配置机器、数据库和其他基础设施的能力,导致了我们以前从未见过的开发者生产力的提升。

虽然启动简单的云架构很容易,但在配置复杂的云架构时,错误很容易发生。人为错误始终存在,尤其是在你可以通过点击云提供商网页控制台上的按钮来启动云基础设施的情况下。

避免这些错误的方法是通过自动化,而基础设施即代码 (IaC) 正在帮助工程师快速且无错误地自动启动云环境。DevOps 的增长和对其实践的采用导致了更多工具可以更大程度地实现 IaC 范式。

在过去,设置 CI/CD 工作流程一直是一个手动过程。通常是通过定义一系列针对各种管道任务的独立作业来完成的。每个作业都是通过网页表单配置的——填写文本框、从下拉列表中选择条目等等。然后,这些作业系列被串联起来,每个作业触发下一个作业,形成一个管道。

在 Jenkins 2 发布之前,Jenkins 在这个领域有些落后。尽管它被广泛使用,并且是创建 CI/CD 管道的首选工作流程工具,但这种方式创建和连接 Jenkins 作业以形成一个管道具有挑战性。它不符合 IaC 的定义。作业配置仅以可扩展标记语言 (XML) 文件的形式存储在 Jenkins 配置区域中。这意味着文件不易阅读或直接修改。Jenkins 应用程序本身提供了用户的主要视图和访问方式。

注意,Jenkins 2 是我们通常用于指代支持 pipeline-as-code 功能以及其他新特性的新版本的名称。

由于它是每个项目的重要部分,因此管道配置应作为代码进行管理并自动部署。这也允许我们管理管道本身,应用适用于应用程序代码的相同标准。这就是管道作为代码发挥作用的地方。

2.1 介绍 Jenkinsfile

管道 作为代码 (PaC) 描述了一组功能,允许 Jenkins 用户使用代码定义管道作业过程,这些代码存储和版本化在源代码仓库中。这些功能允许 Jenkins 发现、管理和运行多个源代码仓库和分支的作业—消除了手动创建和管理作业的需求。

PaC 帮助您以可重复、一致的方式自动化 CI/CD 工作流程,这带来了许多好处:

  • 速度—您可以快速轻松地为沙盒、预发布和生产环境编写 CI/CD 工作流程,这有助于您按时交付产品。

  • 一致性—PaC 完全标准化了 CI/CD 的设置,因此减少了任何人为错误或偏差的可能性。

  • 风险管理—由于管道可以进行版本控制,PaC 允许记录、日志记录、跟踪和测试您 CI/CD 工作流程中的每个更改,就像应用程序代码一样。因此,在出现故障的情况下,您可以回滚到工作版本。

  • 效率—它最小化了人为错误的发生,并有助于您的应用程序部署更加顺畅。

核心是简单的:采用 PaC 范式将创造一种产生更好软件的文化,并将节省您大量金钱、时间和因通过 UI 和网页表单实施复杂的 CI/CD 工作流程而带来的头痛。那么 PaC 是如何与 Jenkins 一起工作的呢?

要使用 PaC 与 Jenkins 一起使用,项目必须在代码仓库顶层文件夹中包含一个名为 Jenkinsfile 的文件。此模板文件包含一组指令或步骤,称为 阶段,每次开发团队将新功能推送到代码仓库时,都会在 Jenkins 上执行。由于 Jenkinsfile 与源代码一起生活,我们可以像对任何其他文件一样,始终在源代码控制中拉取、编辑和推送 Jenkinsfile。我们还可以对管道脚本进行代码审查。

Jenkinsfile 使用基于 Groovy 编程语言的领域特定语言 (DSL) 来定义整个 CI/CD 工作流程。图 2.1 是一个经典 CI/CD 工作流程的示例。

图片

图 2.1 CI/CD 工作流程

这些阶段可以使用 stage 关键字在 Jenkinsfile 中描述。阶段是一个包含一系列步骤的块。它可以用来可视化管道过程。以下列表是用于图 2.1 的简单 Jenkinsfile 的示例。

列表 2.1 Jenkinsfile 阶段

node('workers'){
    try {
        stage('Checkout'){
            checkout scm
        }

        stage('Quality Test'){.
            echo "Running quality tests"
        }

        stage('Unit Test'){
            echo "Running unit tests"
        }

        stage('Security Test').
            echo "Running security checks"
        }

        stage('Build'){.
            echo "Building artifact"
        }

        stage('Push'){.
           echo "Storing artifact"
        }

        stage('Deploy').
            echo "Deploying artifact"
        }

        stage('Acceptance Tests'){
            echo "Running post-integrations tests"
        }
    } catch(err){
        echo "Handling errors".
    } finally.
       echo "Cleaning up"
    }
}

我们将在下一章深入探讨语法,但就目前而言,让我们关注阶段正在做什么:

  • 检出—从源代码仓库拉取最新更改,这些仓库可以是 GitHub、Bitbucket、Mercurial 或任何源代码管理工具。

  • 质量测试—包含执行静态代码分析以衡量代码质量、识别错误、漏洞和代码问题的说明。可以通过集成外部工具(如 SonarQube)来自动化修复代码质量违规行为并减少技术债务。

  • 单元测试——在这个阶段,将执行单元测试。如果测试成功,将生成一个代码覆盖率报告,该报告可以被 Jenkins 插件消费,以显示项目的健康状况的可视概述,并跟踪随着项目增长而变化的代码覆盖率指标。代码覆盖率可以表明您的应用程序代码在测试期间执行的程度,并可以提供一些关于您的团队是否应用了良好的测试实践(如测试驱动开发(TDD)或行为驱动开发(BDD))的指示。

  • 安全测试——负责识别项目依赖项并检查是否存在任何已知、公开披露的漏洞。将发布一个安全报告,其中包含按严重程度(关键、高、中、低)分组的发现总数。一个著名的开源 Jenkins 插件是 OWASP Dependency-Check (mng.bz/MvR7)。

  • 构建——在这个阶段,将安装所需的依赖项,编译源代码,并构建一个工件(Docker 镜像、zip 文件、Maven JAR 等)。

  • 推送——前一个阶段构建的工件将被版本化并存储在远程仓库中。

  • 部署——在这个阶段,工件将被部署到沙盒/测试环境进行质量保证,或者用户批准部署后部署到生产环境。

  • 验收测试——更改部署后,将执行一系列的烟雾测试和验证测试,以验证部署的应用程序是否按预期运行。这些测试可以是简单的健康检查,使用 cURL 命令,或者复杂的 API 调用。

如果这些阶段中的任何一个抛出异常或错误,管道构建的状态将被设置为失败。这种默认行为可以通过使用try-catch块来覆盖。finally块可以用来清理 Jenkins 工作空间(临时文件或构建包)或执行后脚本命令,例如向开发团队发送 Slack 通知以提醒构建状态。

注意:如果您不完全理解列表 2.1 中 Jenkinsfile 的步骤,请不要担心。您将在第 7、8 和 9 章中深入解释如何实现每个阶段。

当谈到 CI 工具时,使 Jenkins 成为领导者之一的因素是其背后的生态系统。您可以使用免费的开源插件自定义您的 Jenkins 实例。一个必备的插件是 Pipeline Stage View (plugins.jenkins.io/pipeline-rest-api),如图 2.2 所示。它允许您可视化您的管道阶段。当您有复杂的构建管道并希望跟踪每个阶段的进度时,这个插件非常方便。

管道输出被组织成一个矩阵,每一行代表一个作业的运行,每一列映射到管道中定义的阶段。当你运行一些构建时,阶段视图将显示 Checkout(检出)、Quality Test(质量测试)、Unit Test(单元测试)、Security Test(安全测试)、Build(构建)、Push(推送)和 Deploy(部署)列,每行显示这些阶段的状态。当鼠标悬停在阶段单元格上时,你可以点击日志按钮来查看在该阶段打印的日志消息。

注意:本书的第三部分介绍了如何创建 Jenkins 作业并定义如图 2.2 所示的管道。

图片

图 2.2 Jenkins Pipeline 阶段视图

你可以将这个 UI 进一步扩展,并安装 Blue Ocean 插件(plugins.jenkins.io/blueocean/),以便快速直观地理解 CI/CD 阶段,如图 2.3 所示。此插件需要 Jenkins 版本 2.7 或更高版本。

图片

图 2.3 Blue Ocean 插件对管道的详细视图

注意:第五章介绍了如何安装和配置 Jenkins Blue Ocean 插件。

2.1.1 Blue Ocean 插件

你也可以通过点击红色的阶段来排查管道故障,轻松识别问题,而无需查看数千个输出日志。

在选择 Jenkins 时,用户界面是一个很大的担忧,许多用户认为它过时、不直观,当有多个项目时很难导航。这就是为什么 Jenkins 核心团队在 2017 年 4 月推出了 Blue Ocean,为 Jenkins 提供了一个新的、现代的用户体验。

Blue Ocean 是基于现代设计的新 Jenkins 用户体验,它允许用户图形化创建、个性化、可视化和诊断 CD 管道。它捆绑了 Jenkins Pipeline 插件,或作为单独的插件提供(www.jenkins.io/doc/book/blueocean/getting-started/)。

注意:Jenkins 经典 UI 与其通常的 JENKINS_URL/jenkins 位置并排存在。Blue Ocean 插件可以通过在 Jenkins 服务器 URL 末尾附加/blue来获取。

你的团队中的任何人都可以通过几步点击创建一个 CI/CD 管道。Blue Ocean 与 Git 和 GitHub 无缝集成。它提示你输入凭证以访问 Git 服务器上的存储库,以便根据这些存储库创建管道(图 2.4)。

图片

图 2.4 Blue Ocean 模式中的新管道

你还可以通过使用直观的视觉管道编辑器(图 2.5)从头到尾创建一个完整的 CI/CD 管道。这是一种编写管道原型和调试管道阶段的好方法,在生成可工作的 Jenkinsfile 之前。

图片

图 2.5 使用管道编辑器定义阶段

使用可视化编辑器创建的任何管道都可以在您喜欢的文本编辑器中进行编辑,从而带来 PaC 的所有好处。图 2.6 展示了 Windows 用户按 Ctrl-S 和 macOS 用户按 Command-S 按钮生成的管道脚本示例。

图片

图 2.6 由管道编辑器生成的 Jenkinsfile。

您现在可以将内容复制并粘贴到名为 Jenkinsfile 的新文件中,并将其放置在代码仓库中的源代码旁边。或者,您可以直接从 Blue Ocean 编辑器上传文件,提供适当的描述和目标 Git 分支(图 2.7)。

图片

图 2.7 将 Jenkinsfile 提交到 Git 仓库。

文件提交后,管道将被触发,并执行管道中定义的阶段。

请记住,Blue Ocean 不支持 Jenkins 的所有功能,例如管理、节点管理或凭据设置。然而,您始终可以通过点击 Blue Ocean 导航栏右上角的退出图标切换回经典 Jenkins UI。

注意:这只是 Blue Ocean 主要功能的预览。在第七章中,我们将更深入地探讨每个功能。

现在您已经熟悉了 Jenkinsfile 的工作原理,让我们看看如何使用 Jenkins 编写自己的管道代码。Jenkins 2 允许两种结构和语法的风格来构建工作流程。这些被称为脚本和声明式管道。

2.1.2 脚本管道

脚本管道是编写管道代码的传统方式。在这个管道中,Jenkinsfile 是在 Jenkins UI 实例上编写的。管道步骤被包裹在一个节点块中(由开闭花括号表示)。在这里,节点指的是 Jenkins 代理(以前称为 从属实例)。

节点通过标签映射到 Jenkins 集群。标签只是一个标识符,当通过 Jenkins 的“管理节点”部分配置节点时添加,如图 2.8 所示。

图片

图 2.8 分配标签到 Jenkins 工作节点

注意:下一章将介绍 Jenkins 分布式模式的工作原理以及如何使用节点代理来从 Jenkins 转移工作。

节点块内的步骤可以包括并使用任何有效的 Groovy 代码。可以通过创建一个新的管道项目并在管道编辑器部分输入代码来定义管道,如图 2.9 所示。

图片

图 2.9 使用内联 Jenkinsfile 与 Pipeline 脚本

虽然这个简单的节点块在技术上是一个有效的语法,但 Jenkins 管道通常具有更细粒度的级别——阶段。阶段是将管道划分为逻辑功能单元的一种方式。它还用于将步骤和 Groovy 代码分组在一起,以创建目标功能。图 2.10 展示了使用阶段的先前管道的示例。

图片

图 2.10 使用stage关键字定义逻辑单元

管道有两个阶段:

  • 检出—用于克隆项目的 GitHub 仓库

  • 构建—用于构建项目的 Docker 镜像

管道逻辑中有多少进入特定的阶段取决于开发者。然而,一般的做法是创建模仿传统管道各个部分的阶段。

脚本式管道使用基于 Groovy 的更严格的语法,因为它是在 Groovy 基础上构建的第一个管道。由于此 Groovy 脚本通常不适合所有用户,因此引入了声明式管道,以提供更简单、更灵活的 Groovy 语法。

注意:第十四章介绍了如何编写带有自定义 Groovy 脚本的共享 Jenkins 库,以实现代码模块化。

2.1.3 声明式管道

声明式管道是一个相对较新的功能(在 Pipeline 2.5 中引入,plugins.jenkins.io/workflow-aggregator),它支持 PaC 方法。它使得新 Jenkins 用户更容易阅读和编写管道代码。

此代码编写在 Jenkinsfile 中,可以将其检查到版本控制系统(VCS)中,如 SVN 或 SCM 系统,如 GitHub、GitLab、Bitbucket 或其他。图 2.11 是 GitHub 仓库根目录中存储的 Jenkinsfile 的一个示例。

图片

图 2.11 存储在源控制仓库中的 Jenkinsfile

在声明式语法中,您不能使用 Groovy 代码,如变量、循环或条件。您被限制在结构化部分/块和 DSL(Jenkins 领域特定语言)步骤中。

图 2.12 显示了脚本式和声明式管道之间的差异。声明式管道是受限的,并且具有明确的结构(例如,所有 DSL 语句都必须包含在steps指令中)。

图片

图 2.12 脚本式和声明式管道之间的差异

声明式管道提供了一种更严格的语法,因为每个管道都必须使用这些预定义的块属性或部分:

  • agent

  • environment

  • post

  • stages

  • steps

agent部分定义了管道将要执行的工作节点或机器。此部分必须在管道块内部的最顶层定义,或在阶段级别覆盖。代理可以是以下任何一种:

  • Jenkins 工作节点或节点(有关 Jenkins 上的分布式构建,请参阅第三章)

  • 基于 Docker 镜像或自定义 Dockerfile 的 Docker 容器(在第九章中介绍)

  • 部署在 Kubernetes 集群上的 Pod(在第十四章中介绍)

例如,您可以定义管道在自定义 Docker 容器上运行,如下所示。

列表 2.2 声明式管道代理定义

pipeline{
    agent {
        node {
            label 'workers'
        }

        dockerfile {
            filename 'Dockerfile'
            label 'workers'
        }

        kubernetes {
            label 'workers'
            yaml """
            kind: Pod
            metadata:
            name: jenkins-worker
            spec:
            containers:
            - name: nodejs
              image: node:lts
              tty: true
            """
        }
    }
}

注意:有关代理语法的更多信息,请参阅官方文档:www.jenkins.io/doc/book/pipeline/syntax/

environment部分包含了一组运行管道步骤所需的环境变量。如果环境块在管道顶层定义,则这些变量将可用于所有步骤;否则,变量可以是阶段特定的。您还可以通过使用辅助方法credentials()来引用凭据变量,该方法以目标凭据的 ID 作为参数,如下面的列表所示。

列表 2.3 环境变量定义

pipeline{
    environment {
        REGISTRY_CREDENTIALS= credentials('DOCKER_REGISTRY')
        REGISTRY_URL = 'https://registry.domain.com'
    }

    stages {
        stage('Push'){
            steps{
                sh 'docker login $REGISTRY_URL --username $REGISTRY_CREDENTIALS_USR --password $REGISTRY_CREDENTIALS_PSW'
            }
        }
    }
}

通过引用REGISTRY_CREDENTIALS_USRREGISTRY_CREDENTIALS_PSW环境变量,Docker 注册表的用户名和密码可以自动访问。然后,这些凭据被传递给docker login命令,在推送 Docker 镜像之前与 Docker 注册表进行身份验证。

post部分包含在管道或阶段运行完成后要运行的命令或脚本,具体取决于此部分在管道中的位置。然而,传统上,post部分应该放在管道的末尾。可以在post部分使用的命令示例包括提供 Slack 通知、清理作业工作空间以及根据构建状态执行后脚本。可以通过使用currentBuild.result变量或后置条件块alwayssuccessunstablefailure等来获取管道构建状态。

以下列表是一个示例 Slack 通知。由always指令包裹的指令将无论构建状态如何都会运行,并且不会干扰最终状态。

列表 2.4 声明式管道中的构建后操作

pipeline{
    post {
        always {
            echo 'Cleaning up workspace'
        }
        success {
            slackSend (color: 'GREEN', message: \
                "${env.JOB_NAME} Successful build")
        }
        failure {
           slackSend (color: 'RED', message: "${env.JOB_NAME} Failed build")
        }
    }
}

此代码引用了env.JOB_NAME变量,它包含 Jenkins 作业的名称。

注意:第十章有专门的部分介绍如何使用 Jenkins 实现 Slack 通知。

stages部分是管道的核心。本部分定义了在高级别上要执行的操作。它包含了一系列针对 CI/CD 工作流程每个离散部分的更多阶段指令。

最后,steps部分包含了一系列在给定阶段指令中要执行的步骤。以下列表定义了一个Test阶段,其中包含运行单元测试和生成代码覆盖率报告的指令。

列表 2.5 在管道中运行自动化测试

pipeline{
    agent any
    stages {
        stage('Test'){
            steps {
                sh 'npm run test'
                sh 'npm run coverage'
            }
        }
    }
}

这些是在编写声明式管道时最常用的指令和部分。本书将涵盖更多指令。有关所有可用块的概述,请参阅 Pipeline 语法文档(www.jenkins.io/doc/book/pipeline/syntax/#stages)。

声明性和脚本样式都可以用于在 Web UI 或使用 Jenkinsfile 中构建 CI/CD 管道。然而,通常认为将 Jenkinsfile 创建并提交到源代码控制仓库是一个最佳实践,以确保有一个单一的事实来源,并能够跟踪管道所经历的所有更改(审计)。

注意:在第七章至第十一章中,您将学习如何从头开始为各种应用程序架构编写脚本管道,以及如何将 Jenkinsfile 从脚本格式转换为声明性格式。

2.2 理解多分支管道

当您构建应用程序时,您必须将您的部署环境分开,以测试新更改而不影响生产。因此,为您的应用程序拥有多个环境是有意义的。为了能够实现这一点,您需要结构化您的代码仓库以使用多个分支,每个分支代表一个环境。例如,master 分支对应于当前的生产代码。

虽然如今随着云计算和 IaC 工具的采用,复制多个基础设施环境变得更加容易,但您仍然需要为每个目标分支配置 CI 工具。

幸运的是,在使用 Jenkinsfile 时,您的管道定义与应用程序代码源一起存储。Jenkins 将自动扫描应用程序代码仓库中的每个分支,并检查该分支是否有 Jenkinsfile。如果有,Jenkins 将自动在多分支管道项目中创建和配置一个子项目来运行该分支的管道。这消除了手动创建和管理管道的需求。

图 2.13 显示了在执行 Jenkinsfile 和源代码仓库后,多分支管道项目中的作业。Jenkins 自动扫描指定的仓库,并为包含 Jenkinsfile 的每个分支创建适当的项目。

图 2.13 显示了 Jenkins 为每个带有 Jenkinsfile 的分支自动创建作业。

在图 2.13 中,每当 develop、preprod 或 master 分支中的任何分支发生新的代码更改时,Jenkins 都会触发构建。此外,每个分支可能具有不同的管道阶段。例如,您可能为 master 分支执行完整的 CI/CD 管道,而为 develop 分支仅执行 CI 管道(见图 2.14)。您可以使用多分支管道项目来实现这一点。

图 2.14 每个 Git 分支都可以有自己的 Jenkinsfile 阶段。

多分支管道还可以用于在合并到目标分支之前验证拉取请求。您可以配置 Jenkins 对应用程序代码执行预集成测试,如果测试失败,则阻止拉取请求合并,如图 2.15 所示。

图 2.15 GitHub 拉取请求中的 Jenkins 构建状态

注意:第九章介绍了使用多分支管道来验证拉取/更改请求。

现在你已经熟悉了 Jenkins 多分支管道的基本知识,你必须遵循 Git 分支指南,以便在开发团队内部有一个共同的愿景和方法。那么,你应该为你的开发周期使用哪种 Git 分支策略?

2.3 探索 GitFlow 分支模型

存在几种 Git 分支策略。最有趣且使用最广泛的是 GitFlow。它包括以下基本分支:

  • 主分支—一个对应当前生产代码的分支。除了热修复之外,你不能直接提交。可以使用 Git 标签来为主分支上的所有提交添加版本号(例如,你可以使用在semver.org/中详细说明的语义版本控制约定)。

  • 预生产—一个发布分支,是生产的镜像。在将它们合并到主分支之前,可以用来测试在开发分支上开发的所有新功能。

  • 开发—一个包含最新集成开发代码的开发集成分支。

  • 功能/X—一个正在开发中的单个功能分支。每个新功能都位于自己的分支中,通常是从最新的开发分支创建的。

  • 热修复/X—当你需要在生产代码中解决问题时,你可以使用热修复分支并为主分支打开一个拉取请求。这个分支基于主分支。

注意:在第七章到第十一章中给出了一个使用 GitFlow 与 Jenkins 多分支管道项目的完整示例。

Jenkins 中 GitFlow 的整体流程可以总结如下:

  • 从主分支创建一个开发分支。

  • 从开发分支创建一个预生产分支。

  • 开发者基于开发分支创建一个新的功能分支。当功能完成时,创建一个拉取请求。

  • Jenkins 会自动运行此单个功能的预集成测试。如果测试成功,Jenkins 会将提交标记为成功。然后,开发团队将审查更改并将新功能分支的拉取请求合并到开发分支,并删除功能分支。

  • 将在开发分支上触发构建,并将更改部署到沙盒/开发环境。

  • 创建一个拉取请求以将开发分支合并到预生产分支。

  • 当开发分支合并到预生产分支时,管道将在管道完成后触发将新功能部署到预发布环境。

  • 一旦发布开始验证,预生产分支将被合并到主分支,并在用户批准后,将更改部署到生产环境。

  • 如果在生产中检测到问题,将从一个主分支创建一个热分支。一旦热修复完成,它将被合并到开发和主分支。

注意:您可以使用 Git 命令行周围的 GitFlow 包装器(在多个操作系统上可用)来创建包含所有所需分支的项目蓝图。

图 2.16 总结了 GitFlow 的工作方式。

图 2.16 GitFlow 分支概述

GitFlow 并不能解决所有关于分支的问题。但是,当在大团队中工作时,它提供了一个更合理的分支结构和优秀的流程组织模型。此外,许多功能分支是并发开发的,这使得并行开发变得容易。对于较小项目(和较小团队),GitFlow 可能有些过度。因此,在接下来的章节中,我们通常会使用三个主要分支:

  • 分支,用于存储官方发布历史和在生产环境中运行的应用程序的源代码

  • 预生产分支,用于存储在预发布环境中运行的新集成功能和准备合并到主分支的内容

  • 开发分支,用于最新交付的开发更改和沙盒环境中运行的应用程序的镜像

2.4 使用 Jenkins 进行测试驱动开发

使用 Jenkinsfile 有一个潜在的缺点:当您在外部文件中工作而不是在 Jenkins 服务器环境中时,可能更难在前期发现问题。处理这个问题的一个方法是在 Jenkins 服务器上首先作为一个管道项目开发代码。然后,您可以在之后将其转换为 Jenkinsfile。

您还可以像本章前面所看到的那样,使用 Blue Ocean 模式作为游乐场,从头开始使用现代直观的管道编辑器设置 Jenkinsfile。测试新管道的另一种方法是声明性管道 lint 器应用程序,您可以在 Jenkins 之外运行它,以提前检测问题。

2.4.1 Jenkins 回放按钮

有时,当在 Jenkins 作业上工作时,您可能会发现自己陷入提交 Jenkinsfile、推送它并反复运行作业的循环。这可能是一个耗时且繁琐的工作流程,尤其是如果您的构建时间本身就很长。此外,您的 Git 历史记录将被垃圾提交(不必要的调试提交)填满。

如果您能在“沙盒”中修改您的 Jenkinsfile 并实时在系统上测试 Jenkinsfile,会怎么样?一个巧妙的小功能允许您修改 Jenkins 文件并重新运行作业。您可以反复进行,直到对结果满意,然后提交工作状态的 Jenkinsfile 而不会破坏任何东西。

现在,这要容易一些。如果您有一个没有按照预期进行构建的 Pipeline 构建,您可以使用构建侧边栏中的回放按钮,如图 2.17 所示。

图 2.17 使用回放按钮重新运行构建

它与重建按钮有些相似,但允许你在运行作业之前编辑 Jenkinsfile 的内容。因此,你可以使用 UI 中的内置 Jenkinsfile 块(图 2.18),在提交到源代码控制(如 GitHub)之前测试你的管道。

图片

图 2.18 在重新播放管道之前更新 Jenkinsfile

你可以更改你的管道代码并点击运行按钮重新运行作业。一旦你对更改满意,你就可以更新 Jenkinsfile 并应用更改,然后将它们提交到你的源代码管理(SCM)。

重放按钮功能允许快速修改和执行现有的管道,而无需更改管道配置或创建新的提交。这对于快速迭代和原型设计管道非常理想。

2.4.2 命令行管道检查器

对于高级用户,你可以使用 Jenkins RESTful API 通过发出如图 2.19 所示的参数的 HTTP/HTTPS POST 请求来验证 Jenkinsfile 的语法。

注意:为了在启用了跨站请求伪造(CSRF)保护的 Jenkins 服务器上使 API 端点工作,你需要请求一个 crumb 发行者并将其包含在发出的 HTTP 请求的 Authorization 头中。要生成这个 crumb,你需要请求以下 URL:JENKINS_URL/jenkins/crumbIssuer/api/json。

图 2.19 是使用 Jenkins Linter API 验证 Jenkinsfile 语法的示例。在这个例子中,我们使用了 Postman,并且 Jenkinsfile 表单数据已从开发机器加载。

图片

图 2.19 使用 Jenkins Linter API 的示例

API 响应将返回错误和警告,这可以在开发过程中节省时间,并允许你在编写 Jenkinsfile 时遵循最佳实践。

支持指定真实密码,但出于泄露密码的风险以及人们倾向于在不同地方重复使用相同密码的倾向,这并不推荐。验证 Jenkinsfile 的另一种方法是,从终端会话中运行以下命令(大多数操作系统都可用 cURL):

curl -X POST -L --user USERNAME:TOKEN JENKINS_URL/pipeline-model-converter/validat.
-F "jenkinsfile=<Jenkinsfile"

注意:第七章介绍了从 Jenkins Web 仪表板创建 Jenkins API 令牌的另一种方法。

Jenkins 命令行界面(CLI),www.jenkins.io/doc/book/managing/cli/,也可以使用declarative-lint选项在命令行中检查声明式管道,在真正运行之前。你可以通过 SSH 使用以下命令来检查 Jenkinsfile:

ssh -p $JENKINS_SSHD_PORT $JENKINS_HOSTNAME declarative-linter < Jenkinsfile

根据你运行 Jenkins 的 URL 和端口替换JENKINS_HOSTNAMEJENKINS_SSHD_PORT变量。如果你在自己的机器上运行 Jenkins,也可以使用 localhost 作为 URL。

2.4.3 IDE 集成

Jenkins CLI 或 API 在编写 Jenkinsfile 时减少了周转时间,但它的使用也有其不便之处。你需要像 SSH 这样的工具来连接到你的 Jenkins 服务器,并且你需要记住正确的命令来验证你的 Jenkinsfile。

幸运的是,你可以在你喜欢的集成开发环境(IDE)上安装扩展来自动化验证过程。例如,在 Visual Studio Code (VSCode) 中,你可以从市场安装 Jenkins Validation Linter。这个扩展,如图 2.20 所示,通过将 Jenkinsfile 发送到 Jenkins 服务器的 Pipeline Linter 端点来验证 Jenkinsfile。

注意:类似扩展和包可用于验证 Eclipse、Atom 和 Sublime Text 的 Jenkinsfile。

图 2.20 VSCode 的 Jenkins Pipeline Linter 扩展

一旦安装了扩展,你必须通过点击顶部导航栏中的“首选项”,然后选择“设置”,提供 Jenkins 服务器设置,包括服务器 URL(格式如下:JENKINS_URL/pipeline_model_converter/validate)和凭据(Jenkins 用户名和密码,或者如果启用了 CSRF 保护,则为令牌)。

图 2.21 Jenkins Pipeline Linter 配置

一旦配置了设置,你可以在命令面板搜索栏中输入“验证 Jenkinsfile”命令(关键字快捷键 ⇧⌘P),如图 2.22 所示。

图 2.22 VSCode 命令面板

检查器将在终端中报告管道验证结果,如图 2.23 所示。

图 2.23 Jenkins Linter 输出示例

注意:在第八章中,你将学习如何为 CI 管道编写单元测试,并使用 Jenkins Pipeline Unit (github.com/jenkinsci/JenkinsPipelineUnit) 测试框架在本地模拟管道执行器。

摘要

  • 基础设施即代码影响了 CI/CD 工具,使其接受管道即代码的概念。

  • Jenkinsfile 使用 Groovy 语法,并利用共享的 Jenkins 库来自定义 CI/CD 工作流程。

  • 声明式管道鼓励声明式编程模型。脚本式管道遵循更命令式的编程模型。

  • 蓝海编辑器可以简化新 Jenkins 管道的快速和轻松设置,最小化麻烦。

  • 功能分支工作流程促进了拉取请求和更高效的协作。

  • GitFlow 为生产提供了专门的通道,以进行热修复,而不会中断其他工作流程或等待下一个发布周期。

  • Jenkins UI、回放按钮和代码检查器可以用来在提交到源代码控制之前测试新的管道,从而帮助你避免大量不必要的调试提交。

5 使用 Terraform 发现 Jenkins 作为代码

本章涵盖

  • 介绍基础设施即代码 (IaC)

  • 使用支持基础设施即代码 (IaC) 的 HashiCorp Terraform

  • 在安全的私有网络中部署 Jenkins

  • 使用 AWS Auto Scaling 动态扩展 Jenkins 工作节点

在上一章中,我们使用了 HashiCorp Packer 来创建自定义 Jenkins 机器镜像;在本章中,我们将使用这些镜像(图 5.1)来部署机器。为此,我们将编写我们希望存在的 Jenkins 基础设施声明性定义,并使用自动化工具在给定的基础设施即服务 (IaaS) 提供商上部署资源。

在过去,管理 IT 基础设施是一项艰巨的工作。系统管理员必须手动管理和配置所有必需的硬件和软件,以便应用程序运行。然而,近年来,情况发生了巨大变化。云计算等趋势彻底改变并改善了组织设计、开发和维护 IT 基础设施的方式。这一趋势的关键组成部分之一被称为基础设施即代码。

图 5.1 Jenkins 自定义机器镜像

5.1 介绍基础设施即代码

基础设施 即代码 (IaC) 允许您通过使用配置文件来管理您的基础设施。这降低了成本,减少了风险,并在云上更快地部署资源。另一个好处是,您的基础设施变得可测试、可重复、自我修复、幂等,最重要的是,易于理解,因为您的基础设施代码本质上将是您的文档。

可用几种 IaC 工具,每种都有自己的实现(图 5.2)。一些工具专注于特定的云,包括 AWS CloudFormation (aws.amazon.com/cloudformation/)、Azure Resource Manager (azure.microsoft.com/features/resource-manager/)、OpenStack Heat (wiki.openstack.org/wiki/Heat) 和 Google Cloud Deployment Manager (cloud.google.com/deployment-manager)。其他工具试图连接所有云提供商并屏蔽它们的语义差异,以提供云无关的实现。这一类别包括 HashiCorp Terraform、HashiCorp Vagrant、Chef Provisioning 和 Pulumi。

图 5.2 基础设施即代码工具

在本书中,我们将专注于使用 HashiCorp Terraform 来部署 Jenkins 组件。Terraform 提供了灵活的资源和服务提供者抽象,平台无关,并支持多个 IaaS 提供商,如 AWS、Microsoft Azure、Google Cloud Platform 和 DigitalOcean。此外,Terraform 是开源的,并附带简单统一的语法,对于新用户来说没有陡峭的学习曲线,并且对于任何基础设施部署用例都有易于访问的在线资源。

注意:配置管理工具如 Ansible 和 Puppet 是为了在现有服务器上安装和管理配置而构建的。Terraform 专注于服务器的引导和初始化以及其他基础设施资源。

在接下来的几节中,您将学习如何使用 Terraform 在 AWS 上部署 Jenkins 集群。

5.1.1 Terraform 使用

Terraform 使用推送方法:开发者或运维工程师在模板文件中描述所需的架构,Terraform 通过其 API 直接与云服务提供商交互。例如,如果目标云服务提供商是 AWS,Terraform 使用 Terraform AWS 提供商插件 (registry.terraform.io/providers/hashicorp/aws/latest),它底层使用 AWS 官方 SDK 来创建/更新或销毁资源。

为了维护基础设施的期望状态并检测变更,Terraform 生成一个名为 terraform.tfstate 的 JSON 文件,该文件存储了您管理的基础设施和配置的状态。Terraform 使用差异技术来在任何操作之前检测变更。因此,个人和团队可以安全且可预测地更改基础设施。

Terraform 本身是一个 CLI 工具,可以从其官方发布页面 (www.terraform.io/downloads.html) 下载,如图 5.3 所示,通过安装适用于您操作系统和架构的二进制文件。它支持所有主要操作系统。Windows、macOS 以及任何 Linux 发行版都支持 32 位和 64 位版本。

图 5.3 Terraform 下载页面

下载 zip 压缩文件后,将其解压到任何方便的文件夹中。请确保此文件夹包含在您的 PATH 环境变量中。要检查 Terraform 是否正确安装,请执行以下命令:

terraform --version

注意:在编写本书时,HashiCorp Terraform 的最新稳定版本是 1.0.0。

如果您得到类似 Terraform vX.Y.Z 的输出,恭喜!您已成功安装 Terraform。我们现在可以编写 Terraform 模板文件了。

5.2 部署 AWS VPC

如第三章所述,我们的 Jenkins 集群将部署在 VPC 内的私有子网中;请参阅图 5.4。我们可以将集群部署在 AWS 创建的默认 VPC 中。然而,为了完全控制网络拓扑,我们将从头创建一个 VPC,以将 Jenkins 集群与我们将在高级章节中部署的应用程序工作负载隔离开来。以下方案总结了目标 VPC 架构:

注意:为了理解 Amazon VPC 术语(子网、安全组、路由表等),请参阅第三章。

图片

图 5.4 AWS 虚拟专用云架构

从本质上讲,这个 VPC 将被划分为子网。一些子网将是公共的,可以访问互联网;而一些将是私有的。然后,我们定义子网之间的路由规则,允许流量通过互联网网关或 NAT 网关进行传输。我们还将部署一个堡垒主机,以便能够 SSH 到 Jenkins 的私有实例,而无需将其暴露给公众。

5.2.1 AWS VPC

Terraform 使用一种称为 HashiCorp 配置语言 (HCL) 的 DSL,这是一种声明性语言,用于描述基础设施资源。这些资源以 .tf 扩展名的简单文本文件进行描述。

我们不会编写一个大型的模板文件,而是将采用模块化开发方法,并将 Jenkins 集群部署拆分为多个模板文件。每个文件负责部署目标基础设施的一个组件或 AWS 资源。首先,创建一个包含以下内容的 terraform.tf 文件:

provider "aws" {
  region                  = var.region
  shared_credentials_file = var.shared_credentials_file
  profile                 = var.aws_profile
}

注意:在本章的其余部分,Terraform 将本地存储状态,这对于团队协作来说并不理想,因为状态可能包含敏感信息(如果您计划使用版本控制系统进行版本控制)。我建议使用远程后端,如 Amazon S3 来存储状态。

为了使 Terraform 能够与 IaaS 交互,它需要配置一个提供程序。在前面的代码块中,我们定义了 AWS 作为提供程序,并配置了与 AWS API 交互所需的凭据,以便随后创建 AWS 资源。AWS 提供程序支持多种身份验证方法:

  • 通过在 aws 提供程序块中内联提供 access_keysecret_key 属性来提供静态凭据。

  • 通过 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 变量提供环境变量。

  • 默认情况下,位于 Linux 和 macOS 的 ~/.aws/credentials 文件以及 Windows 用户的 %USERPROFILE%.aws\credentials 文件中的共享凭据文件。默认情况下,Terraform 将检查这些位置,但您可以通过提供 shared_credentials_file 属性在配置中指定不同的位置。此外,如果您在凭据文件中定义了多个配置文件,您可以通过设置 profile 属性通过 AWS_PROFILE 环境变量来指定要使用的配置文件。

  • 如果你从 EC2 实例使用 Terraform,则需要一个 EC2 IAM 实例配置文件。Terraform 将从实例的元数据中获取临时访问令牌。当在 EC2 实例中运行时,这是一种比前面策略更受欢迎的方法,因为它可以避免硬编码凭证。

接下来,我们将在 vpc.tf 文件中声明一个 AWS VPC 资源。以下代码片段使用 CIDR 块 10.0.0.0/16 为 VPC,但您可以选择不同的 CIDR 块:

resource "aws_vpc" "default" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true

  tags {
    Name   = var.vpc_name
    Author = var.author
  }
}

注意:所有可用的 AWS 资源都可以在 Terraform AWS 文档中找到,请参阅www.terraform.io/docs/providers/aws/index.html

注意使用变量而不是硬编码的值来创建可重用的资源(可移植性)并允许用户在运行时覆盖它们。我们将在 variables.tf 文件中定义变量列表,如下所示。

列表 5.1 Terraform 变量文件

variable "region" {
  description = "AWS region"
  type = string
}

variable "cidr_block" {
  description = "VPC CIDR block"
  default     = "10.0.0.0/16"
}

Terraform 变量是通过variable块创建的。它们有一个名称,以及可选的类型、默认值和描述参数。表 5.1 提供了变量的完整列表。

表 5.1 VPC 的 Terraform 变量

变量 类型 描述
region 字符串 None 部署 VPC 的区域名称,例如eu-central-1
shared_credentials_file 字符串 ~/.aws/credentials 共享凭证文件的路径。如果未设置且指定了配置文件,则使用~/.aws/credentials
aws_profile 字符串 profile 在共享凭证文件中设置的 AWS 配置文件名称。
cidr_block 字符串 10.0.0.0/16 VPC 的 CIDR 块。允许的块大小介于/16 子网掩码(65,536 个 IP 地址)和/28 子网掩码(16 个 IP 地址)之间。
vpc_name 字符串 management 确保您的 VPC 使用适当的命名来标记,以便更有效地管理,并遵循 AWS 资源标记最佳实践。
author 字符串 None VPC 的所有者名称。这是可选的,但建议您对 AWS 资源进行标记,以便按所有者或环境跟踪月度成本。

在运行 Terraform 之前,我们需要安装 Terraform 的 AWS 插件。您可以通过执行以下命令来完成此操作:

terraform init

这将安装 AWS 提供者插件并初始化一个新的配置:

注意:为了能够使用本章中的 Terraform 示例,请将 VPCFullAccess 策略添加到与 Terraform 关联的 IAM 用户。

使用以下命令生成将要应用的变化的执行计划(用于干运行):

terraform plan --var-file="variables.tfvars"

当运行terraform plan时,您可以使用-var选项在命令行上指定单个变量。然而,由于我们需要设置很多变量,使用名为 variables.tfvars 的变量定义文件会更方便和实用。

此文件包含在 variables.tf 文件中声明的动态变量,例如 AWS 区域和凭证文件。任何你定义了值的变量都需要存在于 variables.tf 中,如下所示。

列表 5.2 Terraform 动态变量

region="YOUR AWS REGION"
shared_credentials_file="PATH TO .aws/credentials FILE"
aws_profile="AWS PROFILE"
author="AUTHOR NAME"

注意:如果你将变量定义文件命名为 terraform.tfvars 或 terraform.tfvars.json,它们将被 Terraform 自动加载。

你也可以从环境变量中加载变量。Terraform 将解析任何以TF_VAR为前缀的环境变量。例如,如果 Terraform 找到一个名为TF_VAR_aws_profile的环境变量,它将使用其值作为aws_profile变量的字符串值。

terraform plan命令将显示目标计划,这在提前验证更改和避免不希望的变化方面特别有用。输出应该看起来像这样:

注意:我强烈建议加密状态和计划文件,因为它们可能存储机密信息。

我们可以看到将创建一个资源。现在我们放心了,Terraform 将会做正确的事情!我们可以使用以下命令应用更改:

terraform apply --var-file="variables.tfvars"

输入yes以应用操作,Terraform 将创建 AWS VPC 资源:

在 AWS VPC 仪表板上,你应该看到一个新的 VPC,名为management,并创建了 10.0.0.0/16 CIDR 块,如图 5.5 所示。

图 5.5 AWS VPC 仪表板

太棒了——我们有一个定制的 VPC!

5.2.2 VPC 子网

创建一个 VPC 是不够的;为了能够在这个隔离的网络中放置 Jenkins 实例,我们还需要一个子网。这个子网属于之前创建的 VPC,因此我们在创建它时必须传递一个 VPC ID。不过,我们不需要硬编码它。Terraform 通过插值语法允许我们通过其 ID 引用任何其他资源。

创建一个 subnets.tf 文件,其中包含两个公共子网和两个私有子网,分别位于不同的可用区以实现弹性,如下所示。每个子网都有自己的 CIDR 块,它是 VPC CIDR 块的子集。

列表 5.3 VPC 子网

resource "aws_subnet" "public_subnets" {
  vpc_id                  = aws_vpc.management.id
  cidr_block              = "10.0.${count.index * 2 + 1}.0/24"            ❶
  availability_zone       = element(var.availability_zones, count.index)  ❶
  map_public_ip_on_launch = true                                          ❷

  count = var.public_subnets_count

  tags = {
    Name   = "public_10.0.${count.index * 2 + 
              1}.0_${element(var.availability_zones, count.index)}"       ❸
    Author = var.author
  }
}

resource "aws_subnet" "private_subnets" {
  vpc_id                  = aws_vpc.management.id
  cidr_block              = "10.0.${count.index * 2}.0/24"
  availability_zone       = element(var.availability_zones, count.index)
  map_public_ip_on_launch = false

  count = var.private_subnets_count

  tags = {
    Name   = "private_10.0.${count.index * 2}.0_${element(var.availability_zones, count.index)}"
    Author = var.author
  }
}

count.index变量具有独特的索引号(从 0 开始),用于在 10.0.0.0/16 范围内构建唯一的 CIDR 块

❷ 指定 true 以指示在子网中启动的实例应分配一个公共 IP 地址

❸ 为子网提供一个唯一的名称;例如,public_10.0.0.0_eu-central-1

代码使用count属性进行插值,为我们提供了一个参数化的子网。有了这个,我们可以使用如10.0.${count.index*2+1}.0/24之类的表达式来计算子网 CIDR 块。你也可以使用cidrsubnet(prefix, newbits, netnum)方法来计算 VPC CIDR 块内的子网地址。(有关更多详细信息,请参阅mng.bz/WBj0上的文档。)

在 variables.tf 文件中将子网默认数量设置为 2,并定义子网所在的可用区作为变量。(您可以使用 aws ec2 describe-availability-zones 命令查看 AWS 区域内的可用区。)表 5.2 提供了 Terraform 变量的完整列表。

表 5.2 子网 Terraform 变量

变量 类型 描述
availability_zones 列表 启动 VPC 子网可用区的列表
public_subnets_count 数字 2 要创建的公共子网数量
private_subnets_count 数字 2 要创建的私有子网数量

运行 terraform plan 命令以生成操作计划。这验证了将应用于当前基础设施的配置:

如果您对部署计划感到满意,请使用 terraform apply 命令应用配置。子网应创建在 VPC 内,如图 5.6 所示。

图 5.6 VPC 的公共和私有子网

在创建 VPC 和子网之后,您需要创建私有和公共路由表以定义 VPC 子网中的流量路由机制。

5.2.3 VPC 路由表

如前所述,VPC 的典型配置将其分为公共和私有子网。为了让部署在私有子网中的实例能够访问互联网而不暴露在公共网络中,我们将创建私有和公共路由表以进行细粒度的流量控制。

创建一个 public_rt.tf 文件,定义一个互联网网关资源,并将其附加到之前创建的 VPC:

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.management.id

  tags = {
    Name   = "igw_${var.vpc_name}"
    Author = var.author
  }
}

在 public_rt.tf 文件中,定义一个公共路由表和一个将所有流量(0.0.0.0/0)指向互联网网关的路由:

resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.management.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name   = "public_rt_${var.vpc_name}"
    Author = var.author
  }
}

到目前为止,公共路由表尚未与任何子网关联。您需要将其与 VPC 中的公共子网关联,以便来自这些子网的流量被路由到互联网网关:

resource "aws_route_table_association" "public" {
  count          = var.public_subnets_count
  subnet_id      = element(aws_subnet.public_subnets.*.id, count.index)
  route_table_id = aws_route_table.public_rt.id
}

注意:我建议在用 Terraform 部署资源之前生成执行计划,以避免 Terraform 操作基础设施时出现任何意外。

一旦您使用 terraform apply 应用 Terraform 变更,请转到 VPC 仪表板并跳转到路由表部分。您应该看到公共路由表,如图 5.7 所示。

图 5.7 VPC 的公共路由表

在创建公共路由表后,继续创建私有路由表。

创建一个 private_rt.tf 文件,并在公共子网内定义一个 NAT 网关资源,以使稍后将在私有子网中部署的 Jenkins 实例能够连接到互联网。然后,将弹性 IP 地址与 NAT 网关关联,如下所示。

列表 5.4 VPC NAT 网关

resource "aws_eip" "nat" {
  vpc = true

  tags = {
    Name   = "eip-nat_${var.vpc_name}"
    Author = var.author
  }
}

resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = element(aws_subnet.public_subnets.*.id, 0)

  tags = {
    Name   = "nat_${var.vpc_name}"
    Author = var.author
  }
}

在同一文件中,创建一个具有将所有流量(0.0.0.0/0)转发到您创建的 NAT 网关 ID 的私有路由表,如下所示。

列表 5.5 私有路由表

resource "aws_route_table" "private_rt" {
  vpc_id = aws_vpc.management.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat.id
  }
  tags = {
    Name   = "private_rt_${var.vpc_name}"
    Author = var.author
  }
}

注意:如果您更喜欢管理 NAT 实例,可以将指向 NAT 网关的当前路由替换为指向 NAT 实例的路由。

最后,使用以下代码块将私有子网分配给私有路由表:

resource "aws_route_table_association" "private" {
  count          = var.private_subnets_count
  subnet_id      = element(aws_subnet.private_subnets.*.id, count.index)
  route_table_id = aws_route_table.private_rt.id
}

弹性 IP 地址是一个静态的公共 IPv4 地址,因此通过快速将地址重新映射到另一个 NAT 网关来掩盖 NAT 网关的故障可能是有用的。

使用terraform apply应用基础设施更改。应创建一个私有路由表,如图 5.8 所示。

图 5.8 VPC 的私有路由表

应创建一个额外的路由表规则,将互联网流量指向 NAT 网关。这使私有子网中的 Jenkins 实例能够访问互联网。

我们的 Jenkins 集群将部署在私有子网中。因此,实例不会从互联网公开访问(因为集群没有公共 IP)。为了安全地访问 Jenkins 实例,我们将部署一个堡垒主机。

注意:如果您设置了类似 OpenVPN Access Server 的远程访问虚拟专用网络(VPN),则可以跳过此解决方案。有关说明,请参阅官方指南openvpn.net/aws-video-tutorials/byol/

5.2.4 VPC 堡垒主机

一个堡垒主机,也称为跳板机,通过一个受控的入口点提供对位于私有子网中的 EC2 实例的安全访问。堡垒主机是一种专用机器,部署在公共子网中,并可以访问私有子网内的私有实例。

这些实例通过 SSH 或 RDP 协议访问。与堡垒主机建立连接后,它允许使用 SSH 或 RDP 登录到其他实例。这样,它就像一个跳板。

在新的 bastion.tf 文件中,定义一个位于公共子网中的 EC2 实例资源,以便从外部互联网访问它:

resource "aws_instance" "bastion" {
  ami           = data.aws_ami.bastion.id
  instance_type = var.bastion_instance_type
  key_name = aws_key_pair.management.id
  vpc_security_group_ids = [aws_security_group.bastion_host.id]
  subnet_id = element(aws_subnet.public_subnets, 0).id
  associate_public_ip_address = true

  tags = {
    Name = "bastion"
    Author = var.author
  }
}

EC2 实例使用 Amazon 2 Linux 机器镜像。我们使用aws_ami数据源从 AWS 市场获取 AMI ID。启用most_recent属性以使用最近的 AMI,如果返回多个结果:

data "aws_ami" "bastion" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-ebs"]
  }
}

注意:如果您想为堡垒主机添加额外的安全层,可以使用与第四章中描述的相同程序使用 HashiCorp Packer 创建自己的机器镜像。

在创建 EC2 时,我们附加了一个 SSH 密钥对,以便能够使用私钥通过 SSH 访问堡垒主机。该密钥对使用位于工作目录下的.ssh 文件夹中的我们的公共 SSH 密钥。您也可以使用ssh-keygen命令生成一个新的密钥对。以下是一个 Terraform 代码片段;aws_key_pair资源将 SSH 公钥文件位置作为参数:

resource "aws_key_pair" "management" {
  key_name   = "management"
  public_key = file(var.public_key)
}

默认情况下,SSH 访问新创建的 EC2 实例是禁用的。为了允许对堡垒主机进行 SSH 访问,我们将安全组关联到正在运行的实例。该安全组将允许来自任何地方(0.0.0.0/0)的端口 22(SSH)的入站(入口)流量。CIDR 源块可以用你自己的公共 IP 地址/32 或网络地址替换,以增强安全性并防止安全漏洞:

resource "aws_security_group" "bastion_host" {
  name        = "bastion_sg_${var.vpc_name}"
  description = "Allow SSH from anywhere"
  vpc_id      = aws_vpc.management.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name   = "bastion_sg_${var.vpc_name}"
    Author = var.author
  }
}

你可以使用诸如 icanhazip.com 这样的网站,通过以下代码块检索你的机器的公共 IP 地址:

data "http" "ip" {
  url = "http://ipv4.icanhazip.com"
}

如果你想在网络入口规则中使用它,你可以使用 data.http.ip.body 属性引用 IP 地址。

一旦我们的网络设置就绪,就在 variables.tf 中声明新的 Terraform 变量。有关变量的完整列表,请参阅 chapter5/variables.tf。

然后,使用 terraform apply 应用更改。一个公共 EC2 实例应该部署在 VPC 的公共子网中,如图 5.9 所示。

图 5.9 在公共子网中部署的堡垒主机

我们可以直接从 EC2 控制台中复制实例的公共 IP 地址。或者,我们可以使用 Terraform 输出功能,通过定义一个包含以下内容的 outputs.tf 文件,在终端会话中显示 IP 地址:

output "bastion" {
  value = ${aws_instance.bastion.public_ip}
}

要获取实例的 IPv4 公共 IP,你可以重新发出 terraform applyterraform output 命令:

使用这段 Terraform 代码,我们已经准备好了堡垒主机,并可以使用它来设置 SSH 隧道以访问私有实例:

ssh -L TARGET_PORT:TARGET_INSTANCE_PRIVATE_IP:22 ec2-user@BASTION_IP

注意:你可以进一步部署一个自动扩展组(min=1max=1),以确保堡垒主机实例始终可用。此外,为了成本优化,你可以使用 Spot 实例而不是按需实例。

创建这些文件后,目录结构应如下所示:

terraform.tf
vpc.tf
subnets.tf
private_rt.tf
public_rt.tf
bastion.tf
variables.tf
variables.tfvars
outputs.tf

文件可以命名为任何东西。我们根据每个文件上声明的 AWS 资源进行了命名,以便于方便和识别。请记住,所有以 .tf 结尾的文件都将由 Terraform 加载。

5.3 设置自愈 Jenkins 主

现在我们已经创建了 VPC,我们可以在私有子网中部署一个专门的 EC2 实例来托管 Jenkins 主组件,通过在 jenkins_master.tf 文件中定义 aws_instance 资源,并具有以下属性。该实例由一个 30 GB 的 EBS 卷(SSD)支持,这使得它适用于广泛的负载:

resource "aws_instance" "jenkins_master" {
  ami                    = data.aws_ami.jenkins-master.id
  instance_type          = var.jenkins_master_instance_type
  key_name               = aws_key_pair.management.id
  vpc_security_group_ids = [aws_security_group.jenkins_master_sg.id]
  subnet_id              = element(aws_subnet.private_subnets, 0)

  root_block_device {
    volume_type           = "gp3"
    volume_size           = 30
    delete_on_termination = false
  }

  tags = {
    Name   = "jenkins_master"
    Author = var.author
  }
}

30 GB 的存储值可以根据你将连续构建的项目数量和大小而变化,因为 Jenkins 设置和构建日志默认存储在主节点上。

注意:为 Jenkins 实例制定适当的标记策略对于云成本优化至关重要。它利用 AWS 账单中的过滤器功能,并强制执行跟踪和成本分配。

EC2 实例使用第四章中由 Packer 烘焙的 Jenkins 主 AMI,通过 aws_ami 数据资源引用:

data "aws_ami" "jenkins-master" {
  most_recent = true
  owners      = ["self"]

  filter {
    name   = "name"
    values = ["jenkins-master-*"]
  }
}

我们将向实例附加一个安全组,仅允许从堡垒主机进行 SSH 连接,并允许来自 VPC CIDR 块的端口 8080(Jenkins 网页仪表板)的入站流量;请参阅以下列表。

列表 5.6 Jenkins 安全组

resource "aws_security_group" "jenkins_master_sg" {
  name        = "jenkins_master_sg"
  description = "Allow traffic on port 8080 and enable SSH"
  vpc_id      = aws_vpc.management.id

  ingress {
    from_port       = "22"
    to_port         = "22"
    protocol        = "tcp"
    security_groups = [aws_security_group.bastion_host.id]
  }

  ingress {
    from_port       = "8080"
    to_port         = "8080"
    protocol        = "tcp"
    cidr_blocks     = [var.cidr_block]
  }

  egress {
    from_port   = "0"
    to_port     = "0"
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name   = "jenkins_master_sg"
    Author = var.author
  }
}

接下来,将用于部署 EC2 实例的实例类型定义为变量。为了简化,t2.large(8 GB 内存和 2vCPU)应该足够了,因为我们不会在主节点上分配执行器/工作者。因此,Jenkins 主机不会被构建作业所超载。

然而,Jenkins 需要的内存量取决于您的项目构建需求和相同构建所需的工具。每个构建节点连接将占用两个到三个线程,这相当于大约 2 MB 或更多的内存。如果您有很多用户将访问 Jenkins 用户界面,您还需要考虑 Jenkins 的 CPU 负载。

正因如此,我们将在稍后部署 Jenkins 工作者,将构建委托给工作者,并将大部分工作从主节点本身移除。因此,用于托管 Jenkins 主机的一般用途实例可以在计算和内存资源之间提供平衡。

注意:有关更多信息,请参阅 EC2 通用实例文档:aws.amazon.com/ec2/pricing/on-demand/

t2.large 实例类型可能是一个不错的选择(尽管这种实例类型不属于 AWS 免费层,所以当你完成实验后应该终止它或将其关闭)。在 variables.tfvars 文件中将它声明为一个变量:

variable "jenkins_master_instance_type" {
  type = string
  description = "Jenkins master EC2 instance type"
  default = "t2.large"
}

注意:我鼓励您在几个 Amazon EC2 实例类型上对项目构建进行基准测试,以选择最合适的配置。

使用以下命令生成执行计划:

terraform plan --var-file=variables.tfvars

您应该看到类似以下输出(为了简洁,已裁剪完整的 terraform plan):

图片

由于执行计划看起来很好,输入 yes,您将看到您的 Jenkins 主机 EC2 实例正在部署。一旦配置过程完成,实例应该可以在 EC2 仪表板上看到,如图 5.10 所示。

图片

图 5.10 Jenkins 主机 EC2 实例

虽然这个实例是私有的(它没有公共 IP 地址),但我们可以通过使用堡垒主机并执行以下命令来设置 SSH 隧道(显然,使用不同的值):

ssh -L 4000:10.0.0.71:22  ec2-user@35.180.122.81
ssh ec2-user@localhost -p 4000

您可以通过发出 service jenkins status 命令来检查 Jenkins 是否正在运行。图 5.11 显示了输出。

图片

图 5.11 SSH 隧道连接

要访问 Jenkins 仪表板,我们将在 EC2 实例前面创建一个公共负载均衡器。这个弹性负载均衡器将在端口 80 上接受 HTTP 流量,并将其转发到端口 8080 上的 EC2 实例。它还会自动检查注册的 EC2 实例在端口 8080 上的健康状态。如果弹性负载均衡(ELB)发现实例不健康,它将停止向 Jenkins 实例发送流量。在 jenkins_master.tf 中声明负载均衡器资源:

resource "aws_elb" "jenkins_elb" {
    subnets                   = \
     [for subnet in aws_subnet.public_subnets : subnet.id]
    cross_zone_load_balancing = true
    security_groups           = [aws_security_group.elb_jenkins_sg.id]
    instances                 = [aws_instance.jenkins_master.id]

    listener {
      instance_port      = 8080
      instance_protocol  = "http"
      lb_port            = 80
      lb_protocol        = "http"
    }

    health_check {
      healthy_threshold   = 2
      unhealthy_threshold = 2
      timeout             = 3
      target              = "TCP:8080"
      interval            = 5
    }
    tags = {
      Name   = "jenkins_elb"
      Author = var.author
    }
}

负载均衡器将通过分配以下安全组配置接受来自任何地方的传入 HTTP 流量(您应该锁定来自您期望流量来源的特定 IP 地址范围的传入流量)。稍后,我们将添加一个 HTTPS 监听器,使用 SSL 协议在 HTTP 层上建立安全连接。在 jenkins_master.tf 中定义负载均衡器的安全组;以下是资源代码块:

resource "aws_security_group" "elb_jenkins_sg" {
    name        = "elb_jenkins_sg"
    description = "Allow http traffic"
    vpc_id      = aws_vpc.management.id

    ingress {
      from_port   = "80"
      to_port     = "80"
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }

    egress {
      from_port   = "0"
      to_port     = "0"
      protocol    = "-1"
      cidr_blocks = ["0.0.0.0/0"]
    }

    tags = {
      Name   = "elb_jenkins_sg"
      Author = var.author
    }
}

接下来,更新 Jenkins 主安全组,仅允许来自负载均衡器安全组 ID 的流量在端口 8080 上:

ingress {
    from_port       = "8080"
    to_port         = "8080"
    protocol        = "tcp"
    security_groups = [aws_security_group.elb_jenkins_sg.id]
}

通过在 outputs.tf 文件中定义一个新的输出部分来输出负载均衡器的 DNS URL:

output "jenkins-master-elb" {
  value = aws_elb.jenkins_elb.dns_name
}

在您使用 Terraform 应用更改后,Jenkins 主负载均衡器 URL 应在您的终端会话中显示:

将您喜欢的浏览器指向 URL,您应该可以访问 Jenkins 网络仪表板。您可以在主页上看到“欢迎使用 Jenkins!”的消息(图 5.12)。

图 5.12 Jenkins 网络仪表板

太棒了!您已经有一个位于弹性负载均衡器后面的运行中的 Jenkins 服务器。

如果您的目标是设计高可用性架构,您需要在不同的可用区维护一个冗余的 Jenkins 主实例。然而,由于 Jenkins 主配置存储在\(JENKINS_HOME 目录中,而不是集中式数据库中,您需要使用外部插件,如 CloudBees 的 High Availability Management 插件([`docs.cloudbees.com/plugins/ci/cloudbees-ha`](https://docs.cloudbees.com/plugins/ci/cloudbees-ha)),或者在一个共享网络驱动器上设置\)JENKINS_HOME 目录,以便多个 Jenkins 主实例可以访问它。

注意:在第十四章中,我们将介绍如何使用类似 Amazon Elastic File System (EFS)的解决方案将卷挂载到多个实例共享$JENKINS_HOME 文件夹。

5.4 使用原生 SSL/HTTPS 运行 Jenkins

能够安全地访问 Jenkins 仪表板是一个加分项。这就是为什么我们将使用 AWS 提供的免费 SSL,在自定义域名上以 HTTPS 方式提供内容,并提供加密的网络连接;见图 5.13。

注意:如果您在本地运行 Jenkins,您可以生成一个自签名证书并部署一个反向代理,如 NGINX。如果您选择使用不同的云服务提供商,您可以使用 Let’s Encrypt 免费生成由证书颁发机构(CA)签发的证书。

图 5.13 来自 AWS 证书管理器的免费 SSL 证书

您可以使用 AWS 证书管理器 (ACM) 轻松获取 SSL 证书。此服务使您能够在 AWS 管理的资源上轻松配置、管理和部署 SSL/TLS 证书。

前往 ACM 控制台并点击“请求证书”按钮以创建新的 SSL 证书。选择“请求公共证书”并添加您的域名。您还可能想通过添加一个星号来保护您的子域名。一旦 AWS 验证您拥有这些域名,状态将从“待验证”变为“已发行”。复制 SSL Amazon 资源名称 (ARN)。

更新负载均衡器资源以在端口 443 上启用 HTTPS 监听器。在 HTTPS 监听器上设置 ACM SSL ARN。负载均衡器使用证书终止连接,然后在将请求发送到 Jenkins 实例之前解密来自客户端的请求:

listener {
    instance_port      = 8080            ❶
    instance_protocol  = "http"          ❶
    lb_port            = 443             ❶
    lb_protocol        = "https"         ❶
    ssl_certificate_id = var.ssl_arn     ❶
}

❶ 暴露 HTTPS 监听器并将传入请求从端口 443 转发到 EC2 实例的端口 8080。

向负载均衡器安全组添加一个入站规则,以允许传入的 HTTPS 流量:

ingress {
    from_port   = "443"             ❶
    to_port     = "443"             ❶
    protocol    = "tcp"             ❶
    cidr_blocks = ["0.0.0.0/0"]     ❶
}

❶ 允许来自任何地方的端口 443 的入站流量

然后在 Route 53 服务中创建一个 A 记录,指向负载均衡器的完全限定域名 (FQDN)。DNS 记录的 Terraform 代码将类似于以下内容:

resource "aws_route53_record" "jenkins_master" {
  zone_id = var.hosted_zone_id
  name    = "jenkins.${var.domain_name}"                      ❶
  type    = "A"                                               ❶

  alias {                                                     ❶
    name                   = aws_elb.jenkins_elb.dns_name     ❶
    zone_id                = aws_elb.jenkins_elb.zone_id      ❶
    evaluate_target_health = true
  }
}

❶ 设置一个别名记录(jenkins.domain.com),指向 Jenkins 负载均衡器 FQDN

注意:如果您在 Amazon Route 53 中没有托管区域,您可以跳到下一节,并继续使用负载均衡器 FQDN。

此资源块将创建一个 A 记录,将 jenkins.domain.com URL 映射到 AWS 负载均衡器 FQDN 的别名。

最后,在 variables.tf 文件中定义引用的 Terraform 变量。表 5.3 列出了除本章先前定义的变量外还需要定义的变量。

表 5.3 DNS Terraform 变量

变量 类型 描述
hosted_zone_id 字符串 包含 A 记录的托管区域 ID
domain_name 字符串 要使用的域名,例如 domain.com
ssl_arn 字符串 您在 AWS ACM 中创建的 SSL 证书的 ARN

定义一个 output 部分,通过引用 Route 53 A 记录资源来显示 Jenkins 公共 DNS URL:

output "jenkins-dns" {
  value = "https://${aws_route53_record.jenkins_master.name}"     ❶
}

❶ 将别名记录名称与 https:// 关键字连接起来,构建 Jenkins HTTPS URL

执行 terraform apply 命令以使更改生效。它应部署所需的资源并显示 Jenkins 仪表板 URL:

Jenkins 负载均衡器现在应该正在监听 HTTP(80)和 HTTPS(433)端口,如图 5.14 所示。

图 5.14 在 ELB 上允许 HTTPS 和 HTTP

将您的浏览器指向由 Terraform 创建的子域名。Jenkins 网络仪表板应通过 HTTPS 提供服务,如图 5.15 所示。

图 5.15 Jenkins 仪表板现在通过 HTTPS 提供服务。如果你使用的是 Chrome,你应该在 URL 栏中看到一个绿色的锁。

到目前为止,我们已经在公共负载均衡器后面部署了一个私有独立的 Jenkins 主实例,如图 5.16 所示。

图 5.16 AWS 上的 Jenkins 独立设置

在下一节中,我们将部署额外的 Jenkins 工作节点以减轻 Jenkins 主节点的负载。

注意:定期备份你的 Jenkins EBS 卷对于确保在数据损坏或丢失的情况下可以恢复 Jenkins 实例至关重要。请参阅官方文档以获取说明:mng.bz/807P

5.5 动态自动扩展 Jenkins 工作节点池

运行单个 Jenkins 实例是一个好的开始,但在现实世界中,单个实例是一个单点故障。如果该实例崩溃或因构建过多而超负荷,开发者将无法交付他们的发布。解决方案是运行一个 Jenkins 工作节点集群,并根据资源利用率调整集群的大小。

5.5.1 启动配置

你当然可以部署 Jenkins 工作节点作为单独的 EC2 实例(重新运行前面的步骤)。然而,我们希望实例能够自动部署和替换以实现自动恢复。这就是为什么我们将依赖于一个标准的 AWS 功能,称为 自动扩展组

注意:有关 AWS EC2 自动扩展功能如何工作的更多详细信息,请参阅第三章关于扩展 Jenkins 的架构。

创建自动扩展组的第一步是创建一个启动配置,它描述了如何配置每个 Jenkins 工作节点实例。在 jenkins_workers.tf 文件中声明一个 aws_launch_configuration 资源:

resource "aws_launch_configuration" "jenkins_workers_launch_conf" {
  name            = "jenkins_workers_config"
  image_id        = data.aws_ami.jenkins-worker.id                        ❶
  instance_type   = var.jenkins_worker_instance_type                      ❶
  key_name        = aws_key_pair.management.id                            ❶
  security_groups = [aws_security_group.jenkins_workers_sg.id]            ❶
  user_data       = data.template_file.user_data_jenkins_worker.rendered  ❷

  root_block_device {
    volume_type           = "gp2"                                         ❸
    volume_size           = 30                                            ❸
    delete_on_termination = false                                         ❸
  }

  lifecycle {
    create_before_destroy = true
  }
}

❶ 配置蓝图,使用预制的 Jenkins 工作节点 AMI 和应用于实例的密钥名称,并分配一个安全组

❷ 启动实例时提供的用户数据。它将自动将运行实例加入 Jenkins 集群。

❸ 自定义实例根块设备的详细信息

注意:你应该对你的项目进行性能基准测试,以确定所需的适当实例类型以及磁盘空间的大小。

与 Jenkins 主节点类似,工作节点将在私有子网中部署,并使用第四章中用 Packer 构建的 Jenkins 工作节点 AMI。

data "aws_ami" "jenkins-worker" {
  most_recent = true
  owners      = ["self"]            ❶

  filter {
    name   = "name"
    values = ["jenkins-worker*"]    ❶
  }
}

❶ 使用数据源资源获取预制的 Jenkins 工作节点 AMI 的 ID。

要设置 Jenkins 集群,主节点需要与工作节点建立双向连接。因此,我们需要允许来自 Jenkins 主安全组 ID 的 SSH 访问(允许堡垒主机进行 SSH 访问有助于未来的调试和故障排除):

resource "aws_security_group" "jenkins_workers_sg" {
  name        = "jenkins_workers_sg"
  description = "Allow traffic on port 22 from Jenkins master SG"
  vpc_id      = aws_vpc.management.id

  ingress {
    from_port       = "22"                                          ❶
    to_port         = "22"                                          ❶
    protocol        = "tcp"                                         ❶
    security_groups = [aws_security_group.jenkins_master_sg.id,     ❶
aws_security_group.bastion_host.id]                                 ❶
  }

  egress {
    from_port   = "0"                                               ❷
    to_port     = "0"                                               ❷
    protocol    = "-1"                                              ❷
    cidr_blocks = ["0.0.0.0/0"]                                     ❷
  }

  tags = {
    Name   = "jenkins_workers_sg"
    Author = var.author
  }
}

❶ 允许来自 Jenkins 主和工作堡垒主机的安全组对端口 22(SSH)的入站流量

❷ 允许所有协议从任何地方出站流量(–1)

最后,我们定义了 user-data,这是一个在 Jenkins 工作者实例启动时执行的脚本。该脚本将 Jenkins 管理员凭据、Jenkins SSH 凭据 ID 以及 Jenkins IP 地址作为参数。

SSH 凭据 ID 指的是我们在第四章初始化时使用 Groovy 脚本创建的凭据;该凭据包含位于工作目录 .ssh 文件夹中的私有 SSH 密钥。私有 SSH 密钥将由 Jenkins 主服务器用于通过 SSH 添加 Jenkins 工作者:

data "template_file" "user_data_jenkins_worker" {
  template = "${file("scripts/join-cluster.tpl")}"

  vars = {
    jenkins_url            = "http://${aws_instance.jenkins_master.private_ip}:8080"
    jenkins_username       = var.jenkins_username
    jenkins_password       = var.jenkins_password
    jenkins_credentials_id = var.jenkins_credentials_id
  }
}

scripts/join-cluster.tpl 脚本将从 EC2 元数据(可在 169.254.169.254/latest/meta-data 获取)获取运行实例的私有 IP 地址。然后,脚本将使用以下列表中的 Groovy 脚本向 Jenkins 发送 HTTP 请求,将实例添加到集群中。

列表 5.7 自动加入 Jenkins 工作者

#!/bin/bash
JENKINS_URL="${jenkins_url}"                                              ❶
JENKINS_USERNAME="${jenkins_username}"                                    ❶
JENKINS_PASSWORD="${jenkins_password}"                                    ❶
TOKEN=$(curl -u $JENKINS_USERNAME:$JENKINS_PASSWORD                       ❷
''$JENKINS_URL'/crumbIssuer/api/xml?xpath= \                              ❷
concat(//crumbRequestField,":",//crumb)')                                 ❷
INSTANCE_NAME=$(curl -s 169.254.169.254/latest/meta-data/local-hostname)  ❸
INSTANCE_IP=$(curl -s 169.254.169.254/latest/meta-data/local-ipv4)        ❸
JENKINS_CREDENTIALS_ID="${jenkins_credentials_id}"

curl -v -u $JENKINS_USERNAME:$JENKINS_PASSWORD -H "$TOKEN" -d 'script=    ❹
import hudson.model.Node.Mode                                             ❹
import hudson.slaves.*                                                    ❹
import jenkins.model.Jenkins                                              ❹
import hudson.plugins.sshslaves.SSHLauncher                               ❹
DumbSlave dumb = new DumbSlave("'$INSTANCE_NAME'",                        ❹
"'$INSTANCE_NAME'",                                                       ❹
"/home/ec2-user",                                                         ❹
"3",                                                                      ❹
Mode.NORMAL,                                                              ❹
"workers",                                                                ❹
new SSHLauncher("'$INSTANCE_IP'", 22, "'$JENKINS_CREDENTIALS_ID'"),       ❹
RetentionStrategy.INSTANCE)                                               ❹
Jenkins.instance.addNode(dumb)                                            ❹
' $JENKINS_URL/script                                                     ❹

❶ 在 user_data_jenkins_worker Terraform 资源中将变量替换为给定的值

❷ 从 Jenkins 主服务器获取有效令牌

❸ 从 EC2 元数据获取实例私有 IP 地址和主机名

❹ 在请求负载中使用 Groovy 脚本向 Jenkins 服务器发送 GET 请求。该脚本将当前实例添加为 Jenkins 代理。

此配置允许每个工作者上并行运行三个执行器。如果您计划仅使用主服务器作为作业调度器,可以将其执行器数量设置为零,以确保项目构建仅在工作者机器上发生。资源块还定义了工作实例上的工作空间目录,工作代理可以使用该目录运行构建作业。此配置使用 /home/ec2-user 作为工作空间。此目录中不存储任何关键任务;所有重要内容在构建完成后都会传输回主实例,因此您通常不需要担心备份此目录。

我们还定义了一个名为 workers 的标签,因此每个工作实例都将加入该标签下的 Jenkins 集群。因此,您可以配置您的构建作业仅在工作者机器上运行。

接下来,在 variable.tf 文件中将 Jenkins 主服务器凭据和工作者实例类型定义为变量。表 5.4 列出了变量。

表 5.4 Jenkins 工作者的 Terraform 变量

变量 类型 描述
jenkins_username 字符串 Jenkins 管理员用户名
jenkins_password 字符串 Jenkins 管理员密码
jenkins_credentials_id 字符串 Jenkins 工作者基于 SSH 的凭据 ID
jenkins_worker_instance_type 字符串 t2.medium Jenkins 工作者 EC2 实例类型

注意:您可以通过使用 Amazon EC2 Spot 实例(aws.amazon.com/ec2/spot)或订阅 Amazon Savings Plans([aws.amazon.com/savingsplans/](https://aws.amazon.com/savingsplans/))来显著降低 Jenkins 工作者的成本(高达 90% 的成本节省)。

最后,执行 terraform apply 命令以部署 Jenkins 工作节点。

5.5.2 自动扩展组

现在由于 Jenkins 工作节点的蓝图已定义在启动配置中,我们可以部署一个自动扩展组来基于启动配置部署类似的 Jenkins 工作节点。

使用 jenkins_workers.tf 文件中的 aws_autoscaling_group 资源创建 ASG:

resource "aws_autoscaling_group" "jenkins_workers" {
  name                 = "jenkins_workers_asg"
  launch_configuration = aws_launch_configuration.jenkins_workers_launch_conf.name
  vpc_zone_identifier  = \
 [for subnet in aws_subnet.private_subnets : subnet.id]     ❶
  min_size             = 2                                  ❶
  max_size             = 10                                 ❶
  depends_on = [aws_instance.jenkins_master, aws_elb.jenkins_elb]
  lifecycle {
    create_before_destroy = true
  }
  tag {
    key                 = "Name"
    value               = "jenkins_worker"
    propagate_at_launch = true
  }
  tag {
    key                 = "Author"
    value               = var.author
    propagate_at_launch = true
  }
}

❶ 在不同的子网中部署两个 EC2 实例的 ASG(最小),以提高容错能力

此 ASG 将运行 2 到 10 个工作节点(默认为 2 个初始启动),每个都带有名称 jenkins_worker 的标签。ASG 使用引用来填充启动配置名称。

注意:使用关键字 depends_on 确保在部署工作节点之前 Jenkins 主实例正在运行,因为工作节点需要 Jenkins 主 IP 以成功加入集群。

启动配置是不可变的,因此创建后无法修改它(例如,升级 Jenkins 工作节点实例类型或更改基础 AMI)。因此,您需要销毁启动配置并创建一个新的;这就是为什么使用 create_before_destroy 生命周期设置的原因。

要创建自动扩展组,请在您的终端会话中运行 terraform apply 命令:

配置过程应只需几秒钟。当您刷新 EC2 控制台时,您将在仪表板中看到图 5.17 的输出。

图 5.17 Jenkins 工作节点在 ASG 内部部署

注意:第十四章介绍了另一种方法:我们将使用 Docker 容器部署工作节点,以有效地使用 EC2 实例(在相同的服务器上独立运行多个构建)以及每次都在“干净”的构建环境中运行。

太好了!我们现在在 ASG 内部运行着两个 Jenkins 工作节点。

5.5.3 自动扩展缩放策略

到目前为止,工作节点的数量是静态和固定的。为了动态地调整工作节点的数量,我们将基于 CPU 利用率定义缩放策略。这为您提供了额外的容量来处理额外作业的构建,而无需维护过多的空闲 Jenkins 工作节点并支付额外费用。

创建一个 cloudwatch.tf 文件,并基于 CPU 利用率定义一个 AWS CloudWatch 指标警报。如果平均 CPU 利用率在 2 分钟内超过 80%,CloudWatch 警报将触发扩容事件以添加新的 Jenkins 工作节点实例,如下所示。

列表 5.8 CloudWatch 扩容警报

resource "aws_cloudwatch_metric_alarm" "high-cpu-jenkins-workers-alarm" {
  alarm_name          = "high-cpu-jenkins-workers-alarm"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "120"
  statistic           = "Average"
  threshold           = "80"

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.jenkins_workers.name
  }

  alarm_description = "This metric monitors workers cpu utilization"
  alarm_actions     = [aws_autoscaling_policy.scale-out.arn]
}

resource "aws_autoscaling_policy" "scale-out" {
  name                   = "scale-out-jenkins-workers"
  scaling_adjustment     = 1
  adjustment_type        = "ChangeInCapacity"
  cooldown               = 300
  autoscaling_group_name = aws_autoscaling_group.jenkins_workers.name
}

注意:您可以选择要监控的内容,但最有用的指标可能是 CPU 利用率、内存利用率和网络利用率,以了解何时应该扩展并添加另一个 Jenkins 工作节点或通过终止工作节点进行缩容。

类似地,我们定义另一个 CloudWatch 警报来触发缩容事件,以移除 Jenkins 工作节点,如果平均 CPU 利用率在 2 分钟内低于 20%,请参阅以下列表。

列表 5.9 CloudWatch 缩容警报

resource "aws_cloudwatch_metric_alarm" "low-cpu-jenkins-workers-alarm" {
  alarm_name          = "low-cpu-jenkins-workers-alarm"
  comparison_operator = "LessThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "120"
  statistic           = "Average"
  threshold           = "20"

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.jenkins_workers.name
  }

  alarm_description = "This metric monitors ec2 cpu utilization"
  alarm_actions     = [aws_autoscaling_policy.scale-in.arn]
}

resource "aws_autoscaling_policy" "scale-in" {
  name                   = "scale-in-jenkins-workers"
  scaling_adjustment     = -1
  adjustment_type        = "ChangeInCapacity"
  cooldown               = 300
  autoscaling_group_name = aws_autoscaling_group.jenkins_workers.name
}

冷却时间设置为 300 秒,以确保 ASG 在之前的扩展活动生效之前不会启动或终止额外的 Jenkins 工作节点。

注意:当发生缩容事件时,ASG 将根据终止策略终止一个 Jenkins 工作节点。更多详细信息请参阅第三章。

如果你运行 terraform apply 命令,你会看到 Terraform 想要创建两个 CloudWatch 警报(输出已被裁剪以节省空间):

图片

你可以通过登录 AWS 管理控制台,从控制台主页选择 EC2,然后在导航面板中选择自动扩展组来访问 Amazon EC2 自动扩展(图 5.18)。

图片

图 5.18 自动扩展组扩展策略

接下来,我们将运行 Stress 工具来测试工作节点 ASG 的扩展策略。

5.5.4 工作节点 CPU 利用率负载

通过从堡垒主机设置 SSH 隧道来 SSH 连接到 Jenkins 工作节点。使用 Yum 软件包管理器安装 Stress 工具:

sudo yum update
sudo yum install -y stress

要运行 Stress 工具,请输入以下命令。它将生成一个线程以使两个 CPU 核心达到最大(因为我们使用的是 t2.large 实例,所以这已经足够了):

stress --cpu 2

这让你有机会看到当在 Jenkins 上构建实际工作负载并且 CloudWatch 警报开始触发时,自动扩展策略会发生什么。

你可以使用 top 命令来监控 Stress 工具创建的进程的 CPU 利用率,或者使用 EC2 实例上的 CloudWatch 指标。CPU 利用率将保持在 100% 一段时间,如图 5.19 所示。

注意:CloudWatch 基本监控每 5 分钟刷新一次,而我们的自动扩展策略需要一个指标连续满足 2 分钟,因此我们必须运行至少 5 分钟的负载测试,以确保我们的策略有足够的时间被触发。

图片

图 5.19 Jenkins 工作节点 CPU 利用率使用情况

CloudWatch 根据与 CloudWatch 警报相关的 CPU 利用率统计信息聚合指标数据点。当警报被触发时,扩展策略被触发,如图 5.20 所示。

图片

图 5.20 CloudWatch 扩展警报触发

当指标值达到 80% 时,组的期望容量增加一个实例,达到两个实例;请参阅图 5.21。

图片

图 5.21 触发扩展策略

新实例运行后,用户数据脚本将被执行,工作节点将加入集群,如图 5.22 所示。

图片

图 5.22 新的工作节点已自动加入集群。

如果指标值达到 20%,组的期望容量将减少一个实例;请参阅图 5.23。

图片

图 5.23 由于缩容事件终止未使用的节点

因此,终止的工作节点将无法访问,并在 Jenkins 网络仪表板上标记为离线(图 5.24)。

图 5.24 终止的 Jenkins 工作节点无法访问。

注意:当你完成 Terraform 的实验后,一个好的做法是删除你创建的所有资源,这样 AWS 就不会为此向你收费。运行 terraform destroy 命令以删除现有的 AWS 基础设施。

在本章中,你学习了如何使用基础设施即代码工具 Terraform 在 AWS 上部署一个高可用性、安全且弹性的 Jenkins 集群,以及如何使用预制的 Packer 镜像部署工作节点以进行扩展。图 5.25 总结了部署的架构。

图 5.25 Jenkins 分布式构建在 AW 上。

Terraform 是一个与供应商无关的工具,可以管理多个资源提供者的基础设施。因此,在接下来的章节中,你将学习如何使用相同的配置文件在其他云提供商上部署前面的架构,例如 Microsoft Azure 和 Google Cloud Platform。

摘要

  • 基础设施即代码是一种通过描述性或高级代码定义基础设施和网络组件的方法。

  • Terraform 是一个与任何云环境兼容的基础设施即代码工具,无论是私有云、本地部署还是公共提供商。Terraform 允许安全且方便地管理基础设施资源。

  • Jenkins 主节点应托管在具有足够 CPU 和网络带宽的实例上,以便处理并发用户。

  • Jenkins 工作节点应该是不可变的,能够快速丢弃,并以尽可能少的手动交互将其提升或添加到集群中。这可以通过利用 AWS 自动扩展组来实现。

  • 通过在多个可用区之间分散 Jenkins 工作节点,为高可用性和容错性设计 Jenkins。

第二部分. 运行自愈的 Jenkins 集群

你已经阅读了第一部分,现在对管道即代码的一些核心概念和原则感到很自在。现在是时候动手,使用基础设施即代码工具在云上从头开始部署一个 Jenkins 集群,包括亚马逊网络服务、谷歌云平台、微软 Azure 和 DigitalOcean。

在这个过程中,你会发现如何动态扩展 Jenkins 工作节点,以及如何使用分布式构建模式来设计可扩展的 Jenkins 架构。然后我们将探讨 Jenkins 的基本插件,以及如何使用 Packer 和 Groovy 脚本来配置一个预配置的 Jenkins 集群,其中包含所有必需的依赖项和配置。

3 定义 Jenkins 架构

本章涵盖

  • 理解 Jenkins 分布式构建的工作原理

  • 理解 Jenkins 主节点和工作节点的角色

  • 在云中架构 Jenkins 以实现可扩展性

  • 配置多个 Jenkins 主节点

  • 准备 AWS 环境和 CLI 配置

在分布式微服务架构中,你可能需要定期构建、测试和部署多个服务。因此,拥有多个构建机器是有意义的。虽然你始终可以在独立模式下运行 Jenkins,但将所有构建都运行在中央机器上可能不是最佳选择,并且会导致单点故障(单个 Jenkins 服务器无法处理更大和更重的项目整个负载)。幸运的是,Jenkins 也可以配置为通过设置主/工作节点集群在机器/节点群上运行分布式构建,如图 3.1 所示。

图片

图 3.1 分布式主-工作节点架构

Jenkins 使用主-工作节点架构来管理分布式构建。每个组件都有特定的角色:

  • Jenkins 主节点——负责调度构建作业并将构建作业分配给工作节点以进行实际执行。它还监控工作节点的状态,并在 Web 仪表板上收集和汇总构建结果。

  • Jenkins 工作节点——也称为slavebuild agent,这是一个在远程机器上运行的 Java 可执行程序,它监听来自 Jenkins 主节点的请求,并执行构建作业。你可以拥有任意数量的工作节点(多达 100+个节点)。工作节点可以动态添加和移除。因此,工作负载将自动分配给它们,并且工作节点将减轻主 Jenkins 服务器的负载。

注意:在 2016 年,Jenkins 社区决定开始从项目中移除冒犯性的术语。在 Jenkins 2.0 中,slave这个术语已被弃用,并由agent替代。

总结来说,Jenkins 可以以独立模式部署。然而,当你想在不同的环境中定期运行多个构建作业以满足不同项目的构建环境需求时,单个 Jenkins 服务器就不能简单地处理工作负载了。这就是为什么在这本书中,我们将重点关注主-工作节点架构

3.1 理解主-工作节点架构

在主-工作节点架构中,Web 仪表板运行在 Jenkins 主实例上。主节点的角色是处理构建作业的调度,将构建作业派遣和委派给工作节点以进行实际执行,监控工作节点的状态(在线或离线),以及记录和展示构建结果。即使在分布式架构中,Jenkins 的一个主实例也可以直接执行构建作业。

Jenkins 工作节点可以在 Jenkins 仪表板或通过 Jenkins RESTful API 进行添加和配置。工作节点的角色是执行由主节点分配的构建作业。您可以通过给节点分配标签来配置一个项目始终在特定的节点上运行。标签是一个强大的功能;它们是虚拟的组名。在配置时,您可以给工作节点分配多个标签。标签还可以用来限制构建作业仅在具有特定标签名称的工作节点上运行——例如,限制作业仅在 CPU 优化的实例上构建。

要添加一个工作节点,您可以在管理员页面菜单中点击“管理 Jenkins”,然后点击“管理节点”并选择“添加新节点”。填写配置信息,包括节点名称、工作区名称和节点的 IP 地址。然后,输入一个标签,例如 workers(您可以在“标签”输入框中通过空格分隔来分配多个标签)。图 3.2 展示了如何将新的工作节点添加到 Jenkins 中。

图 3.2 使用标签进行 Jenkins 作业分配

图 3.2 使用标签进行 Jenkins 作业分配

通过将 workers 标签分配给节点,您可以在 Jenkinsfile 中轻松引用它。在声明式管道中,您可以通过设置以下 agent 指令来限制管道仅在带有 workers 标签的节点上运行:

pipeline{
    agent{
        label 'workers'
    }
    stages{
       stage('Checkout'){}
    }
}

然而,脚本化的管道使用带有标签名称的 node 块包装器作为参数来定义管道的执行环境:

node('workers'){
    stage('Checkout'){}
}

如果对同一节点请求更多的构建作业,Jenkins 将自动创建一个作业队列。默认情况下,每个节点可以执行一个作业;然而,您可以通过设置标记为“执行器数量”的字段来增加节点运行作业的容量。在先前的例子中,节点配置了三个执行器,这意味着一次最多可以执行三个作业。如果启动了四个作业,前三个将执行,第四个将被添加到构建队列中。一旦节点可用,Jenkins 将按照请求的顺序执行剩余的作业。

要能够将工作节点添加到 Jenkins 集群,工作节点和主节点需要通过 TCP/IP 建立双向通信。另一个要求是在工作机器上安装 Java。由于 Java 是一种平台无关的编程语言,一个 Jenkins 集群可能包含运行在多种操作系统平台上的工作节点,如 Windows、Linux 或 macOS。这种架构带来了多个好处,例如拥有一个异构的构建农场,支持您可能需要使用不同操作系统或 CPU 架构运行构建/测试的所有环境。

在图 3.3 的示例中,使用工作节点代表您所需的环境中的每一个,会导致您拥有多个环境和配置来测试、构建和部署您的项目。构建作业的委托行为取决于每个项目的配置;一些项目可能选择使用标签“粘附”到特定机器上进行构建,而其他项目可能选择在可用的节点之间自由漫游。

图 3.3 您可以通过使用 SSH 或 Java 网络启动协议(JNLP)来设置运行不同操作系统的多个工作节点

3.2 管理 Jenkins 工作节点

在管理 Jenkins 工作节点时,根据目标操作系统和其他架构考虑因素,有多种策略可供选择。这些策略会影响您配置工作节点的方式,因此我们需要分别考虑每个策略。

3.2.1 SSH

如果您在 UNIX 环境中工作,无疑最方便启动 Jenkins 工作节点的方式是使用安全外壳(SSH)。Jenkins 具有自己的内置 SSH 客户端,几乎所有 UNIX 环境都支持 SSH(通常为sshd)。

工作节点需要从主服务器可达,您将需要提供主机名、登录名和密码。您还可以为主实例上的 SSH 私钥文件提供路径,以使用公私钥认证,如图 3.4 所示。

图 3.4 通过 SSH 启动 Jenkins 工作节点

注意:在第五章中,我们将使用 SSH 启动方法来设置 Jenkins 集群。

3.2.2 命令行

您可以通过 Jenkins 从主服务器执行命令来添加工作节点,如图 3.5 所示。当主服务器能够远程在其他机器上执行进程时,使用此方法。然而,远程模式自 Jenkins 2.54 以来已被弃用(因此在最新版本的 Jenkins 中可能不是一个有效的选项)。

图 3.5 通过命令行启动 Jenkins 工作节点

3.2.3 JNLP

另一种选项是使用 Java Web Start(JWS)从工作节点本身启动代理。如果主服务器无法到达工作节点,例如,如果工作节点运行在防火墙的另一侧,这种方法很有用。它适用于您的节点运行在任何操作系统上。然而,它更适合管理 Windows 工作节点。

这种方法确实存在一些主要缺点:Jenkins 无法自动启动或重启工作节点。如果工作节点出现故障,主实例无法重启它。在 Windows 机器上执行此操作时,您至少需要手动启动 Jenkins 工作节点一次。这需要在该机器上打开浏览器,在 Jenkins 主服务器上打开工作节点页面,并使用非常显眼的 JNLP 图标启动工作节点。然而,一旦启动了工作节点,您就可以将其安装为 Windows 服务。

3.2.4 Windows 服务

Jenkins 还可以通过 Windows DCOM 服务器进程启动器服务(该服务在 Windows 上默认安装)将远程 Windows 工作节点作为 Windows 服务管理。选择此选项时,您需要提供 Windows 主机名、用户名和密码,如图 3.6 所示。

图片

图 3.6 启动 Windows 工作节点

这种启动模式很方便,因为它不需要您物理连接到 Windows 机器来设置。然而,它也有局限性——特别是,您不能运行任何需要图形界面的应用程序。

一旦将工作节点添加到 Jenkins 集群中,主节点将主动监控其状态,如果认为某个工作节点无法安全执行构建作业,则会将其下线。您可以在“管理节点”页面中精细调整 Jenkins 要监控的内容,如图 3.7 所示。

图片

图 3.7 定义节点监控阈值

Jenkins 监控每个工作节点上$JENKINS_HOME 的可用磁盘空间,以及临时目录和交换空间的磁盘空间。它还跟踪主节点和工作节点之间的系统时钟差异。最后,它监控从主节点到工作节点的往返网络响应时间。如果这些标准中的任何一个低于某个阈值,则工作节点将被标记为离线。

最后,值得一提的是,默认情况下,Jenkins 尽可能多地使用工作节点。只要某个工作节点可以执行构建,Jenkins 就会使用它。

要控制 Jenkins 如何在可用工作节点上调度构建,您可以在图 3.8 所示的“使用”字段中进行配置,使用“仅构建与此节点匹配标签表达式的作业”选项来限制作业只能在工作节点名称和/或标签匹配的情况下执行。如果您想为某种特定的 Jenkins 作业保留工作节点,这将非常有用。此外,如果将“执行器数量”字段的值设置为 1,您可以确保在任何给定时间只执行一个作业。因此,不会有其他构建干扰。

图片

图 3.8 配置 Jenkins 工作节点使用

3.3 在 AWS 中为 Jenkins 进行扩展架构设计

到目前为止,我们已经介绍了 Jenkins 分布式构建的工作方式。本节将介绍如何在 AWS 上对 Jenkins 进行扩展架构设计。因此,您需要 AWS 账户来跟随示例。使用新的 AWS 账户,免费层应该可以覆盖所有示例,无需您支付任何费用。有关 AWS 免费层的更多信息以及如何创建新 AWS 账户的逐步指南,请访问aws.amazon.com/free/

注意:尽管本节侧重于 AWS,但此内容同样可用于帮助在其他云服务提供商上设置 Jenkins 集群。第六章提供了逐步指南。

你可以部署的简单架构是一个独立或单节点设置。你只需从 AWS Marketplace(aws.amazon.com/marketplace)部署一个 Jenkins 服务器到 Amazon 弹性计算云(EC2)实例,如图 3.9 所示。

图 3.9 AWS Marketplace 上可用的 Jenkins 机器镜像

AWS Marketplace 包含来自安全、网络、存储、机器学习、商业智能、数据库和 DevOps 等流行类别的预配置的 Amazon 机器镜像(AMIs)。你可以通过选择 Jenkins 长期支持(LTS)版本和机器实例类型(基于资源需求)来快速通过几个点击启动 Jenkins 服务器。

你也可以通过使用包管理器(例如,APT 或 Yum)在基础机器镜像上安装 Jenkins。Jenkins 安装程序适用于多个 Linux 发行版以及 Windows 和 macOS。否则,你可以使用 Jenkins 官方 Docker 镜像设置一个 Jenkins 操场。

注意:第四章介绍了如何使用 HashiCorp Packer 从头开始创建自己的 Jenkins 机器镜像。

一旦你在 EC2 实例上安装了 Jenkins,你需要配置实例关联的安全组,以允许 8080 端口的流量。这是 Jenkins 仪表板暴露的端口。

安全组充当防火墙,控制允许到达 EC2 实例的流量(图 3.10)。为了控制流量,我们在安全组中创建规则。对于这种情况,需要添加以下安全规则。

  • 允许入站(ingress)流量在端口 8080(Jenkins 仪表板端口号)。

  • (可选)允许从你的计算机的公网地址入站 SSH 流量,以便你可以连接到你的 Jenkins 实例进行调试或维护。

  • 默认情况下,安全组包含一个出站规则,允许所有出站(egress)流量。

图 3.10 AWS 上的 Jenkins 独立架构由一个位于安全组后的 EC2 实例组成。

你可能需要设置一个与安全组规则类似的网络访问控制列表(ACL),以在你的实例上添加额外的安全层。安全组充当你的 Amazon EC2 实例的防火墙,在实例级别控制入站和出站流量。ACL 充当相关子网的防火墙,在子网级别控制入站和出站流量。

注意:虽然你可以通过垂直扩展 Jenkins 主机来吸收构建作业的负载峰值,但实例可扩展的程度是有限的。

虽然这种架构适用于较小的项目,但它无法扩展到更大和更复杂的项目。因此,我们将部署一个 Jenkins 集群,以在多个工人之间共享负载。而不是在 Jenkins 主实例上调度构建作业,它们将被分配给 Jenkins 工人。结果,将部署额外的 EC2 实例(图 3.11)作为构建服务器或 Jenkins 代理。

图 3.11 AWS 上的 Jenkins 分布式架构

这种架构要好得多。然而,分布式构建通常用于通过动态添加额外机器来吸收额外负载(例如,在构建活动中),因此工人的数量不应该预先固定。我们希望根据队列中等待作业的数量或工人集群的 CPU 利用率来添加或删除工人。这就是为什么,我们不会独立部署工人,而是将它们部署在 AWS 自动缩放组(ASG)内部;请参阅aws.amazon.com/autoscaling/ .

ASG 功能随 EC2 提供,允许您部署一组 EC2 实例,这些实例被视为自动缩放目的的逻辑分组。此外,Amazon EC2 自动缩放通过指定任何给定时间的最小和最大实例数量,帮助确保您拥有正确的实例数量。

为了根据构建作业按需创建和终止 Jenkins 工人,我们可以创建缩放策略。缩放策略是一组指令,用于根据 Amazon CloudWatch 警报调整 ASG 中实例的大小(docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail)。

Amazon CloudWatch 警报将监控 EC2 实例的 CPU 使用情况,例如。然后它将触发一个扩展或缩放事件,以自动向 Jenkins 集群添加或删除一个工人。例如,如果 Jenkins 工人的平均 CPU 利用率超过 80%,将触发一个扩展事件,并部署一个新的工人并将其添加到 Jenkins 集群中。同样,如果 Jenkins 工人的平均 CPU 利用率低于 20%,将触发一个缩放事件,并删除未使用的工人(提供基础设施成本优化)。

注意:在自动缩放组上创建警报时,警报使用所有 Jenkins 工人实例的聚合指标(平均 CPU 利用率)。这样,它不会仅仅因为一个工人太忙就添加实例。

当 CPU 利用率低于 20%时,缩放策略生效,ASG 将在可用实例上终止。如果您没有为 ASG 分配特定的终止策略,它将使用默认的终止策略。这意味着 ASG 将根据以下因素选择要终止的实例:

  • 接近下一个计费小时的实例。

  • 运行时间最长/最老的 EC2 实例。

  • 最老的启动配置。启动配置是描述 Jenkins 工作节点实例应如何样子的蓝图或模板。

然而,您可以使用 Amazon EC2 终止保护来防止 Jenkins 工作节点意外终止。请参阅官方指南以获取说明:mng.bz/ePwz

我们还可以根据内存利用率来配置扩展策略。然而,内存利用率是 CloudWatch 默认不可用的指标之一。由于 AWS 在 OS 级别无法访问实例,因此只能记录通过虚拟化层(如 CPU 和网络利用率)可监控的指标。

我们有多种方法可以解决这个问题。最常用的方法是安装一个指标收集代理在 EC2 实例上。有关如何获取内存利用率的更多详细信息,请参阅第十三章。

注意:为了能够自动添加工作节点,工作节点机器在启动时会运行一个 shell 脚本,并使用 Jenkins RESTful API 通过机器的私有 IP 地址(称为集群发现)自动注册到集群中。第四章和第五章将深入解释这一部分。

图 3.12 说明了如何通过使用 CloudWatch 扩展策略动态扩展 Jenkins 工作节点。

图片

图 3.12 显示 Jenkins 工作节点属于一个 AWS 自动扩展组,并将根据组的平均 CPU 利用率动态扩展。

我们还可以使用自定义指标,如构建队列中等待作业的数量来触发扩展策略。要获取此信息,您可以使用开源解决方案(如 Prometheus prometheus.io/docs/introduction/overview/)来导出 Jenkins 集群指标,并创建一个 Lambda 函数来消费/抓取这些指标。从 Lambda 函数中,您可以使用 AWS API/SDK 在 Jenkins 工作节点自动扩展组上触发扩展或缩减事件。

注意第十三章介绍了如何监控 Jenkins 集群的健康状况以及如何在 Jenkins 上使用 Prometheus 导出器插件来公开服务器端指标。

图 3.13 演示了如何根据自定义指标动态扩展 Jenkins 工作节点。

图片

图 3.13 您可以通过集成 Prometheus 和 AWS Lambda 根据构建队列中等待作业的数量动态扩展 Jenkins 工作节点。

到目前为止,该架构很有前景。然而,它并不安全且弹性不足。为了确保我们的 Jenkins 集群安全,我们将在虚拟私有云(VPC)和私有子网中精确部署该架构。实际上,默认情况下,任何 EC2 实例都部署在 AWS 默认 VPC 中。但我们将创建一个非默认 VPC,以满足我们的特定需求,并使用特定的无类别域间路由(CIDR)块范围和子网大小。

亚马逊 VPC (aws.amazon.com/vpc) 允许您在 AWS 云中配置一个逻辑上隔离的部分,您可以在其中定义一个虚拟网络来启动 AWS 资源。您对虚拟网络环境拥有完全的控制权,包括选择自己的 IP 地址范围、创建子网以及配置路由表和网络网关。

这里需要注意的一个重要点是,VPC 仍然是 AWS 云的一部分。它不是 AWS 提供的物理上分离的托管服务;它是 EC2 基础设施的逻辑隔离部分。这种隔离是在网络层完成的,类似于传统数据中心网络的隔离;只是我们作为最终用户,被屏蔽了其复杂性。图 3.14 显示了 AWS VPC 的网络拓扑。

图 3.14 虚拟私有云由私有和公共子网组成。

我们将创建一个包含多个子网的 AWS VPC。子网不过是有效 IP 地址的一个范围。为了提高弹性,这些子网将在所选 AWS 区域的不同可用区中部署。

接下来,我们将部署一个互联网网关(IGW)并将其连接到 VPC。IGW 主要用于为 Jenkins 实例提供互联网连接(如果您的 Jenkins 工作节点中的构建作业需要从互联网下载外部包,这可能是必需的)。此外,IGW 将实例的私有 IP 地址映射到相关的公共或弹性 IP 地址 (mng.bz/p9QG),然后将子网外的流量路由到互联网。最后,我们创建一个公共路由表,其中包含将网络流量从公共子网路由到 IGW 的规则,如表 3.1 所示。

表 3.1 公共路由表

目标地址 目标 备注
10.0.0.0/16 local 允许流量与特定的子网(10.0.0.0/16)流动
0.0.0.0/0 IGW ID 允许子网流量通过互联网。

但是私有子网中的实例怎么办?这就是网络地址转换(NAT)实例或网关发挥作用的地方。NAT 网关/实例将在公共子网内部创建,并将转发出站流量,不允许任何来自互联网的流量到达私有子网。这意味着实例可以访问互联网,而不会暴露在公共网络中(不会分配公共 IP 地址)。一旦 NAT 网关部署完成,我们需要在私有子网的路由表中添加一个条目,指向 NAT 网关;请参阅表 3.2。

表 3.2 私有路由表

目标地址 目标 备注
10.0.0.0/16 local 允许流量与特定的子网(10.0.0.0/16)流动
0.0.0.0/0 NAT ID 允许子网流量通过 NAT 网关/实例

由于 Jenkins 实例将被部署到与互联网隔离的私有子网中,我们无法从本地桌面直接 SSH 到它们。一个基本的解决方案是部署一个特殊的实例,该实例充当代理,您可以使用它来 SSH 到您的 Jenkins 实例。这个特殊的实例被称为堡垒主机跳板机。这个实例将部署在您的公共子网上,并且基本上只通过设置安全的 SSH 隧道/桥接路由来自本地网络的 SSH 流量。

注意:一个高级的解决方案是部署 OpenVPN 来建立安全的 TLS VPN 会话,以便安全地访问您的私有 Jenkins 实例。请参考“在 Amazon VPC 中设置 OpenVPN 访问服务器”的说明,链接为mng.bz/OQVn

一旦配置了 VPC,我们就可以继续部署一个在私有子网上运行的 Jenkins 服务器的专用 EC2 实例。同时,Jenkins 工作节点自动扩展组将在多个私有子网上部署。我们使用 CloudWatch 警报配置扩展策略,以根据构建活动动态扩展 Jenkins 工作节点。图 3.15 总结了当前的部署架构。

图 3.15 这个部署在私有子网中的 Jenkins 集群由一个工作节点自动扩展组(ASG)和一个持有 Jenkins 仪表板的 EC2 实例组成。

我们可以将这种架构进一步扩展,并在 Jenkins 实例前面配置一个面向公众的弹性负载均衡器,以便访问 Jenkins 网络仪表板。这样,您的 Jenkins 实例就不必直接暴露在互联网上。

注意:即使 Jenkins 核心默认不支持多个主节点,也可以有多个 Jenkins 实例。然后,使用负载均衡器来获取请求并将它们分配给多个 Jenkins 主节点。

负载均衡器将监听 HTTP(80)和 HTTPS(443)端口,并将传入请求发送到端口号为 8080 的实例。这样,它使用加密连接与 Jenkins 实例通信。表 3.3 总结了端口配置。

表 3.3 负载均衡器监听器配置

负载均衡器协议 负载均衡器端口 实例协议 实例端口
HTTP 80 HTTP 8080
HTTPS 443 HTTP 8080

如果您指定了 HTTPS 监听器,您将需要选择一个私有的安全套接字层(SSL)证书。负载均衡器使用证书来终止连接,然后在将请求发送到 Jenkins 实例之前解密来自客户端的请求。您可以使用 AWS 证书管理器(ACM)获取免费的 SSL 证书;您也可以导入自己的证书。

负载均衡器有一个公开可解析的 DNS 名称,因此它可以路由来自互联网的客户端请求到已注册到负载均衡器的 Jenkins 实例。此外,在设置 GitHub webhook 以在推送事件上持续触发 Jenkins 构建时,它也将非常有用。

注意:如果您计划坚持使用私有 Jenkins 实例,第七章解释了如何在防火墙后面的 Jenkins 实例上设置 GitHub webhook。

最后,如果您想使用友好的 DNS 名称来访问您的负载均衡器,而不是自动分配给负载均衡器的默认 DNS 名称,您可以为负载均衡器创建一个自定义域名并将其与负载均衡器的 DNS 名称关联。DNS 配置可以在 Amazon Route 53 上完成(aws.amazon.com/route53/))。图 3.16 显示了最终的架构图。

图 3.16 自定义 VPC 上的 Jenkins 集群部署

向 Jenkins 集群添加工作节点是扩展 Jenkins 的典型方式。然而,您可以使用带有代理(通常是 HAProxy 或 NGINX)的多个 Jenkins 主实例来主动监控主实例,并在活动主实例下线时将请求重定向到备份主实例。主实例的 Jenkins 架构将类似于图 3.17。

图 3.17 Jenkins 主实例高可用设置使用 Amazon Elastic File System 持久化 Jenkins 主目录。

如您所见,第一层是反向代理。每当有构建请求到来时,它将首先到达代理。然后,代理将决定请求可以路由到的实例。在这里,一个主实例将处于活动状态以处理请求,而另一个将处于被动状态。每当活动主实例出现问题并下线时,另一个主实例将变为活动状态,请求将恢复。(我们还可以在 ASG 内部部署 Jenkins 主实例以确保始终有足够的主实例用于备份)。这些请求将由变为活动状态的主实例处理。

第二层是 Amazon Elastic File System,或 EFS(aws.amazon.com/efs/),它用作存储解决方案以持久化 Jenkins 主目录$JENKINS_HOME,以便两个 Jenkins 主实例都可以访问和存储 Jenkins 作业。此存储解决方案可以同时挂载到多个 Jenkins 实例。Amazon EFS,就像任何网络文件系统(NFS)服务器一样,支持完整的文件系统访问语义,例如强一致性和文件锁定。

如果您计划在 Kubernetes 集群或基于 Docker 的编排平台(如 AWS ECS 或 Fargate)上部署 Jenkins,也可以使用 EFS。由于 Jenkins 主容器可以在集群中的任何节点上启动,因此可以使用 EFS 将 Jenkins 数据目录持久化以保留其状态。

注意:第十四章介绍了如何在$JENKINS_HOME 目录中挂载 EFS 以确保 100%的数据共享,并且在发生故障的情况下数据不会丢失。

现在 Jenkins 架构已经明确,接下来我们将准备我们的 AWS 环境,然后安装和配置后续章节所需的工具。

3.3.1 准备 AWS 环境

本节将指导您安装和配置 AWS 命令行。命令行界面(CLI)是一个强大且必需的工具,我们将在后续章节中使用它。它将通过自动化 AWS 上 Jenkins 集群的部署和配置以及定义云原生应用的 CI/CD 步骤来为我们节省大量时间。

3.3.2 配置 AWS CLI

AWS CLI (aws.amazon.com/cli/) 是一个强大的工具,可以从终端会话中管理您的 AWS 服务和资源。它是建立在 AWS API 之上的,因此可以通过 CLI 完成通过 AWS 管理控制台 (console.aws.amazon.com/console/home) 可以完成的所有操作;这使得它成为一个方便的工具,可以通过脚本自动化和控制您的 AWS 基础设施。后续章节将提供有关使用 CLI 与 Jenkins 管理 AWS 中的云原生应用的信息。

让我们通过 AWS CLI 的安装过程;您可以在 AWS 管理控制台部分找到有关其配置和测试的信息。要开始,请参考官方文档,并根据您的操作系统说明安装 AWS CLI (mng.bz/Yw8N)。

一旦安装了 AWS CLI,您需要将 AWS CLI 二进制文件路径添加到 PATH 环境变量中,如下所示。

  • 对于 Windows,按 Windows 键并输入 环境变量。在环境变量窗口中,在系统变量部分突出显示 PATH 变量。编辑它,并在最后一个路径后放置一个分号,然后输入 CLI 二进制文件安装的文件夹的完整路径。

  • 对于 Linux、Mac 或任何 UNIX 系统,打开您的 shell 配置文件(.bash_profile、.profile 或 .bash_login)并在文件末尾添加以下行:

export PATH=~/.local/bin:$PATH

最后,将配置文件加载到当前会话中。

source ~/.bash_profile

通过打开新的终端会话并输入以下命令来验证 CLI 是否正确安装:

aws --version

您应该能够看到 AWS CLI 版本;在我的情况下,已安装 2.0.0 版本。让我们测试一下,并以法兰克福地区为例列出 Amazon S3 存储桶。

aws s3 ls --region eu-central-1

之前的命令显示以下输出。

![Images/CH03_F17_UN_code.png]

当使用 CLI 时,您通常需要 AWS 凭据来验证 AWS 服务。您可以通过多种方式配置 AWS 凭据。

  • 环境凭证—使用 AWS_ACCESS_KEY_IDAWS_SECRET_KEY 变量。它们对于脚本编写或临时设置一个命名的配置文件为默认值非常有用。

    注意:如果您在终端提示符下设置环境变量,则这些值仅保存为当前会话的持续时间。为了使环境变量设置在所有终端会话中持久,请将它们存储在 /etc/profile 或当前用户的 ~/.bash_profile 中。

  • 共享凭证文件——AWS CLI 将凭证存储在您主目录下 .aws 文件夹中的名为 credentials 的本地文件中。您可以通过将 AWS_SHARED_CREDENTIALS_FILE 环境变量设置为另一个本地路径来指定凭证文件的默认位置。

  • IAM 角色——如果您在 EC2 实例中使用 CLI,这可以消除在生产环境中管理凭证文件的需求。每个 Amazon EC2 实例都包含 AWS CLI 可以直接查询以获取临时凭证的元数据。

在下一节中,我将向您展示如何使用 AWS 身份和访问管理(IAM)服务为 AWS CLI 创建新用户。

3.3.3 创建和管理 IAM 用户

IAM (aws.amazon.com/iam/) 是一项服务,允许您管理用户、组和他们对 AWS 服务的访问级别。强烈建议您不要使用 AWS 根账户执行任何除计费任务以外的任务,因为它具有创建和删除 IAM 用户、更改计费、关闭账户以及执行您 AWS 账户上所有其他操作的最终权限。因此,我们将创建一个新的 IAM 用户,并按照最小权限原则授予它访问正确 AWS 资源所需的权限。

注意:最小权限原则(PoLP)通过仅授予用户执行所需任务所需的最小访问级别或权限来实现。

使用您的 AWS 电子邮件地址和密码登录 AWS 管理控制台。然后,从安全、身份与合规部分打开 IAM 控制台或直接在搜索栏中输入 IAM;图 3.18 展示了控制台界面。

图 3.18 AWS 管理控制台

从导航面板中选择用户。点击添加用户按钮。然后为用户设置一个名称,并选择程序访问(如果您希望同一用户能够访问控制台,也可以选择 AWS 管理控制台访问),如图 3.19 所示。

图 3.19 创建新的 IAM 用户

在设置权限部分,将 AmazonS3FullAccess 策略分配给用户,如图 3.20 所示。

图 3.20 将 IAM 策略附加到用户

注意:最好细化权限,仅指定完成任务所需的权限(保留特权访问)。从最小权限集开始,仅在必要时添加更多权限。

在最后一页,您应该能看到用户的 AWS 凭证(图 3.21)。请确保将访问密钥保存在安全的位置,因为您将无法再次看到它们。

图 3.21 AWS 凭证生成

注意:您可以使用 IAM 用户来代表用户、应用程序或服务。在下一章中,我们将为 HashiCorp Terraform 和 Packer 工具创建专门的 IAM 用户。

接下来,使用aws configure命令配置 AWS CLI。CLI 会将前一个命令中指定的凭证存储在本地文件~/.aws/credentials下(或在 Windows 上的%UserProfile%\.aws\credentials),内容如下(将eu-central-1替换为您的 AWS 区域):

[default]
region=eu-central-1
aws_access_key_id=ACCESS KEY ID
aws_secret_access_key=SECRET ACCESS KEY

注意:您可以通过使用--region命令行选项的AWS_DEFAULT_REGION环境变量来覆盖您的 AWS 资源所在区域。

应该就是这样;尝试以下命令,如果您有一个 S3 存储桶,您应该能够看到列出的凭证。否则,命令将返回无结果:

aws s3 ls

现在 AWS 环境已经设置好了,让我们开始部署 AWS 上的 Jenkins 集群。

摘要

  • 在分布式构建模式下部署 Jenkins 允许解耦编排、构建执行,并提高性能。

  • Jenkins 是 DevOps 链中的关键组件,其停机可能会对 DevOps 环境产生不利影响。为了克服这些,您需要一个高可用性的 Jenkins 设置。

  • AWS CloudWatch 提供了一套丰富的指标来监控 EC2 实例的健康状况。收集的指标可以用于设置警报,并在警报触发时触发扩展策略,例如扩展 Jenkins 工作节点。

  • 将构建项目的负载委托给工作节点被称为分布式构建。

  • 您可以通过使用 Jenkins 标签来配置构建在特定的工作机器上运行。

  • 非常推荐在 VPC 的私有子网中启动您的 Jenkins 部署,出于安全考虑。

  • 通过给节点分配标签,您可以指定用于特定作业的资源,并为您的测试设置优雅的排队。

4 使用 Packer 烘焙机器镜像

本章 涵盖

  • 不可变基础设施概述

  • 使用 Packer 烘焙 Jenkins 机器镜像

  • 发现 Jenkins 基础插件

  • 执行 Jenkins Groovy 脚本

  • 使用 Packer 提供程序自动化 Jenkins 设置

在上一章中,你学习了 Jenkins 分布式模式架构的工作原理。在这一章中,我们将亲自动手,在 AWS 上部署一个 Jenkins 集群。为了快速回顾,你了解到 Jenkins 集群分为两个主要组件:主节点和从节点。在深入实施分布式构建架构之前,我们将部署独立模式,如图 4.1 所示,以涵盖一些基础知识。

要部署此架构,我们需要配置一台服务器(例如,AWS 中的 EC2 实例)。然后,我们将在该机器上安装和配置 Jenkins。虽然这个过程是可行的,但当我们想要扩展 Jenkins 部署时,它并不高效。此外,更新或升级 Jenkins 可能会耗时且痛苦,事情很容易出错——破坏你的 CI/CD 管道,并影响你的产品发布。

图 4.1 AWS 上的 Jenkins 独立架构

因此,我们不是在基础设施创建(EC2 实例部署)后安装 Jenkins 并在现有的 Jenkins 实例上应用更新(升级或维护的情况),所有更改都必须打包到一个新的机器镜像中。应该基于新镜像部署一个新的 Jenkins 实例,然后销毁旧服务器。这个过程创建了一个被称为 不可变基础设施的东西。

4.1 不可变基础设施

不可变基础设施的核心是那些在基础设施创建后重新创建和替换的不可变组件,而不是更新。这种不可变基础设施减少了出错的地方数量。这有助于减少不一致性并提高部署过程中的可靠性。

当不可变基础设施需要更新时,会使用预配置的镜像部署新的服务器,并销毁旧服务器。我们创建一个新的机器镜像,用于部署和使用,以创建新的服务器。在不可变基础设施中,我们将配置设置从服务器创建过程移动到构建过程。由于所有部署都是通过新镜像完成的,因此我们可以保留以前发布的版本历史,以便在需要回滚到旧构建时使用。这使我们能够减少部署时间和配置失败的机会,并扩展部署。图 4.2 阐述了不可变和可变基础设施之间的差异。

注意,从“黄金”机器镜像生成的新的实例 B 在不可变模式中实例 A 被销毁时进行配置。此外,请注意,在具有多个实例同时运行的精心设计的不可变模式中替换实例时,没有 Jenkins 停机时间。相比之下,在可变模式中,实例 A 并没有被替换。相同的实例通过手动或使用脚本或工具进行修改,Jenkins 从 v1.0 更新到 v2.0。

图 4.2 通过可变和不可变基础设施进行更新

在云计算时代,许多公司正在采用不可变基础设施来简化配置管理并通过使用基础设施即代码来提高可靠性。使用不可变基础设施时,我们不是在运行的服务器上进行更改,而是创建一个新的服务器。创建不可变基础设施很困难,需要复杂的构建和测试过程。实现不可变基础设施的最佳方式是使用经过充分测试和验证的工具。

多种工具和框架允许您构建不可变基础设施。其中最著名的是 HashiCorp Packer、HashiCorp Vagrant 和 Docker。在这本书中,我们将通过使用 Packer 来专注于机器镜像。目标是展示构建不可变基础设施的工作流程,并展示如何使用 Packer 实现完全自动化。然而,相同的流程也可以在采用其他替代方案时应用。

4.2 介绍 Packer

HashiCorp Packer (www.packer.io) 是一个轻量级且易于使用的开源工具,可以自动化为多个平台创建任何类型的机器镜像。Packer 不是配置管理工具(如 Ansible、Puppet 或 Chef)的替代品。Packer 与这些工具协同工作,在创建镜像的同时安装和配置软件及其依赖项。

Packer 使用配置文件来创建机器镜像。然后它使用构建器在目标平台上启动实例,并运行配置器来配置应用程序或服务。一旦设置完成,它将关闭实例并保存带有任何所需后处理的新的烘焙机器实例。

使用 Packer 有许多优点。以下是一些:

  • 快速基础设施部署—机器镜像使我们能够更快地启动配置好的机器。

  • 可扩展性—Packer 在镜像创建过程中为机器安装和配置所有需要的软件和依赖项。相同的镜像可以用来生成任意数量的实例,而无需进行额外配置。(例如,可以使用相同的镜像部署多个 Jenkins 工作节点。)

  • 多提供者支持—Packer 可以用于为多个云提供商(如 AWS、GCP 和 Microsoft Azure)创建镜像。

图 4.3 使用 Packer 的典型机器镜像构建过程

图 4.3 使用 Packer 构建 Jenkins 机器镜像

使用 Packer 的缺点是管理现有镜像:您需要通过使用标签或版本自行管理它们,并持续删除旧的、未使用的镜像(在 AWS 中,您需要为构成您的机器镜像或 AMI 的位存储付费)。

4.2.1 它是如何工作的?

图 4.4 说明了 Packer 用于烘焙机器镜像的过程。

图 4.4 Packer 烘焙工作流程

下面是过程中的步骤:

  1. 使用模板文件中定义的基镜像启动临时实例。

  2. 使用配置管理工具如 Ansible、Chef 或 Puppet,或者使用简单的自动化脚本来配置实例到所需的状态。

  3. 从临时运行实例创建新的机器镜像,并在镜像烘焙完成后关闭临时实例。

创建新的机器镜像后,从这个新镜像启动新的服务器将提供与临时实例上已完成的相同配置。这有助于提供平滑的部署过程。这也帮助我们快速扩展服务。

Packer 配置,也称为模板文件,可以写成 JSON 或 YAML 格式。它由以下三个主要组件组成:

  • 用户变量—本节用于参数化 Packer 模板文件,以便我们可以将秘密、环境变量和其他参数从模板中排除。本节有助于模板文件的便携性,并有助于分离出我们模板中可以修改的部分。变量可以通过命令行、环境变量、HashiCorp Vault (www.vaultproject.io)或文件传递。本节是一个键值映射,变量名被分配了一个默认值。

  • 构建器—本节包含 Packer 用于生成机器镜像的构建器列表。构建器负责创建实例并从它们生成机器镜像。构建器映射到单个机器镜像。本节包含包括类型(即构建器的名称)、访问密钥以及连接到平台(例如 AWS)所需的凭证等信息。

  • 提供者—本节是可选的,其中包含 Packer 在创建机器镜像之前,在运行实例中安装和配置软件所使用的提供者列表。类型指定了提供者的名称,例如 Shell、Chef 或 Ansible。

注意:有关支持的构建器的完整列表,请参阅官方文档www.packer.io/docs/builders/。有关支持的提供者的完整列表,请参阅www.packer.io/docs/provisioners/

Packer 在创建镜像时将配置烘焙到机器镜像中。这有助于在出现问题时创建相同的服务器。

4.2.2 安装和配置

Packer 是用 Go 编写的,它是一种编译型语言。因此,安装 Packer 很简单;您只需从 www.packer.io/downloads/ 下载适合您的系统和架构的二进制文件即可。图 4.5 显示了下载页面。

图 4.5 Packer 下载页面

注意:请确保您安装 Packer 二进制文件的目录位于 PATH 变量中。

安装 Packer 后,通过打开新的终端会话并执行以下命令来验证安装是否正常工作:

注意:在撰写本书时,Packer 的最新稳定版本为 1.7.2。

如果您收到错误信息,表明找不到 Packer,那么您的 PATH 环境变量设置不正确。否则,Packer 已安装,您就可以开始了!

4.2.3 烘焙机器镜像

安装 Packer 后,让我们直接进入主题并构建我们的第一个镜像。我们的第一个机器镜像将是一个预安装 Jenkins 的 Amazon EC2 AMI。要创建此 AMI,我们需要编写一个 Packer 配置文件。

注意:为了简洁起见,以下 Packer 模板文件已被裁剪。完整的模板可在 GitHub 仓库的 chapter4 文件夹下找到:mng.bz/GO8q

创建一个 template.json 文件,并填充以下内容。

列表 4.1 用于独立 Jenkins 服务的 Packer 模板。

{
    "variables" : { .
        "region" : "AWS REGION",
        "aws_profile": "AWS PROFILE",
        "source_ami" : "AMAZON LINUX AMI ID",
        "instance_type": "EC2 INSTANCE TYPE"
    },
    "builders" : [.
        {
            "type" : "amazon-ebs",
            "profile" : "{{user `aws_profile`}}",
            "region" : "{{user `region`}}",
            "instance_type" : "{{user `instance_type`}}",
            "source_ami" : "{{user `source_ami`}}",
            "ssh_username" : "ec2-user",
            "ami_name" : "jenkins-master-2.204.1",
            "ami_description" : "Amazon Linux Image with Jenkins Server",
    ],
    "provisioners" : [            {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

此模板文件由三个主要部分组成:

  • variables

  • builders

  • provisioners

我们不是在模板文件中硬编码值,而是在 Packer 运行时可以覆盖的变量中使用。在我们的示例中,我们在表 4.1 中定义了变量。

source_ami 的值替换为适当的 Amazon Linux AMI ID。Amazon Linux AMI ID 可以通过前往 AWS 管理控制台并导航到 EC2 仪表板找到。点击启动 EC2 实例。在“选择 AMI”选项卡中,在图 4.6 所示的搜索栏中输入Amazon Linux AMI

表 4.1 Packer 变量

变量 描述
region 启动 EC2 实例以创建 AMI 的 AWS 区域名称,例如 eu-central-1。虽然您始终可以从一个区域复制 AMI 到另一个区域,但为了简单起见,AMI 位置将与 Jenkins EC2 实例部署到的区域相同。
aws_profile 使用的 AWS 配置文件。有关 AWS CLI 配置的详细信息,请参阅第三章。如果您计划在 EC2 实例内部运行 Packer,也可以通过环境变量或使用 EC2 元数据提供 AWS 凭据。如果您计划使用 AWS 访问和秘密密钥,请勿在模板中保留它们,并且仅通过使用 -var 标志在运行时提供。
instance_type 在构建 AMI 时使用的 EC2 实例类型,例如t2.micro。支持的实例类型列表可以在aws.amazon.com/ec2/instance-types/找到。
source_ami 用于启动临时 EC2 实例的基 AMI。在先前的示例中,我们使用的是官方的 Amazon Linux 镜像。您可能需要根据此模板运行时存在的镜像和您使用的 AWS 区域更改源 AMI ID。

图 4.6 Amazon Linux 镜像标识符

您也可以通过在 Packer 模板文件中使用source_ami_filter属性以编程方式找到 ID。此属性将根据定义的过滤器自动填充source_ami属性。例如,以下片段选择最新的 Amazon Linux AMI(完整的模板文件可以从 chapter4/standalone/template-with-filter.json 复制)。

"builders" : [
        {
            "ami_name" : "jenkins-master-2.204.1",
            "ami_description" : "Amazon Linux Image with Jenkins Server",
            "source_ami_filter": {
                "filters": {
                  "virtualization-type": "hvm",
                  "name": "Amazon Linux AMI-*",
                  "root-device-type": "ebs"
                },
                "owners": ["amazon"],
                "most_recent": true
            }
        }
]

如果多个 AMI 都满足source_ami_filter中提供的所有过滤条件,则most_recent属性将选择最新的 Amazon Linux 镜像。

因为目标机器镜像是一个亚马逊机器镜像,所以我们使用amazon-ebs构建器。这是与 Packer 一起提供的亚马逊 EC2 AMI 构建器。这个构建器通过启动源 AMI,在其上提供配置,并将其重新打包成新的 AMI 来构建基于 EBS 的 AMI。根据目标平台,有多种构建器可供选择。EC2、VMware、VirtualBox 和其他平台都有单独的构建器。Packer 默认包含许多构建器,也可以扩展以添加新的构建器。

builder部分中的ami_name属性是将在 AWS 控制台中管理 AMI 时出现的最终 AMI 的名称。名称必须是唯一的。为了帮助确保唯一性,我已经将其添加为安装的 Jenkins 服务器版本的名称前缀,但您也可以使用以下格式的当前时间戳:

"ami_name" : "jenkins-master-2.204.1-{{timestamp}}"

{{timestamp}}将被 Packer 模板引擎替换为当前协调通用时间(UTC)的 UNIX 时间戳。

provisioners阶段负责安装和配置所有必需的依赖项。Packer 完全支持多种现代配置管理工具,如 Ansible、Chef 和 Puppet。Bash 脚本也受到支持。为了简化 Jenkins AMI 的烘焙过程,我们定义了一个名为 setup.sh 的 bash 脚本,其内容如下。

列表 4.2 安装 Jenkins LTS 的 Bash 脚本

#!/bin/bash
yum remove -y java
yum install -y java-1.8.0-openjdk
wget -O /etc/yum.repos.d/jenkins.repo
http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
yum install -y jenkins
chkconfig jenkins on
service jenkins start

脚本是自我解释的:它安装了 Java 开发工具包(JDK),这是运行 Jenkins 所必需的,然后安装 Jenkins 的最新稳定版本。在这里,我们安装了 Jenkins LTS 版本。尽管它在新功能方面可能落后,但它比每周发布版本提供了更多的稳定性。每周的 Jenkins 发布快速向需要它们的问题修复和新功能的用户和插件开发者提供。但对于更保守的用户来说,坚持一个变化较少且只接收重要问题修复的发布线更为可取。

一旦使用 Yum 软件包管理器安装了 Jenkins 软件包,脚本就会配置 Jenkins 在机器使用 chkconfig 命令重启后自动启动。

现在我们已经定义了模板文件,我们可以执行以下命令来验证模板文件的语法:

packer validate template.json

命令将返回零退出状态,表示 template.json 语法有效。

在我们将此模板用于构建镜像之前,我们需要将 AmazonEC2FullAccess 策略分配给第三章中为 Packer 创建的 IAM 用户,以便 Packer 能够部署 EC2 实例并从中创建机器镜像。

返回 AWS 控制台,导航到 IAM 仪表板,然后跳转到用户部分。然后,选择 Packer 用户并附加列表 4.3 中的策略,如图 4.7 所示。

图片

图 4.7 将 EC2 策略附加到 IAM 用户

注意:推荐的做法是提供 Packer 运行所需的最小权限集。以下列表是一个 IAM 策略,其中包含了 Amazon 插件运行所需的最小权限集。

列表 4.3 Packer 的 AWS IAM 策略

{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action" : [
          "ec2:AttachVolume",
          "ec2:AuthorizeSecurityGroupIngress",
          "ec2:CopyImage",
          "ec2:CreateImage",
          "ec2:CreateKeypair",
          "ec2:CreateSecurityGroup",
          "ec2:CreateSnapshot",
          "ec2:CreateTags",
          "ec2:CreateVolume",
          "ec2:DeleteKeyPair",
          "ec2:DeleteSecurityGroup",
          "ec2:DeleteSnapshot",
          "ec2:DeleteVolume",
          "ec2:DeregisterImage",
          "ec2:DescribeImageAttribute",
          "ec2:DescribeImages",
          "ec2:DescribeInstances",
          "ec2:DescribeInstanceStatus",
          "ec2:DescribeRegions",
          "ec2:DescribeSecurityGroups",
          "ec2:DescribeSnapshots",
          "ec2:DescribeSubnets",
          "ec2:DescribeTags",
          "ec2:DescribeVolumes",
          "ec2:DetachVolume",
          "ec2:GetPasswordData",
          "ec2:ModifyImageAttribute",
          "ec2:ModifyInstanceAttribute",
          "ec2:ModifySnapshotAttribute",
          "ec2:RegisterImage",
          "ec2:RunInstances",
          "ec2:StopInstances",
          "ec2:TerminateInstances"
        ],
        "Resource" : "*"
    }]
  }

配置了 IAM 用户后,现在是时候构建您的第一个镜像了。这是通过使用模板文件作为参数调用 packer build 命令来完成的:

packer build template.json

Packer 将根据模板文件中指定的配置部署 EC2 实例,然后在部署的实例上执行 bash 脚本。输出应该类似于以下内容。请注意,这个过程通常需要几分钟。

图片

在运行 packer build 命令的末尾,Packer 将输出作为构建部分创建的工件。工件是构建的结果,通常代表 AMI ID。(您的 ID 一定会与前面的不同。)在这个例子中,我们只有一个工件:AMI 是在法兰克福区域(eu-central-1)创建的。

您可以使用相同的模板文件为不同的平台创建 Jenkins 机器镜像,所有这些镜像都来自相同的规范。这是一个很好的功能,允许您创建不同类型提供商的机器镜像,而无需重复编码。例如,我们可以修改模板以添加 Google Compute Cloud 和 Microsoft Azure 构建器,如下面的列表所示。完整的模板可在 GitHub 存储库中找到(chapter4/standalone/template-multiple-builders.json)。

列表 4.4 Jenkins 多平台机器镜像构建

{
    "builders": [
        {
            "type": "amazon-ebs",
            "profile": "{{user `aws_profile`}}",
            "region": "{{user `region`}}",
            "instance_type": "{{user `instance_type`}}",
            "source_ami": "{{user `source_ami`}}",
            "ssh_username": "ec2-user",
            "ami_name": "jenkins-master-2.204.1",
            "ami_description": "Amazon Linux Image with Jenkins Server",
        },
        {
            "type": "azure-arm",
            "subscription_id": "{{user `subscription_id`}}",
            "client_id": "{{user `client_id`}}",
            "client_secret": "{{user `client_secret`}}",
            "tenant_id": "{{user `tenant_id`}}",
            "managed_image_resource_group_name": "{{user `resource_group`}}",
            "managed_image_name": "jenkins-master-v22041",
            "os_type": "Linux",
            "image_publisher": "OpenLogic",
            "image_offer": "CentOS",
            "image_sku": "8.0",
            "location": "{{user `location`}}",
            "vm_size": "Standard_B1ms"
        },
        {
            "type": "googlecompute",
            "image_name": "jenkins-master-v22041",
            "account_file": "{{user `service_account`}}",
            "project_id": "{{user `project`}}",
            "source_image_family": "centos-8",
            "ssh_username": "packer",
            "zone": "{{user `zone`}}"
        }
    ]
}

Packer 将并行创建多个平台的多个 Jenkins 镜像,所有这些镜像都从单个模板配置。在这个例子中,Packer 可以并行创建 Amazon Machine Image、Azure 镜像和 Google Compute Engine 镜像,使用相同的脚本进行配置,从而产生几乎相同的 Jenkins 镜像。

注意:有关如何为 Azure 虚拟机和 Google Compute Engine 实例烘焙机器图像的逐步指南,请参阅第六章。

一旦创建 AMI,Packer 将终止临时 EC2 实例,烘焙好的 AMI 将在 EC2 仪表板上的镜像部分下的 AMIs 部分中可用,如图 4.8 所示。

图片

图 4.8 在镜像部分有一个新的烘焙镜像可用。

现在我们已经创建了 Jenkins AMI,让我们测试一下,看看 Jenkins 是否已正确安装。跳转到实例,点击启动实例按钮。然后,从我的 AMIs 部分选择 Packer 构建的 AMI,如图 4.9 所示。

图片

图 4.9 新 AMI 可以从我的 AMIs 部分选择。

对于实例类型,选择一种通用实例,例如t2.micro,它是免费层合格的。我们将在下一章中介绍 Jenkins 资源需求。

目前,将所有其他值保留在默认设置。导航到添加标签部分,并在值框中为您的 EC2 实例输入一个名称。这个名称,更准确地说是标签,将在实例启动时出现在控制台中。这使得跟踪正在运行的 Jenkins 实例变得容易。

配置安全组(控制实例流量的防火墙)以允许来自任何地方的 8080 端口的流量。8080 端口是 Jenkins Web 仪表板的默认端口。

注意:实例将在默认 VPC 内部署。在第五章中,我们将从头开始部署 Jenkins 集群到自定义 VPC,并介绍高级网络配置。

图片

图 4.10 允许 8080 端口的流量

EC2 实例的安全组规则应类似于图 4.10。

确保允许来自您计算机的公共 IPv4 地址的 22 端口的入站流量,以便授权 SSH 流量。这是强制性的;否则,您将无法稍后解锁 Jenkins 仪表板。

最后,在审查部分验证配置细节,并选择一个 SSH 密钥对,或者如果你是第一次启动 EC2 实例,创建一个新的密钥对。这个配置将允许你通过 SSH 连接到你的实例。

一旦实例运行,将你的浏览器指向实例的公共 IP 地址,并指定端口 8080。Jenkins 设置向导应该会出现在屏幕上,如图 4.11 所示。恭喜你——你已经成功从使用 Packer 构建的定制 AMI 中部署了 Jenkins 实例。

图片

图 4.11 Jenkins 设置向导

你将被要求使用初始密码解锁 Jenkins。你可以在这个文件中找到这个密码:/var/lib/jenkins/secrets/initialAdminPassword。(以下章节将介绍如何为 Jenkins 创建自定义管理员账户。)

到目前为止,我们已经以独立模式部署了 Jenkins。图 4.12 总结了当前部署的架构。

图片

图 4.12 AWS 中的 Jenkins 独立模式

注意:确保当你不再需要实例时终止它,以停止为此实例产生费用。

接下来,你将学习如何使用 Groovy 脚本来定制和配置 Jenkins 设置,同时烘焙 Jenkins 主 AMI。此外,我们还将创建另一个 Jenkins 工作节点镜像,以实现 Jenkins 的规模部署。

4.3 烘焙 Jenkins 主 AMI

我们可以使用上一节中构建的 AMI,但最终的 Jenkins 实例仍然有许多需要手动配置的设置,包括 Jenkins 管理员凭证、设置 CI/CD 管道的所需插件和安全检查。虽然你可以手动配置这些设置,但本书的目的是尽可能避免运营开销。我们希望在部署一个高可用性和容错性强的 Jenkins 集群时,使用自动化工具如 HashiCorp Packer 和 Terraform,通过少量命令自动化繁琐的任务。

注意:当我提到“高可用性”时,我指的是一个可以持续运行而不会出现故障的 Jenkins 集群。

要完全自动化 Jenkins 主实例,我们将使用 Jenkins 初始化脚本。我们将利用 Groovy 脚本的力量,并将它们放置在 $JENKINS_HOME/init.groovy.d 目录中。这个目录将在 Jenkins 启动时被消耗。因此,它可以用来预先配置 Jenkins 到目标所需状态。

4.3.1 启动时配置 Jenkins

这些脚本是用 Groovy 编写的,并在与 Jenkins 相同的 Java 虚拟机(JVM)中执行,允许完全访问 Jenkins 的领域模型(我们可以访问 Jenkins 及其所有插件中的类)。

注意:Groovy 脚本的另一种选择是 Jenkins 配置为代码(JCasC)插件。更多详情,请参考 GitHub 上的官方指南:mng.bz/zEJa

列表 4.5 中的 basic-security.groovy 脚本创建了一个具有完全管理员访问权限的 Jenkins 用户。(您需要将 USERNAMEPASSWORD 属性替换为您自己的值。)此外,默认情况下,匿名读取访问被禁用,这意味着 Jenkins 需要身份验证才能访问网页仪表板。但是,您可以在 instance.save() 语句之前添加 strategy.setAllowAnonymousRead(true) 指令来启用匿名读取访问。

列表 4.5 basic-security.groovy 脚本

#!groovy

import jenkins.model.*
import hudson.security.*

def instance = Jenkins.getInstance()                     ❶
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount('USERNAME','PASSWORD')         ❷
instance.setSecurityRealm(hudsonRealm)

def strategy = new FullControlOnceLoggedInAuthorizationStrategy()
instance.setAuthorizationStrategy(strategy)              ❸
instance.save()

获取 Jenkins 模型的一个实例

通过注册密码为用户创建一个新的账户

❸ 允许已登录用户完全访问

除了用户管理之外,我们还将为加强 Jenkins 的安全性设置一些基本配置,以防止 CSRF 攻击。启用 CSRF 保护后,所有发出的令牌都应该包含一个网络会话,以防止外部攻击者获取网络会话。但是,如果您的自动化脚本使用 CSRF 令牌进行身份验证,您可以通过安装 Strict Crumb Issuer 插件(在构建 Jenkins 图像时安装的插件列表中可用)来排除网络会话 ID 的验证标准。我们将在以下列表中通过 csrf-protection.groovy 脚本启用 CSRF 保护。

列表 4.6 csrf-protection.groovy 脚本

#!groovy

import hudson.security.csrf.DefaultCrumbIssuer
import jenkins.model.Jenkins

def instance = Jenkins.getInstance()
instance.setCrumbIssuer(new DefaultCrumbIssuer(true))     ❶
instance.save()

通过设置 crumb 发起器启用 CSRF 保护

此选项在新安装中默认启用,从 Jenkins 2.x 开始。您也可以通过更新 JENKINS_JAVA_OPTIONS 来启用 CSRF。添加以下参数:

JENKINS_JAVA_OPTIONS="-Dhudson.security.csrf.DefaultCrumbIssuer=true"

注意:如果您正在使用 Jenkins 代码检查功能来验证 Jenkinsfile 与受 CSRF 保护的 Jenkins 服务器,由于 Jenkins 2.96,您需要使用不需要 CSRF 令牌(crumb)的 API 令牌。

Jenkins 内置了一个 CLI,允许用户和管理员从脚本或 shell 环境访问 Jenkins。出于安全原因(防止远程访问),不建议使用 CLI。因此,我们将通过以下列表中的 disable-cli.groovy 脚本来禁用它。

列表 4.7 disable-cli.groovy 脚本

#!groovy

import jenkins.model.Jenkins

Jenkins jenkins = Jenkins.getInstance()      ❶
jenkins.CLI.get().setEnabled(false)          ❶
jenkins.save()

❶ 获取 Jenkins 实例并禁用 CLI 访问

我们还将禁用 JNLP 和旧的未加密协议(JNLP-connect、JNLP2-connect、JNLP3-connect 和 CLI-connect),以消除网页仪表板中的警告信息。禁用-jnlp.groovy 脚本如下所示。

列表 4.8 disable-jnlp.groovy 脚本

#!groovy

import jenkins.model.Jenkins
import jenkins.security.s2m.*

Jenkins jenkins = Jenkins.getInstance()
jenkins.setSlaveAgentPort(-1)                                               ❶
HashSet<String> newProtocols = new HashSet<>(jenkins.getAgentProtocols());  ❷
newProtocols.removeAll(Arrays.asList(                                       ❷
        "JNLP3-connect", "JNLP2-connect", "JNLP-connect", "CLI-connect"     ❷
));                                                                         ❷
jenkins.setAgentProtocols(newProtocols);                                    ❷
jenkins.save()

设置 0 以指示随机可用的 TCP 端口,-1 以禁用此服务

❷ 使用可用的代理协议初始化 HashSet 结构,从结构中删除旧的未加密协议,并保存新的列表

为开发或故障排除向新的本地 Jenkins 服务器添加凭据可能是一项艰巨的任务。然而,通过 Groovy 脚本和正确的设置,开发者可以自动化将所需的凭据添加到新的 Jenkins 服务器中。

列表 4.9 中的 Groovy 脚本基于我们将用于部署 Jenkins 工作节点实例的 AWS 密钥对创建 SSH 凭据。SSH 凭据对象是通过使用 BasicSSHUserPrivateKey 构造函数创建的,它接受凭据范围、用户名、SSH 私钥和密码短语作为参数。这些 SSH 凭据的使用将在第五章中说明。

列表 4.9 节点代理 Groovy 脚本

import jenkins.model.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.common.*
import com.cloudbees.plugins.credentials.domains.*
import com.cloudbees.plugins.credentials.impl.*
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*
import hudson.plugins.sshslaves.*;

domain = Domain.global()
store = Jenkins.instance
.getExtensionList('com.cloudbees.plugins.credentials \
                   .SystemCredentialsProvider')[0].getStore()

slavesPrivateKey = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL,   ❶
         "Jenkins-workers",                                              ❶
         "Ec2-user",                                                     ❶
         new BasicSSHUserPrivateKey.UsersPrivateKeySource(),             ❶
         "", "")                                                         ❶
store.addCredentials(domain, slavesPrivateKey)                           ❶

❶ 创建一个类型为“SSH 用户名与私钥”的 Jenkins 凭据。构造函数接受用户名、私钥、密码短语和描述作为参数。

注意:现在每次 Jenkins 服务器重启时,脚本都会运行并为你应用配置。你不需要担心每次服务器重启时都要手动执行这些设置。

你可以使用 Groovy 初始化脚本来自定义 Jenkins 并强制执行所需的状态。尽管编写 Groovy 脚本需要了解 Jenkins 内部和 API,但你已经看到了如何在 Jenkins 初始化时使用 Groovy 脚本配置常见任务和设置。我们仍然需要安装插件以扩展 Jenkins 功能,以便能够构建 CI/CD 管道。

4.3.2 发现 Jenkins 插件

插件可以从 Jenkins 仪表板轻松安装。然而,本节的目的在于构建一个完全自动化的 Jenkins AMI,因为如果你想要安装许多插件,这个手动过程可能会相当长且无聊。因此,我们将使用 Jenkins 社区提供的脚本安装插件,包括它们的依赖项。这些脚本接受一个包含要安装的 Jenkins 插件列表的文件作为参数。

表 4.2 列出了一些最有用的插件,这些插件可以帮助开发者节省时间,并使他们的生活更加轻松。完整的列表可以在 GitHub 仓库的 chapter4/distributed/master/config/plugins.txt 中找到。

表 4.2 必备的 Jenkins 插件

插件 描述
blueocean 提供了新的 Jenkins 用户体验,具有复杂的 CI/CD 管道可视化以及一个捆绑的管道编辑器,该编辑器通过引导用户通过直观和可视化的过程创建管道,使自动化 CI/CD 工作流程变得易于接近。请参阅第二章以探索 Blue Ocean 模式的关键特性。
git 提供对任何 Git 服务器访问权限,并在 Jenkins 管道中支持基本的 Git 操作。它可以拉取、获取、检出、分支、列出、合并、标记和推送 Git 仓库。
ssh-agent 允许你通过 Jenkins 中的 ssh-agent 为构建提供 SSH 凭据。ssh-agent 是一个辅助程序,用于保存用于公钥认证的私钥。
ssh-credentials 允许你在 Jenkins 中存储 SSH 凭据。它用于通过 SSH 启动 Jenkins 工作节点,并在 Kubernetes 集群上远程执行 Docker 命令。
slack 提供了 Jenkins 与 Slack 的通知集成。它可以在 CI/CD 管道完成后发送 Slack 通知,显示 Jenkins 作业的构建状态。此插件需要在 Slack 端进行一些简单的设置,以便连接和发布消息。
credentials-binding 允许将凭证绑定到环境变量,以便在杂项构建步骤中使用。它为您提供了一个简单的方法来打包所有作业的秘密文件和密码,并在构建期间使用环境变量访问它们。
github-pullrequest 对于将 Jenkins 与 GitHub 仓库集成至关重要,它支持 GitHub 拉取请求、分支和自定义 webhook。每次打开拉取请求时,GitHub 都会触发一个新的钩子,一旦 Jenkins 收到钩子,它就会运行相关的作业。
job-dsl 允许以程序化的形式在可读文件中定义作业。它可以用来为 Jenkins freestyle 作业创建复杂的管道。
jira 几乎做了它所说的所有事情。它允许开发者在 Jenkins 中集成 Jira (www.atlassian.com/software/jira),以在 CI/CD 管道中更新 Jira 开放问题。它还将构建和部署信息与相关的 Jira 票据关联,并在 Jira 板上暴露有关管道的关键信息。
htmlpublisher 在构建时生成 HTML 报告非常有用。它可以用来生成代码覆盖率 HTML 报告,并以用户友好的方式跟踪测试覆盖应用程序源代码的百分比。
email-ext 可以用来发送电子邮件通知。它高度可定制:您可以配置通知触发器、内容和收件人。此外,它支持电子邮件正文的纯文本和 HTML 格式。
sonar 允许轻松集成 SonarQube (www.sonarqube.org),这是一个开源平台,用于持续检查代码质量和代码安全性。
embeddable-build-status 为您的所有 Jenkins 作业生成徽章,实时显示它们的构建状态。您可以将这些徽章添加到 Git 仓库的 README.md 文件中。

注意:这些只是我们将要使用的一些插件,未来的章节将提供更多插件以供探索。

在 Jenkins 管道中,有超过一千个插件可供使用,几乎支持构建、部署和自动化项目的所有解决方案、工具和流程。如图 4.13 所示的 Jenkins 插件索引,在 plugins.jenkins.io/ 上有超过 1,800 个插件,免费下载和使用。

图 4.13 Jenkins 插件

注意:在安装 Jenkins 插件之前,请务必查看插件描述页面中的变更日志,因为并非所有插件都安全使用。此外,始终选择可用的最新稳定版本。

现在您对基本的 Jenkins 插件更加熟悉了。让我们继续安装它们。

列表 4.10 中的脚本将逐行遍历包含 Jenkins 插件列表的文件,然后向 Jenkins 插件索引发出 cURL 命令以下载插件。最后,脚本将下载的插件文件复制到 /var/lib/jenkins/plugins 文件夹。列表展示了主要功能,完整的脚本可以从 GitHub 仓库 chapter4/distributed/master/config/install-plugins.sh 下载。

列表 4.10 install-plugins.sh 脚本

#!/bin/bash
installPlugin() {
  if [ -f ${plugin_dir}/${1}.hpi -o -f ${plugin_dir}/${1}.jpi ]; then
    if [ "$2" == "1" ]; then
      return 1
    fi
    echo "Skipped: $1 (already installed)"
    return 0
  else
    echo "Installing: $1"
    curl -L --silent --output ${plugin_dir}/${1}.hpi  https://updates.jenkins-ci.org/latest/${1}.hpi
    return 0
  fi
}

.hpi 扩展名代表 Hudson 插件(记住,Jenkins 是 Hudson 项目的分支)。随着从 Hudson 转向 Jenkins,这变成了 Jenkins 插件,因此出现了 .jpi 格式。自从 Jenkins v1.5 版本发布以来,所有 .hpi 插件文件在启动时都会自动重命名为 .jpi。

到目前为止,我们已经配置并自动化了所有设置即插即用 Jenkins 服务器所需的任务。因此,在 Jenkins 启动时无需设置向导(见图 4.11)。因此,我们将通过编写 Groovy 初始化脚本来禁用它。创建一个包含以下内容的 skip-jenkins-setup.groovy 脚本。

列表 4.11 skip-jenkins-setup.groovy 脚本

#!groovy

import jenkins.model.*
import hudson.util.*;
import jenkins.install.*;

def instance = Jenkins.getInstance()
instance.setInstallState(InstallState.INITIAL_SETUP_COMPLETED)

最后,我们将更新第一部分使用的 Packer 模板文件,通过使用文件 provisioner (www.packer.io/docs/provisioners/file/) 将之前描述的 Groovy 脚本复制到临时实例。接下来,我们使用 shell provisioner 将这些文件移动到 init.groovy.d 文件夹。template.json 文件应类似于以下列表。

列表 4.12 Jenkins 主机模板文件

{
    "variables" : {...},                                         ❶
    "builders" : [
        {
            "type" : "amazon-ebs",
            "profile" : "{{user `aws_profile`}}",
            "region" : "{{user `region`}}",
            "instance_type" : "{{user `instance_type`}}",
            "source_ami" : "{{user `source_ami`}}",
            "ssh_username" : "ec2-user",
            "ami_name" : "jenkins-master-2.204.1",               ❷
            "ami_description" : "Amazon Linux Image with Jenkins Server"
        }
    ],
    "provisioners" : [
        {
            "type" : "file",                                     ❸
            "source" : "./scripts",                              ❸
            "destination" : "/tmp/"                              ❸
        },
        {
            "type" : "file",                                     ❹
            "source" : "./config",                               ❹
            "destination" : "/tmp/"                              ❹
        },
        {
            "type" : "file",                                     ❺
            "source" : "{{user `ssh_key`}}",                     ❺
            "destination" : "/tmp/id_rsa"                        ❺
        },
        {
            "type" : "shell",                                    ❻
            "script" : "./setup.sh",                             ❻
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"    ❻
        }
    ]
}

❶ 应在此处声明变量列表,例如:aws_profile、region、instance_type 和 source_ami

❷ 烘焙机器镜像的名称。版本号(2.204.1)应根据您当前安装的版本进行替换。

❸ 将 Groovy 脚本文件夹从本地机器复制到主机机的 /tmp 文件夹

❹ 将配置文件从本地机器复制到主机机的 /tmp 文件夹

❺ 将用户私有的 SSH 密钥复制到 /tmp 文件夹

❻ 执行 setup.sh 脚本来将文件从 /tmp 文件夹复制到正确的文件夹,并安装 Jenkins 及其依赖项

备注:为了简洁,省略了变量部分。完整的模板文件可以在 GitHub 上找到,位于 chapter4/distributed/master/template.json。

SSH 密钥可以使用 ssh-keygen 生成。该命令将提供一系列提示。请随意使用默认值。然而,从安全角度考虑,输入密码短语是个好主意。表 4.3 提供了 Packer 变量的完整列表。

表 4.3 Jenkins 主机 Packer 变量

变量 描述
region Jenkins 主机机器镜像将被创建的 AWS 区域,例如 eu-central-1 (也称为法兰克福)。
aws_profile 在 AWS 共享凭证文件中使用的配置文件。有关指定配置文件的更多详细信息,请参阅 Amazon 的文档:docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html
instance_type 在烘焙目标 AMI 时使用的 EC2 实例类型,例如 t2.micro,它是免费层合格的。
source_ami 将作为临时实例基础的源 AMI。我们使用官方的 Amazon Linux 映像。ID 应根据您使用的 AWS 区域进行更新。请参考图 4.6 以获取示例。
ssh_key 私有 SSH 密钥位置 (~/.ssh/id_rsa),这是您将用于 SSH 到 Jenkins 工作实例的相同密钥。在启动时将执行一个 Groovy 脚本,将私有密钥作为凭证添加到 Jenkins 主机,以通过 SSH 设置与 Jenkins 工作实例的初始连接。

一旦文件上传到由 Packer 构建的临时实例,就会执行 setup.sh 脚本来安装 Jenkins LTS 版本。接下来,脚本安装 Git 客户端(用于在高级章节中克隆 GitHub 仓库)。然后,它将工作实例的私有 SSH 密钥复制到 /var/lib/jenkins/.ssh 文件夹并设置权限。最后,它将 Groovy 脚本移动到初始化文件夹,通过执行 install-plugins.sh 脚本来安装基本插件,并启动 Jenkins 服务器。

值得注意的是,脚本文件已上传到 /tmp 文件夹;Packer 只能上传到配置用户 (ec2-user) 有权限访问的位置。以下列表包含 setup.sh 的内容。

列表 4.13 setup.sh 脚本(安装 Jenkins)

#!/bin/bash
yum remove -y java
yum install -y java-1.8.0-openjdk                                        ❶
wget -O /etc/yum.repos.d/jenkins.repo                                    ❶
http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo                     ❶
rpm --import https://jenkins-ci.org/redhat-stable/jenkins-ci.org.key     ❶
yum install -y jenkins                                                   ❶
chkconfig jenkins on                                                     ❶

yum install -y git                                                       ❷
mkdir /var/lib/jenkins/.ssh                                              ❸
touch /var/lib/jenkins/.ssh/known_hosts                                  ❸
chown -R jenkins:jenkins /var/lib/jenkins/.ssh                           ❸
chmod 700 /var/lib/jenkins/.ssh                                          ❸
mv /tmp/id_rsa /var/lib/jenkins/.ssh/id_rsa                              ❸
chmod 600 /var/lib/jenkins/.ssh/id_rsa                                   ❸
chown -R jenkins:jenkins /var/lib/jenkins/.ssh/id_rsa                    ❸

mkdir -p /var/lib/jenkins/init.groovy.d                                  ❹
mv /tmp/*.groovy /var/lib/jenkins/init.groovy.d/                         ❹
mv /tmp/jenkins /etc/sysconfig/jenkins
chmod +x /tmp/install-plugins.sh                                         ❺
bash /tmp/install-plugins.sh                                             ❺
service jenkins start                                                    ❻

❶ 安装 JDK(最低版本 v1.8.0),这是 Jenkins 运行所必需的

❷ 安装 Git 客户端,在后续章节中克隆 GitHub 项目仓库时将需要使用

❸ 将用于部署 Jenkins 工作实例/代理的私有 SSH 密钥复制到 JENKINS_HOME

❹ 将 Groovy 脚本移动到 init.groovy.d

❺ 通过运行 install-plugins.sh 安装所需的依赖项

❻ 启动 Jenkins 服务

模板目录结构应如下所示。脚本目录包含初始配置和种子脚本。配置文件夹包含要安装的基本插件的列表,以及从 Jenkins 插件索引安装插件的 shell 脚本:

├── config
│     ├── install-plugins.sh
│     ├── jenkins
│     └── plugins.txt
├── scripts
│     ├── basic-security.groovy
│     ├── csrf-protection.groovy
│     └── disable-cli.groovy
│     ├── disable-jnlp.groovy
│     ├── node-agent.groovy
│     └── skip-jenkins-setup.groovy
├── setup.sh
└── template.json

注意:Jenkins 在 /etc/sysconfig/jenkins 文件中捕获启动配置参数。如果您想添加 Java 参数,那就是您要找的文件。

在构建 AMI 之前,通过发出 packervalidate 命令验证模板文件的语法正确性是个好主意。如果模板有效,预期的输出是 Template validated successfully

现在模板已验证,我们将使用 packerbuild 命令来烘焙 AMI:

packer build template.json

该过程可能需要几分钟。预期输出类似于以下内容。

如果脚本成功,Packer 应该显示包含 AMI ID 的消息,并且 Jenkins 主节点 AMI 将在 EC2 仪表板中可用,如图 4.14 所示。

图 4.14 Jenkins 主节点 AMI

注意:AMI 名称应该是唯一的。因此,如果已经存在,您可能需要从您的 AWS 账户中删除现有的镜像。

最后,我们可以基于烘焙的 AMI 启动一个 EC2 实例。一旦实例运行,将您的浏览器指向实例的公共 IP 地址的 8080 端口。过一会儿,您将看到图 4.15 中的屏幕。

图 4.15 Jenkins 网页仪表板

这次,设置向导应该会消失,并且应该添加许多功能。使用列表 4.5 中的基本安全.groovy 脚本中定义的管理员凭据登录。登录后,您可以通过转到左侧的凭据项来验证 Jenkins 凭据是否已创建;请参见图 4.16。到目前为止,只创建了 Jenkins 工作节点 SSH 凭据(请参见列表 4.9),但您可以通过自定义 Groovy 脚本来创建针对 GitHub、Nexus 或 SonarQube 等外部服务的其他凭据。

图 4.16 Jenkins 凭据

此外,还安装了基本插件。从主页跳转到管理 Jenkins,然后导航到插件。您应该在已安装选项卡上看到默认安装的插件列表,如图 4.17 所示。

图 4.17 Jenkins 已安装插件

现在我们已经将 Jenkins 配置定义为代码,我们可以在不同的机器上尽可能多地启动它,以获得相同的结果。而且我们没有经历任何繁琐的 GUI 手动操作。

4.4 烘焙 Jenkins 工作节点 AMI

Jenkins 工作节点 AMI 烘焙过程应该是直接的;请参见以下列表。一个实例要成为 Jenkins 工作节点或构建代理的唯一要求是拥有 JDK。现代 Jenkins 版本需要 Java 8 运行环境。

列表 4.14 Jenkins 工作节点模板文件

{
    "variables" : {...},
    "builders" : [
        {
            "type" : "amazon-ebs",
            "profile" : "{{user `aws_profile`}}",
            "region" : "{{user `region`}}",
            "instance_type" : "{{user `instance_type`}}",
            "source_ami" : "{{user `source_ami`}}",
            "ssh_username" : "ec2-user",
            "ami_name" : "jenkins-worker",
            "ami_description" : "Jenkins worker's AMI",

    ],
    "provisioners" : [
        {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

表 4.4 中的变量应在模板文件构建时间内提供,或使用 -var 标志。

表 4.4 Jenkins 工作节点 Packer 变量

变量 描述
region AWS 区域,其中将创建 Jenkins 工作节点机器镜像。类似于 Jenkins 主节点 AWS 区域值。
aws_profile 在 AWS 共享凭据文件中使用的配置文件。有关指定配置文件的更多详细信息,请参阅 Amazon 的文档:mng.bz/01Yx
instance_type 在烘焙目标 AMI 时使用的 EC2 实例类型,例如t2.micro,它符合免费层资格。
source_ami 将作为临时实例基础的源 AMI。我们使用官方的 Amazon Linux 镜像。ID 应根据您使用的 AWS 区域进行更新。

Packer 将使用 shell 提供程序来安装 JDK,以及您可能需要运行构建的任何工具(例如 Git 或 Docker)。您可以将此脚本进一步扩展,创建一个名为jenkins的用户,并为其创建一个主目录以存储 Jenkins 作业工作空间,如下所示。

列表 4.15 setup.sh 脚本。

#!/bin/bash
yum remove -y java
yum update -y
yum install -y git docker java-1.8.0-openjdk
usermod -aG docker ec2-user
systemctl enable docker

注意:Docker 是必需的,因为我们将在接下来的章节中为 Docker 化的微服务定义 CI/CD 管道。

输出packer build命令以烘焙 Jenkins 工作节点 AMI。一旦图像烘焙过程完成,工作节点的 AMI 将在 EC2 仪表板上可用,如图 4.18 所示。

图片

图 4.18 Jenkins 工作节点 AMI

注意:在运行前面的示例之后,您的 AWS 账户现在已关联了一个 AMI。这些 AMI 由 Amazon 存储在 S3 中,因此除非您希望每月支付约 0.01 美元的费用,否则如果您不需要这些镜像,您可能希望删除它们。

现在我们已经准备好了 Jenkins 集群 AMI,我们将在下一章中使用它们,通过 HashiCorp Terraform IaC 工具在 AWS 上部署我们的集群。图 4.19 说明了 Terraform 的集成方式。

图片

图 4.19 Packer 将从模板文件中提供一个临时实例,并使用所有需要的配置和依赖项配置该实例。然后,Terraform 将基于烘焙的镜像部署 EC2 实例。

如果您计划采用不可变基础设施方法来升级 Jenkins 或安装额外的插件,使用 Packer 触发配置过程可能会变得具有挑战性。这就是为什么您应该选择自动化,并使用 Jenkins 设置一个管道来自动化 AMI 的烘焙工作流程。基本工作流程将使用 GitHub 存储 Packer 模板文件,并在推送事件上触发 Jenkins 上的构建。作业将验证模板更改,启动烘焙过程(1),并基于新的烘焙 AMI 创建一个 EC2 实例(2)。图 4.20 总结了整个工作流程。

图片

图 4.20 使用 Jenkins 自动化 AMI

注意:第七章介绍了如何设置 GitHub 钩子,以便在发生推送或合并事件时持续触发 Jenkins 构建作业。

摘要

  • HashiCorp Packer 利用不可变基础设施的力量来烘焙包含所有所需依赖项的自定义机器镜像。

  • 设置 Jenkins 是一个复杂的过程,因为 Jenkins 及其插件都需要调整和配置,在“管理 Jenkins”部分的 Web UI 中需要设置数十个参数。

  • 在 init.groovy 目录中的配置脚本在 Jenkins 启动时按字母顺序执行。这对于设置种子和配置作业接口是理想的。

  • Jenkins 提供了数千个插件来支持构建、部署和自动化任何项目。

  • 每周发布的 Jenkins 版本迅速为需要它们的用户和插件开发者提供错误修复和新功能。然而,长期支持版本因其稳定性而更受欢迎。

6 在多个云服务提供商上部署高可用性 Jenkins

本章涵盖

  • 使用 Packer 自动化 Jenkins VM 的构建过程

  • 在 Azure、GCP 和 DigitalOcean 上部署 Jenkins 集群

  • 通过按需创建 Jenkins 工作节点来降低部署成本

  • 使用相同的 Packer 模板在不同的云服务提供商中创建相同的 Jenkins 机器镜像

你已经看到了如何在 AWS 上部署 Jenkins 集群来实现容错。本章将尝试通过使用相同的工具和流程来自动化在 Microsoft Azure、Google Cloud Platform 和 DigitalOcean 等不同云服务提供商上创建集群,从而在基础设施级别实现相同的需求速度和自动化——从基础设施即服务(IaaS)到平台即服务(PaaS)提供商。

你可能会注意到本章的一些部分与上一章中你阅读的内容相似,甚至完全相同。这种部分重复的原因是为了实现本书的目标,即说明如何使用 Jenkins 与云原生应用结合使用——因为并非每个人都将 AWS 作为他们的主要云服务提供商,我希望这本书对其他人以及跳过第五章直接到这里的人都有用。

注意:使用本章中详细说明的提供商带来一些好处和缺点。无论你选择哪个提供商,你都会在某个阶段遇到问题。

6.1 谷歌云平台

我们都知道 AWS 没有最友好的 Web 控制台。谷歌云平台(GCP)通过提供更好的用户体验而成功超越了 AWS。GCP 由各种服务组成,从计算到网络,再到比其竞争对手(AWS)便宜 25%的提取-转换-加载(ETL)管道,这得益于更低的增量计费(10 分钟而不是 1 小时)。

此外,GCP 在处理大数据方面拥有更多专业知识,例如 BigQuery (cloud.google.com/bigquery)、Cloud Bigtable (cloud.google.com/bigtable) 和 Dataflow (cloud.google.com/dataflow)等服务。此外,您还可以在 Kubernetes 上运行容器工作负载,并使用 TensorFlow 部署机器学习(ML)模型;Kubernetes 和 TensorFlow 都源自 Google。然而,与市场上最老练、最成熟的云服务提供商 AWS 相比,GCP 仍缺乏一些功能。

那么,为什么要在 GCP 上使用 Jenkins 呢?您可以与 Kubernetes 实现无缝集成;使用 Google Kubernetes Engine (GKE) 等服务,您可以运行短暂的 Jenkins 工作节点,确保每个构建都在干净的环境中运行。原生支持 Docker 容器是另一个原因,例如使用容器注册服务来存储和管理 CI/CD 管道中构建的 Docker 镜像。此外,您还可以集成安全性和合规性,并获取有关构建工件漏洞影响和可用修复的详细报告。最后,当您使用 GCP 虚拟机 (VM) 加速 Jenkins 构建时,您将按使用量付费。

话虽如此,让我们转到 GCP 上使用 Terraform 和 Packer 部署 Jenkins 集群的步骤。要开始,请使用 Gmail 地址注册一个免费帐户(console.cloud.google.com/)。您将自动获得 12 个月的免费试用,并享有 300 美元的信用额度。您需要提供信用卡详细信息,但在试用期间结束或用完 300 美元信用额度之前,您不会产生额外费用。

注意:部署 Jenkins 集群的预估成本为 $0.00。此成本假设您处于 GCP 免费层限制内,并且在部署基础设施后的 1 小时内终止所有资源。

6.1.1 构建 Jenkins 虚拟机镜像

为了让 Packer 构建自定义镜像,它需要与 GCP 交互。因此,我们需要为 Packer 创建一个专用的服务帐户,以便授权其访问 Google API 中的资源。

前往 GCP 控制台并导航到图 6.1 所示的 IAM & Admin 仪表板。在服务帐户部分,创建一个名为Packer的新服务帐户,然后单击“创建”按钮。

图 6.1 创建 Packer 服务帐户

将项目所有者角色分配给服务帐户(或至少选择 Compute Engine Instance Admin 和 Service Account User 角色),然后单击“继续”按钮,如图 6.2 所示。

图 6.2 设置 Packer 服务帐户权限

每个服务帐户都与一个密钥(JSON 或 P12 格式)相关联,该密钥由 GCP 管理。此密钥用于服务到服务的身份验证。通过单击“创建密钥”按钮下载 JSON 密钥。服务帐户文件将在计算机上创建和下载。复制此 JSON 文件并将其放置在安全文件夹中。确保在您的 GCP 项目中启用了 Google Compute Engine API。

注意:如果您不熟悉 Packer,请参阅第四章以获取安装和配置的逐步指南。

接下来,更新第四章列表 4.16 中提供的 Jenkins 工作节点 Packer 模板文件,或从第六章 GitHub 仓库 chapter6/gcp/packer/worker/setup.sh 复制并粘贴以下内容。

列表 6.1 Jenkins 工作节点模板文件。

{
    "variables" : {
        "service_account" : "SERVICE ACCOUNT JSON FILE PATH",     ❶
        "project": "GCP PROJECT ID",                              ❶
        "zone": "GCP ZONE ID"                                     ❶
    },
    "builders" : [
        {
            "type": "googlecompute",
            "image_name" : "jenkins-worker",
            "account_file": "{{user `service_account`}}",
            "project_id": "{{user `project`}}",
            "source_image_family": "centos-8",
            "ssh_username": "packer",
            "zone": "{{user `zone`}}"
          }
    ],
    "provisioners" : [
        {
            "type" : "shell",                                     ❷
            "script" : "./setup.sh",                              ❷
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"     ❷
        }
    ]
}

❶ 定义在运行时提供的变量。这些值可以从 GCP 仪表板中获取。

❷ 以特权模式运行 shell 脚本以安装 Git 客户端、Docker 和所需的依赖项

注意:如果您从配置了 GCE 服务账户的 Google Compute Engine (GCE)实例运行烘焙过程,则不需要 JSON 账户文件。Packer 将从元数据服务器获取凭证。

列表 6.1 使用googlecompute构建器在 CentOS 基础镜像之上创建机器镜像。然后它使用第四章列表 4.13 中提供的 shell 脚本来配置临时机器以安装所有需要的依赖项——Git、JDK 和 Docker。

Packer 的强大之处在于利用模板文件来创建与目标平台无关的相同虚拟机镜像。因此,我们可以使用相同的模板文件为 AWS、GCP 或 Azure 构建相同的 Jenkins 镜像。

注意:脚本化的 shell 在第四章中有详细解释。所有源代码都可以在 GitHub 仓库的 chapter6 文件夹中找到。

列表 6.1 中的模板文件使用了一组变量,例如之前创建的服务账户密钥文件、构建器机器将要部署的区域名称以及将拥有镜像的 Google Cloud 项目 ID。"service_account"变量可以隐式指定,如果您指定了带有GOOGLE_APPLICATION_CREDENTIALS环境变量的 JSON 文件路径。

Packer 将从 CentOS 8 部署一个临时实例。可在“镜像”仪表板中找到可用镜像列表,如图 6.3 所示。

图片

图 6.3 来自 GCE 镜像的 CentOS 基础镜像

注意:您也可以使用gcloud compute images list命令列出特定 GCP 位置上的可用镜像。

在提供所有必要的变量后,发出packer build命令。输出应类似于以下输出,这里为了简洁已裁剪:

图片

一旦烘焙过程完成,Jenkins 工作节点镜像应该可以在 Google Compute Engine (GCE)控制台上找到,如图 6.4 所示。

图片

图 6.4 Jenkins 工作节点自定义镜像

接下来,为了构建 Jenkins 主机的机器镜像,我们将使用第四章列表 4.12 中提供的相同蓝图。唯一的区别是builders部分使用了googlecompute。完整的模板文件,如以下列表所示,可以从 chapter6/gcp/packer/master/setup.sh 下载。

列表 6.2 Jenkins 主模板文件。

{
    "variables" : {
        "service_account" : "SERVICE ACCOUNT JSON PATH",
        "project": "PROJECT ID",
        "zone": "ZONE ID",
        "ssh_key" : "PRIVATE SSH KEY PATH"
    },
    "builders" : [
        {
            "type": "googlecompute",
            "image_name" : "jenkins-master-v22041",
            "account_file": "{{user `service_account`}}",
            "project_id": "{{user `project`}}",
            "source_image_family": "centos-8",
            "ssh_username": "packer",
            "zone": "{{user `zone`}}"
          }
    ],
    "provisioners" : [
       ...
    ]
}

注意:此代码列表已在 GitHub 仓库中存在。您无需输入它。这里仅展示用于说明。

在我们从模板中构建镜像之前,让我们通过运行以下命令来验证模板:

packer validate template.json

在模板经过适当验证后,是时候构建 Jenkins 镜像了。这是通过调用带有模板文件的packer build命令来完成的。输出应类似于以下内容。请注意,此过程通常需要几分钟时间:

图片

当 Packer 完成镜像构建后,前往 GCP 控制台,新创建的镜像将在“镜像”部分,如图 6.5 所示。

图片

图 6.5 Jenkins 主自定义镜像

到目前为止,你已经学会了如何在 GCP 上自动化构建 Jenkins 机器镜像的过程。在下一节中,我们将使用 Terraform 根据这些镜像部署 VM 实例。但首先,我们将部署一个私有网络,我们的 Jenkins 集群将在这个网络中隔离。

6.1.2 使用 Terraform 配置 GCP 网络

在本节结束时,你将有一个在不同区域运行的独立 VPN,如图 6.6 所示。

图片

图 6.6 Google VPN 架构由多个在不同区域部署的子网络组成。要访问私有实例,可以使用堡垒主机。

VPC 将在单个 GCP 区域启动。它将被细分为子网,每个子网都在单个区域中。在公共子网中,将部署一个 Google 计算实例,其角色为堡垒主机,以提供对私有子网中部署的实例的远程访问。

在 IAM 控制台,如图 6.7 所示,为 Terraform 创建一个具有项目所有者权限的专用服务账户并下载 JSON 私钥。此文件包含 Terraform 管理 GCP 项目中资源所需的凭证。

图片

图 6.7 Terraform 服务账户

创建一个 terraform.tf 文件,声明google作为提供者,并将其配置为使用之前步骤中创建的服务账户;请参阅以下列表。

列表 6.3 声明 Google 作为提供者

provider "google" {
  credentials = file(var.credentials_path)
  project     = var.project
  region      = var.region
}

创建一个 network.tf 文件并定义一个区域 VPC 网络,如下所示列表。(如果你计划在多个 GCP 区域部署 Jenkins 实例,你需要将路由模式更改为全局。)

列表 6.4 定义名为 management 的 GCP 网络

resource "google_compute_network" "management" {
  name = var.network_name
  auto_create_subnetworks = false
  routing_mode = "REGIONAL"
}

在同一文件中,声明两个公共子网和两个私有子网,如下所示列表。每个子网都有自己的 CIDR 块,它是网络 CIDR 块的子集(10.0.0.0/16)。

列表 6.5 定义公共和私有子网

resource "google_compute_subnetwork" "public_subnets" {
  count = var.public_subnets_count
  name          = "public-10-0-${count.index * 2 + 1}-0"
  ip_cidr_range = "10.0.${count.index * 2 + 1}.0/24"           ❶
  region        = var.region
  network       = google_compute_network.management.self_link
}

resource "google_compute_subnetwork" "private_subnets" {
  count = var.private_subnets_count
  name          = "private-10-0-${count.index * 2}-0"
  ip_cidr_range = "10.0.${count.index * 2}.0/24"               ❶
  region        = var.region
  network       = google_compute_network.management.self_link
  private_ip_google_access = true
}

❶ 使用 count.index 变量在 10.0.0.0/16 块内定义一个唯一的 CIDR 范围

在使用terraform apply应用更改之前,声明用于参数化和自定义部署的变量在 variables.tf 中。表 6.1 列出了变量。

表 6.1 GCP Terraform 变量

名称 类型 描述
credentials_path 字符串 JSON 格式的服务账户密钥文件的路径。这可以使用 GOOGLE_CREDENTIALS 环境变量来指定。
project 字符串 管理资源时的默认项目。如果在资源上指定了另一个项目,则该项目将具有优先权。这也可以使用 GOOGLE_PROJECT 环境变量来指定。
region 字符串 管理资源时的默认区域。如果在区域资源上指定了另一个区域,则该区域将具有优先权。或者,也可以使用 GOOGLE_REGION 环境变量来指定。
network_name 字符串 management 虚拟网络名称。名称长度必须为 1-63 个字符,并匹配正则表达式 a-z?
public_subnets_count 数字 2 公共子网的数量。默认情况下,我们将为容错性在不同的区域创建两个公共子网。
private_subnets_count 数字 2 私有子网的数量。默认情况下,我们将为容错性在不同的区域创建两个私有子网。

我们现在可以运行 Terraform 来部署基础设施。首先,初始化 Terraform 以下载 Google Cloud 提供者插件的最新版本:

terraform init

命令输出如下:

运行 plan 步骤以验证配置语法并预览将要创建的内容:

terraform plan --var-file=variables.tfvars

注意:为了设置大量变量,在变量定义文件(文件名以 .tfvars 或 .tfvars.json 结尾)中指定它们的值会更方便,然后使用 -var-file 标志在命令行上指定该文件。

现在执行 terraform apply 命令以应用这些更改:

terraform apply --var-file=variables.tfvars

您将看到类似以下内容的输出(为了简洁而裁剪):

配置私有网络应该只需几分钟。完成后,您应该看到类似于图 6.8 的内容。

图 6.8 VPC 网络及其公共和私有子网

要能够通过 SSH 连接到私有 Jenkins 实例,我们将部署一个堡垒主机。创建 bastion.tf 并在公共子网中定义一个具有静态 IPv4 公共 IP 地址的 VM 实例。要使用终端(而不是 GCP 控制台)通过 SSH 连接到堡垒实例,您必须生成并上传一个公共 SSH 密钥(默认位于 ~/.ssh/id_rsa.pub 下,或使用 ssh-keygen 生成一个新的密钥)。以下列表中定义的 metadata 属性引用了公共 SSH 密钥。

列表 6.6 堡垒主机资源

resource "google_compute_address" "static" {
  name = "ipv4-address"
}
resource "google_compute_instance" "bastion" {
  project      = var.project
  name         = "bastion"
  machine_type = var.bastion_machine_type
  zone         = var.zone
  tags = ["bastion"]
  boot_disk {
    initialize_params {
      image = var.machine_image
    }
  }
  network_interface {
    subnetwork = google_compute_subnetwork.public_subnets[0].self_lin.

    access_config {
      nat_ip = google_compute_address.static.address
    }
  }
  metadata = {
    ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key)}"
  }
}

在同一文件中,创建一个防火墙规则以允许从堡垒主机上的任何地方进行 SSH,如下所示列表。(建议只启用来自您希望允许访问的 IP 地址的入站流量..)

列表 6.7 堡垒主机防火墙规则

resource "google_compute_firewall" "allow_ssl_to_bastion" {
  project = var.project
  name    = "allow-ssl-to-bastion"
  network = google_compute_network.management.self_link

  allow {
    protocol = "tcp"                ❶
    ports    = ["22"]               ❶
  }                                 ❶

  source_ranges = ["0.0.0.0/0"]     ❶

  source_tags = ["bastion"]
}

❶ 允许来自任何地方的端口 22(SSH)的入站流量

最后,创建一个 outputs.tf 文件,并使用 Terraform 的 output 变量作为助手来暴露堡垒虚拟机的公共 IP 地址:

output "bastion" {
    value = "${google_compute_instance.bastion.network_interface
    .0.access_config.0.nat_ip }"       ❶
}

❶ 输出堡垒实例的公共 IP 地址

terraform apply 命令完成后,您应该看到类似以下输出的内容:

在 GCE 控制台中,应该部署一个新的虚拟机实例,如图 6.9 所示。

图 6.9 堡垒虚拟机实例

跳转盒部署完成后,我们现在可以访问 VPC 网络中的私有实例。

6.1.3 在 Google Compute Engine 上部署 Jenkins

现在 VPC 已创建,我们将在私有子网中基于 Jenkins 主镜像部署一个虚拟机实例,并公开一个负载均衡器以访问端口 8080 上的 Jenkins 网络仪表板,如图 6.10 所述。

图 6.10 VPC 内的 Jenkins 主虚拟机

创建一个 jenkins_master.tf 文件,并定义一个具有以下列表中属性的私有计算实例。

列表 6.8 Jenkins 主计算实例

resource "google_compute_instance" "jenkins_master" {
  project      = var.project
  name         = "jenkins-master"
  machine_type = var.jenkins_master_machine_type
  zone         = var.zone

  tags = ["jenkins-ssh", "jenkins-web"]      ❶

  depends_on = [google_compute_instance.bastion]

  boot_disk {
    initialize_params {
      image = var.jenkins_master_machine_image
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.private_subnets[0].self_lin.
  }

  metadata = {
    ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key)}"
  }
}

❶ 将 jenkins-ssh 和 jenkins-web 网络连接到虚拟机实例。这些组分别允许端口 22 和 8080(Jenkins 仪表板)上的入站流量。

计算实例使用以下防火墙,仅允许堡垒主机上的 SSH 访问和来自任何地方的端口 8080 的入站流量。(我建议限制流量到您的网络 CIDR 块。)

列表 6.9 Jenkins 主防火墙和流量控制

resource "google_compute_firewall" "allow_ssh_to_jenkins" {
  project = var.project
  name    = "allow-ssh-to-jenkins"
  network = google_compute_network.management.self_link

  allow {
    protocol = "tcp"           ❶
    ports    = ["22"]          ❶
  }

  source_tags = ["bastion", "jenkins-ssh"]
}

resource "google_compute_firewall" "allow_access_to_ui" {
  project = var.project
  name    = "allow-access-to-jenkins-web"
  network = google_compute_network.management.self_link

  allow {
    protocol = "tcp"          ❷
    ports    = ["8080"]       ❷
  }

  source_ranges = ["0.0.0.0/0"]

  source_tags = ["jenkins-web"]
}

❶ 允许端口 22(SSH)上的入站流量

❷ 允许端口 8080 上的入站流量,其中 Jenkins 仪表板被暴露

使用 terraform apply 来部署 Jenkins 计算实例。一旦部署完成,将部署一个新的虚拟机,如图 6.11 所示。

图 6.11 Jenkins 主虚拟机实例

实例部署在私有子网内。为了能够访问 Jenkins 网络仪表板,我们需要在虚拟机实例前面部署一个公共负载均衡器。

GCP 上的负载均衡与其他云提供商不同。主要区别在于 GCP 使用转发规则而不是路由实例。这些转发规则与后端服务、目标池和健康检查结合,在实例组中构建一个功能性的负载均衡器。

首先,我们定义一个目标池资源,该资源定义了应接收传入流量的实例,如下一列表所示。在我们的情况下,目标池将包括 Jenkins 主 VM 实例。

列表 6.10 Jenkins 主目标池

resource "google_compute_target_pool" "jenkins-master-target-pool" {
    name             = "jenkins-master-target-pool"
    session_affinity = "NONE"
    region = var.region

    instances = [
        Google_compute_instance.jenkins_master.self_link     ❶
    ]

    health_checks = [
        google_compute_http_health_check.jenkins_master_health_check.name
    ]
}

❶ 将 Jenkins 主 VM 实例定义为网络负载均衡器的目标

只有当云负载均衡器处于运行状态并准备好接收流量时,才会将流量转发到 Jenkins 主机。这就是为什么我们定义了一个健康检查资源,在端口 8080 上以特定频率向 Jenkins 主机发送健康检查请求;请参阅以下列表。

列表 6.11 Jenkins 主健康检查

resource "google_compute_http_health_check" "jenkins_master_health_check" {
  name         = "jenkins-master-health-check"
  request_path = "/"         ❶
  port = "8080"              ❶
  timeout_sec        = 4     ❶
  check_interval_sec = 5     ❶
}

❶ 定义了如何通过 HTTP 检查 Jenkins 主机的健康状态模板

最后,在下一个列表中,我们定义了一个转发规则,将流量导向之前定义的目标池。

列表 6.12 负载均衡器转发规则

resource "google_compute_forwarding_rule" "jenkins_master_forwarding_rule" {
  name   = "jenkins-master-forwarding-rule"
  region = var.region
  load_balancing_scheme = "EXTERNAL"                                        ❶
  target = google_compute_target_pool.jenkins-master-target-pool.self_link  ❶
  port_range            = "8080"                                            ❶
  ip_protocol           = "TCP"                                             ❶
}

❶ 如果入站数据包与给定的 IP 地址、IP 协议和端口号范围匹配,它将被转发到 Jenkins 主机目标池。

使用 terraform apply 来部署公共负载均衡器。在网络服务仪表板上,您应该看到图 6.12 中所示的配置。

图 6.12 以 Jenkins VM 作为后端的公共负载均衡器

作为后端,负载均衡器使用 Jenkins 主机实例,并将入站流量在 8080 端口转发到同一端口的后端。同时,它还在 8080 端口上设置了一个 HTTP 健康检查。

要显示负载均衡器的 IP 地址,在 outputs.tf 文件中创建一个输出部分:

output "jenkins" {
   value = google_compute_forwarding_rule \
.jenkins_master_forwarding_rule.ip_address
}

在控制台上执行 terraform output 命令,Jenkins 负载均衡器 IP 地址应该会显示出来:

您现在可以将浏览器指向 8080 端口的 IP 地址,并看到 Jenkins 欢迎屏幕。如果您看到如图 6.13 所示的屏幕,您已成功在 GCP 上部署了 Jenkins!

图 6.13 访问 Jenkins 仪表板的公共负载均衡器 IP 地址

注意:转发规则可能需要几分钟才能配置。在创建过程中,您可能在浏览器中看到 404 和 500 错误。

6.1.4 在 GCP 上启动自动管理的工人

不可否认,Jenkins 最强大的功能之一是能够在多个工作节点之间调度构建作业。设置一个构建机器的农场相当简单,无论是为了在多台机器之间共享负载,还是为了在不同的环境中运行构建作业。这是一种有效的策略,有可能显著提高您的 CI 基础设施的容量。

Jenkins 工作节点的需求也可能随时间波动。如果您与产品发布周期一起工作,您可能需要在周期的后期运行更多的工人。因此,为了避免在 Jenkins 工作节点空闲时支付额外资源,我们将在实例组内部部署 Jenkins 工作节点,并设置自动扩展策略来触发扩展或缩减事件,分别添加或删除 Jenkins 工作节点,基于如 CPU 利用率等指标。

注意:在第十三章中,我们将介绍如何使用 Prometheus 等开源解决方案来导出 Jenkins 自定义指标,包括其与 Jenkins 工作节点扩展过程的集成。

图 6.14 总结了本节将要部署的架构。

图 6.14 Google Cloud 上 Jenkins 集群部署

首先,创建一个 jenkins_workers.tf 文件,并定义将用作定义 Jenkins 工作节点配置的蓝图;请参阅以下列表。

列表 6.13 Jenkins 工作模板配置

resource "google_compute_instance_template" "jenkins-worker-template" {
  name_prefix = "jenkins-worker"
  description = "Jenkins workers instances template"
  region       = var.region

  tags = ["jenkins-worker"]
  machine_type         = var.jenkins_worker_machine_type
  metadata_startup_script = data.template_file.jenkins_worker_startup_script.rendered      ❶
  disk {
    source_image = var.jenkins_worker_machine_image
    disk_size_gb = 50
  }
  network_interface {
    network = google_compute_network.management.self_lin.
    subnetwork = google_compute_subnetwork.private_subnets[0].self_lin.
  }

  metadata = {
    ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key)}"
  }
}

❶ 一个在 VM 实例首次启动时执行的 shell 脚本。该脚本将自动将实例加入为 Jenkins 代理。

我们将在私有子网络内部署实例,并执行以下列表中的启动脚本,使正在运行的虚拟机加入集群。此脚本类似于第五章列表 5.7 中提供的 shell 脚本。

列表 6.14 Jenkins 工作启动脚本

data "template_file" "jenkins_worker_startup_script" {
  template = "${file("scripts/join-cluster.tpl")}"

  vars = {
    jenkins_url            = "http://${google_compute_forwarding_rule.
jenkins_master_forwarding_rule.ip_address}:8080"                 ❶
    jenkins_username       = var.jenkins_username                ❶
    jenkins_password       = var.jenkins_password                ❶
    jenkins_credentials_id = var.jenkins_credentials_id          ❶
  }
}

join-cluster.tpl 模板文件接受 Jenkins 凭据和 URL 作为参数。这些值将在运行时进行插值。

我们将使用 Google Cloud 元数据服务器来获取实例名称和私有 IP 地址。元数据服务器请求的输出是 JSON 格式,因此我们将使用jq实用程序来解析 JSON 并获取目标属性。

INSTANCE_NAME=$(curl -s metadata.google.internal/0.1/meta-data/hostname)
INSTANCE_IP=$(curl -s metadata.google.internal/0.1/meta-data/networ.
| jq -r '.networkInterface[0].ip')

接下来,我们将定义一个防火墙规则,允许从 Jenkins 主节点和堡垒主机到 Jenkins 工作节点的 SSH 访问,如下所示。

列表 6.15 Jenkins 主防火墙和流量控制

resource "google_compute_firewall" "allow_ssh_to_worker" {
  project = var.project
  name    = "allow-ssh-to-worker"
  network = google_compute_network.management.self_link
  allow {
    protocol = "tcp"      ❶
    ports    = ["22"]     ❶
  }
  source_tags = ["bastion", "jenkins-ssh", "jenkins-worker"]
}

❶ 允许端口 22(SSH)上的入站流量

然后,我们基于模板文件定义一个实例组,默认目标大小为两个工作节点;请参阅下一列表。

列表 6.16 Jenkins 工作实例组

resource "google_compute_instance_group_manager" "jenkins-workers-group" {
  provider = google-beta
  name = "jenkins-workers"
  base_instance_name = "jenkins-worker"
  zone               = var.zone

  version {
    instance_template  = google_compute_instance_template
.jenkins-worker-template.self_link                            ❶
  }

  target_pools = [google_compute_target_pool
.jenkins-workers-pool.id]
  target_size = 2                                             ❶
}

resource "google_compute_target_pool" "jenkins-workers-pool" {
  provider = google-beta
  name = "jenkins-workers-pool"
}

❶ 从一个公共实例模板(jenkins-worker-template)创建和管理同质 VM 实例池(两个实例)

一旦使用 terraform apply 部署了新资源,应该有两个工作实例正在运行,如图 6.15 所示。

图片

图 6.15 Jenkins 工作实例组

然而,目前工作节点的数量是静态和固定的。为了能够为重构建作业扩展 Jenkins 工作节点,我们将基于 CPU 利用率部署一个自动扩展器。定义以下资源以触发超过 80%CPU 利用率的扩展事件。在 jenkins_workers.tf 中添加以下列表中的代码。

列表 6.17 Jenkins 工作自动扩展器

resource "google_compute_autoscaler" "jenkins-workers-autoscaler" {
  name   = "jenkins-workers-autoscaler"
  zone   = var.zone
  target = google_compute_instance_group_manager.jenkins-workers-group.id

  autoscaling_policy {
    max_replicas    = 6       ❶
    min_replicas    = 2       ❶
    cooldown_period = 60      ❶

    cpu_utilization {
      target = 0.8            ❶
    }
  }
}

❶ 根据自动扩展策略在托管实例组中扩展 Jenkins 工作实例。该策略基于实例的 CPU 利用率。

一旦使用 Terraform 部署了更改,将在 Jenkins 工作实例组上配置自动扩展策略,如图 6.16 所示。

图片

图 6.16 基于 CPU 利用率的实例组扩展

因此,在启动脚本执行后,工作节点将自动加入集群(图 6.17)。太棒了!您已经在 GCP 上运行了一个 Jenkins 集群。

图片

图 6.17 Jenkins 工作虚拟机实例已加入集群。

6.2 微软 Azure

微软 Azure 和 AWS 通过提供一个云服务集合,采用类似的方法。然而,通常使用微软软件的组织有一个企业协议,该协议提供该软件的折扣。这些组织通常可以获得使用 Azure 的显著激励。

如果你计划使用 Azure,你可以从 Azure Marketplace 部署 Jenkins 解决方案模板。然而,如果你想要完全控制 Jenkins,请遵循本节学习如何从头开始构建 Jenkins 集群,并根据 Azure 虚拟机按需扩展你的 Jenkins 工作节点。

注意:虽然 Azure 和 Google Cloud 已经看到了相当显著的增长,但 AWS 仍然是领导者。这主要是因为 AWS 是第一个投资并塑造云计算行业的。Google Cloud 和 Azure 还有一些追赶的余地。

在开始之前,如果你是 Azure 的新用户,你可以注册一个 Azure 免费账户(portal.azure.com/),以免费$200 信用额度开始探索。

6.2.1 在 Azure 中构建金 Jenkins VM 镜像

在构建过程中,Packer 在构建源虚拟机时会创建临时 Azure 资源。因此,它需要被授权与 Azure API 交互。

使用以下命令创建一个具有创建和管理资源权限的 Azure 服务主体(SP)。SP 代表一个访问你的 Azure 资源的应用程序。它通过客户端 ID(也称为应用程序 ID)进行标识,可以使用密码或证书进行身份验证。

要创建一个服务主体(SP),请复制以下命令:

$sp = New-AzADServicePrincipal -DisplayName "PackerServicePrincipal"
$BSTR = [System.Runtime
.InteropServices.Marshal]::SecureStringToBSTR($sp.Secret)
$plainPassword = [System.Runtime
.InteropServices.Marshal]::PtrToStringAuto($BSTR)
New-AzRoleAssignment -RoleDefinitionName
 Contributor -ServicePrincipalName $sp.ApplicationId

你可以在 Azure PowerShell 上执行这些命令,如图 6.18 所示。

图 6.18 创建 Azure 凭据

然后通过执行以下命令输出密码和应用程序 ID:

$plainPassword
$sp.ApplicationId

将应用程序 ID 和密码保存以备后用。

要对 Azure 进行身份验证,你还需要获取你的 Azure 租户和订阅 ID,这些可以通过Get-AzSubscription或从 Azure Active Directory (AD)获取。AD,如图 6.19 所示,是一个身份管理服务,它通过正确的角色和权限控制对 Azure 资源的访问和安全。

图 6.19 Packer 在 Azure Active Directory 上的注册

注意客户端 ID 和密钥。这将在 Packer 中用作在 Azure 中配置资源的凭据。

要构建 Jenkins 工作节点镜像,创建一个 template.json 文件。在模板中,你定义执行实际构建过程的构建器和配置器。Packer 有一个名为azure-arm的 Azure 构建器,允许你定义 Azure 镜像。将以下内容添加到 template.json 或从 chapter6/azure/packer/worker/template.json 下载完整的模板。

列表 6.18 带有 Azure 构建器的 Jenkins 工作节点模板

{
    "variables" : {
        "subscription_id" : "YOUR SUBSCRIPTION ID",     ❶
        "client_id": "YOUR CLIENT ID",                  ❶
        "client_secret": "YOUR CLIENT SECRET",          ❶
        "tenant_id": "YOUR TENANT ID",                  ❶
        "resource_group": "RESOURCE GROUP NAME",        ❶
        "location": "LOCATION NAME"                     ❶
    },
    "builders" : [
        {
            "type": "azure-arm",
            "subscription_id": "{{user `subscription_id`}}",
            "client_id": "{{user `client_id`}}",
            "client_secret": "{{user `client_secret`}}",
            "tenant_id": "{{user `tenant_id`}}",
            "managed_image_resource_group_name": "{{user `resource_group`}}",
            "managed_image_name": "jenkins-worker",
            "os_type": "Linux",
            "image_publisher": "OpenLogic",            ❷
            "image_offer": "CentOS",                   ❷
            "image_sku": "8.0",                        ❷
            "location": "{{user `location`}}",         ❷
            "vm_size": "Standard_B1s"                  ❷
        }
    ],
    "provisioners" : [
        {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

❶ 运行时变量列表,使 Packer 模板可移植和可重复使用

❷ Packer 将基于 CentOS 8.0 机器镜像部署一个类型为 Standard_B1s(1 RAM 和 1vCPU)的实例。

如果你在一个虚拟机上运行 Packer,你可以为虚拟机分配一个托管标识。不需要设置任何配置属性。

列表 6.18 中的模板部署了一个基于 CentOS 8.0 的临时实例,并使用 shell 脚本安装所需的依赖项。选择 CentOS 并非偶然。Amazon Linux 镜像和 CentOS 具有相似之处,特别是对 Yum 包管理器的支持。为了使用前几章中提供的相同脚本并保持 Jenkins 镜像的一致性和相同性,我们将使用 CentOS。

使用packer build命令烘焙镜像。以下是一个输出示例:

图片

Packer 构建虚拟机、运行配置程序和烘焙 Jenkins 工作镜像需要几分钟时间。一旦完成,镜像将在resource_group变量设置的资源组中创建,如图 6.20 所示。

图片

图 6.20 Jenkins 工作机镜像

将应用类似的流程来构建 Jenkins 主镜像。以下为 template.json 文件(完整的模板可在 chapter6/azure/packer/master/template.json 中找到)。

列表 6.19 带有 Azure 构建器的 Jenkins 工作模板

{
    "variables" : {...},     ❶
    "builders" : [
        {
            "type": "azure-arm",
            "subscription_id": "{{user `subscription_id`}}",
            "client_id": "{{user `client_id`}}",
            "client_secret": "{{user `client_secret`}}",
            "tenant_id": "{{user `tenant_id`}}",
            "managed_image_resource_group_name": "{{user `resource_group`}}",
            "managed_image_name": "jenkins-master-v22041",
            "os_type": "Linux",
            "image_publisher": "OpenLogic",
            "image_offer": "CentOS",
            "image_sku": "8.0",
            "location": "{{user `location`}}",
            "vm_size": "Standard_B1ms"
        }
    ],
    "provisioners" : [
       ...
    ]
}

❶ 为了简洁起见,省略了变量列表;完整的列表在列表 6.18 中。

一旦定义了模板,就使用 Packer 烘焙镜像。烘焙过程需要几分钟来创建镜像。一旦镜像创建完成,它应该可以从 Azure 门户的镜像仪表板中访问,如图 6.21 所示。

图片

图 6.21 Jenkins 主机机器镜像

现在有了 Jenkins 主和工镜像,您现在可以使用 Terraform 从自定义镜像创建 Jenkins 集群。

6.2.2 部署私有虚拟网络

在部署 Jenkins 集群之前,我们需要设置一个与图 6.22 所示的架构相同的私有网络,以保护集群的访问安全。

图片

图 6.22 Azure 上的 VPN

注意:为了使 Terraform 能够将资源部署到 Azure,请按照第 6.2.1 节中描述的相同步骤创建一个 Azure Active Directory 服务主体。

创建一个 terraform.tf 文件并声明azurerm为提供者,如下所示。provider部分告诉 Terraform 使用 Azure 提供者。要获取subscription_idclient_idclient_secrettenant_id的值,请参阅第 6.2.1 节。

列表 6.20 定义 Azure 提供者

provider "azurerm" {
  version = "=1.44.0"

  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

运行terraform init以下载最新的 Azure 插件并构建.terraform 目录:

图片

接下来,创建一个 virtual_network.tf 文件,在该文件中定义一个名为management的虚拟网络,该网络位于 10.0.0.0/16 地址空间内,包含公共和私有子网,以及一个名为AzureBastionSubnet的额外子网,用于堡垒主机,如下所示。

列表 6.21 Azure 虚拟网络定义

data "azurerm_resource_group" "management" {
  name = var.resource_group
}

resource "azurerm_virtual_network" "management" {
  name                = "management"
  location            = var.location
  resource_group_name = data.azurerm_resource_group.management.name
  address_space       = [var.base_cidr_block]
  dns_servers         = ["10.0.0.4", "10.0.0.5"]                ❶

  dynamic "subnet" {
    for_each = [for s in var.subnets: {                         ❷
      name   = s.name                                           ❷
      prefix = cidrsubnet(var.base_cidr_block, 8, s.number)     ❷
    }]                                                          ❷

    content {                                                   ❷
      name           = subnet.value.name                        ❷
      address_prefix = subnet.value.prefix                      ❷
    }                                                           ❷
  }

  subnet {
    name           = "AzureBastionSubnet"                       ❸
    address_prefix = cidrsubnet(var.base_cidr_block, 11, 224)   ❸
  }

  tags = {
    environment = "management"
  }
}

❶ DNS 服务器 IP 地址列表

❷ 在 10.0.0.0/16 空间内定义子网列表

❸ 定义一个专用的子网,其中将部署堡垒主机

注意:我们可以在 Azure 中使用键值对标记我们的资源。这对于成本优化很有用。因此,我们将添加 environment 标记并设置值为 management,以标记我们创建的所有资源。

在应用更改之前,在 variables.tf 中声明用于参数化和自定义 Terraform 部署的变量。表 6.2 列出了变量。

表 6.2 Azure Terraform 变量

名称 类型 描述
subscription_id 字符串 None 要使用的订阅 ID。这也可以从 ARM_SUBSCRIPTION_ID 环境变量中获取。
client_id 字符串 None 要使用的客户端 ID。这也可以从 ARM_CLIENT_ID 环境变量中获取。
client_secret 字符串 None 要使用的客户端密钥。这也可以从 ARM_CLIENT_SECRET 环境变量中获取。
tenant_id 字符串 None 要使用的租户/目录 ID。这也可以从 ARM_TENANT_ID 环境变量中获取
resource_group 字符串 None 创建虚拟网络的资源组名称。
location 字符串 None 虚拟网络创建的位置/区域。更改此设置将强制创建新的资源。有关支持位置的全列表,请参阅 Azure 位置文档。
base_cidr_block 字符串 10.0.0.0/16 用于虚拟网络的地址空间(CIDR 块)。
subnets 映射 None 一个包含要在虚拟网络内部创建的子网列表的映射。

当使用客户端证书作为服务主体进行身份验证时,应设置以下字段:client_certificate_passwordclient_certificate_path

现在是时候运行 terraform apply 命令了。Terraform 将调用 Azure API 来设置新的虚拟网络,如下所示:

要在 Azure 门户中验证结果,请浏览到管理资源组。新的虚拟网络位于此组下,如图 6.23 所示。

要访问私有 Jenkins 机器,我们需要部署网关或代理服务器,也称为跳板或堡垒主机。幸运的是,Azure 提供了一个名为 Azure Bastion 的托管服务,它提供远程桌面协议 (RDP) 和 SSH 访问任何 VM,无需管理加固的堡垒实例并应用安全补丁(无运营开销)。

图 6.23 管理虚拟网络

要将 Azure Bastion 服务部署到现有的 Azure 虚拟网络中,创建一个包含以下内容的 bastion.tf 文件。堡垒主机服务将部署到专用的 AzureBastionSubnet 子网中:

列表 6.22 Azure Bastion 服务部署

resource "azurerm_public_ip" "bastion_public_ip" {
  name                = "bastion-public-ip"
  location            = var.location
  resource_group_name = data.azurerm_resource_group.management.name
  allocation_method   = "Static"                                       ❶
  sku                 = "Standard"
}
data "azurerm_subnet" "bastion_subnet" {
  name                 = "AzureBastionSubnet"
  virtual_network_name = azurerm_virtual_network.management.name
  resource_group_name  = data.azurerm_resource_group.management.name
  depends_on = [azurerm_virtual_network.management]
}
resource "azurerm_bastion_host" "bastion" {
  name                = "bastion"
  location            = var.location
  resource_group_name = data.azurerm_resource_group.management.name
  depends_on = [azurerm_virtual_network.management]
.
  ip_configuration {
    name                 = "bastion-configuration"
    subnet_id            = data.azurerm_subnet.bastion_subnet.id       ❷
    public_ip_address_id = azurerm_public_ip.bastion_public_ip.id      ❷
  }
}

❶ 请求静态公共 IP 地址

❷ 引用要创建堡垒主机的子网。它还将已配置的公共 IP 地址关联到堡垒主机。

使用 Terraform 输出变量作为辅助工具,通过引用azurerm_public_ip资源来公开堡垒 IP 地址。

列表 6.23 堡垒主机公共 IP 地址

output "bastion" {
    value = azurerm_public_ip.bastion_public_ip.ip_address
}

运行terraform apply以应用配置。堡垒服务将部署到management资源组中,如图 6.24 所示。

图片

图 6.24 Azure 堡垒主机

6.2.3 部署 Jenkins 主虚拟机

部署 VPN 后,我们可以部署我们的 Jenkins 集群。图 6.25 总结了目标架构。

图片

图 6.25 私有子网内的 Jenkins VM

部署一个基于之前使用 Packer 构建的 Jenkins 主镜像的虚拟机。在jenkins_master.tf中定义资源,以下代码。

列表 6.24 Jenkins 主虚拟机

data "azurerm_image" "jenkins_master_image" {
  name                = var.jenkins_master_image
  resource_group_name = data.azurerm_resource_group.management.name
}

resource "azurerm_virtual_machine" "jenkins_master" {
  name                = "jenkins-master"
  resource_group_name = data.azurerm_resource_group.management.name
  location            = var.location
  vm_size             = var.jenkins_vm_size

  network_interface_ids = [
    azurerm_network_interface.jenkins_network_interface.id,
  ]

  os_profile {
    computer_name  = var.config["os_name"]
    admin_username = var.config["vm_username"]
  }

  os_profile_linux_config {
    disable_password_authentication = true                                 ❶
    ssh_keys {                                                             ❶
      path     = "/home/${var.config["vm_username"]}/.ssh/authorized_keys" ❶
      key_data = file(var.public_ssh_key)                                  ❶
    }                                                                      ❶
  }

  storage_os_disk {
    name = "main"
    caching           = "ReadWrite"
    managed_disk_type = "Standard_LRS"                                     ❷
    create_option     = "FromImage"                                        ❷
    disk_size_gb      = "30"                                               ❷
  }

  storage_image_reference {
    id = data.azurerm_image.jenkins_master_image.i                         ❸
  }

  delete_os_disk_on_termination = true                                     ❹
}

❶ 禁用密码验证并启用 SSH 作为认证机制

❷ 指定应创建的托管磁盘类型。可能的值是 Standard_LRS、StandardSSD_LRS 或 Premium_LRS。

❸ 从烘焙的 Jenkins 主镜像配置 VM

❹ 删除 VM 时自动删除 OS 磁盘

注意:我们为虚拟机允许了 30GB 的磁盘大小。Jenkins 需要一些磁盘空间来执行构建并保存存档和构建日志。

SSH 密钥数据位于ssh_key部分,用户名位于禁用密码验证的os_profile部分。

Jenkins 虚拟机使用具有可扩展 CPU 性能的 B 系列 Azure VM 家族。这个 VM 家族在计算和网络带宽之间提供了正确的平衡。我建议根据你的项目构建需求和需求选择你的 VM 家族类型。

列表 6.24 创建了一个名为jenkins-master的 VM,现在我们将附加虚拟网络接口,如下所示。

列表 6.25 Jenkins VM 网络配置

data "azurerm_subnet" "private_subnet" {
  name                 = var.subnets[2].name
  virtual_network_name = azurerm_virtual_network.management.name
  resource_group_name  = data.azurerm_resource_group.management.name
  depends_on = [azurerm_virtual_network.management]
}

resource "azurerm_network_interface" "jenkins_network_interface" {
  name                = "jenkins_network_interface"
  location            = var.location
  resource_group_name = data.azurerm_resource_group.management.name
  depends_on = [azurerm_virtual_network.management]

  ip_configuration {
    name                          = "internal"                             ❶
    subnet_id                     = data.azurerm_subnet.private_subnet.id  ❶
    private_ip_address_allocation = "Dynamic"                              ❶
  }
}

❶ 在私有子网中部署 Jenkins 主实例并分配一个动态私有 IP 地址

虚拟网络接口将 Jenkins 主实例连接到私有网络子网。

一旦你在variables.tfvars中提供了所需的 Terraform 变量,就执行terraform apply。从你的 Packer 镜像和预期资源创建 Jenkins VM(如图 6.26 所示)需要几分钟。

图片

图 6.26 Jenkins 主虚拟机

Jenkins 虚拟机应仅通过堡垒主机访问。图 6.27 确认该机器是在私有子网中部署的。

图片

图 6.27 在私有子网中部署的 Jenkins 主实例

然而,要访问 Jenkins 仪表板,我们将在 VM 前面部署一个负载均衡器。在loadbalancers.tf文件上创建一个文件,定义一个 Azure 负载均衡器和一条安全规则来服务 Jenkins 仪表板,并将其附加到公共 IP 地址,如下所示。

列表 6.26 Jenkins 仪表板负载均衡器配置

resource "azurerm_public_ip" "jenkins_lb_public_ip" {
 name                         = "jenkins-lb-public-ip"
 location                     = var.location
 resource_group_name          = data.azurerm_resource_group.management.name
 allocation_method            = "Static"
}
resource "azurerm_lb" "jenkins_lb" {
 name                = "jenkins-lb"
 location            = var.location
 resource_group_name = data.azurerm_resource_group.management.name

 frontend_ip_configuration {
   name                 = "publicIPAddress"                             ❶
   public_ip_address_id = azurerm_public_ip.jenkins_lb_public_ip.id     ❶
 }
}
resource "azurerm_lb_rule" "jenkins_lb_rule" {
  name = "jenkins-lb-rule"
  resource_group_name = data.azurerm_resource_group.management.name
  protocol = "tcp"                                                      ❷
  enable_floating_ip = false                                            ❷
  probe_id = azurerm_lb_probe.jenkins_lb_probe.id                       ❷
  loadbalancer_id = azurerm_lb.jenkins_lb.id                            ❷
  backend_address_pool_id = azurerm_lb_backend_address_pool
.jenkins_backend.id    .
  frontend_ip_configuration_name = "publicIPAddress"                    ❸
  frontend_port = 80                                                    ❸
  backend_port = 8080                                                   ❸
}

❶ 将公网 IP 地址关联到负载均衡器

❷ 负载均衡器监听 80 端口以接收传入请求,并通过 8080 端口与 Jenkins 主实例通信。

❸ 负载均衡器监听 80 端口以接收传入请求,并通过 8080 端口与 Jenkins 主实例通信。

在同一文件中,定义一个 Azure 后端地址池并将其分配给负载均衡器。然后设置端口 8080 的健康检查,如下所示。

列表 6.27 Jenkins 仪表板健康检查

resource "azurerm_lb_backend_address_pool" "jenkins_backend" {
 resource_group_name = data.azurerm_resource_group.management.name
 loadbalancer_id     = azurerm_lb.jenkins_lb.id
 name                = "jenkins-backend"
}
resource "azurerm_lb_probe" "jenkins_lb_probe" {
  resource_group_name = data.azurerm_resource_group.management.name
  loadbalancer_id     = azurerm_lb.jenkins_lb.id
  name                = "jenkins-lb-probe"
  protocol            = "Http"
  request_path        = "/"        ❶
  port                = 8080       ❷
}

❶ 用于从后端端点请求健康状态的 URI

❷ 探针查询后端端点的端口

Azure 允许通过安全组打开端口以允许流量,这也可以在 Terraform 配置中管理。将以下内容添加到 security_groups.tf 中,然后运行plan/apply以创建允许在端口 8080 上入站流量和在 TCP 端口 22 上 SSH 流量的安全规则。

列表 6.28 Jenkins 主安全组

resource "azurerm_network_security_group" "jenkins_security_group" {
  name          = "jenkins-sg"
  location      = var.location
  resource_group_name   = data.azurerm_resource_group.management.name

  security_rule {
    name            = "AllowSSH"                ❶
    priority        = 100                       ❶
    direction       = "Inbound"                 ❶
    access          = "Allow"                   ❶
    protocol        = "Tcp"                     ❶
    source_port_range           = "*"           ❶
    destination_port_range      = "22"          ❶
    source_address_prefix       = "*"           ❶
    destination_address_prefix  = "*"           ❶
  }

  security_rule {
    name            = "AllowHTTP"               ❷
    priority        = 200                       ❷
    direction       = "Inbound"                 ❷
    access          = "Allow"                   ❷
    protocol        = "Tcp"                     ❷
    source_port_range           = "*"           ❷
    destination_port_range      = "8080"        ❷
    source_address_prefix       = "Internet"    ❷
    destination_address_prefix  = "*"           ❷
  }
}

❶ 允许来自任何地方的 22 端口(SSH)的入站流量

❷ 允许在 8080 端口上入站流量,这是 Jenkins Web 仪表板提供服务的端口

最后,将安全组分配给连接到 Jenkins 主虚拟机的虚拟网络接口,如下所示。

列表 6.29 Jenkins 网络接口配置

resource "azurerm_network_interface" "jenkins_network_interface" {
  name                = "jenkins_network_interface"
  location            = var.location
  resource_group_name = data.azurerm_resource_group.management.name
  network_security_group_id = azurerm_network_security_group.jenkins_security_group.id
  depends_on = [azurerm_virtual_network.management]

  ip_configuration {
    name                          = "internal"                             ❶
    subnet_id                     = data.azurerm_subnet.private_subnet.id  ❶
    private_ip_address_allocation = "Dynamic"                              ❶
    load_balancer_backend_address_pools_ids =                              ❶
    [azurerm_lb_backend_address_pool.jenkins_backend.id]                   ❶
  }
}

❶ 将 Jenkins 安全组分配给配置在私有子网中的虚拟网络接口

使用terraform apply命令应用更改。一旦 Terraform 完成,您的负载均衡器就准备好了。通过在 outputs.tf 中添加以下代码获取其公网 IP 地址。

列表 6.30 Jenkins 主防火墙和流量控制

output "jenkins" {
    value = azurerm_public_ip.jenkins_lb_public_ip.ip_address
}

让我们使用 Azure 门户验证资源。如图 6.28 所示,Terraform 在management资源组下创建了所有预期的资源。

图片

图 6.28 指向 Jenkins 主 VM 的公共负载均衡器

现在,将您的网络浏览器指向地址栏中负载均衡器的公网 IP 地址。默认的 Jenkins 主页将显示,如图 6.29 所示。

图片

图 6.29 可通过 LB 公网 IP 地址访问的 Jenkins 仪表板

在烘焙 Jenkins 主机镜像时,您可以使用 Groovy 初始化脚本中定义的管理凭据进行登录。

6.2.4 将自动扩展应用于 Jenkins 工作节点

我们已经准备好将 Jenkins 工作节点部署到从主节点卸载构建项目。这些工作节点将在一个自动扩展集中动态配置。图 6.30 展示了目标部署架构。

图片

图 6.30 Jenkins 工作节点规模集

我们需要在机器规模集中部署 Jenkins 工作节点。一个 Jenkins 工作节点将基于之前使用 Packer 构建的 Jenkins 工作节点镜像,并在私有子网内部署。使用以下内容创建 jenkins_workers.tf。

列表 6.31 Jenkins 工作节点机器规模集

data "azurerm_image" "jenkins_worker_image" {
  name                = var.jenkins_worker_image                            ❶
  resource_group_name = data.azurerm_resource_group.management.name
}
resource "azurerm_virtual_machine_scale_set" "jenkins_workers_set" {
  name                = "jenkins-workers-set"
  location            = var.location
  resource_group_name = data.azurerm_resource_group.management.name
  upgrade_policy_mode = "Manual"
  sku {
    name     = var.jenkins_vm_size
    tier     = "Standard"
    capacity = 2
  }
  storage_profile_image_reference {
    id = data.azurerm_image.jenkins_worker_image.id
  }
  storage_profile_os_disk {
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }
  os_profile {
    computer_name_prefix = "jenkins-worker"
    admin_username = var.config["vm_username"]
    custom_data = data.template_file.jenkins_worker_startup_script.rendered
  }
  os_profile_linux_config {
    disable_password_authentication = true                                  ❷
    ssh_keys {                                                              ❷
      path     = "/home/${var.config["vm_username"]}/.ssh/authorized_keys"  ❷
      key_data = file(var.public_ssh_key)                                   ❷
    }                                                                       ❷
  }
  network_profile {
    name    = "private-network"                                             ❸
    primary = true                                                          ❸
    network_security_group_id =                                             ❸
      azurerm_network_security_group.jenkins_worker_security_group.id       ❸
    ip_configuration {                                                      ❸
      name = "private-ip-configuration"                                     ❸
      primary = true                                                        ❸
      subnet_id = data.azurerm_subnet.private_subnet.id                     ❸
    }
  }
}

❶ 引用 Jenkins 工作节点机器镜像 ID

❷ 禁用密码认证并配置 SSH 凭证

❸ 将安全组分配给虚拟机实例并请求私有 IP 地址

注意:您应该在多个 Azure VM 家族类型上测试您的项目,以确定适合 Jenkins 工作节点的机器类型以及磁盘空间的大小。

每个工作节点机器将在运行时执行一个自定义脚本(chapter6/azure/terraform/scripts/join-cluster.tpl),以加入 Jenkins 集群;请参阅以下列表。

列表 6.32 Jenkins 工作节点启动脚本

data "template_file" "jenkins_worker_startup_script" {
  template = "${file("scripts/join-cluster.tpl")}"       ❶

  vars = {
    jenkins_url            = "http://${azurerm_public_ip.jenkins_lb_public_ip.ip_address}:8080"
    jenkins_username       = var.jenkins_username
    jenkins_password       = var.jenkins_password
    jenkins_credentials_id = var.jenkins_credentials_id
  }
}

❶ 初始化脚本以自动将虚拟机作为 Jenkins 代理加入

该脚本将使用 Azure 实例元数据服务(IMDS)获取有关机器的私有 IP 地址和主机名的信息,并将向 Jenkins RESTful API 发出 POST HTTP 请求,以建立与机器的双向连接并加入集群:

INSTANCE_NAME=$(curl -s http://169.254.169.254/metadata/instance/compute/name
?api-version=2019-06-01&format=text)
INSTANCE_IP=$(curl -s http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/privateIpAddress
?api-version=2017-08-01&format=text)

将安全组附加到与规模集连接的虚拟网络接口。它允许在 22 端口(SSH)上的入站流量,如下面的列表所示。

列表 6.33 Jenkins 工作节点安全组

resource "azurerm_network_security_group" "jenkins_worker_security_group" {
  name      = "jenkins-worker-sg"
  location    = var.location
  resource_group_name   = data.azurerm_resource_group.management.name
  security_rule {
     name      = "AllowSSH"                 ❶
     priority  = 100                        ❶
     direction = "Inbound"                  ❶
     access    = "Allow"                    ❶
     protocol  = "Tcp"                      ❶
     source_port_range             = "*"    ❶
     destination_port_range      = "22"     ❶
     source_address_prefix       = "*"      ❶
     destination_address_prefix  = "*"      ❶
  }
}

❶ 允许来自任何地方的 22 端口(SSH)的入站流量。建议限制对您的网络 CIDR 块的访问。

部署完成后,资源组的内文将类似于图 6.31 所示。

图 6.31 Jenkins 工作节点虚拟机规模集

默认情况下,将有两个 Jenkins 工作节点运行,如图 6.32 所示。

图 6.32 Jenkins 工作节点静态数量

为了能够根据构建作业和正在运行的流水线进行扩展,我们将使用 Azure 自动缩放策略,根据工作机的 CPU 利用率触发扩展或缩减。在 jenkins_workers.tf 中,添加以下resource块。

列表 6.34 Jenkins 工作节点自动缩放策略

resource "azurerm_monitor_autoscale_setting" "jenkins_workers_autoscale" {
  name                = "jenkins-workers-autoscale"
  resource_group_name = data.azurerm_resource_group.management.name
  location            = var.location
  target_resource_id  = azurerm_virtual_machine_scale_set.jenkins_workers_set.id

  profile {
    name = "jenkins-autoscale"
    capacity {
      default = 2                                                ❶
      minimum = 2                                                ❶
      maximum = 10                                               ❶
    }
    rule {
      metric_trigger {                                           ❷
        metric_name        = "Percentage CPU"                    ❷
        metric_resource_id =                                     ❷
     azurerm_virtual_machine_scale_set.jenkins_workers_set.id    ❷
        time_grain         = "PT1M"                              ❷
        statistic          = "Average"                           ❷
        time_window        = "PT5M"                              ❷
        time_aggregation   = "Average"                           ❷
        operator           = "GreaterThan"                       ❷
        threshold          = 80                                  ❷
      }                                                          ❷
      scale_action {                                             ❷
        direction = "Increase"                                   ❷
        type      = "ChangeCount"                                ❷
        value     = "1"                                          ❷
        cooldown  = "PT1M"                                       ❷
      }                                                          ❷
    }

    rule {
      metric_trigger {                                           ❸
        metric_name        = "Percentage CPU"                    ❸
        metric_resource_id =                                     ❸
     azurerm_virtual_machine_scale_set.jenkins_workers_set.id    ❸
        time_grain         = "PT1M"                              ❸
        statistic          = "Average"                           ❸
        time_window        = "PT5M"                              ❸
        time_aggregation   = "Average"                           ❸
        operator           = "LessThan"                          ❸
        threshold          = 20                                  ❸
      }                                                          ❸

      scale_action {                                             ❸
        direction = "Decrease"                                   ❸
        type      = "ChangeCount"                                ❸
        value     = "1"                                          ❸
        cooldown  = "PT1M"                                       ❸
      }                                                          ❸
    }
  }
}

❶ 定义 Jenkins 工作节点的最小和最大数量

❷ 监控工作节点的 CPU 利用率——如果达到 80%,将部署新的 Jenkins 工作节点虚拟机。

❸ 监控工作节点的 CPU 利用率——如果低于 20%,将终止现有的 Jenkins 工作节点虚拟机。

使用terraform apply应用更改。然后,前往 Jenkins 工作节点规模集配置。在缩放部分,定义一个新的自动缩放策略,如图 6.33 所示。

图 6.33 Jenkins 工作节点自动缩放策略

注意:一旦您完成对 Jenkins 集群的实验,您可能希望拆除所有创建的内容,以免产生额外的费用。

太好了!您现在能够在 Microsoft Azure 上部署一个自我修复的 Jenkins 集群。

6.3 DigitalOcean

当我们想到云服务提供商时,我们通常指的是行业中的三大巨头:Azure、Google Cloud 和 AWS。与众所周知的服务提供商不同,DigitalOcean (www.digitalocean.com) 相对较新。你可能会想知道为什么你应该选择 DigitalOcean 而不是其他提供商。原因在于三大巨头与 DigitalOcean 之间的差异。

它们在许多方面都存在差异。一个是小的,而其他(AWS、GCP 和 Azure)则是巨大的。DigitalOcean 提供虚拟机(称为Droplets)。没有花哨的功能。你不会迷失在服务目录中,因为它们几乎不存在。此外,DigitalOcean 的界面因其友好的设计而允许开发者快速设置机器。而且,它价格合理,有更便宜的实例,这对于初涉商业和初创企业来说是一个良好的起点。(如果你还没有 DigitalOcean 账户,你需要创建一个;你将获得 100 美元的免费信用额。)

要使用 Packer 与 DigitalOcean,我们首先需要生成一个 DigitalOcean API 令牌。这可以在 DigitalOcean 应用程序和 API 页面上完成。点击“生成新令牌”按钮以获取具有读写权限的令牌,如图 6.34 所示。

![Images/CH06_F34_Labouardy.png]

图 6.34 Packer API 访问令牌

6.3.1 创建 Jenkins DigitalOcean 快照

我们使用的是与列表 6.1 和 6.2 中相同的模板;唯一的区别是使用digitalocean Packer 构建器与 DigitalOcean API 交互。构建器使用 CentOS 源镜像并运行必要的配置程序——在启动后安装构建 Jenkins 作业所需的工具,然后将其快照到一个可重复使用的镜像中;请参阅以下列表。这个可重复使用的镜像然后可以作为在 DigitalOcean 中通过 Terraform 启动的新 Jenkins 工作节点的基石。

列表 6.35 使用 DigitalOcean 构建器的 Jenkins 工作节点镜像

{
    "variables" : {
        "api_token" : "DIGITALOCEAN API TOKEN",     ❶
        "region": "DIGITALOCEAN REGION"             ❶
    },
    "builders" : [
        {
            "type": "digitalocean",
            "api_token": "{{user `api_token`}}",
            "image": "centos-8-x64",                ❷
            "region": "{{user `region`}}",
            "size": "512mb",
            "ssh_username": "root",
            "snapshot_name": "jenkins-worker"
        }
    ],
    "provisioners" : [
        {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

❶ DigitalOcean API 令牌和目标区域

❷ 构建的 Droplet 将基于 CentOS 8。

包含你的 DigitalOcean API 令牌和目标区域(有关支持区域的列表,请参阅官方文档:mng.bz/EDRJ)。然后运行packer build template.json命令。几分钟后,你将在 DigitalOcean 账户中获得一个可工作的 Jenkins 工作节点镜像,如图 6.35 所示。

![Images/CH06_F35_Labouardy.png]

图 6.35 Jenkins 工作节点镜像快照

类似地,更新列表 6.2 中引用的 Jenkins 主模板,以使用digitalocean构建器。配置部分创建了一个基于用于部署 Jenkins 工作节点的私有 SSH 密钥的 Jenkins 凭据。这是必需的,因为 Jenkins 需要通过 SSH 与工作节点建立双向连接。

列表 6.36 使用 DigitalOcean 构建器的 Jenkins 主镜像

{
    "variables" : {
        "api_token" : "DIGITALOCEAN API TOKEN",
        "region": "DIGITALOCEAN REGION",
        "ssh_key" : "PRIVATE SSH KEY FILE"
    },
    "builders" : [
        {
            "type": "digitalocean",
            "api_token": "{{user `api_token`}}",
            "image": "centos-8-x64",
            "region": "{{user `region`}}",
            "size": "2gb",
            "ssh_username": "root",
            "snapshot_name": "jenkins-master-2.204.1"
        }
    ],
    "provisioners" : [
        ...
    ]
}

为了简洁,此模板已被裁剪。完整的 JSON 文件可以从 chapter6/digitalocean/packer/master/template.json 下载。

运行 packer validate 命令以确保一切正常。然后发出一个 packer build 命令。一旦构建和配置部分完成,Jenkins 主机快照应该准备好使用,如图 6.36 所示。

图 6.36 Jenkins 主机镜像快照

6.3.2 部署 Jenkins 主机 Droplet

在这一步,你需要编写 Terraform 模板文件来自动部署包含你刚刚使用 Packer 构建的 Jenkins 主机和工作节点的快照。

定义一个 terraform.tf 文件并声明 DigitalOcean 为提供者。在可以使用之前,提供者需要配置正确的 API 令牌,如下所示。

列表 6.37 定义 DigitalOcean 提供者

provider "digitalocean" {
  token = var.token
}

运行 terraform init 下载将 Terraform 指令转换为 API 调用的 DigitalOcean 插件:

在 jenkins_master.tf 文件中定义一个名为 jenkins-masterdigitalocean_droplet 类型的单个资源,如下所示。然后根据变量值设置其参数,并将来自你的 DigitalOcean 账户的 SSH 密钥(使用其指纹)添加到 Droplet 资源中。部署的 Droplet 类型将为 s-1vcpu-2gb,它包含 1 GB RAM 和 1vCPU。

对于更重的负载和更大的项目,以及处理连接到 Jenkins 网络仪表板的并发用户,可能需要一个大型 Droplet 类型。请参阅官方文档以获取可用的 Droplet 大小列表:mng.bz/N4yD

列表 6.38 Jenkins 主机 Droplet

data "digitalocean_image" "jenkins_master_image" {
  name = var.jenkins_master_image
}
resource "digitalocean_droplet" "jenkins_master" {
  name   = "jenkins-master"
  image  = data.digitalocean_image.jenkins_master_image.id     ❶
  region = var.region
  size   = "s-1vcpu-2gb"                                       ❷
  ssh_keys = [var.ssh_fingerprint]
}

❶ 使用之前用 Packer 打包的 Jenkins 主机镜像

❷ 配置一个具有 2 GB RAM 和 1vCPU 的 Droplet

在 DigitalOcean 上,你可以将你的 SSH 公钥上传到你的账户,这样你就可以在创建 Droplet 时将其添加到 Droplet 中(如图 6.37 所示)。这让你可以在 Jenkins 主机上无需密码登录,同时仍然保持安全。

图 6.37 添加公共 SSH 密钥

接下来,将防火墙附加到 Jenkins 主机 Droplet 上,允许来自任何地方的 22 和 8080 端口的入站流量;请参阅以下列表。出于安全考虑,我建议将 SSH 入站流量限制到你的 CIDR 网络块。

列表 6.39 Jenkins 主机 Droplet 的防火墙

resource "digitalocean_firewall" "jenkins_master_firewall" {
  name = "jenkins-master-firewall"

  droplet_ids = [digitalocean_droplet.jenkins_master.id]

  inbound_rule {
    protocol         = "tcp"                          ❶
    port_range       = "22"                           ❶
    source_addresses = ["0.0.0.0/0", "::/0"]          ❶
  }

  inbound_rule {
    protocol         = "tcp"                          ❷
    port_range       = "8080"                         ❷
    source_addresses = ["0.0.0.0/0", "::/0"]          ❷
  }
  outbound_rule {
    protocol              = "tcp"                     ❸
    port_range            = "1-65535"                 ❸
    destination_addresses = ["0.0.0.0/0", "::/0"]     ❸
  }

  outbound_rule {
    protocol              = "udp"                     ❸
    port_range            = "1-65535"                 ❸
    destination_addresses = ["0.0.0.0/0", "::/0"]     ❸
  }

  outbound_rule {
    protocol              = "icmp"                    ❸
    destination_addresses = ["0.0.0.0/0", "::/0"]     ❸
  }
}

❶ 允许来自任何地方的 22(SSH)端口的入站流量

❷ 允许来自任何地方的 8080 端口的入站流量,其中 Jenkins 网络仪表板提供服务

❸ 允许来自任何地方的任何端口出站流量

将以下代码粘贴到 outputs.tf 文件中,以显示部署完成后 Jenkins 主机 Droplet 的 IP 地址。

列表 6.40 Jenkins 主机公共 IP 地址

output "master" {
  value = digitalocean_droplet.jenkins_master.ipv4_address
}

在新的 variable.tf 文件中定义表 6.3 中列出的 Terraform 变量。在 variables.tfvars 中设置它们的值,以将秘密和敏感信息从模板文件中排除。

表 6.3 DigitalOcean Terraform 变量

名称 类型 描述
token 字符串 这是 DigitalOcean API 令牌。或者,也可以使用 DIGITALOCEAN_TOKEN 环境变量指定。
region 字符串 部署 Jenkins 主机的 DigitalOcean 区域。
jenkins_master_image 字符串 使用 Packer 前期构建的 Jenkins 主机镜像的名称。
ssh_fingerprint 字符串 SSH ID 或指纹。要检索信息,请转到 DigitalOcean 安全仪表板。

运行 terraform plan 命令以在执行前查看部署的影响:

现在,您可以使用 terraform apply 命令验证并部署到 Droplet 上。部署过程应该只需几秒钟即可完成。然后,新的 Jenkins 主机 Droplet 将在 Droplets 控制台中可用,Terraform 应该显示 Jenkins 主机 Droplet 的 IP 地址,如图 6.38 所示。

图 6.38 Jenkins 主机 Droplet

打开您喜欢的浏览器,并连接到前一个命令返回的公共 IPv4。应该显示预配置的 Jenkins 仪表板;见图 6.39。

图 6.39 使用 Droplet 公共 IP 访问 Jenkins 仪表板

6.3.3 构建 Jenkins 工作节点 Droplets

现在将构建作业委托给工作节点,并卸载 Jenkins 主机 Droplet。将部署几个构建工作节点以吸收构建活动。

创建一个 jenkins_workers.tf 文件,在其中定义 Jenkins 工作节点 Droplets。工作节点将从 Jenkins 工作节点镜像启动。

列表 6.41 Jenkins 工作节点 Droplets

data "digitalocean_image" "jenkins_worker_image" {
  name = var.jenkins_worker_image
}

data "template_file" "jenkins_worker_startup_script" {
  template = "${file("scripts/join-cluster.tpl")}"                         ❶

  vars = {
    jenkins_url            = "http://${digitalocean_droplet.jenkins_master.ipv4_address}:8080"
    jenkins_username       = var.jenkins_username
    jenkins_password       = var.jenkins_password
    jenkins_credentials_id = var.jenkins_credentials_id
  }
}
resource "digitalocean_droplet" "jenkins_workers" {
  count = var.jenkins_workers_count                                        ❷
  name   = "jenkins-worker"
  image  = data.digitalocean_image.jenkins_worker_image.id
  region = var.region
  size   = "s-1vcpu-2gb"                                                   ❸
  ssh_keys = [var.ssh_fingerprint]
  user_data = data.template_file.jenkins_worker_startup_script.rendered    ❹
  depends_on = [digitalocean_droplet.jenkins_master]
}

❶ 该脚本用于使 Droplet 自动加入集群作为 Jenkins 代理/工作节点。

❷ 指示要创建的 Jenkins 工作节点数量

❸ 在此 Droplet 配置中,我们使用 1 GB 的 RAM 和 1vCPU 作为 Jenkins 工作节点的配置。

❹ 启动脚本通过 user_data 部分传递,以便在 Droplet 首次运行时执行。

count 变量用于定义要部署的工作节点数量。每个 Droplet 启动时将执行一个 shell 脚本。此脚本类似于前面章节中提供的脚本,但使用 DigitalOcean 元数据服务器来获取 Droplet IP 地址和主机名:

INSTANCE_NAME=$(curl -s http://169.254.169.254/metadata/v1/hostname)
INSTANCE_IP=$(curl -s http://169.254.169.254/metadata/v1/
interfaces/public/0/ipv4/address)

最后,为了在 Jenkins 主机和节点之间设置双向连接,我们定义了一个防火墙,允许 TCP 端口 22 的入站流量。

列表 6.42 Jenkins 工作节点防火墙

resource "digitalocean_firewall" "jenkins_workers_firewall" {
  name = "jenkins-workers-firewall"

  droplet_ids .
[for worker in digitalocean_droplet.jenkins_workers : worker.id   ]

  inbound_rule {
    protocol         = "tcp"                                         ❶
    port_range       = "22"                                          ❶
    source_droplet_ids = [digitalocean_droplet.jenkins_master.id]    ❶
  }
}

❶ 允许 Jenkins 主机通过 SSH 连接到 Jenkins 工作节点

几分钟后,工作节点的 Droplets 将完成配置,您将看到类似于图 6.40 的输出。

图 6.40 Jenkins 工作节点 Droplets

返回 Jenkins 仪表板。新部署的工作节点应在执行第五章列表 5.7 中的用户数据脚本后加入集群;见图 6.41。

图 6.41 工作节点 Droplets 加入集群

您可以通过在 Jenkins 主 Droplet 前部署负载均衡器,将流量转发到端口 8080,并创建一个指向负载均衡器 FQDN 的 DNS 记录来进一步扩展此架构;见图 6.42。

图 6.42 DigitalOcean 上的 Jenkins 集群架构

完成后,通过运行以下命令清理基础设施:

terraform destroy --var-file=variables.tfvars

本章介绍了如何使用 IaC 工具从零开始部署和操作具有弹性和自我修复能力的 Jenkins 集群,并在多个云服务提供商上实现。我还解释了如何通过自动缩放策略和指标警报来设计可扩展的 Jenkins 工作节点。在下一章中,我们将实现 Jenkins 上的代码管道,用于多种云原生应用程序,如 Docker 化的微服务和无服务器应用程序。

摘要

  • Packer 的强大之处在于利用模板文件来创建与目标平台无关的相同 Jenkins 机器镜像。

  • 在 Google Cloud Platform 上部署 Jenkins 带来了对 Kubernetes 的无缝原生支持。

  • Azure 提供了各种基于云的服务,可能是在云上运行 Jenkins 的良好替代方案。

  • 在 DigitalOcean 上运行 Jenkins 可以是初涉商业和初创企业的成本效益解决方案。

第三部分. 实战 CI/CD 流水线

你已经完成了第一部分和第二部分,但你仍然渴望更多。我理解。幸运的是,这部分内容设计得可以让你有很多东西去消化。

你将实现针对现实世界、云原生应用的 CI/CD 工作流程。在接下来的几章中,你将使用 Docker 运行自动化测试,分析你的 Docker 镜像以查找安全漏洞,并在 Docker Swarm 和 Kubernetes 上部署容器化微服务。你将学习如何自动化你的无服务器应用的部署过程。这只是冰山一角,所以卷起袖子,让我们深入探索吧!

7 为微服务定义管道代码

本章涵盖了

  • 使用 Jenkins 多分支管道插件和 GitFlow 模型

  • 定义容器化微服务的多分支管道

  • 使用 GitHub 钩子触发 Jenkins 作业的推送事件

  • 将 Jenkins 作业配置导出为 XML 并克隆 Jenkins 作业

之前的章节介绍了如何通过使用自动化工具:HashiCorp Packer 和 Terraform 在多个云服务提供商上部署 Jenkins 集群。在本章中,我们将定义一个针对 Docker 化微服务的持续集成(CI)管道。

在第一章中,你了解到 CI 是在将源代码更改集成到中央存储库之前,持续测试和构建所有更改。图 7.1 总结了此工作流程的阶段。

图 7.1 持续集成阶段

每次对源代码的更改都会触发 CI 管道,启动自动化测试。这带来了许多好处:

  • 早期检测错误和问题,这导致维护时间和成本显著降低

  • 确保随着系统的发展,代码库继续工作并满足规范要求

  • 通过建立快速反馈循环来提高团队速度

尽管自动化测试带来了许多好处,但它们的实现和执行非常耗时。因此,我们将使用基于目标服务运行时和要求的测试框架。

一旦测试成功,源代码将被编译并构建一个工件。然后它将被打包并存储在远程注册表中,以便进行版本控制和后续部署。

第八章介绍了如何编写一个经典的 CI 管道,用于容器化微服务。最终结果将类似于图 7.2 中的 CI 管道。

图 7.2 目标 CI 管道

这些步骤涵盖了持续集成过程的最基本流程。在接下来的章节中,一旦你对这个工作流程感到舒适,我们将更进一步。我们将从零开始创建我们的多分支管道,使用 Jenkins 和 GitHub 钩子持续运行管道。

7.1 介绍基于微服务的应用程序

为微服务架构创建一个可靠的 CI/CD 流程可能具有挑战性。管道的目标是允许团队快速独立地构建和部署他们的服务,而不会干扰其他团队或破坏整个应用程序的稳定性。

为了说明如何从头开始定义容器化微服务的 CI/CD 管道,我实现了一个基于微服务架构的简单 Web 应用程序。我们将集成并部署一个名为 Watchlist 的基于 Web 的应用程序,用户可以浏览史上最伟大的 100 部电影,并将它们添加到他们的观看列表中。

项目包括测试、基准测试以及运行应用程序本地和云端的所需一切。部署的应用程序将类似于图 7.3。

图 7.3 观看列表市场 UI

图 7.4 说明了应用程序架构和流程。

图 7.4 加载器服务将 JSON 格式的电影数组逐个转发到消息队列(例如,Amazon SQS)。从那里,解析器服务将消费这些项目,从 IMDb 数据库中获取电影的详细信息,并将结果保存到 MongoDB 中。最后,通过商店服务通过 RESTful API 提供数据,并通过市场 UI 进行可视化。

注意 Amazon Simple Queue Service(SQS)是一个分布式消息队列服务。它旨在提供一个高度可扩展的托管消息队列,以解决由生产者-消费者问题引起的问题,并解耦分布式应用程序服务。有关更多详细信息,请参阅aws.amazon.com/sqs/

架构由多种语言编写的多个服务组成,以展示微服务范式的优势以及使用 Jenkins 来自动化不同运行时环境的构建和部署过程。表 7.1 列出了微服务。

表 7.1 应用程序微服务

服务 语言 描述
加载器 Python 负责读取包含电影列表的 JSON 文件,并将每个电影项目推送到 Amazon SQS。
解析器 Golang 负责通过订阅 SQS 并从 IMDb 网站(www.imdb.com)抓取电影信息,并将元数据(电影名称、封面、描述等)存储到 MongoDB 中。
商店 Node.js 负责提供 RESTful API,具有端点用于从 MongoDB 服务器中的观看列表数据库中获取电影列表和插入新电影。
市场平台 Angular 和 TypeScript 负责通过调用 Store RESTful API 提供前端以浏览电影。

在我们深入探讨应用程序的 CI 工作流程之前,让我们看看分布式应用程序源代码将如何组织。当你开始转向微服务时,你将面临的一个重大挑战就是代码库的组织。

你是为每个服务创建一个仓库,还是为所有服务创建一个单一仓库?每种模式都有其自身的优缺点。

  • 多个仓库——你可以有多个团队独立开发一个服务(清晰的归属感)。此外,较小的代码库更容易维护、测试和部署,且团队协调较少。然而,独立的团队可能会在组织内部产生局部知识,导致团队缺乏对项目整体图景的理解。

  • 单仓库——拥有单个源代码控制仓库可以简化项目组织,减少管理项目依赖的开销。它也有助于提高团队在单仓库上工作时的工作文化。然而,版本控制可能会变得更加复杂,性能和可扩展性问题也可能出现。

这两种模式都有优点和缺点,都不是万能的解决方案。你应该了解它们的优点和局限性,并据此做出明智的决定,选择最适合你和你项目的方案。

你的代码库结构将影响 CI/CD 管道的构建。一个项目托管在单个仓库中可能会导致一个相当复杂的单一管道。管道的大小和复杂性通常是巨大的痛点。随着组织内部服务数量的增加,管道的管理也成为一个更大的问题。最终,大多数管道都变成了一个混乱的混合体,其中包含了 npm、pip 和 Maven 脚本,到处散布着一些 bash 脚本。另一方面,采用多仓库策略可能会导致需要管理的多个管道和代码重复。幸运的是,有解决方案可以减少管道管理,包括使用共享管道段和共享 Groovy 脚本。

注意:第十四章介绍了如何在 Jenkins 中编写共享库以在多个管道之间共享通用代码和步骤。

本书将说明如何为这两种模式构建 CI/CD 管道。对于微服务,我们将采用多仓库策略。在构建无服务器函数的 CI/CD 管道时,我们将涵盖单仓库方法。

首先,创建四个 Git 仓库以存储每个服务的源代码(Loader、Parser、Store 和 Marketplace)。在这本书中,我使用 GitHub,但任何源代码管理系统都可以使用,例如 GitLab、Bitbucket,甚至是 SVN。确保你在将要执行以下章节中提到的步骤的机器上安装了 Git。

注意:在本书中,我们将使用 GitFlow 模型进行分支管理。更多信息请参阅第二章。

一旦创建了仓库,就将它们克隆到你的工作区,并创建三个主要分支:develop、preprod 和 master 分支,以帮助组织代码并将正在开发中的代码与生产中运行的代码隔离开。这种分支策略是 GitFlow 工作流分支模型的简化版本。

注意:每个服务的完整 Jenkinsfile 可以在本书 GitHub 仓库的 chapter7/microservices 文件夹中找到。

使用以下命令创建目标分支并将它们推送到远程仓库:

git clone https://github.com/mlabouardy/movies-loader.git 
cd movies-loader
git checkout -b preprod
git push origin preprod
git checkout -b develop
git push origin develop

要查看 Git 仓库中的分支,请在你的终端中运行以下命令:

git branch -a

星号 (*) 将会出现在你当前所在的分支旁边(develop)。在你的终端会话中应该会显示类似以下的输出:

图片

接下来,将书籍 GitHub 仓库中的代码复制到每个 Git 仓库的开发分支,然后将更改推送到远程仓库:

git add .
git commit -m "loading from json file"
git push origin develop

GitHub 仓库应类似于图 7.5。

图片

图 7.5 Loader GitHub 仓库包含服务源代码。

注意:目前,我们直接将更改推送到开发分支。稍后,您将看到如何创建拉取请求并使用 Jenkins 设置审查流程。

movies-loader 源代码位于 chapter7/microservices/movies-loader 文件夹中。重复相同的步骤来创建 movies-parser、movies-store 和 movies-marketplace GitHub 仓库。

7.2 定义多分支流水线作业

要将应用程序源代码与 Jenkins 集成,我们需要创建 Jenkins 作业以持续构建它。转到 Jenkins 网页仪表板,点击左上角的“新建项目”按钮,或点击“创建新作业”链接来创建新作业,如图 7.6 所示。

图片

图 7.6 Jenkins 新作业创建

注意:有关部署 Jenkins 的分步指南,请参阅第五章。

在结果页面上,您将看到各种类型的 Jenkins 作业可供选择。输入项目名称,向下滚动,选择多分支流水线,然后点击“确定”按钮。多分支流水线选项允许我们自动为源控制仓库中的每个分支创建流水线。

图 7.7 显示了 movies-loader 服务的多分支作业流水线。

图片

图 7.7 Jenkins 新作业设置

注意:Jenkins 多分支流水线插件([plugins.jenkins.io/workflow-multibranch/](https://plugins.jenkins.io/workflow-multibranch/))默认安装在预制的 Jenkins 主机 AMI 上。

我将简要总结这里的新作业类型,然后在接下来的章节中更详细地解释每个类型:

  • 自由风格项目—这是创建 Jenkins 作业的经典方式,其中每个 CI 阶段都通过 UI 组件和表单来表示。作业是基于网页的配置,任何修改都是通过 Jenkins 仪表板进行的。

  • 继承项目—此项目类型的目的在于在多个作业定义之间实现 Jenkins 中的真正属性继承。它允许您只需共享一次公共属性,然后创建 Jenkins 作业以在许多项目中继承它们。

  • 流水线—此作业类型允许您直接将 Jenkinsfile 粘贴到作业 UI 中,或者将单个 Git 仓库作为源并指定 Jenkinsfile 所在的单个分支。如果您计划使用基于主干的工作流程来管理项目源代码,此作业可能很有用。

  • 文件夹—这是一种将多个项目组合在一起的方法,而不是项目本身的一种类型。这与 Jenkins 仪表板上的视图选项卡不同,后者仅提供过滤器。相反,这就像服务器上的目录文件夹,存储嵌套项。

  • 多分支管道——这是我们将在本书中使用的项目类型。正如其名称所示,它允许我们为包含 Jenkinsfile 的每个 Git 分支自动创建嵌套作业。

  • 组织——某些源代码控制平台提供了一种将多个仓库分组到组织中的机制。此项目类型允许你在组织内的仓库中使用 Jenkinsfile,并基于 Jenkinsfile 执行管道。目前,此项目类型仅支持 GitHub 和 Bitbucket 组织。

注意:基于主干的策略使用一个中央仓库,所有对项目的更改都通过一个单一入口(称为主干master)进行。

为了明确起见,这些新工作类型的可用性取决于是否安装了必要的插件。如果你使用第四章 4.3.2 节中提供的插件列表烘焙了 Jenkins 主机机器镜像,你将获得前面列表中讨论的所有工作类型。

7.3 Git 和 GitHub 集成

管道脚本(Jenkinsfile)将在 GitHub 上版本化。因此,我们需要配置 Jenkins 作业以从远程仓库获取它。

在“常规”部分设置名称和描述。然后,从“分支源”部分选择代码源。通过从下拉列表中选择 GitHub 来配置管道以引用 GitHub 进行源代码管理;参见图 7.8。

图 7.8 分支源配置

对于检出凭证,打开一个新标签页并转到 Jenkins 仪表板。点击凭证,然后点击系统。在全局凭证页面上,从左侧菜单中,点击添加凭证链接。接下来,创建一个新的 Jenkins 全局凭证,类型为用户名和密码,以访问 Git 中的微服务项目。GitHub 用户名和密码可以设置如图 7.9 所示。然而,不建议使用个人 GitHub 账户。

注意:Jenkins 凭证插件([plugins.jenkins.io/credentials/](https://plugins.jenkins.io/credentials/))默认安装在烘焙的 Jenkins 主机机器镜像上。它是第四章 4.3.2 节中列出的基本插件之一。

图 7.9 Jenkins 凭证提供者

因此,我在 GitHub 上创建了一个专门的 Jenkins 服务账户,并使用访问令牌而不是账户密码。你可以通过使用 GitHub 凭证登录并导航到设置来创建访问令牌。然后,从左侧菜单中选择开发者设置,并选择个人访问令牌,如图 7.10 所示。

图 7.10 GitHub 个人访问令牌

点击生成新令牌按钮,为访问令牌命名,并从授权范围列表中选择 repo 访问,如图 7.11 所示。对于私有仓库,您必须确保已选择 repo 范围,而不仅仅是 repo:statuspublic_repo 范围。令牌名称很有帮助,因为您可能为许多应用程序拥有许多这样的令牌。

图 7.11 Jenkins 专为 GitHub 访问的令牌

图 7.11 Jenkins 专为 GitHub 访问的令牌

如图 7.12 所示的 GitHub 警告指出,您必须在生成令牌后复制它,因为您将无法再次看到它。如果您未能这样做,您唯一的选择将是重新生成令牌。

图 7.12 Labouardy

图 7.12 Jenkins 个人访问令牌

将 GitHub 个人访问令牌粘贴到密码字段中。通过在 ID 字段中输入一个字符串为您的 GitHub 凭据提供一个唯一的 ID,并在描述字段中添加一个有意义的描述,如图 7.13 所示。然后点击保存按钮。

图 7.13 Labouardy

图 7.13 Jenkins 上的 GitHub 凭据配置

返回图 7.14 所示的作业配置选项卡,从凭据下拉列表中选择您创建的凭据。设置仓库 HTTPS 克隆 URL 并将发现行为设置为允许扫描所有仓库分支。然后,滚动到页面底部并点击应用和保存按钮。

图 7.12 Jenkins 个人访问令牌

图 7.14 Jenkins 上的 GitHub 仓库配置

注意:我们将在第九章中介绍 Jenkins 高级扫描行为和策略。

Jenkins 将扫描 GitHub 仓库,寻找根仓库中具有 Jenkinsfile 的分支。到目前为止,还没有找到,我们可以通过从左侧侧边栏点击扫描仓库日志按钮来验证这一点。

注意:在这本书中,我们将使用“管道即代码”的概念,而不是像 Jenkins 经典自由式作业那样在 UI 中表示每个 CI 阶段。管道将在 Jenkinsfile 中进行描述。

日志输出确认,GitHub 仓库中尚未找到 Jenkinsfile,如图 7.15 所示。

图 7.15 Labouardy

图 7.15 Jenkins 仓库扫描日志

是时候创建一个 Jenkinsfile 了。使用您喜欢的文本编辑器或 IDE,在您本地 movies-loader Git 仓库的根目录下创建并保存一个名为 Jenkinsfile 的新文本文件。复制以下脚本化管道代码并将其粘贴到您的空 Jenkinsfile 中。

列表 7.1 使用脚本方法的 Jenkinsfile

node('workers'){
    stage('Checkout'){
        checkout scm
    }
}

注意:我们将在 Jenkinsfile 中使用脚本化管道语法来编写大部分内容。然而,当 CI 管道完成时,将采用声明式方法。

Checkout阶段,正如其名称所示,将简单地检出触发运行的参考点处的代码。您可以通过提供额外的参数来自定义检出过程。此外,阶段将在 Jenkins 工作节点上执行——因此,在节点块中使用了workers标签。我们假设我们已经在标记为workers的 Jenkins 实例上设置了一个 Jenkins 工作节点。如果没有提供标签,Jenkins 将在任何机器(主节点或工作节点)上可用的第一个执行器上运行管道。

保存您编辑后的 Jenkinsfile,并通过运行以下命令将更改推送到 develop 分支:

git add Jenkinsfile
git commit -m "creating Jenkinsfile"
git push origin develop

Jenkinsfile 与源代码一起存储在 GitHub 中。因此,就像任何代码一样,它可以在合并到主分支之前进行同行评审、评论和批准;见图 7.16。

图片

图 7.16 Jenkinsfile 与源代码一起存储

返回 Jenkins 仪表板,并再次触发扫描,请点击“立即扫描仓库”按钮。默认情况下,这将自动触发所有新发现的分支的构建,如图 7.17 所示。

图片

图 7.17 在 develop 分支上检测到的 Jenkinsfile

在我们当前的设置中,仅在 develop 分支上找到了 Jenkinsfile。如果我们再次点击 movies-loader 作业,Jenkins 应该为 develop 分支创建了一个嵌套作业,如图 7.18 所示。由于预生产和 master 分支上还没有 Jenkinsfile,因此没有为这些分支安排任何管道。

图片

图 7.18 在 develop 分支上触发的构建作业

注意:如果您遇到分支作业未自动创建或构建的问题,请检查左侧作业侧边栏中的“扫描仓库日志”项。

构建应该自动在 develop 分支上触发,检出阶段将被执行并变为绿色。请注意,Git 客户端应该安装在执行构建的工作节点上。

Jenkins 阶段视图,如图 7.19 所示,让我们能够实时可视化管道各个阶段的进度。

图片

图 7.19 管道执行

注意:Jenkins 阶段视图是作为 2.x版本的一部分新功能出现的。它仅与 Jenkins Pipeline 和 Jenkins Multibranch pipeline 作业一起工作。

点击检出阶段列以查看阶段的日志。您可以看到 Jenkins 已克隆了 movies-loader GitHub 仓库,并检出 develop 分支以从远程仓库获取最新的源代码更改,如图 7.20 所示。

图片

图 7.20 检出阶段日志

要查看完整的构建日志,请查找左侧的“构建历史”。构建历史记录选项卡将列出所有已运行的构建。点击最后一个构建编号;见图 7.21。

图片

图 7.21 构建编号设置

然后,点击左上角的控制台输出项。完整的构建日志将会显示,如图 7.22 所示。

图片

图 7.22 构建控制台日志

现在我们已经为 movies-loader 创建了一个 Jenkins 作业,接下来让我们为 movies-parser 服务创建另一个 Jenkins 作业;再次,前往 Jenkins 主页并点击新建项目按钮。然而,为了节省时间,请复制前一个作业的配置,如图 7.23 所示。

图片

图 7.23 解析作业创建

点击确定按钮。movies-parser 作业将反映克隆的 movies-loader 作业的所有功能。根据图 7.24 更新 GitHub 仓库 HTTPS 克隆 URL、作业描述和显示名称。

图片

图 7.24 解析作业 GitHub 配置

将之前作业中使用的相同的 Jenkinsfile 推送到 movies-parser GitHub 仓库的 develop 分支。然后点击应用更改以使更改生效。

保存后,构建将始终从 Jenkinsfile 的当前版本运行到仓库中,如图 7.25 所示。

图片

图 7.25 解析作业活动分支列表

按照相同的步骤创建 movies-store 和 movies-marketplace 服务的 Jenkins 作业。

虽然 Git 是目前最常用的分布式版本控制系统,但 Jenkins 内置了对 Subversion 的支持。要使用来自 Subversion 仓库的源代码,你只需提供相应的 Subversion URL——它将很好地与 HTTP、SVN 或 File 中的任何三个 Subversion 协议一起工作。Jenkins 将在您输入 URL 时立即检查该 URL 是否有效。如果仓库需要身份验证,您可以从图 7.26 所示的凭据下拉列表中创建一个类型为用户名的凭据,并选择它。

图片

图 7.26 SVN 仓库配置

你可以通过在“检出策略”下拉列表中选择适当的值来微调 Jenkins 从你的 Subversion 仓库获取最新源代码的方式。

7.4 发现 Jenkins 作业的 XML 配置

创建或克隆一个多分支管道作业的另一种方法是导出现有作业的 config.xml 文件。如你所预期,该 XML 文件包含了构建作业的配置细节。

你可以通过将浏览器指向 JENKINS _DNS/job/JOB_NAME/config.xml 来查看作业的 XML 配置。它应该在浏览器页面中输出作业 XML 定义,如图 7.27 所示。

图片

图 7.27 作业 XML 配置

将作业定义保存为 XML 文件,并根据你计划创建的目标 Jenkins 作业,在表 7.2 中更新相应的 XML 标签。

表 7.2 XML 标签

XML 标签 描述
<description> 有意义的描述,用几句话解释 Jenkins 作业的目的
<displayName> Jenkins 作业的显示名称;通常的做法是将存储源代码的仓库名称用作显示名称的值
<repository> 存储源代码的 GitHub 仓库名称,例如 movies-store
<repositoryURL> GitHub 仓库 HTTPS 克隆 URL,格式如下:https://github.com/username/repository.git

注意:在第十四章中,我们将介绍如何使用 Jenkins CLI 自动化导入和导出 Jenkins 中的多个作业和插件。

以下列表是 movies-store 作业的 XML 配置文件示例。它展示了 Jenkins 作业 XML 配置的典型结构。

列表 7.2 电影存储配置.xml

<?xml version="1.0" encoding="UTF-8"?>
<org.jenkinsci.plugins.workflow
.multibranch.WorkflowMultiBranchProject plugin="workflow-multibranch@2.21">
   <actions />
   <description>Movies store RESTful API</description>                      ❶
   <displayName>movies-store</displayName>                                  ❶
   <sources class="jenkins.branch
.MultiBranchProject$BranchSourceList" plugin="branch-api@2.5.5">
      <data>
         <jenkins.branch.BranchSource>
            <source class="org.jenkinsci.plugins
.github_branch_source.GitHubSCMSource" plugin="github-branch-source@2.5.8">
               <id>bf197dad-7d42-4a00-be25-7ae8ea7fef15</id>
               <apiUri>https://api.github.com</apiUri>                      ❷
               <credentialsId>github</credentialsId>                        ❷
               <repoOwner>mlabouardy</repoOwner>                            ❷
               <repository>movies-store</repository>                        ❷
               <repositoryUrl>
https://github.com/mlabouardy/movies-store.git
               </repositoryUrl>                                             ❸
               <traits>                                                     ❸
                  <org.jenkinsci.plugins.github__branch__source.BranchDiscoveryTrait>    ❸
                     <strategyId>1</strategyId>                             ❸
</org.jenkinsci.plugins.github__branch__source.BranchDiscoveryTrait>        ❸
               </traits>
            </source>
         </jenkins.branch.BranchSource>
      </data>
   </sources>
</org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject>

❶ 定义作业的名称和描述

❷ 定义项目的 GitHub 仓库 URL(HTTPS 格式)

❸ 告知 Jenkins 扫描 GitHub 仓库中的所有分支以查找 Jenkinsfile

注意:为了简洁,XML 已被裁剪。完整的作业 XML 定义可在 GitHub 仓库的 chapter7/jobs/movies-store.xml 中找到。

一旦您已使用适当的值更新了 config.xml 文件,请向包含作业 XML 定义的负载的 Jenkins URL 发出 HTTP POST 请求,查询参数name等于目标作业的名称。图 7.28 展示了使用 Postman HTTP API 客户端创建 movies-store 作业的示例。

注意:如果 Jenkins 启用了 CSRF 保护,您需要创建一个 API 令牌而不是 crumb 发行者令牌。有关更多信息,请参阅第二章。

图片

图 7.28 使用 Postman 创建作业的 Jenkins RESTful API

可以使用一行 cURL 命令来克隆并创建一个新的作业:

curl -s https:///<USER>:<API_TOKEN>@JENKINS_HOST/job/JOBNAME/config.xml
| curl -X POST 'https:///<USER>:<API_TOKEN>@JENKINS_HOST/createItem?name=JOBNAME.
--header "Content-Type: application/xml" -d @-

Jenkins API 令牌(API_TOKEN变量)可以通过登录到 Jenkins 仪表板并使用您想要生成 API 令牌的用户来创建。然后打开用户配置文件页面,并点击“配置”以打开用户配置页面。

定位到“添加新令牌”按钮,为新令牌命名,然后点击“生成”按钮,如图 7.29 所示。获取令牌,并将前面的 cURL 命令中的API_TOKEN变量替换为生成的令牌值。

图片

图 7.29 Jenkins API 令牌生成

注意:Jenkins 作业也可以通过直接将 XML 文件复制到 Jenkins 主实例的/var/lib/jenkins/jobs/<作业名称>文件夹中,并使用service jenkins restart命令重启 Jenkins 来创建,以便更改生效。

一旦创建了四个 Jenkins 作业,您应该在 Jenkins 主页上看到图 7.30 所示的作业。您可以通过创建一个 Jenkins 文件夹来将这些作业组织在一个视图中。您可以创建一个名为 Watchlist 的文件夹,并将这些作业移动到其中。

图片

图 7.30 Jenkins 中的微服务作业

要这样做,请按照以下步骤操作:从侧边栏中点击新建项目,在文本框中输入 Watchlist 作为名称,并选择文件夹以创建文件夹。要将现有作业移动到文件夹中,点击作业右侧的箭头并选择移动。选择 Watchlist 作为目标文件夹并点击移动。

微服务作业可以通过以下 URL 格式访问:JENKINS_DNS/job/Watchlist/job。

即使 Jenkins CLI 的使用已被弃用且不推荐用于安全漏洞(至少对于 Jenkins 2.53 及更早版本),也可以使用它来导入或导出作业。你可以运行以下命令来导入你的 Jenkins 作业 XML 文件:

java -jar jenkins-cli.jar -s JENKINS_URL 
-auth USERNAME:PASSWOR.
create-job movies-marketplace < config.xml

另一种身份验证方法是使用访问令牌,通过将 -auth 选项替换为 username:token 参数。

7.5 配置 Jenkins 的 SSH 验证

之前,你学习了如何使用用户名和密码凭据在 Jenkins 上配置 GitHub。我们还介绍了如何创建具有细粒度权限的 GitHub API 访问令牌。本节将介绍如何使用 SSH 密钥而不是凭据来验证项目仓库。

注意:你可以使用 ssh-keygen 命令生成一个用于 SSH 验证的单一用途 SSH 密钥。

首先,配置 GitHub 上的 Jenkins 公共 SSH 密钥。你可以通过访问仓库设置并在部署密钥部分添加部署密钥来配置 GitHub 仓库上的 SSH。或者,简单地从用户配置文件设置中全局配置 SSH 密钥。给一个如 Jenkins 的名称,并将公钥(从 id_rsa.pub 文件中)粘贴进去;见图 7.31。

图 7.31 GitHub SSH 配置

注意:一旦一个密钥被用作一个仓库的部署密钥,它就不能用于另一个仓库。

要确定密钥是否已成功配置,请在你的 Jenkins SSH 会话中输入以下命令。使用 -i 标志提供 Jenkins 私钥的路径:

ssh -T -ai PRIVATE_KEY_PATH git@github.com

如果响应看起来像 Hi username,则密钥已正确配置。

现在,转到 Jenkins 控制台左侧的凭据,并点击全局。然后选择添加凭据,创建一个类型为 SSH 用户名的凭据。给它一个名称,并设置 SSH 私钥的值,如图 7.32 所示。用户名应该是托管项目的 GitHub 账户的用户名。在密码短语文本框中,写下生成 SSH RSA 密钥时给出的密码短语。如果没有设置,请留空。

图 7.32 在 Jenkins 上配置 GitHub SSH 凭据

返回 Jenkins 作业,在分支源下,从下拉列表中选择 Git,设置仓库 SSH 克隆 URL,并选择已保存的凭据标题名称;见图 7.33。

图 7.33 配置 Jenkins 作业以使用 SSH 密钥

如果您查看构建输出,它应该清楚地列出正在使用 SSH 密钥进行身份验证。以下是一个突出显示相同内容的示例输出:

图片

到目前为止,Checkout阶段一直使用当前 Jenkins 作业中配置的凭据和设置。如果您想自定义设置并使用特定的凭据,可以替换以下列表。

列表 7.3 定制的 git clone 命令

stage('Checkout') {
    steps {
        git branch: 'develop',
            credentialsId: 'github-ssh',
            url: 'git@github.com:mlabouardy/movies-loader.git'
    }
}

本例将使用存储在 github-ssh Jenkins 凭证中的 SSH 凭据克隆 movies-loader GitHub 仓库的 develop 分支。

7.6 使用 GitHub webhooks 触发 Jenkins 构建

到目前为止,我们总是通过点击构建现在按钮手动构建管道。它工作得很好,但不是很方便。所有团队成员都必须记住,在提交到仓库后,他们需要打开 Jenkins 并开始构建。

要通过推送事件触发作业,我们将在每个服务的 GitHub 仓库上创建一个 webhook,如图 7.34 所示。记住,相应的分支上也应该有一个 Jenkinsfile,以便告诉 Jenkins 在发现仓库中的更改时需要做什么。

注意,Webhooks是用户定义的 HTTP 回调。它们由 Web 应用程序中的事件触发,可以促进不同应用程序或第三方 API 的集成。

图片

图 7.34 Webhook 解释

导航到您想要连接到 Jenkins 的 GitHub 仓库,并点击仓库设置选项。在左侧菜单中,点击 Webhooks,如图 7.35 所示。

GitHub webhooks 允许您通过向配置的服务 URL 发送 POST 请求,在发生某些 Git 事件(推送、合并、提交、分支等)时通知外部服务。

图片

图 7.35 GitHub Webhooks 部分

点击添加 Webhook 按钮,弹出相关对话框,如图 7.36 所示。填写以下值:

  • 负载 URL 应采用以下格式:JENKINS_URL/github-webhook/(确保包括最后一个正斜杠)。

  • 内容类型可以是 application/json 或 application/x-www-form-urlencoded。

  • 选择推送事件作为触发器,并保留密钥字段为空(除非在 Jenkins Configure System > GitHub Plugin 部分创建了并配置了密钥)。

图片

图 7.36 Jenkins webhook 设置

将其余选项保留为默认值,然后点击添加 Webhook 按钮。应向 Jenkins 发送测试负载以设置钩子。如果负载成功被 Jenkins 接收,您应该看到带有绿色勾选标记的 webhook,如图 7.37 所示。

图片

图 7.37 Jenkins webhook 设置

完成这些 GitHub 更新后,如果您将更改推送到 Git 仓库,应该会自动触发一个新事件。在这种情况下,我们更新 README.md 文件:

图片

返回您的 Jenkins 项目,您将看到在上一步骤中执行的提交自动触发了新的作业。点击作业旁边的箭头并选择控制台输出。图 7.38 显示了输出。

更新 README 的消息确认,在将新的 README.md 推送到 GitHub 仓库后,构建被自动触发。现在,每次您将更改发布到远程仓库时,GitHub 都会触发您的新 Jenkins 作业。通过遵循相同的程序,在剩余的 GitHub 仓库上创建类似的 webhook。

图片

图 7.38 GitHub 推送事件

注意:如果您想使 SVN 用户在每次提交后持续触发 Jenkins 作业,您可以选择配置 Jenkins 定期轮询 SVN 服务器,或者在远程仓库上设置 post-commit 钩子。

在不同的情况下,Jenkins 仪表板可能无法从公共网络访问。您可以通过在 GitHub 服务器和 Jenkins 之间设置一个公共反向代理作为中间件,并配置 GitHub webhook 使用中间件 URL 来代替手动执行作业。图 7.39 解释了如何使用 AWS 托管服务在 VPC 内为 Jenkins 实例设置 webhook 转发器。

图片

图 7.39 使用 API 网关设置 GitHub webhook

注意:您可以将这种方法推广到其他服务,例如 Bitbucket 或 DockerHub——或者任何实际上会发出 webhooks 的服务。

如果您使用 AWS 作为云提供商,您可以使用名为 Amazon API Gateway 的托管代理,在特定端点上对 POST 请求进行调用时调用 Lambda 函数,如图 7.40 所示。

图片

图 7.40 使用 API 网关触发 Lambda 函数

Lambda 函数将从 API 网关接收 GitHub 有效负载并将其转发到 Jenkins 服务器。以下列表是一个用 JavaScript 编写的函数入口点。

列表 7.4 Lambda 函数处理程序

const Request = require('request');
exports.handler = (event, context, callback) => {
    Request.post({
        url: process.env.JENKINS_URL,
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-GitHub-Event": event.headers["X-GitHub-Event"]
        },
        json: JSON.parse(event.body)
    }, (error, response, body) => {
        callback(null, {
            "statusCode": 200,
            "headers": {
                "content-type": "application/json"
            },
            "body": "success",
            "isBase64Encoded": false
        })
    })
};

要部署 GitHub webhook 和 AWS 资源,我们将使用 Terraform。但首先,我们需要创建一个包含 Lambda 函数 index.js 入口点的部署包。部署包是一个可以通过以下命令生成的 zip 文件:

zip deployment.zip index.js

注意:本节假设您熟悉 Terraform 的常规计划/应用工作流程。如果您是 Terraform 的新手,请参阅第五章。

接下来,我们定义一个名为 lambda.tf 的文件,其中包含 AWS Lambda 函数的 Terraform 资源定义。我们将运行时设置为 Node.js 运行环境(Lambda 处理程序是用 JavaScript 编写的)。我们定义一个名为JENKINS_URL的环境变量,其值指向 Jenkins 网页仪表板的 URL,如下一列表所示。

列表 7.5 基于 Node.js 运行的 Lambda 函数

resource "aws_lambda_function" "lambda" {
  filename = "../deployment.zip"
  function_name = "GitHubWebhookForwarder"
  role = aws_iam_role.role.arn
  handler = "index.handler"
  runtime = "nodejs14.x"
  timeout = 10
  environment {
    variables = {
      JENKINS_URL = var.jenkins_url
    }
  }
}

然后,我们定义一个 API Gateway RESTful API,当在 /webhook 端点发生 POST 请求时触发前面的 Lambda 函数。在上一步骤的 lambda.tf 相同目录下创建一个新文件 apigateway.tf,并粘贴以下内容。

列表 7.6 API Gateway RESTful API

resource "aws_api_gateway_rest_api" "api" {
  name        = "GitHubWebHookAPI"
  description = "GitHub Webhook forwarder"
}

resource "aws_api_gateway_resource" "path" {
   rest_api_id = aws_api_gateway_rest_api.api.id
   parent_id   = aws_api_gateway_rest_api.api.root_resource_id
   path_part   = "webhook"
}

resource "aws_api_gateway_integration" "request_integration" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  resource_id = aws_api_gateway_method.request_method.resource_id
  http_method = aws_api_gateway_method.request_method.http_method
  type        = "AWS_PROXY"
  uri         = aws_lambda_function.lambda.invoke_arn
  integration_http_method = "POST"
}

最后,在以下列表中,我们创建一个 API Gateway 部署以激活配置,并在可用于 webhook 配置的 URL 上公开 API。我们使用 Terraform 输出变量通过引用 API 部署阶段来显示 API 部署 URL。

列表 7.7 API 新部署阶段

resource "aws_api_gateway_deployment" "stage" {
   rest_api_id = aws_api_gateway_rest_api.api.id
   stage_name  = "v1"
}

output "webhook" {
    value = "${aws_api_gateway_deployment.stage.invoke_url}/webhook"
}

在发出 terraform apply 命令之前,您需要定义前面资源中使用的变量。variables.tf 文件将包含变量列表,这些变量在表 7.3 中详细说明。

表 7.3 GitHub webhook 代理的 Terraform 变量

变量 类型 描述
region 字符串 none 部署 AWS 资源的 AWS 区域。它也可以从 AWS_REGION 环境变量中获取。
shared_credentials_file 字符串 none 共享凭据文件的路径。如果没有设置此值且指定了配置文件,则使用 ~/.aws/credentials。
aws_profile 字符串 profile 在共享凭据文件中设置的 AWS 配置文件名称。
jenkins_url 字符串 none Jenkins URL,格式为 http://IP:8080,或使用 HTTPS(如果使用 SSL 证书)。

当 Terraform 完成部署 AWS 资源后,应创建一个名为 GitHubWehookForwarder 的新 Lambda 函数,其触发类型为 API Gateway,如图 7.41 所示。

图片

图 7.41 GitHubWebhookForwarder Lambda 函数

此外,Terraform 将显示 RESTful API 部署 URL,您可以使用它来在目标 GitHub 仓库上创建 webhook,如图 7.42 所示。

图片

图 7.42 基于 API Gateway URL 的 GitHub webhook

Webhooks 应该现在正在流动。您可以对您的仓库进行更改,并检查构建是否很快开始。您还可以通过要求请求密钥并在 Lambda 函数端验证传入请求签名来添加额外的安全层。

如果您在本地运行 Jenkins,可以使用构建触发器轮询 SCM 并定期运行,如图 7.43 所示。在这种情况下,Jenkins 将定期检查仓库,如果发生任何更改,它将运行作业。

图片

图 7.43 在作业设置下,您可以定义检查的间隔。

首次手动运行管道后,自动触发器被设置。然后它每分钟检查 GitHub,对于新的提交,开始构建。为了测试它是否按预期工作,您可以将任何内容提交到 GitHub 仓库并查看构建是否开始。

注意:轮询源代码管理(SCM),即使它不太直观,如果 Git 提交频繁且构建时间较长,可能仍然有用,因为每次推送事件都执行构建可能会导致过载。

到目前为止,你已经学习了如何将 Git 仓库与 Jenkins 集成并定义多分支流水线作业。我们最终创建了我们第一个完整的提交流水线。然而,在当前状态下,它并没有做太多。在接下来的章节中,我们将看到可以对提交流水线进行哪些改进,以便使其更加完善,并且我们将从在 Jenkins 流水线中运行自动化测试开始。

摘要

  • Webhook 是一种机制,可以在远程 Git 仓库中提交提交时自动触发 Jenkins 项目的构建。

  • 团队或组织内部应仔细选择开发工作流程,因为它会影响持续集成(CI)过程并定义代码的开发方式。

  • 使用多仓库或单仓库策略来组织代码库将定义持续集成/持续部署(CI/CD)管道的复杂性,因为组织内部的应用程序数量会随着时间而演变。

  • 当 Jenkinsfile 和应用程序源代码位于同一 Git 仓库中时,流水线可以经过标准的代码开发过程(代码审查、拉取请求、自动化测试等)。

  • Jenkins 将运行作业的配置文件存储在一个 XML 文件中。编辑这些 XML 配置文件的效果与通过 Web 仪表板编辑 Jenkins 作业相同。

  • 反向代理可以用来让 Git webhook 到达位于防火墙后面的运行中的 Jenkins 服务器。

8 使用 Jenkins 运行自动化测试

本章涵盖

  • 为基于 Python、Go、Node.js 和 Angular 的服务实现 CI 管道

  • 使用无头 Chrome 运行预集成测试和自动化 UI 测试

  • 在 Jenkins 管道中执行 SonarQube 静态代码分析

  • 在 Docker 容器内运行单元测试并发布代码覆盖率报告

  • 在 Jenkins 管道中集成依赖性检查并在 DevOps 中注入安全性

在上一章中,你学习了如何为容器化微服务设置多分支管道作业,以及如何使用 webhooks 在推送事件上持续触发 Jenkins。在本章中,我们将在 CI 管道中运行自动化测试。图 8.1 总结了当前的 CI 工作流程阶段。

图 8.1 本章涵盖的测试阶段

测试自动化通常被认为是敏捷开发的基础。如果你想快速发布——甚至每天——并且保持合理的质量,你必须转向自动化测试。另一方面,对测试的重视不足可能导致客户不满和产品延迟。然而,自动化测试过程比自动化构建、发布和部署过程要困难一些。通常需要大量努力来自动化应用程序中使用的几乎所有测试用例。这是一个随着时间的推移而成熟的活动。并不是所有的测试都可以自动化。但想法是自动化尽可能多的测试。

到本章结束时,我们将实现如图 8.2 所示的 CI 管道中的测试阶段。

图 8.2 目标 CI 管道

在继续 CI 管道实现之前,关于我们与 Jenkins 集成的 Web 分布式应用程序的快速提醒:它基于微服务架构,并分为用不同编程语言和框架编写的组件/服务。图 8.3 说明了这种架构。

图 8.3 观察列表微服务架构

在接下来的章节中,你将学习如何将各种类型的测试集成到我们的 CI 工作流程中。我们将从单元测试开始。

8.1 在 Docker 容器内运行单元测试

单元测试是尽早识别问题的前沿工作。测试需要小而快速执行,以便高效。

movies-loader 服务是用 Python 编写的。为了定义单元测试,我们将使用 unittest 框架(它随 Python 的安装一起提供)。要使用它,我们需要导入 unittest 模块,它提供了一套丰富的用于构建和运行测试的方法。以下列表,test_main.py,演示了一个简短的单元测试,用于测试 JSON 加载和解析机制。

列表 8.1 Python 中的单元测试

import unittest
import json

class TestJSONLoaderMethods(unittest.TestCase):
    movies = []

    @classmethod
    def setUpClass(cls):
        with open('movies.json') as json_file:
            cls.movies = json.load(json_file)

    def test_rank(self):
        self.assertEqual(self.movies[0]['rank'], '1')

    def test_title(self):
        self.assertEqual(self.movies[0]['title'], 'The Shawshank Redemption')

   def test_id(self):
        self.assertEqual(self.movies[0]['id'], 'tt0111161')

if __name__ == '__main__':
    unittest.main()

setUpClass() 方法允许我们在每个测试方法执行之前加载 movies.json 文件。三个单独的测试是通过以 test 前缀开头的方法定义的。这种命名约定通知测试运行器哪些方法代表测试。每个测试的核心是一个调用 assertEqual() 的操作,以检查预期的结果。例如,我们检查从 JSON 文件解析出的第一部电影标题属性是否为 The Shawshank Redemption

要运行测试,我们可以在 Jenkins 上执行 python test_main.py 命令。但是,它需要安装 Python 3。为了避免为构建的每个服务安装运行时环境,我们将在 Docker 容器中运行测试。这样,我们将在所有 Jenkins 工作节点上使用 Docker 作为执行环境。

在 movies-loader 仓库中,使用您喜欢的文本编辑器或 IDE 创建一个名为 Dockerfile.test 的文件,内容如下。

列表 8.2 Movie loader 的 Dockerfile.test

FROM python:3.7.3
WORKDIR /ap.
COPY test_main.py .
COPY movies.json .

Dockerfile 是从 Python 3.7.3 官方镜像构建的。它设置了一个名为 app 的工作目录,并将测试文件复制到工作目录中。

注意:使用 Dockerfile.test 的命名约定是为了避免与用于构建主应用程序 Docker 镜像的 Dockerfile 发生名称冲突。

现在,更新列表 7.1 中给出的 Jenkinsfile,并添加一个新的 Unit Test 阶段,如下所示。该阶段将基于 Dockerfile .test 创建 Docker 镜像,然后从创建的镜像启动 Docker 容器以运行 python test_main.py 命令以启动单元测试。Unit Test 阶段使用类似 DSL 的语法来定义 shell 指令。

列表 8.3 Movie loader 的 Jenkinsfile

def imageName = 'mlabouardy/movies-loader'

node('workers'){
    stage('Checkout'){
        checkout scm
    }

    stage('Unit Tests'){
        sh "docker build -t ${imageName}-test -f Dockerfile.test ."
        sh "docker run --rm ${imageName}-test"
    }
}

使用 docker builddocker run 命令分别创建镜像和从镜像构建容器。

注意:docker run 命令中的 --rm 标志用于在容器退出时自动清理容器并删除文件系统。

您可以在 Windows 工作节点上的管道中使用 powershell 步骤。此步骤具有与 sh 指令相同的选项。

使用以下命令将更改提交到 develop 分支:

git add Dockerfile.test Jenkinsfile
git commit -m "unit tests execution"
git push origin develop

在几秒钟内,应该会在 develop 分支的 movies-loader 作业上触发一个新的构建。从 movies-loader 多分支流水线作业中,点击相应的 develop 分支。在结果页面上,您将看到 develop 分支流水线的阶段视图,如图 8.4 所示。

图 8.4 单元测试阶段执行

点击控制台输出选项以查看测试结果。所有三个测试用例都已运行,日志中的状态显示为 SUCCESS,如图 8.5 所示。

图 8.5 单元测试成功执行日志

可以用 Docker DSL 指令替换 shell 命令。我建议在适当的地方使用它们,而不是通过 shell 运行 Docker 命令,因为它们提供了高级封装和易于使用:

stage('Unit Tests'){
        def imageTest= docker.build("${imageName}-test",
 "-f Dockerfile.test .")
        imageTest.inside{
            sh 'python test_main.py'
        }
}

docker.build()方法类似于运行docker build命令。该方法返回的值可以用于后续调用以创建 Docker 容器并运行单元测试。图 8.6 显示了管道成功运行的示例。

图片

图 8.6 使用 Docker DSL 运行测试

为了以图形化、可视化的方式显示结果,我们可以在 Jenkins 上使用 JUnit 报告集成插件来消费由 Python 单元测试生成的 XML 文件。

注意:JUnit 报告集成插件(plugins.jenkins.io/junit/)默认安装在预制的 Jenkins 主机机器镜像中。

更新 test_main.py 文件以使用 xmlrunner 库,并将其传递给 unittest.main 方法:

import xmlrunner
...
if __name__ == '__main__':
    runner = xmlrunner.XMLTestRunner(output='reports')
    unittest.main(testRunner=runner)

这将在“reports”目录中生成测试报告。然而,我们需要解决一个问题:测试容器将存储它自身执行的测试结果。我们可以通过将一个卷映射到“reports”目录来解决这个问题。更新 Jenkinsfile 以告诉 Jenkins 在哪里找到 JUnit 测试报告:

stage('Unit Tests'){
        def imageTest= docker.build("${imageName}-test",
 "-f Dockerfile.test .")
        sh "docker run --rm -v $PWD/reports:/app/reports ${imageName}-test"
        junit "$PWD/reports/*.xml"
}

注意:您也可以通过使用docker cp命令将报告文件复制到当前工作区来获取报告结果。然后,将工作区作为 JUnit 命令的参数设置。

让我们继续执行这个操作。这将添加一个图表到 Jenkins 的项目页面,在更改推送到 develop 分支并且 CI 执行完成后;参见图 8.7。

图片

图 8.7 JUnit 测试图表分析器

历史图表显示了在一段时间内与测试执行相关的几个指标(包括失败、总数和持续时间)。您还可以点击图表以获取有关单个测试的更多详细信息。

8.2 使用 Jenkins 自动化代码检查器集成

CI 管道中要实现的测试示例之一是代码检查。检查器可以用来检查源代码,查找拼写错误、语法错误、未声明的变量以及对未定义或已弃用的函数的调用。它们可以帮助您编写更好的代码并预测潜在的 bug。让我们看看如何将代码检查器与 Jenkins 集成。

movies-parser 服务是用 Go 编写的,因此我们可以使用 Go 检查器来确保代码遵循代码风格。检查器可能听起来像是一个可选的工具,但对于大型项目来说,它有助于在整个项目中保持一致的样式。

Dockerfile.test 使用 golang:1.13.4 作为基础镜像,并安装了golint工具和服务依赖项,如下所示。

列表 8.4 Movie 解析器的 Dockerfile.test

FROM golang:1.13.4
WORKDIR /go/src/github.com/mlabouardy/movies-loader
ENV GOCACHE /tmp
WORKDIR /go/src/github/mlabouardy/movies-parser
RUN go get -u golang.org/x/lint/golint
COPY . .
RUN go get -v

Quality Tests阶段添加到 Jenkinsfile 中,使用docker.build()命令基于 Dockerfile.test 构建 Docker 镜像,然后使用inside()指令在构建的镜像上以守护模式启动 Docker 容器来执行golint命令:

def imageName = 'mlabouardy/movies-parser'
node('workers'){
    stage('Checkout'){
        checkout scm
    }

    stage('Quality Tests'){
       def imageTest= docker.build("${imageName}-test", "-f Dockerfile.test .")
        imageTest.inside{
            sh 'golint'
        }
    }
}

注意:如果 Dockerfile.test 中定义了ENTRYPOINT指令,inside()指令将把其作用域内定义的命令作为参数传递给ENTRYPOINT指令。

golint执行将产生如图 8.8 所示的日志。

图 8.8 golint命令输出标识了缺失的注释。

默认情况下,golint仅打印样式问题,并返回(带有 0 退出代码),因此 CI 永远不会考虑出了问题。如果您指定-set_exit_status,如果golint报告问题,则流水线将失败。

我们还可以为 movies-parser 服务实现一个单元测试。Go 有一个内置的测试命令go test和包testing,它们结合提供了一个最小但完整的单元测试体验。

类似于 movies-loader 服务,我们将编写一个 Dockerfile.test 文件来执行在 main_test.go 文件中编写的测试。以下列表中的代码为了简洁和突出主要部分而进行了裁剪。您可以在第七章的chapter7/microservices/movies-parser/main_test.go中浏览完整代码。

列表 8.5 电影解析器的单元测试

package main

import (
    "testing"
)
const HTML = `
<div class="plot_summary ">
    <div class="summary_text">
        An ex-hit-man comes out of retirement to track down the gangster.
that killed his dog and took everything from him.
    </div>
    ...
</div>
`
func TestParseMovie(t *testing.T) {
    expectedMovie := Movie{
            Title:       "John Wick (2014)",
            ReleaseDate: "24 October 2014 (USA)",
            Description: "An ex-hit-man comes ...",
    }

    currentMovie, err := ParseMovie(HTML)
    if expectedMovie.Title != currentMovie.Title {
     t.Errorf("returned wrong title: got %v want %v"
, currentMovie.Title, expectedMovie.Title)
    }
}

此代码展示了 Go 中单元测试的基本结构。内置的测试包由 Go 的标准库提供。单元测试是一个接受类型为*testing.T的参数并调用t.Error()方法来指示失败的函数。此函数必须以Test关键字开头,并且后缀名应以大写字母开头。在我们的用例中,该函数测试ParseMovie()方法,该方法接受HTML参数并返回Movie结构。

8.3 生成代码覆盖率报告

Unit Tests阶段很简单:它将在从 Docker 测试镜像创建的 Docker 容器内执行go test。我们不是在每个阶段构建测试镜像,而是将docker.build()指令移出阶段以加快流水线执行时间,如下所示。

列表 8.6 电影解析器的 Jenkinsfile

def imageName = 'mlabouardy/movies-parser'
node('workers'){
    stage('Checkout'){
        checkout scm
   }

    def imageTest= docker.build("${imageName}-test", "-f Dockerfile.test .")
    stage('Quality Tests'){
        imageTest.inside{
            sh 'golint'
        }
    }
    stage('Unit Tests'){
        imageTest.inside{
            sh 'go test'
        }
    }
}

将更改推送到 develop 分支,并触发 Jenkinsfile 中定义的三个阶段执行,如图 8.9 所示。

图 8.9 Go CI 流水线

go test命令的输出如图 8.10 所示。

图 8.10 go test命令输出

注意:Go 提供了-cover标志作为go test命令的内置功能,用于检查代码覆盖率。

如果我们想以 HTML 格式获取覆盖率报告,需要添加以下命令:

go test -coverprofile=cover/cover.cov
go tool cover -html=cover/coverage.cov -o coverage.html

图 8.11 覆盖率.html 内容可以在测试阶段结束时从 Jenkins 仪表板提供。

命令会渲染一个 HTML 页面,如图 8.11 所示,该页面可视化地显示了 main.go 文件中每个受影响行的逐行覆盖率。

您可以将前面的命令包含在 CI 工作流程中,以生成 HTML 格式的覆盖率报告。

8.4 在 CI 管道中注入安全

确保不会将任何漏洞发布到生产环境中非常重要——至少不要发布关键或重大的漏洞。在 CI 管道中扫描项目依赖项可以确保这一额外的安全级别。存在几种依赖项扫描解决方案,包括商业和开源。在本部分,我们将使用 Nancy。

Nancy (github.com/sonatype-nexus-community/nancy) 是一个开源工具,用于检查你的 Go 依赖项中的漏洞。它使用 Sonatype 的 OSS Index (ossindex.sonatype.org/),这是公共漏洞和暴露(CVE)数据库的镜像,来检查你的依赖项是否存在公开报告的漏洞。

注意:第九章介绍了如何在 Jenkins 上使用 OWASP Dependency-Check 插件来检测对已分配 CVE 条目的依赖项的引用。

该过程的第一个步骤是从官方发布页面安装 Nancy 二进制文件。更新 movies-parser 项目的 Dockerfile.test,以安装 Nancy 版本 1.0.22(本书编写时),并在PATH变量上配置可执行文件,如下所示。

列表 8.7 Movie parser 的 Dockerfile.test

FROM golang:1.13.4
ENV VERSION 1.0.22
ENV GOCACHE /tmp
WORKDIR /go/src/github/mlabouardy/movies-parser
RUN wget https://github.com/sonatype-nexus-community/nancy/releases/download/$VERSION/nancy

linux.amd64-$VERSION -O nancy && \
    chmod +x nancy && mv nancy /usr/local/bin/nancy
RUN go get -u golang.org/x/lint/golint
COPY . .
RUN go get -v

要开始使用此工具,请在 Jenkinsfile 中添加一个Security Tests阶段,以使用 Gopkg.lock 文件作为参数运行 Nancy,该文件包含 movies-parser 服务中使用的 Go 依赖项列表:

stage('Security Tests'){
        imageTest.inside(‘-u root:root’){
           sh 'nancy /go/src/github/mlabouardy/movies-parser/Gopkg.lock'
        }
}

将更改推送到远程仓库。将启动一个新的管道。在Security Tests阶段,将执行 Nancy,并且不会报告任何依赖项安全漏洞,如图 8.12 所示。

图片

图 8.12 已知漏洞的依赖项扫描

如果 Nancy 在您的依赖项中找到一个漏洞,它将以非零代码退出,这样您就可以将 Nancy 作为 CI/CD 流程中的工具使用,并使构建失败。

尽管你应该努力解决所有安全漏洞,但某些安全扫描结果可能包含误报。例如,如果你在不太可能适用于你项目的隐蔽条件下看到一个理论上的拒绝服务攻击,那么可能安全地安排在未来一周或两周内修复。另一方面,一个更严重的漏洞可能会允许未经授权访问客户信用卡数据,应该立即修复。无论情况如何,都要掌握漏洞知识,这样你和你的团队就可以确定适当的行动方案来减轻安全威胁。

将依赖项扫描添加到你的管道(图 8.13)是减少你的攻击面的简单第一步。这很容易实现,因为它不需要服务器重新配置或额外的服务器来工作。在其最基本的形式中,只需安装 Nancy 二进制文件并将其部署。

图片

图 8.13 CI 管道中的安全注入

8.5 使用 Jenkins 运行并行测试

到目前为止,预集成测试是顺序运行的。我们总是遇到的一个问题是如何在保持管道时间合理和更改流畅的同时运行所有确保高质量更改所需的测试。更多的测试意味着更大的信心,但也意味着更长的等待时间。

注意:在第九章中,我们将介绍如何使用并行测试执行插件在多个 Jenkins 工作者之间并行运行测试。

你经常看到的 Jenkins 管道的一个特性是它能够通过使用 parallel DSL 步骤并行运行构建的部分。

更新 Jenkinsfile 以使用 parallel 关键字,如下所示列表。parallel 部分包含一个要并行运行的嵌套测试阶段列表。此外,你可以通过添加 failFast true 指令来强制所有并行阶段在任何一个失败时全部中止。

列表 8.8 并行运行测试

node('workers'){
    stage('Checkout'){
        checkout scm
    }

    def imageTest= docker.build("${imageName}-test", "-f Dockerfile.test .")
    stage('Pre-integration Tests'){
            parallel(
                'Quality Tests': {
                    imageTest.inside{
                        sh 'golint'
                    }
                },
                'Unit Tests': {
                    imageTest.inside{
                        sh 'go test'
                    }
                },
                'Security Tests': {
                    imageTest.inside('-u root:root'){
                        sh 'nancy Gopkg.lock'
                    }
                }
            )
    }
}

如果你将这些更改推送到远程仓库,将触发一个新的构建(图 8.14)。然而,标准管道视图的一个缺点是你无法轻易地看到并行步骤的进度,因为管道是线性的,就像管道一样。Jenkins 通过提供另一种视图:Blue Ocean 来解决这个问题。

图片

图 8.14 预集成测试的并行执行

图 8.15 显示了相同管道的结果,其中在 Blue Ocean 模式下执行并行测试。

图片

图 8.15 Blue Ocean 中的并行阶段

这看起来很棒,为并行管道阶段提供了很好的可视化。

8.6 通过代码分析提高质量

除了持续集成代码之外,现在的 CI 管道还包括执行持续检查的任务——以持续的方式检查代码的质量。

电影商店应用程序是用 TypeScript 编写的。我们将使用 Dockerfile.test 来构建 Docker 镜像以运行自动化测试,如下所示列表。

列表 8.9 电影商店的 Dockerfile.test

FROM node:14.0.0
WORKDIR /app
COPY package-lock.json .
COPY package.json .
RUN npm i
COPY . .

第一类测试将是源代码的代码检查。正如你在这章前面看到的,代码检查是检查源代码中程序性、语法和风格错误的流程。代码检查使整个服务保持统一格式。代码检查可以通过编写一些规则来实现。有许多代码检查工具可用,包括 JSLint、JSHint 和 ESLint。

当涉及到 TypeScript 代码的代码检查时,ESLint (eslint.org/) 比其他工具具有更高的性能架构。因此,我正在使用 ESLint 对 Node.js 项目进行代码检查,如下所示列表。

列表 8.10 电影存储的 Jenkinsfile

def imageName = 'mlabouardy/movies-store'

node('workers'){
    stage('Checkout'){
        checkout scm
   }

    def imageTest= docker.build("${imageName}-test", "-f Dockerfile.test .")

    stage('Quality Tests'){
        imageTest.inside{
            sh ‘npm run lint'
         }
    }
}

将此内容复制到 movies-store Jenkinsfile,并将更改推送到 develop 分支。应该触发一个新的构建。在质量 测试阶段,我们将看到有关未定义关键字(如图 8.16 所示)的错误,例如describebefore,它们是 Mocha (mochajs.org/) 和 Chai (www.chaijs.com) JavaScript 框架的一部分。这些框架用于高效且方便地描述单元测试(位于 test 文件夹下)。

图 8.16 ESLint 问题检测

ESLint 将返回退出码 1 的错误,这将中断管道。为了修复发现的问题,通过启用 Mocha 环境来扩展 ESLint 规则。我们使用 eslintrc.json 中的key属性来指定我们想要启用的环境,将mocha设置为true

{
    "env": {
        "node": true,
        "commonjs": true,
        "es6": true,
        "mocha": true
    },

}

如果你这次推送更改,静态代码分析的结果将会成功,如图 8.17 所示。

图 8.17 修复 ESLint 错误后的 CI 管道执行

8.7 运行模拟数据库测试

当许多开发者专注于单元测试的 100%覆盖率时,你编写的代码不能仅仅在隔离状态下进行测试。集成和端到端测试通过一起测试应用程序的各个部分,为你提供了额外的信心。这些部分可能各自运行良好,但在一个大型系统中,代码单元很少单独工作。

通常,对于集成或端到端测试,你的脚本将需要连接到用于测试目的的真实、专用数据库。这涉及到编写在每次测试用例/套件开始和结束时运行的代码,以确保数据库处于干净、可预测的状态。

使用真实数据库进行测试确实存在一些挑战:数据库操作可能相对较慢,测试环境可能很复杂,运营开销可能会增加。Java 项目广泛使用 DbUnit 与内存数据库(例如,H2,www.h2database.com/html/main.html)进行此目的。从另一个平台重用良好的解决方案并将其应用于 Node.js 世界可能是这里的方法。

Mongo-unit (www.npmjs.com/package/mongo-unit) 是一个 Node.js 包,可以通过使用 Node 包管理器(npm)或 Yarn 进行安装。它可以在内存中运行 MongoDB。通过与 Mocha 框架的良好集成并提供一个简单的 API 来管理数据库状态,它使得集成测试变得容易。

注意:在第九章和第十章中,我们将在 Jenkins 管道中运行侧车容器,例如 MongoDB 数据库,以运行端到端测试。

下面的列表是一个简单的测试 (/chapter7/microservices/movies-store/test/dao.spec.js),使用 Mocha 和 Chai 编写,并使用 mongo-unit 包通过运行内存数据库来模拟 MongoDB。

列表 8.11 Mocha 和 Chai 单元测试

const Expect = require('chai').expect
const MongoUnit = require('mongo-unit')
const DAO = require('../dao')
const TestData = require('./movies.json')

describe('StoreDAO', () => {
  before(() =>  MongoUnit.start().then(() => {
    process.env.MONGO_URI = MongoUnit.getUrl()
    DAO.init(.
  }))
  beforeEach(() => MongoUnit.load(TestData))
  afterEach(() => MongoUnit.drop())
  after(() => {
    DAO.close()
    return MongoUnit.stop()
  })
 it('should find all movies', () => {
   return DAO.Movie.find()
    .then(movies => {
      Expect(movies.length).to.equal(8)
      Expect(movies[0].title).to.equal('Pulp Fiction (1994)')
    })
 })
})

接下来,我们更新 Jenkinsfile 以添加一个新的阶段,该阶段执行 npm run test 命令:

stage('Integration Tests'){
        sh "docker run --rm ${imageName}-test npm run test"
}

npm run test 命令是一个别名;它运行 Mocha 命令行对测试文件夹中的测试用例进行操作(图 8.18)。该命令在 package.json 中定义,如下所示。

列表 8.12 电影商店的 package.json

"scripts": {
    "start": "node index.js",
    "test": "mocha ./test/*.spec.js",
    "lint": "eslint .",
    "coverage-text": "nyc --reporter=text mocha",
    "coverage-html": "nyc --reporter=html mocha"
}

图 8.18 使用 Mocha 框架进行单元测试

注意 如果您的测试依赖于其他服务,可以使用 Docker Compose 来简化所有依赖服务的启动和连接。

8.8 生成 HTML 覆盖率报告

我们创建一个新的阶段来运行覆盖率工具,并使用文本输出格式:

stage('Coverage Reports'){
        sh "docker run --rm ${imageName}-test npm run coverage-text"
}

这将输出文本报告到控制台输出,如图 8.19 所示。

注意 Istanbul 是一个 JavaScript 代码覆盖率工具。更多信息,请参阅官方指南 istanbul.js.org

图 8.19 Istanbul 文本格式覆盖率报告

您在覆盖率报告中可能看到的指标可以定义为表 8.1 所示。

表 8.1 覆盖率报告指标

指标 描述
语句 程序中实际调用的语句数量,占总语句数量的比例
分支 执行的控制结构分支数量
函数 调用的函数数量,占总定义函数数量的比例
行数 被测试的源代码行数,占总代码行数的比例

默认情况下,Istanbul 使用文本报告器,但还有各种其他报告器可用。您可以在 mng.bz/DKoE 查看完整列表。

要生成 HTML 格式,我们将一个卷映射到 /app/coverage,这是 Istanbul 生成报告的文件夹。然后,我们将使用 Jenkins HTML 发布插件来显示生成的代码覆盖率报告,如下所示。

列表 8.13 发布代码覆盖率 HTML 报告

stage('Coverage Reports'){
        sh "docker run --r.
-v $PWD/coverage:/app/coverage ${imageName}-tes.
npm run coverage-html"
        publishHTML (target: [
          allowMissing: false,
          alwaysLinkToLastBuild: false,
          keepAll: true,
          reportDir: "$PWD/coverage",
          reportFiles: "index.html",
          reportName: "Coverage Report"
        ])
}

publishHTML 命令将 target 块作为主要参数。在此参数内,我们有几个子参数。allowMissing 参数设置为 false,因此如果在生成覆盖率报告时出现问题且报告缺失,publishHTML 指令将引发错误。

在 CI 管道结束时,将生成一个 HTML 文件,并由 HTML 发布插件使用,如图 8.20 所示。

图 8.20 使用 Istanbul 生成 HTML 报告

通过点击左侧面板的覆盖率报告项,可以从 Jenkins 访问 HTML 报告;见图 8.21。

图 8.21 覆盖率报告可以从 Jenkins 面板访问。

注意:Cobertura 插件([plugins.jenkins.io/cobertura/](https://plugins.jenkins.io/cobertura/))也可以用来发布 HTML 报告。这两个插件显示相同的结果。

我们可以深入挖掘以识别未覆盖的行和函数,如图 8.22 所示。

图片

图 8.22 深入覆盖率报告

注意:根据你使用的语言,存在多种创建覆盖率报告的工具(例如,SimpleCov 用于 Ruby,Coverage.py 用于 Python,JaCoCo 用于 Java)。

你可以将这一过程进一步扩展,并行运行阶段以减少测试运行的等待时间,如下面的列表所示。

列表 8.14 并行运行预集成测试

stage('Tests'){
        parallel(
            'Quality Tests': {
                sh "docker run --rm ${imageName}-test npm run lint"
            },
            'Integration Tests': {
                sh "docker run --rm ${imageName}-test npm run test"
            },
            'Coverage Reports': {
                sh "docker run --r.
-v $PWD/coverage:/app/coverage ${imageName}-tes.
npm run coverage-html"
                publishHTML (target: [
                    allowMissing: false,
                    alwaysLinkToLastBuild: false,
                    keepAll: true,
                    reportDir: "$PWD/coverage",
                    reportFiles: "index.html",
                    reportName: "Coverage Report"
                ])
            }
        )
}

图 8.23 显示了在 Blue Ocean 视图中运行此作业的最终结果。

图片

图 8.23 并行运行测试

8.9 使用无头 Chrome 自动化 UI 测试

对于 Angular 应用程序,我们将创建一个 Dockerfile.test 文件,该文件安装 Angular CLI(angular.io/cli)和运行自动化测试所需的依赖项;请参见以下列表。

列表 8.15 电影市场 Dockerfile.test 文件

FROM node:14.0.0
ENV CHROME_BIN=chromium
WORKDIR /app
COPY package-lock.json .
COPY package.json .
RUN npm i && npm i -g @angular/cli
COPY . .

代码检查状态与上一部分类似;我们将使用默认安装的 TSLint 代码检查器。因此,我们将运行 package.json 中定义的npm run lint别名命令,如下面的列表所示。

列表 8.16 电影市场 package.json

"scripts": {
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test --browsers=ChromeHeadlessCI --code-coverage=true",
    "lint": "ng lint",
    "e2e": "ng e2e"
  }

我们使用以下内容更新 Jenkinsfile。

列表 8.17 电影市场 Jenkinsfile

def imageName = 'mlabouardy/movies-marketplace'
node('workers'){
    stage('Checkout'){
        checkout scm
    }

    def imageTest= docker.build("${imageName}-test", "-f Dockerfile.test .")
    stage('Pre-integration Tests'){
        parallel(
            'Quality Tests': {
                sh "docker run --rm ${imageName}-test npm run lint"
            }
        )
    }
}

让我们保存这个配置并运行构建。由于 TSLint 上的强制规则,管道应该失败并变成红色,如图 8.24 所示。

图片

图 8.24 CI 管道失败。

如果你点击质量测试阶段的日志,日志应该显示有关缺少分号和尾随空白的错误,如图 8.25 所示。

图片

图 8.25 Angular 代码检查输出日志。

如果你希望让 TSLint 在你的代码中通过(图 8.26),你需要更新 tslint.json 以禁用强制规则或在每个文件的开始处添加/* tslint:disable */指令,以便 TSLint 跳过这些文件的代码检查过程。

图片

图 8.26 Angular 代码检查输出日志。

对于 Angular 单元测试,我们将使用 Jasmine (jasmine.github.io/) 和 Karma (karma-runner.github.io/latest/index.html) 框架。这两个测试框架都支持 BDD 实践,它以人类可读的格式描述测试,便于非技术人员理解。以下列表中的示例单元测试(chapter7/microservices/movies-marketplace/src/app/app.component.spec.ts)是自我解释的。它测试了应用组件是否有一个值为 Watchlisttext 属性,该属性在 span 元素标签内的 HTML 中渲染。

列表 8.18 电影市场 Karma 测试

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));
  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });
  it('should render title', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('.toolbar span').textContent).toContain('Watchlist');
  });
});

注意:当使用 Angular CLI 创建 Angular 项目时,默认使用 Jasmine 和 Karma 创建和运行单元测试。

运行前端 Web 应用的单元测试需要它们在 Web 浏览器中进行测试。虽然在工作站或主机机器上这不是问题,但在受限环境中(如 Docker 容器)运行时可能会变得繁琐。实际上,这些执行环境通常是轻量级的,并且不包含任何图形环境。

幸运的是,Karma 测试可以使用无界面浏览器运行,主要有两种选项:Chrome Headless 或 PhantomJS。以下列表中的示例使用 Chrome Headless 和 Puppeteer,这可以在 Karma 配置中的简单标志上进行配置(chapter7/microservices/movies-marketplace/karma.conf.js)。

列表 8.19 Karma 运行器配置

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    customLaunchers: {
      ChromeHeadlessCI: {
        base: 'Chrome',
        flags: [
          '--headless',
          '--disable-gpu',
          '--no-sandbox',
          '--remote-debugging-port=9222'
        ]
      }
    },
    browsers: ['ChromeHeadless', 'Chrome'],
    singleRun: true,  });
};

Headless Chrome 需要 sudo 权限才能运行,除非使用 --no-sandbox 标志。接下来,我们需要更新 Dockerfile.test 以安装 Chromium:

RUN apt-get update && apt-get install -y chromium

注意:Chromium/Google Chrome 自 59 版本起已包含无头模式。

然后,我们更新 Jenkinsfile 以使用 npm run test 命令运行单元测试。该命令将启动 Headless Chrome 并执行 Karma.js 测试。接下来,我们将生成一个 HTML 格式的覆盖率报告,该报告将由 HTML 发布器插件使用,如下所示。

列表 8.20 将工作区文件夹映射到 Docker 容器卷。

stage('Pre-integration Tests'){
       parallel(
            'Quality Tests': {
                sh "docker run --rm ${imageName}-test npm run lint"
            },
            'Unit Tests': {
                sh "docker run --r.
-v $PWD/coverage:/app/coverage ${imageName}-tes.
npm run test"
                publishHTML (target: [
                    allowMissing: false,
                    alwaysLinkToLastBuild: false,
                    keepAll: true,
                   reportDir: "$PWD/coverage",
                    reportFiles: "index.html",
                    reportName: "Coverage Report"
                ])}
        )
}

一旦将更改推送到 GitHub 仓库,将触发新的构建,并执行单元测试,如图 8.27 所示。

图 8.27 在 Docker 容器内运行无头 Chrome

Karma 启动器将在 Headless Chrome 浏览器上运行测试,并显示代码覆盖率统计信息,如图 8.28 所示。

图 8.28 Karma 单元测试成功执行

此外,生成的 HTML 报告将在 Blue Ocean 视图中的“工件”部分可用,如图 8.29 所示。

图 8.29 覆盖率报告与其他工件并列

如果您点击覆盖率报告链接,它应显示 Angular 组件和服务通过语句和函数的覆盖率,如图 8.30 所示。

图 8.30 按文件名统计的覆盖率统计

完成此操作后,现在可以在 Docker 容器内使用 Chromium 运行单元测试。

8.10 将 SonarQube Scanner 与 Jenkins 集成

虽然代码检查器可以给你一个关于代码质量的总体概述,但如果你想要执行深入的静态代码分析和检查以检测潜在的错误和漏洞,它们仍然有限。这就是 SonarQube 发挥作用的地方。它将通过集成外部库如 PMD、Checkstyle 和 FindBugs,为你提供一个代码库质量的 360 度视角。每次代码提交时,都会执行代码分析。

注意:SonarQube 可以用于检查超过 20 种编程语言的代码,包括 Java、PHP、Go 和 Python。

要部署 SonarQube,我们将使用 Packer 烘焙一个新的 AMI。类似于前面的章节,我们创建一个 template.json 文件,其内容如下(chapter8/sonarqube/packer/template.json)。

列表 8.21 Jenkins 工作节点的 Packer 模板。

{
    "variables" : {...},
   "builders" : [
        {
            "type" : "amazon-ebs",
            "profile" : "{{user `aws_profile`}}",
            "region" : "{{user `region`}}",
            "instance_type" : "{{user `instance_type`}}",
            "source_ami" : "{{user `source_ami`}}",
            "ssh_username" : "ubuntu",
            "ami_name" : "sonarqube-8.2.0.32929",
            "ami_description" : "SonarQube community edition"
        }
    ],
    "provisioners" : [
        {
            "type" : "file",
            "source" : "sonar.init.d",
            "destination" : "/tmp/"
        },
        {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

暂时性 EC2 实例将基于 Amazon Linux AMI,并使用 shell 脚本来配置实例以安装 SonarQube 和配置所需的依赖项。

setup.sh 脚本将从官方发布页面安装 SonarQube。在此示例中,将安装 SonarQube 8.2.0。SonarQube 支持 PostgreSQL、MySQL、Microsoft SQL Server (MSSQL) 和 Oracle 作为后端。我选择使用 PostgreSQL 来存储配置和报告结果。然后,脚本创建一个名为 sonar 的目录,设置权限,并配置 SonarQube 以自动启动;请参见以下列表。

列表 8.22 安装 SonarQube LT。

wget https://binaries.sonarsource.com/
Distribution/sonarqube/$SONAR_VERSION.zip -P /tmp
unzip /tmp/$SONAR_VERSION.zip
mv $SONAR_VERSION sonarqube
mv sonarqube /opt/

apt-get install -y unzip curl
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/
 `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list'
wget -q https://www.postgresql.org/media/keys/ACCC4CF8.as.
-O - | sudo apt-key add -
apt-get install -y postgresql postgresql-contrib
systemctl start postgresql
systemctl enable postgresql
cat > /tmp/db.sql <<EOF
CREATE USER $SONAR_DB_USER WITH ENCRYPTED PASSWORD '$SONAR_DB_PASS';
CREATE DATABASE $SONAR_DB_NAME OWNER $SONAR_DB_USER;
EOF
sudo -u postgres psql postgres < /tmp/db.sql

mv /tmp/sonar.properties /opt/sonarqube/conf/sonar.properties
sed -i 's/#RUN_AS_USER=/RUN_AS_USER=sonar/' sonar.sh
sysctl -w vm.max_map_count=262144
groupadd sonar
useradd -c "Sonar System User" -d /opt/sonarqube -g sonar -s /bin/bash sonar
chown -R sonar:sonar /opt/sonarqube
ln -sf /opt/sonarqube/bin/linux-x86-64/sonar.sh /usr/bin/sonar
cp /tmp/sonar.init.d /etc/init.d/sonar
chmod 755 /etc/init.d/sonar
update-rc.d sonar defaults
service sonar start

注意:完整的 shell 脚本可在 GitHub 仓库中找到,附带逐步指南。同时,请确保您至少有 4 GB 的内存来运行 SonarQube 的 64 位版本。

一旦定义了所需的 Packer 变量,执行 packer build 命令以启动配置过程。一旦 AMI 被烘焙,它应该可以在图 8.31 所示的 EC2 仪表板中的镜像部分找到。

图片

图 8.31 SonarQube 机器镜像

从那里,使用 Terraform 部署基于 SonarQube AMI 的私有 EC2 实例,如下所示。

列表 8.23 使用 Terraform 的 SonarQube EC2 实例资源

resource "aws_instance" "sonarqube" {
  ami                    = data.aws_ami.sonarqube.id
  instance_type          = var.sonarqube_instance_type
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.sonarqube_sg.id]
  subnet_id              = element(var.private_subnets, 0)

  root_block_device {
    volume_type           = "gp2"
    volume_size           = 30
    delete_on_termination = false
  }

  tags = {
    Name   = "sonarqube"
    Author = var.author
  }
}

然后,定义一个公共负载均衡器,将传入的 HTTP 和 HTTPS(可选)流量转发到端口 9000(SonarQube 仪表板暴露的端口)上的实例。同时,在 Route 53 中创建一个指向负载均衡器 FQDN 的 A 记录。

执行 terraform apply 命令以配置实例和其他资源。实例应在几秒钟内部署完成,如图 8.32 所示。

图片

图 8.32 SonarQube 私有 EC2 实例

在终端上,你应该在输出部分看到公共负载均衡器的 URL,如图 8.33 所示。

图片

图 8.33 SonarQube DNS URL

转到 URL 并使用默认凭证(图 8.34)登录。目前,SonarQube 中没有配置用户帐户。然而,默认情况下,存在一个名为admin的管理员帐户,密码为admin

图片

图 8.34 SonarQube 网络仪表板

接下来,确保从 SonarQube 插件部分启用 TypeScript 分析器,如图 8.35 所示。

图片

图 8.35 SonarQube TypeScript 分析插件

然后,为 Jenkins 生成一个新的令牌,以避免出于安全目的使用 SonarQube 管理员凭证。转到管理界面并导航到安全。在同一个页面下的令牌部分有一个生成令牌的选项;点击图 8.36 所示的生成按钮。

图片

图 8.36 SonarQube Jenkins 专用令牌

服务器身份验证令牌应从 Jenkins 创建为Secret text凭证,如图 8.37 所示。

图片

图 8.37 SonarQube 秘密文本凭证

要从 CI 管道触发扫描,我们需要安装 SonarQube Scanner。您可以选择自动安装或为 Jenkins 工作节点提供此工具的安装路径。可以通过选择管理 Jenkins > 全局工具配置来安装它。或者,您可以使用以下列表中的命令创建一个新的 Jenkins 工作节点镜像,其中包含 SonarQube Scanner。

列表 8.24 SonarQube Scanner 安装

wget https://binaries.sonarsource.com/
Distribution/sonar-scanner-cli/sonar-scanner-cli-2.0.1873-linux.zip -P /tmp
unzip /tmp/sonar-scanner-cli-4.2.0.1873-linux.zip
mv sonar-scanner-4.2.0.1873-linux sonar-scanner
ln -sf /home/ec2-user/sonar-scanner/bin/sonar-scanner /usr/bin/sonar-scanner

注意:Jenkins 工作节点的启动配置是不可变的。您需要克隆启动配置,使用新构建的 AMI 更新它,并将其附加到 Jenkins 工作节点的自动扩展组,以创建带有 Sonar Scanner 工具的新工作节点。

最后,从 Manage Jenkins 中的配置菜单使 Jenkins 了解 SonarQube 服务器的安装情况,如图 8.38 所示。

图片

图 8.38 SonarQube 服务器设置

然后,在 movies-marketplace 根目录下创建一个 sonar-project.properties 文件,以便将覆盖率报告发布到 SonarQube 服务器。此文件包含某些 sonar 属性,例如要扫描和排除的文件夹以及项目的名称;请参阅以下列表。

列表 8.25 SonarQube 项目配置

sonar.projectKey=angular:movies-marketplace
sonar.projectName=movies-marketplace
sonar.projectVersion=1.0.0
sonar.sourceEncoding=UTF-8
sonar.sources=src
sonar.exclusions=**/node_modules/**,**/*.spec.ts
sonar.tests=src/app
sonar.test.inclusions=**/*.spec.ts
sonar.ts.tslint.configPath=tslint.json
sonar.javascript.lcov.reportPaths=/home/ec2-user/coverage/marketplace/lcov.info

接下来,更新 Jenkinsfile 以创建一个新的静态 代码 分析阶段。

然后,使用withSonarQubeEnv块注入 SonarQube 全局配置(秘密令牌和 SonarQube 服务器 URL 值),并调用sonar-scanner命令以启动分析过程,如以下列表所示。

列表 8.26 触发 SonarQube 分析

stage('Static Code Analysis'){
        withSonarQubeEnv('sonarqube') {
            sh 'sonar-scanner'
        }
}

您可以使用 -D 标志来覆盖属性值:

sh 'sonar-scanner -Dsonar.projectVersion=$BUILD_NUMBER'

此选项允许我们将 Jenkins 构建号附加到我们执行的每个分析和发布的 SonarQube。

在构建成功后,日志将显示 SonarQube 已扫描的文件和文件夹。扫描后,分析报告将发布到我们已集成的 SonarQube 服务器。此分析基于 SonarQube 定义的规则。如果代码通过错误阈值,则允许其生命周期中的下一步。但如果它超过错误阈值,则会被丢弃:

图片

您可以通过创建质量配置文件来定义自己的自定义阈值,这些配置文件是一组规则,如果代码库中提出问题,则会导致管道失败。

注意:请参考此官方文档以获取创建带有质量配置文件的 SonarQube 自定义规则的逐步指南:mng.bz/l9vy

最后,在访问 SonarQube 服务器时,项目详情应显示所有从代码覆盖率报告中捕获的指标,如图 8.39 所示。

图片

图 8.39 SonarQube 项目指标

现在,您可以进入 movies-marketplace 项目并发现问题、错误、代码异味、覆盖率或重复。仪表板(图 8.40)让您一眼就能看到质量状况。

图片

图 8.40 SonarQube 项目深度指标和问题

此外,当作业完成时,SonarQube Scanner 插件将检测到在构建过程中进行了 SonarQube 分析。然后,插件将在 Jenkins 作业页面上显示一个徽章和一个小部件,其中包含到 SonarQube 仪表板的链接以及质量门状态,如图 8.41 所示。

图片

图 8.41 SonarQube 与 Jenkins 集成

SonarQube 分析速度快,但对于较大的项目,分析可能需要几分钟才能完成。

要等待分析完成,我们将使用withForQualityGate步骤暂停管道,该步骤等待 SonarQube 分析完成。为了通知 CI 管道关于分析完成的情况,我们需要在 SonarQube 上创建一个 webhook,以便在项目分析完成后通知 Jenkins,如图 8.42 所示。

图片

图 8.42 SonarQube webhook 创建

接下来,在下面的列表中,我们更新 Jenkinsfile 以集成waitForQualityGate步骤,该步骤暂停管道直到 SonarQube 分析完成并返回质量门状态。

列表 8.27 在 Jenkinsfile 中添加质量门

stage('Static Code Analysis'){
        withSonarQubeEnv('sonarqube') {
            sh 'sonar-scanner'
        }
}
stage("Quality Gate"){
        timeout(time: 5, unit: 'MINUTES') {
            def qg = waitForQualityGate()
            if (qg.status != 'OK') {
                error "Pipelin.
aborted due to quality gate failure: ${qg.status}"
            }
        }
}

注意:质量门可以移出node{}块之外,以避免占用等待 SonarQube 通知的 Jenkins 工作节点。

提交更改并将它们推送到远程仓库。将触发新的构建,并自动启动 SonarQube 分析。一旦分析完成,就会向 CI 管道发送通知以恢复管道阶段,如图 8.43 所示。

注意:我们可以在 Jenkins 中设置 Post-build 操作来通知用户关于测试结果。

图 8.43 SonarQube 项目分析状态

因此,一旦开发者在 GitHub 上提交代码,Jenkins 就会从 GitHub 仓库中获取/拉取代码,在 Sonar Scanner 的帮助下执行静态代码分析,并将分析报告发送到 SonarQube 服务器。

在本章中,你学习了如何运行各种自动化测试,以及如何集成外部工具如 Nancy 和 SonarQube 来检查代码质量、检测错误,并在 Jenkins CI 管道中持续构建微服务的同时避免潜在的安全漏洞。在下一章中,我们将在测试成功运行后构建 Docker 镜像,并将镜像推送到私有远程仓库。

摘要

  • 使用 Docker 容器来运行测试,以避免为每个我们要集成的服务安装多个运行时环境,并保持所有 Jenkins 工作节点之间的一致执行环境。

  • 将传统安全实践(如外部依赖项扫描)推广到 CI/CD 工作流程中,可以增加一个安全层,以避免安全漏洞和漏洞。

  • 无头 Chrome 是一种在无头环境中运行 UI 测试的方法,而不需要完整的浏览器 UI。

  • 并行 DSL 步骤提供了轻松并行运行管道阶段的能力。

  • SonarQube 是一个代码质量管理工具,允许团队管理、跟踪和改进其源代码的质量。

9 在 CI 管道中构建 Docker 镜像

本章涵盖

  • 在 Jenkins 管道中构建 Docker 镜像和编写 Dockerfile 的最佳实践

  • 在 Jenkins 声明式管道中使用 Docker 代理作为执行环境

  • 将 Jenkins 构建状态集成到 GitHub 拉取请求中

  • 部署和配置托管和管理的 Docker 私有仓库解决方案

  • 开发周期内 Docker 镜像的生命周期和打标签策略

  • 在 Jenkins 管道中扫描 Docker 镜像中的安全漏洞

在上一章中,你学习了如何在 CI 管道中运行 Docker 容器内的自动化测试。在本章中,我们将通过构建 Docker 镜像并将其存储在私有远程仓库中以实现版本控制来完成 CI 工作流程;见图 9.1。

图 9.1 本章将实现构建和推送阶段。

到本章结束时,你应该能够使用以下步骤构建类似的 CI 管道:

  1. 从远程仓库检出源代码。CI 服务器在推送事件发生时从版本控制系统(VCS)中获取代码。

  2. 在 Docker 容器内运行预集成测试,如单元测试、安全测试、质量测试和 UI 测试。这些可能包括生成覆盖率报告和集成质量检查工具,如 SonarQube 进行静态代码分析。

  3. 编译源代码并构建 Docker 镜像(自动化打包)。

  4. 标记最终镜像并将其存储在私有仓库中。

图 9.2 总结了 CI 工作流程的最终结果。

图 9.2 CI 管道过程

本 CI 管道的目的在于自动化持续构建、测试并将 Docker 镜像上传到私有仓库的过程。在每一个阶段都会进行失败/成功的报告。

注意:本章和前几章中讨论的 CI 设计可以根据任何类型项目的需求进行修改;用户只需要确定可以使用 Jenkins 的正确工具和配置即可。

9.1 构建 Docker 镜像

目前,每次向远程仓库的推送事件都会触发 Jenkins 上的管道。管道将根据 Jenkinsfile 中定义的阶段执行。首先启动的阶段将是从远程仓库克隆代码、运行自动化测试和发布覆盖率报告。图 9.3 显示了 movies-loader 服务的当前 CI 工作流程。

图 9.3 当前 CI 工作流程

如果测试成功,下一个阶段将是构建工件;在我们的例子中,它将是一个 Docker 镜像。

注意:当你为你的应用程序构建 Docker 镜像时,你是在一个现有镜像的基础上构建的。一个损坏的基础镜像可能导致生产中断(例如安全漏洞)。我建议使用最新且维护良好的镜像。

9.1.1 使用 Docker DSL

要构建主应用程序 Docker 镜像,我们需要定义一个包含一系列指令的 Dockerfile,这些指令指定了要使用的环境和要运行的命令。在 movies-loader 项目的顶级目录中创建一个 Dockerfile,使用以下代码。

列表 9.1 Movie loader 的 Dockerfile

FROM python:3.7.3
LABEL MAINTAINER mlabouardy
WORKDIR /ap.
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY movies.json main.py ./
CMD python main.py

基于 Python 的应用程序将使用 Python v3.7.3 作为基础镜像,使用 pip 管理器安装运行时依赖项,并将 python main.py 设置为 Docker 镜像的主命令。

注意:为了保持镜像构建的一致性,创建一个包含所有使用依赖项的传递性固定版本的 requirements.txt 文件。

Dockerfile 中指令的顺序很重要。每当源代码发生任何更改时,都会重新构建 Docker 镜像。这就是为什么我把 pip install 命令放在列表 9.1 中的原因,因为依赖项不经常更改。因此,Docker 将依赖于层缓存,这将加快镜像的构建时间。有关 Docker 构建缓存的更多信息,请参阅官方 Docker 文档:mng.bz/B10J

最后,我们在 Jenkinsfile 中添加了一个 Build 阶段,它使用 Docker DSL 基于存储库中的 Dockerfile 构建镜像:

stage('Build'){
        docker.build(imageName)
}

默认情况下,build() 方法会在当前目录中构建 Dockerfile。您可以通过提供 build() 方法的第二个参数作为 Dockerfile 路径来覆盖此行为。

使用以下命令将更改推送到 develop 分支:

git add Jenkinsfile Dockerfile
git commit -m "building docker image"
git push origin develop

然后,应该触发一个新的构建,并构建镜像,如图 9.4 所示。

图 9.4 Python Docker 镜像构建日志

图 9.5 Movie loader CI 管道

到目前为止,我们已经为 movies-loader CI 管道在图 9.5 中定义了 CI 阶段。movies-parser 服务的 Dockerfile 将会有所不同,因为它是用 Go 编写的。由于 Go 是一种编译型语言,我们不需要在服务的运行时使用它。因此,我们将使用 Docker 的多阶段构建功能来减小 Docker 镜像的大小,如下面的列表所示。

列表 9.2 多阶段构建使用

FROM golang:1.16.5
WORKDIR /go/src/github.com/mlabouardy/movies-parser
COPY main.go go.mod .
RUN go get -v
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go

FROM alpine:latest
LABEL Maintainer mlabouardy
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/mlabouardy/movies-parser/app .
CMD ["./app"]

Dockerfile 被分为两个阶段。第一个阶段使用 go build 命令构建二进制文件。第二个阶段使用 Alpine 作为基础镜像,这是一个轻量级镜像,然后从第一个阶段复制二进制文件。

Go 构建工具和编译发生的中间层大约有 300 MB。最终的镜像具有最小的 8 MB 脚印。最终结果是和之前一样的微小生产镜像,但复杂性显著降低。Go SDK 和任何中间工件都被留下,并且没有保存在最终的镜像中。

注意:多阶段构建功能需要在守护程序和客户端上使用 Docker engine 17.05 或更高版本。

在之前的 Dockerfile 中,阶段没有被命名,而是通过它们的整数编号(从 0 开始,对应第一个FROM指令)来引用。然而,我们可以通过将AS NAME传递给FROM指令来命名阶段,如以下列表所示。

列表 9.3 命名 Docker 多阶段

FROM golang:1.16.5 AS builder
WORKDIR /go/src/github.com/mlabouardy/parser
...
FROM alpine:latest
...
COPY --from=builder /go/src/github.com/mlabouardy/movies-parser/app .

Build阶段添加到项目的 Jenkinsfile 中,并将更改推送到 develop 分支。管道将被触发,构建的结果应该类似于图 9.6 中所示。

图片

图 9.6 电影解析 CI 管道

注意:您同样可以基于 scratch 或 distroless 镜像构建最终镜像,但我更喜欢使用 Alpine 的便利性。此外,它是一个减少镜像大小的安全选择。

movies-store Docker 镜像将使用 DockerHub 中的 Node.js 基础镜像;我们使用的是写作时的最新 LTS 节点版本。我更喜欢指定一个特定版本,而不是像node:ltsnode:latest这样的浮动标签,这样如果有人在其他机器上构建此镜像,他们将获得相同的版本,而不是冒着意外升级和随之而来的困惑的风险。

注意:在大多数情况下,从 DockerHub(hub.docker.com/)提供的官方镜像中选择的基镜像是最好的选择。它们通常比社区创建的镜像控制得更好。

然后,我们通过传递--only=prod安装运行时所需的依赖项。最后,我们将npm start命令设置为在容器创建时启动 express 服务器,如以下列表所示。

列表 9.4 电影存储 Dockerfile

FROM node:14.17.0
WORKDIR /app
COPY package-lock.json package.json .
RUN npm i --only=prod
COPY index.js dao.js ./
EXPOSE 3000
CMD npm start

注意,我们不是复制整个工作目录,而是只复制 package.json 和 package-lock.json 文件。这使我们能够利用缓存的 Docker 层。package-lock.json 文件记录了所有依赖项的版本,以确保 Docker 构建中的npm install命令的一致性。

一旦管道更改被版本化并且执行完成,到目前为止的 CI 管道对于 movies-store 应该看起来与图 9.7 中的 Blue Ocean 视图相似。

图片

图 9.7 电影存储 CI 管道

注意:在镜像构建过程中,Docker 会取上下文目录中的所有文件。为了提高 Docker 构建性能,可以通过在上下文目录中添加.dockerignore文件来排除文件和目录。

9.1.2 Docker 构建参数

最后,对于 Angular 应用程序(即 movies-marketplace),我们再次使用多阶段构建功能,通过ng build命令构建静态文件夹。然后我们将文件夹复制到 NGINX 镜像中,通过 Web 服务器提供服务;请参阅以下列表。

列表 9.5 电影市场 Dockerfile

FROM node:14.17.0 as builder
ARG ENVIRONMENT
ENV CHROME_BIN=chromium
WORKDIR /app
RUN apt-get update && apt-get install -y chromium
COPY package-lock.json package.json .
RUN npm i && npm i -g @angular/cli
COPY . .
RUN ng build -c $ENVIRONMENT

FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

注意:ENV指令在构建和运行时都可用。ARG指令(列表 9.5)仅在构建时可用。

由于我们可能基于不同的运行环境拥有多个 Angular 配置(具有不同的设置),我们将在构建时注入一个构建参数来指定目标环境,如下所示:

stage('Build'){
        docker.build(imageName, '--build-arg ENVIRONMENT=sandbox .')
}

当向build()方法传递参数时,最后一个值应以要使用的文件夹作为构建上下文结束。

最后,请确保在项目的根目录中创建一个.dockerignore 文件,以防止本地模块、调试日志和临时文件被复制到 Docker 镜像中。为了排除这些目录,我们创建了一个包含以下内容的.dockerignore 文件:

nodes_modules
coverage
dist
tmp

在推送更改后,管道应类似于图 9.8 中的 Blue Ocean 视图。

图片

图 9.8 电影市场 CI 管道

现在项目 Docker 镜像已构建,我们需要将它们存储在某处。因此,我们将在项目开发周期中构建的所有镜像上部署一个私有注册库。

9.2 部署 Docker 私有注册库

持续集成导致频繁的构建和打包。因此,我们需要一个机制来存储所有这些二进制代码(构建、打包、第三方插件等)在一个类似于版本控制系统的系统中。由于 VCSs 如 Git 和 SVN 存储代码而不是二进制文件,我们需要一个二进制存储库工具。

存在许多解决方案,例如 Nexus 或 Artifactory。然而,它们带来了挑战,包括管理和加固实例。幸运的是,也存在托管解决方案,这取决于您使用的云提供商,例如 Amazon Elastic Container Registry (ECR)、Google Container Registry 和 Azure Container Registry。

注意您也可以在 DockerHub 上托管您的 Docker 镜像。如果您选择这种方法,您可以跳过这部分。

9.2.1 Nexus Repository OSS

Nexus Repository OSS (www.sonatype.com/products/repository-oss)是一个广泛使用的开源、免费工件存储库,可以用于存储二进制和构建工件。它可以用于分发 Maven/Java、npm、Helm、Docker 等。

注意由于您已经熟悉 Docker,您可以使用 Sonatype 的 Docker 镜像在 Docker 容器中运行 Nexus Repository OSS。

要部署 Nexus Repository OSS,我们需要使用 Packer 制作一个新的机器镜像。以下列表提供了 template.json 的内容(完整的模板可在第九章/nexus/packer/template.json 中找到)。

列表 9.6 Nexus Repository OSS Packer 模板

{
    "variables" : {...},
    "builders" : [
        {
            "type" : "amazon-ebs",
            "ami_name" : "nexus-3.22.1-02",
            "ami_description" : "Nexus Repository OSS"
        }
    ],
    "provisioners" : [
        {
            "type" : "file",
            "source" : "./nexus.rc",
            "destination" : "/tmp/nexus.rc"
        },
        {
            "type" : "file",
            "source" : "./repository.json",
            "destination" : "/tmp/repository.json"
        },
        {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

这将基于 Amazon Linux 镜像创建一个临时实例,并使用一个 shell 脚本(列表 9.7)安装从官方仓库的 Nexus OSS 版本,并将其配置为使用 init.d 运行服务,以便在实例重启后重新启动。此示例使用版本 3.30.1-01。完整的脚本可在第九章/nexus/packer/setup.sh 中找到。

列表 9.7 安装 Nexus Repository OSS 版本(setup.sh)

NEXUS_USERNAME="admin"                                                   ❶
NEXUS_PASSWORD="admin123"                                                ❶
echo "Install Java JDK 8"
yum update -y
yum install -y java-1.8.0-openjdk                                        ❷
echo "Install Nexus OSS"
wget https://download.sonatype.com/nexus/3/latest-unix.tar.gz -P /tmp    ❸
tar -xvf /tmp/latest-unix.tar.gz                                         ❸
mv nexus-* /opt/nexus                                                    ❸
mv sonatype-work /opt/sonatype-work                                      ❸
useradd nexu.
chown -R nexus:nexus /opt/nexus/ /opt/sonatype-work/
ln -s /opt/nexus/bin/nexus /etc/init.d/nexus
chkconfig --add nexus
chkconfig --levels 345 nexus on
mv /tmp/nexus.rc /opt/nexus/bin/nexus.rc
echo "nexus.scripts.allowCreation=true" >> nexus-default.propertie.
systemctl enable nexus
Systemctl start nexus

❶ 定义 Nexus OSS 默认凭据(admin/admin123)

❷ 安装 Java JDK 1.8.0,这是运行 Nexus OSS 所必需的

❸ 从官方仓库下载 Nexus OSS 并将存档提取到目标位置

然后,脚本将使用 service nexus restart 命令启动 Nexus 服务器,并等待其启动并准备好,如下一列表所示。

列表 9.8 等待 Nexus 服务器启动(setup.sh)

until $(curl --output /dev/nul.
--silent --head --fail http://localhost:8081); do
    printf '.'
    sleep 2
done

一旦服务器响应,将向 Nexus 脚本 API 发出 POST 请求以创建 Docker 托管仓库。脚本 API 可以用于自动化 Nexus 仓库管理器的复杂任务,如下所示。

列表 9.9 Nexus OSS 脚本 API(setup.sh)

curl -v -X POST -u $NEXUS_USERNAME:$NEXUS_PASSWORD                                        ❶
--header "Content-Type: application/json" 'http://localhost:8081/service/rest/v1/script'  ❶
-d @/tmp/repository.json                                                                  ❶

❶ 通过在请求中包含默认凭据和在请求负载中包含 Docker 仓库配置,对 Nexus 服务器执行 POST 请求

注意:Nexus REST API 端点和功能的完整列表通过 NEXUS_HOST/swagger-ui 端点进行文档化。

请求负载是一个暴露在端口 5000 的 Docker 主机注册表的 Groovy 脚本:

import org.sonatype.nexus.blobstore.api.BlobStoreManager;
import org.sonatype.nexus.repository.storage.WritePolicy;
repository.createDockerHosted('docker-registry'.
5000, 443,
BlobStoreManager.DEFAULT_BLOBSTORE_NAME, true, true, WritePolicy.ALLOW, true)

执行 packer build 命令来烘焙 AMI。一旦配置完成,Nexus AMI 应该可以在 AWS 管理控制台的“图像”部分中找到,如图 9.9 所示。

图 9.9 Nexus OSS AMI

从那里,使用 Terraform 基于烘焙的 Nexus OSS AMI 配置一个 EC2 实例。创建一个包含以下列表内容的 nexus.tf 文件。

列表 9.10 Nexus EC2 实例资源

resource "aws_instance" "nexus" {
  ami                    = data.aws_ami.nexus.id
  instance_type          = var.nexus_instance_type
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.nexus_sg.id]
  subnet_id              = element(var.private_subnets, 0)

  root_block_device {
    volume_type           = "gp2"
    volume_size           = 50
    delete_on_termination = false
  }

  tags = {
    Author = var.author
   Name = "nexus"
  }
}

注意:在没有问题的前提下运行 Nexus OSS 至少需要 8 GB 的内存。此外,我强烈建议使用一个专门的 EBS 用于 blob 存储 (mng.bz/dr7Q)。

此外,还需要配置一个公共负载均衡器,将传入的 HTTP 和 HTTPS 流量转发到 EC2 实例的 8081 端口,这是 Nexus 仓库管理器(仪表板)暴露的端口。创建一个名为 loadbalancers.tf 的新文件,内容如下列表所示。

列表 9.11 Nexus 仓库管理器公共负载均衡器

resource "aws_elb" "nexus_elb" {
  subnets                   = var.public_subnets
  cross_zone_load_balancing = true
  security_groups           = [aws_security_group.elb_nexus_sg.id]
  instances                 = [aws_instance.nexus.id]

  listener {
    instance_port      = 8081
    instance_protocol  = "http"
    lb_port            = 443
    lb_protocol        = "https"
    ssl_certificate_id = var.ssl_arn
  }

  health_check {
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    target              = "TCP:8081"
    interval            = 5
  }

  tags = {
    Name   = "nexus_elb"
    Author = var.author
  }
}

在同一文件中,添加另一个公共负载均衡器,如下一列表所示。这将访问指向 Nexus 仓库管理器托管仓库端口 5000 的 Docker 私有注册表。

列表 9.12 Docker 注册表公共负载均衡器

resource "aws_elb" "registry_elb" {
  subnets                   = var.public_subnets
  cross_zone_load_balancing = true
  security_groups           = [aws_security_group.elb_registry_sg.id]
  instances                 = [aws_instance.nexus.id]

  listener {
    instance_port      = 5000
    instance_protocol  = "http"
    lb_port            = 443
    lb_protocol        = "https"
    ssl_certificate_id = var.ssl_arn
  }
}

使用 terraform apply 配置 AWS 资源、Nexus 仪表板和 Docker 注册表。在配置过程的“输出”部分应显示 URL,如图 9.10 所示。

图 9.10 Nexus Terraform 资源

将您喜欢的浏览器指向 Nexus URL,如图 9.11 所示的 Web 仪表板应该会显示出来。默认的 admin 密码可以在 /opt/sonatype-work/nexus3/admin.password 中找到。

图 9.11 Nexus 仓库管理器

如果您从齿轮图标跳转到设置,然后是仓库,应该会创建一个新的 Docker 托管仓库。该仓库禁用了标签不可变性,并允许后续使用相同标签的镜像推送覆盖图像标签。如果启用此选项,如果您尝试推送一个在仓库中已存在的标签的镜像,将返回错误。其余的配置应类似于图 9.12。

图片

图 9.12 Nexus 上的 Docker 托管注册库

要能够从注册库拉取和推送 Docker 镜像,我们将从安全部分创建一个自定义 Nexus 角色。如图 9.13 所示,此角色将授予对 Docker 托管注册库的完全访问权限。

图片

图 9.13 Docker 注册库的 Nexus 自定义角色

注意:对于推送和拉取操作,只需要 nx-*-registry-addnx-* -registry-read 权限。

接下来,我们创建一个 Jenkins 用户,并将其分配给我们刚刚创建的自定义 Nexus 角色,如图 9.14 所示。

图片

图 9.14 Jenkins 的 Docker 注册库凭证

我们可以通过回到本地机器上的终端会话并发出 docker login 命令来测试身份验证:

图片

注意:默认情况下,托管 Docker 仓库通过 HTTPS 暴露。但是,如果您仅在纯 HTTP 端点暴露私有仓库,则需要配置 Docker 守护程序以允许不安全连接,通过将 –insecure-registry 标志传递给 Docker 引擎。

最后,在 Jenkins 上,使用我们迄今为止为 Jenkins 创建的 Nexus 凭证创建一个类型为用户名的注册凭证,带有密码(如图 9.15)。

图片

图 9.15 Docker 注册库凭证

Nexus 仓库 OSS 的另一个替代方案是 AWS 管理服务。

9.2.2 亚马逊弹性容器注册库

如果您像我一样使用 AWS,可以使用名为 Elastic Container Registry (ECR) 的托管 AWS 服务来托管您的私有 Docker 镜像。从 AWS 管理控制台导航到 Amazon ECR (console.aws.amazon.com/ecr/repositories)。然后,为要托管或存储的每个 Docker 镜像创建一个仓库。在我们的项目中,我们需要创建四个仓库,每个微服务一个。例如,服务加载器仓库如图 9.16 所示。

图片

图 9.16 ECR 新仓库

一旦创建了仓库,您可以点击“查看推送命令”按钮,应该会弹出一个对话框,其中包含有关如何对远程仓库进行标记、推送和拉取镜像的说明列表;参见图 9.17。

图片

图 9.17 电影加载器 ECR 仓库

在与仓库交互之前,您需要使用 ECR 进行身份验证。以下命令适用于 Mac 和 Linux 用户,可用于登录远程仓库:

aws ecr get-login-password --region REGION 
| docker login --username AWS --password-stdi.
ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/
mlabouardy/movies-loader

注意:将ACCOUNT_IDREGION分别替换为您的 Amazon 账户 ID 和 AWS 区域。

对于 Windows 用户,以下是命令:

(Get-ECRLoginCommand).Password | 
docker login --username AWS --password-stdi.
ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/mlabouardy/movies-loader

重复相同的步骤,为每个微服务创建专门的 ECR 仓库,如图 9.18 所示。

图 9.18 每个微服务的 ECR 仓库

9.2.3 Azure Container Registry

对于 Azure 用户,可以使用 Azure Container Registry 服务来存储容器镜像,而无需管理私有注册表。在 Azure 门户(portal.azure.com/)中,导航到容器注册表服务,点击添加按钮以创建新的注册表。指定您想要部署注册表的区域,并给它一个名称,如图 9.19 所示。

图 9.19 Azure 新注册表配置

将其他字段保留为默认值,然后点击创建。一旦创建注册表,导航到设置部分下的访问密钥,在那里您将找到可用于从 Jenkins 推送或拉取 Docker 镜像的 admin 用户名和密码;见图 9.20。

图 9.20 Azure Docker 注册表管理员凭据

您可以使用这些凭据在 Jenkins 中推送 CI 管道内的镜像。然而,我建议通过使用基于角色的访问控制(RBAC)或最小权限原则来创建具有细粒度访问控制的令牌。管理员账户仅设计为单个用户访问注册表,主要用于测试目的。

导航到令牌部分,点击添加按钮以创建新的访问令牌。给它一个名称,并将_repositories_push作用域关联起来,以允许仅执行docker push操作(Jenkins 只需要将镜像推送到注册表);见图 9.21。

图 9.21 Azure Docker 注册表新访问令牌

在创建令牌后,如图 9.22 所示,生成密码。为了与注册表进行身份验证,令牌必须启用并具有有效的密码。

图 9.22 Azure Docker 注册表凭据

在生成密码后,将其复制并保存为 Jenkins 凭据类型为用户名和密码。在关闭对话框屏幕后,您无法检索生成的密码,但可以生成一个新的。

9.2.4 Google Container Registry

对于 Google Cloud Platform 用户,可以使用名为 Google Container Registry (GCR) 的托管服务来托管 Docker 镜像。要开始使用,您需要为您的 GCP 项目启用 API Container Registry (cloud.google.com/container-registry/docs/quickstart),然后安装gcloud命令行工具。对于 Linux 用户,运行以下列表。

列表 9.13 gcloud安装

curl -O https://dl.google.com/dl/cloudsdk/channels/
rapid/downloads/google-cloud-sdk-344.0.0-linux-x86_64.tar.gz
tar zxvf google-cloud-sdk-344.0.0-linux-x86_64.tar.gz
 google-cloud-sdk
./google-cloud-sdk/install.sh

注意:有关如何安装 Google Cloud SDK 的进一步说明,请参阅官方 GCP 指南 cloud.google.com/sdk/install

接下来,运行以下命令以对注册表进行身份验证。生成的身份验证令牌将持久化到 ~/.docker/config.json,并用于对该存储库的任何后续交互:

gcloud auth configure-docker

您需要使用 GCR URI (gcr.io/[PROJECT-ID]) 标记目标镜像,并使用 docker push 命令推送镜像。图 9.23 展示了如何标记和推送 movies-loader Docker 镜像到 GCR:

docker tag mlabouardy/movies-loader
eu.gcr.io/PROJECT_ID/mlabouardy/movies-loader
docker push eu.gcr.io/PROJECT_ID/mlabouardy/movies-loader

图 9.23 Google 容器注册表镜像

现在,我们已经介绍了如何部署私有 Docker 注册表,我们将更新每个服务的 Jenkinsfile,以便在成功的 CI 管道执行结束时将镜像推送到远程私有注册表。

9.3 正确标记 Docker 镜像

在 Jenkinsfile 中添加一个新的推送阶段,使用 withRegistry 块,通过第二个参数提供的凭据对第一个参数中提供的注册表 URL 进行身份验证。然后,它将更改持久化到 ~/.docker/config.json。最后,它使用等于构建编号 ID 的标签值(使用 env.BUILD_ID 关键字)推送镜像。以下是在实现 Push 阶段后 movies-loader 服务的 Jenkinsfile 列表。

列表 9.14 将 Docker 镜像发布到注册表

def imageName = 'mlabouardy/movies-loader'
def registry = 'https://registry.slowcoder.com'
node('workers'){
    stage('Checkout'){
        checkout scm
    }

    stage('Unit Tests'){
        def imageTest= docker.build("${imageName}-test".
"-f Dockerfile.test .")
        imageTest.inside{
            sh 'python test_main.py'
        }
    }

    stage('Build'){
        docker.build(imageName)
    }

    stage('Push'){
        docker.withRegistry(registry, 'registry') {
           docker.image(imageName).push(env.BUILD_ID)
        }
    }
}

注意:imageNameregistry 值必须替换为您自己的 Docker 私有注册表 URL 和要存储的镜像名称。

在这个例子中,构建编号是 2;因此,movies-loader 镜像在标记为 2 后被推送到注册表,如图 9.24 所示。

图 9.24 Docker 推送命令日志

如果我们回到注册表(例如,在 Nexus Repository Manager 上),我们可以看到 movies-loader 镜像已成功推送(图 9.25)。

图 9.25 Nexus 中存储的 Docker 镜像

虽然可以使用 Jenkins 构建编号来标记镜像,但这可能不太方便。更好的标识符是 Git 提交 ID。在这个例子中,我们将使用它来标记构建的 Docker 镜像。在声明性和脚本管道中,此信息不是默认可用的。因此,我们将创建一个函数,该函数使用 Git 命令行获取提交 ID 并返回它:

def commitID() {
   sh 'git rev-parse HEAD > .git/commitID'
   def commitID = readFile('.git/commitID').trim()
   sh 'rm .git/commitID'
   commitID
}

从那里,我们可以更新 Push 阶段,使用 commitID() 函数返回的值标记镜像:

stage('Push'){
        docker.withRegistry(registry, 'registry') {
            docker.image(imageName).push(commitID())
       }
}

注意:在第十四章中,我们将介绍如何创建带有自定义函数的 Jenkins 共享库,以避免在 Jenkinsfile 中重复代码。

使用以下命令将更改推送到 GitHub 仓库:

git add Jenkinsfile
git commit -m "tagging docker image with git commit id"
git push origin develop

新的 CI 管道阶段应该看起来像图 9.26 中的 movies-loader 服务。

图 9.26 电影加载 CI 管道

在 Nexus Repository Manager 上成功运行后,应该有一个带有提交 ID 的新镜像可用(如图 9.27 所示)。

图 9.27 提交 ID 镜像标签

我们将进一步操作,并基于分支名称推送相同的镜像。这个标签在处理持续部署和交付时将非常有用。它将允许我们为每个环境分配特定的标签:

  • Latest—用于将镜像部署到生产环境

  • Preprod—用于将镜像部署到预发布或预生产环境

  • Develop—用于将镜像部署到沙盒或开发环境

Push阶段的代码块如下:

stage('Push'){
        docker.withRegistry(registry, 'registry') {
           docker.image(imageName).push(commitID())

            if (env.BRANCH_NAME == 'develop') {
                docker.image(imageName).push('develop')
            }
        }
}

env.BRANCH_NAME变量包含分支名称。您也可以直接使用BRANCH_NAME而不需要env关键字(自从 Pipeline Groovy Plugin 2.18 以来就没有要求了)。

最后,如果您使用 Amazon ECR 作为私有仓库,在发出推送指令之前,您需要先使用 AWS CLI 对远程仓库进行认证。对于 AWS CLI 2 用户,请使用以下列表中的 shell 指令来调用aws ecr命令。

列表 9.15 将 Docker 镜像发布到 ECR

def imageName = 'mlabouardy/movies-loader'
def registry = 'ACCOUNT_ID.dkr.ecr.eu-west-3.amazonaws.com'
def region = 'REGION'

node('workers'){
    ...
    stage('Push'){
        sh "aws ecr get-login-password --region ${region} .
docker login --username AW.
--password-stdin ${registry}/${imageName}"

        docker.image(imageName).push(commitID())
        if (env.BRANCH_NAME == 'develop') {
            docker.image(imageName).push('develop')
        }
    }
}

确保将ACCOUNT_IDREGION变量分别替换为您的 AWS 账户 ID 和 AWS 区域。如果您使用的是 AWS CLI 1.x 版本,请使用以下代码块:

stage('Push'){
        sh "\$(aws ecr get-logi.
--no-include-email --region ${region}) || true"
        docker.withRegistry("https://${registry}") {
            docker.image(imageName).push(commitID())
            if (env.BRANCH_NAME == 'develop') {
                docker.image(imageName).push('develop')
            }
        }
}

在触发 CI 管道之前,您需要授予 Jenkins 工作节点对 ECR 注册表执行推送操作的权利。因此,您需要将 AmazonEC2ContainerRegistryFullAccess 策略分配给 Jenkins 工作节点实例的 IAM 实例配置文件。图 9.28 展示了分配给 Jenkins 工作节点的 IAM 实例配置文件。

图 9.28 Jenkins 工作节点的 IAM 实例配置文件

一旦您完成了必要的更改,应该触发一个新的构建。在 CI 管道的末尾,应该将一个新的镜像标签推送到 ECR 仓库,如图 9.29 所示。

图 9.29 电影加载器 ECR 仓库镜像

对其他微服务重复相同的步骤,将它们的 Docker 镜像推送到 CI 管道的末尾,如图 9.30 所示。

图 9.30 电影市场 CI 管道

在典型的流程中,Docker 镜像应该进行分析、检查,并针对安全规则进行合规性和审计。这就是为什么在接下来的章节中,我们将集成容器检查和分析平台到 CI 管道中,以持续检查构建的 Docker 镜像中的安全漏洞。

9.4 扫描 Docker 镜像以查找漏洞

Anchore Engine (github.com/anchore/anchore-engine)是一个开源项目,它提供了一个集中的服务,用于检查、分析和认证容器镜像。您可以将 Anchore Engine 作为一个独立的服务或作为 Docker 容器运行。

注意:独立安装至少需要 4 GB 的 RAM 和足够的磁盘空间来支持您打算分析的容器镜像。

您可以从头使用 Packer 制作自己的 AMI 来安装 Anchore Engine 并设置 PostgreSQL 数据库。然后,使用 Terraform 来部署堆栈,或者您可以直接使用 Docker Compose 部署配置好的堆栈。有关如何使用 Terraform 和 Packer 的说明,请参阅第四章和第五章。

在预装了 Docker 社区版 (CE) 的 管理 VPC 中启动一个私有实例,然后从 Docker 官方指南页面安装 Docker Compose 工具。使用以下命令部署 Anchore Engine。

curl https://docs.anchore.com/current/docs/
engine/quickstart/docker-compose.yaml > docker-compose.yaml
docker-compose up -d

几分钟后,您的 Anchore Engine 服务应该已经启动并运行,准备使用。您可以使用 docker-compose ps 命令验证容器是否正在运行。图 9.31 显示了输出。请确保仅从 Jenkins 主安全组 ID 允许端口 8228(Anchore API)的入站流量,如图 9.32 所示。

图片

图 9.31 Docker Compose 堆栈服务

图片

图 9.32 Anchore 实例的安全组

注意:您可以进一步操作,在 EC2 实例前部署一个负载均衡器,并在 Route 53 中创建一个指向负载均衡器 FQDN 的 A 记录。

当涉及到 Jenkins 时,一个可用的插件已经使得集成变得容易得多。从主 Jenkins 菜单中选择“管理 Jenkins”,然后跳转到“管理插件”部分。点击“可用”标签,安装 Anchore 容器镜像扫描插件,如图 9.33 所示。

图片

图 9.33 Anchore 容器镜像扫描插件

接下来,从“管理 Jenkins”菜单中选择“配置系统”,然后滚动到“Anchore 配置”。然后,设置 Anchore URL,包括 /v1 路由和凭据(默认为 admin/foobar),如图 9.34 所示。

图片

图 9.34 Anchore 插件配置

最后,通过在项目工作区中创建一个名为 images 的文件来将 Anchore 集成到 Jenkins 管道中。此文件应包含要扫描的 Docker 镜像的名称,并可选地包含 Dockerfile。然后,使用创建的文件作为参数调用 Anchore 插件,如下所示。

列表 9.16 使用 Anchore 分析 Docker 镜像

stage('Analyze'){
            def scannedImage .
"${registry}/${imageName}:${commitID().
${workspace}/Dockerfile"
            writeFile file: 'images', text: scannedImage
            anchore name: 'images'
}

使用以下命令将更改推送到 develop 分支上的远程仓库:

git add Jenkinsfile
git commit -m "image scanning stage"
git push origin develop

CI 管道将在推送事件上触发。在镜像构建并推送到注册表后,应调用 Anchore Scanner。由于 Anchore 无法从私有注册表中拉取 Docker 镜像进行分析和检查,它将抛出一个错误。

幸运的是,Anchore 集成并支持分析与 Docker v2 兼容的任何仓库中的镜像。为了允许 Anchore 访问远程镜像,从 Anchore EC2 实例安装 anchor-cli 二进制文件:

yum install -y epel-release python-pip
pip install anchorecli

接下来,我们为私有 Docker 仓库定义凭证。运行此命令;REGISTRY 参数应包括仓库的完全限定主机名和端口号(如果公开)。

anchore-cli registry add REGISTRY USERNAME PASSWORD

注意:相同的命令可以用来配置托管在 Nexus 或其他解决方案上的 Docker 仓库。

由于我们使用 Amazon ECR 仓库并在 EC2 实例上运行 Anchore,我们将使用带有 AmazonEC2ContainerRegistryReadOnly 策略的 IAM 实例配置文件。在这种情况下,我们将为 USERNAMEPASSWORD 都传递 awsauto,并指示 Anchore 引擎从底层 EC2 实例继承角色:

anchore-cli --u admin --p foobar registry add ACCOUNT_ID.dkr.ecr.REGION .amazonaws.com awsauto awsauto --registry-type=awsecr

为了验证凭证是否已正确配置,运行以下命令以列出定义的仓库:

anchore-cli --u admin --p foobar registry list

图片

使用重放按钮重新运行管道。这次,Anchore 将检查镜像文件系统中的漏洞。如果发现严重漏洞,这将导致镜像构建失败,如图 9.35 所示。

图片

图 9.35 使用 Anchore 进行镜像扫描

一旦扫描完成,如果镜像有任何已知的严重问题,Anchore 将返回非零退出代码。Anchore 策略评估的结果将保存在 JSON 文件中。此外,管道将显示构建的状态(停止、警告或失败),如图 9.36 所示。

图片

图 9.36 Anchore 报告结果

HTML 报告也会自动发布在新创建的页面上。点击 Anchore 报告链接将显示一个图形化策略报告,显示摘要信息和策略检查的详细列表;见图 9.37。

图片

图 9.37 Anchore 常见漏洞和暴露(CVE)报告

注意:您可以根据自己的安全策略自定义 Anchore 引擎,以允许/阻止外部包、操作系统扫描等。

这就是从头开始在 Jenkins 上为 Docker 化的微服务定义持续集成管道的方法。

注意:另一种解决方案是 Aqua Trivy (github.com/aquasecurity/trivy),这是一个免费提供的社区版。付费解决方案也可以轻松集成到 Jenkins 中,例如 Sysdig (sysdig.com/) 和 Aqua。

9.5 编写 Jenkins 声明式管道

除了前面的章节,我们由于 Groovy 语法的灵活性,使用了脚本式流水线方法来定义我们项目的 CI 流水线。本节将介绍如何使用声明式流水线方法获得相同的流水线输出。这是一个简化且友好的语法,具有用于定义它们的特定语句,无需学习或掌握 Groovy 语言。

以 movies-loader 服务的脚本式流水线为例。以下列表提供了服务的 Jenkinsfile(为了简洁而截断)。

列表 9.17 Jenkinsfile 脚本式流水线

node('workers'){
    stage('Checkout'){
        checkout scm
    }
    stage('Unit Tests'){
        def imageTest= docker.build("${imageName}-test".
"-f Dockerfile.test .")
        imageTest.inside{
            sh "python main_test.py"
        }
    }
    stage('Build'){
        docker.build(imageName)
    }
    stage('Push'){
        docker.withRegistry(registry, 'registry') {
            docker.image(imageName).push(commitID())

            if (env.BRANCH_NAME == 'develop') {
                docker.image(imageName).push('develop')
            }
        }
    }
}

通过以下步骤,可以将此脚本式流水线轻松转换为声明式版本:

  1. node('workers')指令替换为pipeline关键字。所有有效的声明式流水线都必须包含在pipeline块内。

  2. pipeline块内部定义一个顶级agent部分,以定义流水线将要执行的执行环境。在我们的例子中,执行将在 Jenkins 工作节点上进行。

  3. stage块用stages部分包裹。stages部分包含 CI 流水线每个离散部分的阶段,如CheckoutTestBuildPush

  4. 将每个给定的stage命令和指令用steps块包裹。

创建一个包含所需更改的 Jenkinsfile.declarative 文件。最终结果应如下列表所示。

列表 9.18 Jenkinsfile 声明式流水线

pipeline{
    agent{
        label 'workers'                              ❶
    }
    stages{
        stage('Checkout'){
            steps{
                checkout scm                         ❷
            }
        }
        stage('Unit Tests'){
            steps{                                   ❸
                script {
                    def imageTest= docker.build("${imageName}-test".
"-f Dockerfile.test .")
                    imageTest.inside{
                        sh "python test_main.py"
                    }
                }
            }                                        ❸
        }
        stage('Build'){                              ❹
            steps{
                script {
                    docker.build(imageName)
                }
            }
        }                                            ❹
        stage('Push'){                               ❺
            steps{
                script {
                    docker.withRegistry(registry, 'registry') {
                        docker.image(imageName).push(commitID())

                        if (env.BRANCH_NAME == 'develop') {
                            docker.image(imageName).push('develop')
                        }
                    }
                }
            }
        }                                            ❺
    }
}

❶ 定义流水线应该在哪里执行。在示例中,流水线阶段将在带有 workers 标签的代理上执行。

❷ 克隆 Jenkins 作业设置中配置的 GitHub 仓库

❸ 基于 Dockerfile.test 构建 Docker 镜像,并从镜像中部署容器以运行 Python 单元测试

❹ 从 Dockerfile 构建应用程序的 Docker 镜像

❺ 使用 Docker 远程仓库进行身份验证并将应用程序镜像推送到仓库

注意:声明式流水线也可能包含一个post部分来执行如通知或清理环境的后构建步骤。这部分内容在第十章中介绍。

通过更新脚本路径字段,将 Jenkins 作业配置更新为使用新的声明式流水线文件,如图 9.38 所示。

图 9.38 Jenkinsfile 路径配置。

使用以下命令将声明式流水线推送到远程仓库:

git add Jenkinsfile.declarative
git commit -m "pipeline with declarative approach"
git push origin develop

GitHub webhook 会在推送事件发生时通知 Jenkins,并执行新的声明式流水线,如图 9.39 所示。

图 9.39 Jenkinsfile 声明式流水线执行。

您现在可以从该流水线中任何已运行的最高级阶段重新启动任何完成的声明式流水线。您可以通过经典 UI 中的侧面板进行运行,并如图 9.40 所示点击从阶段重启。

图 9.40 从阶段重启功能。

您将提示从原始运行中执行过的顶级阶段列表中选择,按照它们执行的顺序。这允许您从由于暂时性或环境考虑而失败的阶段重新运行管道。

注意:重启阶段也可以在 Blue Ocean UI 中完成,无论您的管道是成功还是失败后。

Docker 也可以在 agent 部分用作运行 CI/CD 管道的执行环境,如下面的列表所示。

列表 9.19 带有 Docker 代理的声明式管道

pipeline{
    agent{
        docker .
            image 'python:3.7.3'
        }
    }
    stages{
       stage('Checkout'){
            steps{
                checkout scm
            }
        }
        stage('Unit Tests'){
            steps{
                script {
                    sh 'python test_main.py'
                }
            }
        }
    }
}

如果我们尝试执行此管道,构建将迅速失败,因为管道假设任何配置的机器/实例都能够运行基于 Docker 的管道。在本例中,构建是在主机器上运行的。然而,由于此机器上未安装 Docker,管道失败了:

仅在 Jenkins 工作节点上运行管道时,请从 Jenkins 作业配置中更新 Pipeline 模型定义设置,并在 Docker 标签字段中设置 workers 标签,如图 9.41 所示。

图 9.41 管道模型定义。

当管道执行时,Jenkins 将自动启动指定的容器并执行其中定义的步骤。此管道执行相同阶段和相同步骤。

9.6 使用 Jenkins 管理拉取请求

目前,我们直接推送到 develop 分支;然而,我们应该创建功能分支,然后创建拉取请求以运行测试并提供反馈给 GitHub,如果测试失败则阻止提交批准。让我们看看如何使用 Jenkins 为拉取请求设置审查流程。

使用以下命令从 develop 分支创建一个新的功能分支:

git checkout -b feature/featureA

进行一些更改;在本例中,我已更新了 README.md 文件。然后,提交更改并将新功能分支推送到远程仓库:

git add README.md
git commit -m "update readme"
git push feature/featureA

转到 GitHub 仓库,并创建一个新的拉取请求以合并功能分支到 develop 分支,如图 9.42 所示。

图 9.42 新的拉取请求

在 Jenkins 上,将在功能分支上触发一个新的构建,如图 9.43 所示。

图 9.43 特定分支上的构建执行

一旦 CI 完成,Jenkins 将更新 GitHub 上的状态(图 9.44)。GitHub 上的构建指示器将根据构建状态变为红色或绿色。

图 9.44 Jenkins 在 GitHub PR 上的构建状态

注意:您还可以配置 SonarQube 以分析拉取请求,以确保代码干净且已批准合并。

此过程允许你在每次提交时运行构建和后续的自动化测试,这样只有最好的代码才会被合并。早期发现并自动修复错误可以减少引入生产环境中的问题数量,因此你的团队能够构建更好、更高效的软件。现在我们可以合并功能分支并删除它;请参见图 9.45。

图 9.45 合并并删除功能分支。

这将触发 develop 分支上的另一个构建,这将触发 CI 阶段并将带有 develop 标签的镜像推送到远程 Docker 仓库。

一旦构建完成,我们可以通过点击 GitHub 仓库中的 Commits 部分,来检查之前提交的状态。根据构建的状态,应该会显示绿色、黄色或红色的勾选标记;请参见图 9.46。

图 9.46 Jenkins 构建状态历史

最后,为了防止开发者直接向 develop 分支推送,以及在没有 Jenkins 构建通过的情况下合并,我们将创建一个新的规则来保护 develop 分支。在 GitHub 仓库设置中,转到 Branches 部分,并添加一个新保护规则,要求在合并之前 Jenkins 状态检查必须成功。图 9.47 显示了规则配置。

图 9.47 GitHub 分支保护

对预生产和 master 分支应用相同的规则。然后,对项目的其余 GitHub 仓库重复相同的程序。

将 Docker 镜像安全存储在私有仓库中,并将构建状态发布到 GitHub 后,我们已经完成了使用 Jenkins 多分支管道实现 Docker 化微服务的 CI 管道的实施。接下来的两章将介绍如何使用 Jenkins 实现持续部署和交付实践,针对云原生应用中最常用的两个容器编排平台:Docker Swarm 和 Kubernetes。

摘要

  • 你可以使用 Docker 缓存层、多阶段构建功能和轻量级基础镜像(如 Alpine 基础镜像)来优化 Docker 镜像以适应生产环境。

  • 提交 ID 和 Jenkins 构建 ID 可以用来对 Docker 镜像进行版本控制,并在应用程序部署失败的情况下回滚到工作版本。

  • 如 Nexus 和 Artifactory 这样的二进制仓库工具可以管理和存储构建工件,以供以后使用。

  • Anchore Engine 是一个开源工具,允许你在 CI 工作流中扫描 Docker 镜像以查找安全漏洞。

  • 在 CI 环境中,构建的频率太高,每次构建都会生成一个包。由于所有构建的包都在一个地方,开发者可以自由选择在更高环境中推广什么,不推广什么。

10 在 Docker Swarm 上运行的云原生应用程序

本章涵盖了

  • 在 AWS 上部署自修复的 Swarm 集群并使用 S3 存储桶进行节点发现

  • 在 Jenkins 管道中运行基于 SSH 的命令并配置 SSH 代理

  • 自动部署 Docker 化应用程序到 Swarm

  • 将 Slack 集成到 CI/CD 管道的发布和构建通知管理中

  • 在 Jenkins 中进行持续交付到生产以及用户手动批准

上一章介绍了如何使用 Jenkins 为容器化的微服务应用程序设置持续集成管道。本章将介绍如何自动化部署和管理多个应用程序环境。在本章结束时,你将熟悉在 Docker Swarm 集群中运行的容器化微服务的持续部署和交付(图 10.1)。

图 10.1 完整的 CI/CD 管道工作流程

在多台机器上运行多个容器的基本解决方案之一是 Swarm (docs.docker.com/engine/swarm/),它捆绑在 Docker 引擎中。在本章结束时,你应该能够从头开始构建一个 CI/CD 管道,用于在 Docker Swarm 集群内部运行的服务,如图 10.2 所示。

图 10.2 目标 CI/CD 管道

10.1 运行分布式 Docker Swarm 集群

Docker Swarm 最初作为一个独立产品发布,在服务器集群上运行主容器和代理容器以编排容器的部署。2016 年 Docker 1.12 版本的发布改变了这一点。Docker Swarm 成为 Docker 引擎的官方部分,并直接集成到每个 Docker 安装中。

注意:这只是 Docker 中 Docker Swarm 功能的简要概述。欲了解更多信息,请自由探索 Docker Swarm 的官方文档(docs.docker.com/engine/swarm/)。

为了说明从 Jenkins 中定义的 CI/CD 管道将容器部署到 Swarm 集群的过程,我们需要部署一个 Swarm 集群。

Swarm 集群将在一个 VPC 内部部署,包含两个自动扩展组:一个用于 Swarm 管理器,另一个用于 Swarm 工作节点。这两个 ASG 都将在多个可用区中启动的私有子网内部部署,以提高弹性。

一旦创建了 ASGs,设置 Swarm 需要手动初始化管理器,并且将新节点添加到集群中需要额外的信息(一个集群加入令牌),这是在创建 Swarm 时由第一个管理器提供的。

此步骤可以使用 Ansible 或 Chef 等配置管理工具自动化。然而,它需要手动交互。为了解决这个问题,并提供自动 Swarm 初始化,我们将在实例启动时运行一个单次 Docker 容器;该容器使用 S3 存储桶作为集群发现注册表以找到活动管理器和加入令牌。

图 10.3 总结了我们将部署的架构。我们将专注于 AWS,但相同的架构也可以应用于其他云提供商或本地环境。

图片

图 10.3 AWS 中的 Swarm 架构

注意:可以使用分布式、一致性的键值存储,如 etcd(etcd.io/)、HashiCorp 的 Consul(www.consul.io)或 Apache ZooKeeper(zookeeper.apache.org/)作为服务发现,使节点自动加入 Swarm 集群。

要部署 Swarm 实例,我们需要提供一个预安装了 Docker Engine 的 AMI。到目前为止,你应该熟悉 Packer。我们将创建一个包含以下列表内容的 template.json 文件。(完整的模板可以从 chapter10/swarm/packer/docker-ce/template.json 下载。)

列表 10.1 Docker AMI 的 Packer 模板

{
    "variables" : {},
    "builders" : [
        {
            "type" : "amazon-ebs",
            "profile" : "{{user `aws_profile`}}",
            "region" : "{{user `region`}}",
            "instance_type" : "{{user `instance_type`}}",
            "source_ami" : "{{user `source_ami`}}",
            "ssh_username" : "ec2-user",
            "ami_name" : "18.09.9-ce",
            "ami_description" : "Docker engine AMI",
        }
    ],
    "provisioners" : [
        {
            "type" : "shell",
            "script" : "./setup.sh",
            "execute_command" : "sudo -E -S sh '{{ .Path }}'"
        }
    ]
}

基础镜像为 Amazon Linux 2,它将通过一个 shell 脚本安装最新的 Docker 社区版软件包。然后它将ec2-user用户名添加到docker组中,以便能够在不使用sudo命令的情况下执行 Docker 命令;请参阅以下列表。

列表 10.2 Docker 社区版安装

#!/bin/bash
yum update -y
yum install docker -y
usermod -aG docker ec2-user
systemctl enable docker

执行packer build命令来烘焙 Docker AMI。一旦配置过程完成,新的烘焙 AMI 应该在 AWS 管理控制台上的“图像”部分可用(图 10.4)。

图片

图 10.4 Docker 社区版 AMI

接下来,使用 Terraform 部署基础设施,并创建一个名为sandbox的专用 VPC,使用 10.1.0.0/16 CIDR 块来隔离沙盒应用程序和工作负载。在 vpc.tf 文件中定义列表 10.3 中的块。

注意:在不同的 VPC 上部署集群不是强制性的,但强烈建议遵循最佳实践,通过隔离工作负载环境以进行审计和安全合规性。

列表 10.3 沙盒 VPC 资源

resource "aws_vpc" "sandbox" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true

  tags = {
    Name   = var.vpc_name
    Author = var.author
  }
}

Swarm 管理器需要在初始化后有一种方式将工作令牌传递给工作节点。最佳方式是让 Swarm 管理器的用户数据触发生成令牌并将其放入 S3 桶中。在 s3.tf 中使用以下列表中的代码定义一个私有 S3 桶资源。

列表 10.4 Swarm 发现 S3 桶资源

resource "aws_s3_bucket" "swarm_discovery_bucket" {
  bucket = var.swarm_discovery_bucket
  acl    = "private"

  tags = {
    Author = var.author
    Environment = var.environment
  }
}

注意:AWS 系统管理器参数存储(mng.bz/r6GX)也可以用作共享加密存储,用于存储和检索 Swarm 工作节点的加入令牌。

IAM 实例配置文件对于 EC2 实例能够与 S3 桶交互以存储或检索用于自动加入操作的工作令牌是必要的。在 iam.tf 文件中定义 IAM 角色策略,如下所示。

列表 10.5 Swarm 节点 IAM 策略

resource "aws_iam_role_policy" "discovery_bucket_access_policy" {
  name = "discovery-bucket-access-policy-${var.environment}"
  role = aws_iam_role.swarm_role.id

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:*"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
EOF
}

然后,我们为 Swarm 管理器创建一个启动配置,该配置使用 Packer 烤制的 Docker AMI,并在用户数据上运行配置的启动脚本。使用以下列表在 swarm_managers.tf 中定义代码。

列表 10.6 Swarm 管理器启动配置

resource "aws_launch_configuration" "managers_launch_conf" {
  name                 = "managers_config_${var.environment}"
  image_id             = data.aws_ami.docker.id
  instance_type        = var.manager_instance_type
  key_name             = var.key_name
  security_groups      = [aws_security_group.swarm_sg.id]
  user_data            = data.template_file.swarm_manager_user_data.rendered
  iam_instance_profile = aws_iam_instance_profile.swarm_profile.id

  root_block_device {
    volume_type = "gp2"
    volume_size = 20
  }

  lifecycle {
    create_before_destroy = true
  }
}

启动脚本使用集群发现 S3 存储桶的名称和运行实例的角色(管理器或工作节点),如下所示。根据实例角色,docker swarm join 命令将使用正确的令牌(workers 令牌或 managers 令牌)。

列表 10.7 Swarm 管理器用户数据

data "template_file" "swarm_manager_user_data" {
  template = "${file("scripts/join-swarm.tpl")}"
  vars = {
    swarm_discovery_bucket = "${var.swarm_discovery_bucket}"
    swarm_name             = var.environment
    swarm_role             = "manager"
  }
}

如下所示,shell 脚本 joint-swarm.tpl 使用 EC2 元数据获取实例的私有 IP 地址。脚本随后执行一个容器,该容器使用 S3 存储桶存储 Swarm 的状态,一旦创建或如果存储桶中不存在状态,则创建一个新的 Swarm。

列表 10.8 Swarm 节点启动脚本

#!/bin/bash
NODE_IP=$(curl -fsS http://169.254.169.254/latest/meta-data/local-ipv4)
docker run -d --restart on-failure:5 \
    -e SWARM_DISCOVERY_BUCKET=${swarm_discovery_bucket} \
    -e ROLE=${swarm_role} \
    -e NODE_IP=$NODE_IP \
    -e SWARM_NAME=${swarm_name} \
    -v /var/run/docker.sock:/var/run/docker.sock \
    mlabouardy/swarm-discovery

注意:您可以使用 CloudWatch 告警定义自动扩展策略,根据 CPU 利用率或 Swarm 节点的自定义指标触发扩展或缩减事件。

从那里,我们将创建一个管理器的自动扩展组(ASG)。默认情况下,我们将为集群创建一个管理器。但是,我建议在生产环境中使用奇数,因为管理器之间需要多数投票来就提议的管理任务达成一致。奇数(而不是偶数)强烈推荐用于打破平局。然而,对于沙盒集群,我们将保持简单,使用一个 Swarm 管理器。在 swarm_mangers.tf 中,定义如下所示的 ASG 资源。

表 10.1 Swarm 集群安全组规则

resource "aws_autoscaling_group" "swarm_managers" {
  name                 = "managers_asg_${var.environment}"
  launch_configuration = aws_launch_configuration.managers_launch_conf.name
  vpc_zone_identifier  = [for subnet in aws_subnet.private_subnets: subnet.id]
  depends_on = [aws_s3_bucket.swarm_discovery_bucket]
  min_size       = 1
  max_size       = 3
  lifecycle {
    create_before_destroy = true
  }
}

列表 10.10 Swarm 工作节点 ASG

同样,我们将为工作节点创建一个 ASG,我们将使用两个 Swarm 工作节点。注意使用 depends_on 关键字创建对 swarm_managers 资源的隐式依赖。Terraform 使用此信息来确定创建资源的正确顺序。

| 协议 | 端口 | 来源 | 描述 |

列表 10.9 Swarm 管理器自动扩展组

resource "aws_autoscaling_group" "swarm_workers" {
  name                 = "workers_asg_${var.environment}"
  launch_configuration = aws_launch_configuration.workers_launch_conf.name
  vpc_zone_identifier  = [for subnet in aws_subnet.private_subnets: subnet.id]
  min_size             = 2
  max_size             = 5
  depends_on = [aws_autoscaling_group.swarm_managers]
  lifecycle {
    create_before_destroy = true
  }
}

最后,允许分配给 Swarm 集群实例的安全组中的表 10.1 中的防火墙规则。

| TCP | 7946 | Swarm | 所有节点之间的控制平面 Gossip 发现通信 |

TCP 2377 Swarm 集群管理和 raft 同步通信
注意:mlabouardy/swarm-discovery 完整的 Python 脚本和 Dockerfile 在 GitHub 仓库中给出:pipeline-as-code-with-jenkins/tree/master/chapter10/discovery。
UDP 7946 Swarm 来自其他 Swarm 节点的容器网络发现
协议 端口 来源 描述
在此示例中,Terraform 将首先创建 Swarm 管理器。这样,我们保证 Swarm 初始化和 S3 存储桶中存在加入令牌的可用性。在 swarm_workers.tf 文件中添加以下列表中的资源。
TCP 22 Jenkins 和防火墙 SG 来自 Jenkins 主和防火墙安全组的 SSH 流量

以下列表提供了安全组定义。

列表 10.11 Swarm 节点安全组

resource "aws_security_group" "swarm_sg" {
  name        = "swarm_sg_${var.environment}"
  description = "Allow inbound traffic fo.
swarm management and ssh from jenkins & bastion hosts"
  vpc_id      = aws_vpc.sandbox.id

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [var.bastion_sg_id, var.jenkins_sg_id]
  }
  ingress {
    from_port   = "2377"
    to_port     = "2377"
    protocol    = "tcp"
    cidr_blocks = [var.cidr_block]
  }
  ...
  egress {
    from_port   = "0"
    to_port     = "0"
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

注意:我建议使用启用加密和版本控制的 S3 后端来远程存储 Terraform 状态文件。

variables.tfvars中定义所需的 Terraform 变量,如表 10.2 所示。

表 10.2 Swarm Terraform 变量

变量 类型 描述
region 字符串 None 部署 Swarm 集群的区域的名称,例如eu-central-1
shared_credentials_file 字符串 ~/.aws/credentials 共享凭据文件的路径。如果没有设置且指定了配置文件,则使用~/.aws/credentials
aws_profile 字符串 profile 在共享凭据文件中设置的 AWS 配置文件名称
author 字符串 None Swarm 集群所有者的名称。标记您的 AWS 资源以跟踪按所有者或环境划分的月度成本是可选的,但建议这么做。
key_name 字符串 None SSH 密钥对
availability_zones 列表 None 启动 VPC 子网的可用区
bastion_sg_id 字符串 None 防火墙主机安全组的 ID
jenkins_sg_id 字符串 None Jenkins 主安全组的 ID
vpc_name 字符串 sandbox VPC 的名称
environment 字符串 sandbox 运行时环境名称
cidr_block 字符串 10.1.0.0/16 VPC 的 CIDR 块
cluster_name 字符串 sandbox Swarm 集群的名称
public_subnets_count 数字 2 需要创建的公共子网数量
private_subnets_count 数字 2 需要创建的私有子网数量
swarm_discovery_bucket 字符串 swarm-discovery-cluster 存储 Swarm 令牌的 S3 存储桶
manager_instance_type 字符串 t2.small Swarm 管理员的 EC2 实例类型
worker_instance_type 字符串 t2.large Swarm 工作节点的 EC2 实例类型

然后,使用terraform apply命令开始部署过程。一旦部署完成,将创建自动扩展组(ASGs),在每个实例上启动 Swarm 发现容器,第一个运行的经理将执行swarm init命令并将令牌存储在 S3 存储桶中(图 10.5),其他实例将使用此令牌加入集群。

注意:您可以拥有任意数量的工作节点组,运行在您选择的任意配置中(CPU 或内存优化的工作节点与通用 Swarm 工作节点一起运行)。

图片

图 10.5 存储在 S3 存储桶中的 Swarm 状态

如果您决定为 Swarm 集群创建一个专用的 VPC,您需要设置管理和沙盒 VPC 之间的 VPC 对等连接,如图 10.6 所示。有关如何使用 Terraform 设置对等的分步指南,请参阅官方 Terraform 文档,网址为 mng.bz/VBw5

图 10.11 创建部署仓库的分支

图 10.6 管理和沙盒 VPC 之间的 VPC 对等连接

注意:如果您打算使用 VPC 对等连接,请确保 VPC 不具有匹配或重叠的 IPv4 CIDR 块。在我们的示例中,管理和沙盒 CIDR 块分别为 10.0.0.0/16 和 10.1.0.0/16。

从 VPC 仪表板导航到对等连接,创建一个新的对等连接。配置对等连接,如图 10.7 所示。

图 10.7 配置管理和沙盒 VPC 的对等连接

图 10.7 配置管理和沙盒 VPC 的对等连接

创建对等连接后,您将在状态栏中看到“待接受”。如果您使用的是不同的账户或不同的区域,请转到相应的 VPC 控制台,在那里您可以在对等连接的状态栏中看到“待接受”。从“操作”下拉菜单中选择“接受请求”,如图 10.8 所示。然后,在“接受 VPC 对等连接请求”提示框中,单击“是,接受”。

图 10.10 Swarm 集群节点列表

图 10.8 接受 VPC 对等连接请求

要通过此 VPC 对等连接发送和接收流量,您必须在您的 VPC 路由表中添加一个路由到对等 VPC。在关联 VPC 子网的路由表中,创建一个以对等 VPC 的 CIDR 块为目标,以 VPC 对等连接的 ID 为目标的路由。

对所有其他 VPC 路由表重复相同的设置。一旦设置完成,您的路由表将类似于图 10.9。

图 10.7 配置管理和沙盒 VPC 的对等连接

图 10.9 沙盒 VPC 的路由表更新

要查看 Swarm 状态,请使用第五章第 5.2.4 节中部署的堡垒主机设置 SSH 隧道:

ssh -N 3000:SWARM_MANAGER_IP:22 ec2-user@BASTION_IP
ssh ec2-user@localhost -p 3000

SWARM_MANAGER_IP 替换为 Swarm 管理器的私有 IP 地址。一旦连接,如果您输入 docker info 命令,Swarm: active 属性应确认 Swarm 已正确配置:

图 10.10 Swarm 的连接节点

从管理机器上运行 docker node ls 命令以查看您的 Swarm 的连接节点。如图 10.10 所示,我们现在有一个管理节点和两个工作节点。

docker node ls

图 10.10 Swarm 的连接节点

图 10.10 Swarm 集群节点列表

随着 Swarm 运行起来,让我们使用 Jenkins 部署基于 Docker 的应用程序。

10.2 定义持续部署流程

为部署创建一个新的 GitHub 仓库。由于部署选项经常更改,我们将部署部分存储在不同的 Git 仓库中。然后,创建三个主要分支:develop、preprod 和 master,如图 10.11 所示。

Docker Swarm 模式现在直接集成到 Docker Compose v3,并正式支持通过 docker-compose.yml 文件部署 stacks(服务组)。您用于在本地测试应用程序的相同的 docker-compose.yml 文件现在可以用于将应用程序部署到 Swarm。

图片

图 10.11 GitHub 部署仓库

要从 Jenkins 进行 Docker Swarm 部署,我们需要一个包含 Docker 镜像引用以及配置设置(如端口、网络名称、标签和约束)的 docker-compose 文件。要运行此文件,我们需要在管理机器上通过 SSH 执行 docker stack deployment 命令。

在 develop 分支上,使用您喜欢的文本编辑器或 IDE 创建 docker-compose.yml 文件,内容如下所示。

列表 10.12 应用 Docker Compose

version: "3.3"
services:
  movies-loader:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-loader:develop
    environment:
      - AWS_REGION=REGION
      - SQS_URL=https://sqs.REGION.amazonaws.com/ID/movies_to_parse_sandbox

  movies-parser:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-loader:develop
    environment:
      - AWS_REGION=REGION
      - SQS_URL=https://sqs.REGION.amazonaws.com/ID/movies_to_parse_sandbox
      - MONGO_URI=mongodb://root:root@mongodb/watchlist
      - MONGO_DATABASE=watchlist
    depends_on:
      - mongodb

  movies-store:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-store:develop
    environment:
      - MONGO_URI=mongodb://root:root@mongodb/watchlist
    ports:
      - 3000:3000
    depends_on:
      - mongodb

  movies-marketplace:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-marketplace:develop
    ports:
      - 80:80

  mongodb:
    image: bitnami/mongodb:latest
    environment:
      - MONGODB_USERNAME=root
      - MONGODB_PASSWORD=root
      - MONGODB_DATABASE=watchlist

注意:将 IDREGIONUSER 替换为您的 AWS 账户 ID、AWS 区域和 ECR URI。

每个服务都使用我们在第九章中构建的镜像,并引用 develop 标签。此标签专门用于沙盒部署,包含 develop 分支的代码库。此外,我们还定义了一个 MongoDB 服务,该服务将被 movies-store 和 movies-parser 服务使用。

MongoDB 服务的凭据是明文。然而,在任何情况下都不应提交敏感信息,并选择像 HashiCorp Vault 或 AWS SSM Parameter Store 这样的托管解决方案来加密您的凭据和访问令牌。您还可以使用 Docker 的集成功能 Secrets 创建数据库凭据:

openssl rand -base64 12 | docker secret create mongodb_password -

并更新 docker-compose.yml 以使用密钥而不是明文密码:

mongodb:
    image: bitnami/mongodb:latest
    environment:
      - MONGODB_USERNAME=root
      - MONGO_ROOT_PASSWORD_FILE: /run/secrets/mongodb_password
      - MONGODB_DATABASE=watchlist

注意:如果 MongoDB 服务因未知原因崩溃或已被删除,其数据将丢失。为了避免数据丢失,您应该挂载一个持久卷。根据所使用的云提供商,Docker 卷支持使用外部持久存储,如 Amazon EBS。

为了解耦 HTML 页面的爬取和解析,我们在 movies-loader 和 movies-parser 服务之间使用分布式队列。除了其高可用性外,这还将允许我们根据要解析的 HTML 页面数量部署额外的 movies-parser 工作节点。使用 Terraform(chapter10/swarm/terraform/sqs.tf)创建一个名为 movies_to_parse_sandbox 的沙盒环境 SQS,如图 10.12 所示。此队列将由 movies-loader 用于推送电影,然后它将被 movies-parser 工作节点消费。

图片

图 10.12 沙盒队列设置

在处理完 Docker Compose 后,我们可以继续创建 Jenkinsfile,如列表 10.13 所示,包含以下步骤:

  1. 克隆 GitHub 仓库(chapter10/deployment/sandbox/Jenkinsfile)并检出 develop 分支。

  2. 通过 SSH 将 docker-compose.yml 文件发送到管理节点并执行 docker stack deploy 命令。

注意:我们使用 master 标签来限制管道仅在 Jenkins 主节点上执行。工作机的机器也可能用于此作业。

列表 10.13 部署 Jenkinsfile

def swarmManager = 'manager.sandbox.domain.com'
def region = 'AWS REGION'                           ❶
node('master'){
    stage('Checkout'){
        checkout scm
    }
    stage('Copy'){
        sh "scp -o StrictHostKeyChecking=no
docker-compose.yml ec2-user@${swarmManager}:/home/ec2-user"
    }
    stage('Deploy stack'){
        sh "ssh -oStrictHostKeyChecking=no ec2-user@${swarmManager} '\$(\$(aws ecr get-login --no-include-email --region ${region}))' || true"
        sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager} docker stack deploy
--compose-file docker-compose.yml
--with-registry-auth watchlist"
    }
}

❶ 替换为你的 AWS 默认区域。

此 Jenkinsfile 使用 Amazon ECR 作为私有仓库。如果你使用的是需要用户名和密码认证的私有仓库(例如 Nexus、DockerHub、Azure 或 Cloud Container Registry),你可以使用默认安装的 Credentials Binding 插件(plugins.jenkins.io/credentials-binding/),将仓库凭据绑定到USERNAMEPASSWORD变量。然后,将这些变量传递给docker login命令进行认证:

stage('Deploy'){
    withCredentials([[
$class: 'UsernamePasswordMultiBinding',
credentialsId: 'registry',
usernameVariable: 'USERNAME',
passwordVariable: 'PASSWORD']]) {
        sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager}
docker login --password $PASSWORD --username $USERNAME
${registry}"
        sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager}
docker stack deploy --compose-file docker-compose.yml
--with-registry-auth watchlist"
    }
}

使用以下命令将 Jenkinsfile 和 docker-compose.yml 文件推送到 develop 分支:

git add . 
git commit -m "deploy watchlist stack to sandbox"
git push origin develop

前往 Jenkins,创建一个名为 watchlist-deployment 的新多分支管道作业。

注意:有关如何在 Jenkins 上创建和配置多分支管道作业的逐步指南,请参阅第七章。

设置 GitHub 仓库 HTTPS 克隆 URL,并允许 Jenkins 发现所有分支,寻找根仓库中的 Jenkinsfile,如图 10.13 所示。

图片

图 10.13 分支源配置

目前,作业管道应该发现 develop 分支并执行 Jenkinsfile 中定义的阶段,如图 10.14 所示。

图片

图 10.14 Jenkins 上的部署作业

管道应该在复制阶段失败并变成红色,如图 10.15 所示。Jenkins 主节点无法 SSH 到 Swarm 管理器,因为 Jenkins 主节点有错误的私钥。

图片

图 10.15 SCP 命令日志

为了让 Jenkins 持续部署到 Swarm,它需要访问 Swarm 管理器。在 Jenkins 上创建一个新的 SSH 用户名凭据,带有私钥,以访问 Swarm 沙盒。在私钥字段中粘贴创建 Swarm EC2 实例时使用的密钥对的内容。然后,将其命名为swarm-sandbox,如图 10.16 所示。

图片

图 10.16 Jenkins 凭据与 Swarm SSH 密钥对

注意:Jenkins 只需要访问 Swarm 管理器。其他节点由 Swarm 管理器管理,因此 Jenkins 不需要直接访问它们。

更新 Jenkinsfile 以使用 SSH 代理插件(Credentials Binding 插件)注入凭据。sshagent块应该包装所有基于 SSH 和 SCP 的命令,如下面的列表所示。

列表 10.14 SSH 代理配置

sshagent (credentials: ['swarm-sandbox']){
   stage('Copy'){
     sh "scp -o StrictHostKeyChecking=no
docker-compose.yml ec2-user@${swarmManager}:/home/ec2-user"
   }

   stage('Deploy stack'){
     sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager}
'\$(\$(aws ecr get-login --no-include-email --region ${region}))'
|| true"
     sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager}
docker stack deploy --compose-file docker-compose.yml
--with-registry-auth watchlist"
   }
}

将更改推送到 develop 分支。watchlist-deployment 项目的 develop 分支的嵌套作业应该触发一个新的构建。

注意:为了实现持续部署,请在 GitHub 仓库上创建一个 GitHub webhook,以便在推送事件时通知 Jenkins。

这次,管道应该成功并变成绿色(图 10.17)。

图片

图 10.17 持续部署管道

在构建日志方面,Jenkins 将在 Swarm 管理器上通过 SSH 运行docker stack deploy,并根据develop标签图像部署图 10.18 中的服务。

图片

图 10.18 docker stack deploy的输出

注意:如果您计划将 Amazon ECR 用作远程存储库,您需要将 ECR IAM 策略分配给分配给 Swarm 实例的 IAM 实例配置文件。

在 Swarm 上,输入以下命令,我们应该能够查看堆栈的状态以及其中运行的服务:

docker service ls

四个微服务应与 MongoDB 服务一起部署,如图 10.19 所示。

图片

图 10.19 Swarm 沙盒上成功部署的堆栈

接下来,我们将部署一个名为 Visualizer 的开源工具,以可视化跨一组机器的 Docker 服务。在 Swarm 管理器机器上执行以下命令:

docker service create --name=visualizer 
--publish=8080:8080/tc.
--constraint=node.role==manager \
     --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
     dockersamples/visualizer

一旦服务部署完成,我们将创建一个公共负载均衡器,将传入的 HTTP 和 HTTPS(可选)流量转发到 8080 端口,这是 Visualizer UI 暴露的端口。在以下列表中声明 ELB 资源或从 chapter8/services/loadbalancers.tf 下载资源文件。

列表 10.15 Visualizer 负载均衡器

resource "aws_elb" "visualizer_elb" {
  subnets                   = var.public_subnets
  cross_zone_load_balancing = true
  security_groups           = [aws_security_group.elb_visualizer_sg.id]
  listener {
    instance_port      = 8080
    instance_protocol  = "http"
    lb_port            = 443
    lb_protocol        = "https"
    ssl_certificate_id = var.ssl_arn
  }
  listener {
    instance_port      = 8080
    instance_protocol  = "http"
    lb_port            = 80
    lb_protocol        = "http"

  }
  health_check {
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    target              = "TCP:8080" resource "aws_autoscaling_attachment" "cluster_attach_visualizer_elb" {
  autoscaling_group_name = var.swarm_managers_asg_id
  elb                    = aws_elb.visualizer_elb.id
}

    interval            = 5
  }
}

然后,我们将负载均衡器附加到 Swarm 管理器的 ASG。负载均衡器也可以分配给 Swarm 工作节点。实际上,Swarm 集群中的所有节点都通过 gossip 网络知道集群中每个容器的位置。如果传入的请求击中了当前未运行该请求所针对服务的服务的节点,请求将被路由到运行该服务容器的节点。

这样做是为了避免节点需要为特定服务专门构建。任何节点都可以运行任何服务,并且每个节点都可以进行均衡负载,从而减少复杂性和应用程序所需的资源数量。这个特性被称为 网状路由

resource "aws_autoscaling_attachment" "cluster_attach_visualizer_elb" {
  autoscaling_group_name = var.swarm_managers_asg_id
  elb                    = aws_elb.visualizer_elb.id
}

以下列表(chapter8/services/dns.tf)不是必需的,但可以用来创建一个友好的 DNS 记录,指向 Visualizer 负载均衡器的 FQDN。

列表 10.16 Visualizer DNS 配置

resource "aws_route53_record" "visualizer" {
  zone_id = var.hosted_zone_id
  name    = "visualizer.${var.environment}.${var.domain_name}"
  type    = "A"
  alias {
    name                   = aws_elb.visualizer_elb.dns_name
    zone_id                = aws_elb.visualizer_elb.zone_id
    evaluate_target_health = true
  }
}

注意:更新 Swarm 集群的安全组,允许来自负载均衡器安全组的 8080 端口进入流量。为 8080 端口添加入站规则,并使用terraform apply使更改生效。

一旦发布更改,将浏览器指向终端会话中“输出”部分显示的负载均衡器 URL。这个实用的工具,如图 10.20 所示,可以帮助您查看哪些容器正在运行,以及它们在哪些节点上。

注意:此工具仅在 Docker Engine 1.12.0 及以后的 Docker Swarm 模式下工作。它不与单独的 Docker Swarm 项目一起工作。

图片

图 10.20 可视化仪表板

注意:容器也部署在管理器上。如果您想限制部署到工作节点,请使用带有标签的 Docker 约束。

我们已成功将应用程序堆栈部署到 Swarm。然而,目前部署是手动触发的。最终,我们希望部署作业在每个 CI 管道成功执行结束时运行。

要这样做,请更新 Jenkinsfile(chapter10/pipelines/movies-loader/Jenkinsfile),使用 build job 关键字触发外部作业。例如,在 movies-loader Jenkinsfile 中,将以下 Deploy 阶段代码块添加到管道末尾:

stage('Deploy'){
        if(env.BRANCH_NAME == 'develop'){
            build job: "watchlist-deployment/${env.BRANCH_NAME}"
        }
}

将更改提交并推送到功能分支。然后创建一个拉取请求(PR)以合并到开发分支。应在功能分支上触发新的构建,一旦完成,Jenkins 将在 PR 上发布构建状态,如图 10.21 所示。

图 10.21 拉取请求构建状态

一旦拉取请求得到验证,我们就将其合并到开发分支,并在此分支上触发新的构建,如图 10.22 所示。

图 10.22 movies-loader 项目的 Jenkins CI/CD 管道

在 CI 管道的末尾,将执行部署阶段,并在开发分支上触发 watchlist-deployment,如图 10.23 所示。

图 10.23 外部作业触发

这将触发部署作业,该作业将部署堆栈并强制拉取带有 develop 标签的新 Docker 镜像。对其他 GitHub 仓库重复相同的流程。最终,如果 CI 成功执行,每个仓库将触发沙盒的部署,如图 10.24 所示。

图 10.24 市场 CI/CD 管道执行

注意:在第十一章和第十二章中,我们将介绍如何在 CI/CD 管道中从 Jenkins 运行部署应用的自动化健康检查和集成测试。

到目前为止,我们的应用程序已部署到 Swarm 沙盒环境。要访问应用程序,我们需要创建两个公共负载均衡器:一个用于 API(movies-store)和另一个用于前端(movies-marketplace)。使用 GitHub 仓库中可用的 Terraform 模板文件(位于 /chapter8/services 文件夹下)创建 AWS 资源,然后发出 terraform apply 命令以配置资源。在部署过程结束时,市场和企业 API 访问 URL 将在 输出 部分显示,如图 10.25 所示。

图 10.25 Terraform 应用输出

注意:确保允许来自连接到 Swarm EC2 实例的安全组对端口 80(前端)、8080(可视化器)和 3000(API)的入站流量。

为了使市场能够与 RESTful API 交互并显示爬取的电影列表,我们需要在市场 Docker 镜像构建时注入 API URL。市场源代码根据目标环境包含多个文件(图 10.26)。

图片

图 10.26 Angular 环境文件

每个文件都包含正确的 API URL。对于沙盒环境,将使用环境.sandbox.ts 文件,如下所示。

列表 10.17 市场沙盒环境变量

export const environment = {
  production: false,
  apiURL: 'https://api.sandbox.slowcoder.com',
};

市场 Docker 镜像将使用ng build -c sandbox标志构建,这将用环境.sandbox.ts 的值替换环境.ts 文件;见图 10.27。

图片

图 10.27 Docker 镜像构建执行

一旦将新镜像部署到 Swarm,请将您的浏览器指向市场 URL。它应该显示历史上 IMDb 排名前 100 的电影,如图 10.28 所示。

图片

图 10.28 观看列表市场仪表板

这就是实现持续部署的方法。然而,我们希望提醒开发和产品团队项目的部署和 CI/CD 状态。

10.3 将 Jenkins 与 Slack 通知集成

在管道的某些阶段,您可能决定想要向团队发送 Slack 通知,告知他们构建状态。为了通过 Jenkins 发送 Slack 消息,我们需要为我们的作业提供一个授权自身与 Slack 的方式。

幸运的是,Slack 有一个预构建的 Jenkins 集成,使得事情变得相当简单。从mng.bz/xXOB安装插件。将WORKSPACE替换为如图 10.29 所示的 Slack 工作区名称。

图片

图 10.29 Jenkins CI Slack 集成

点击“添加到 Slack”按钮。然后选择 Jenkins 发送通知的频道,如图 10.30 所示。

图片

图 10.30 Slack 频道配置

之后,我们需要在 Jenkins Slack 通知插件([plugins.jenkins.io/slack/](https://plugins.jenkins.io/slack/))上设置配置,该插件已经安装在预制的 Jenkins 主机镜像上。输入团队工作区名称、在您的 Slack 上创建的集成令牌和频道名称,如图 10.31 所示,然后点击应用和保存按钮。

图片

图 10.31 Jenkins Slack 通知插件

现在我们已经在 Jenkins 中正确配置了 Slack,我们可以配置我们的 CI/CD 管道,使用以下方法发送通知以广播构建状态:

slackSend (color: colorCode, message: summary)

以电影加载器服务的 CI/CD 管道为例,让我们在管道末尾添加此指令;见以下列表。

列表 10.18 Jenkins Slack 插件 DSL

node('workers'){
    stage('Checkout'){}

    stage('Unit Tests'){}

    stage('Build'){}

    stage('Push'){}

    stage('Deploy'){}

    slackSend (color: '#2e7d32',
message: "${env.JOB_NAME} has been successfully deployed")
}

注意:为了简化,我跳过了运行单元测试、构建镜像和将镜像推送到注册表的步骤。建议您将这些步骤放入我们即将探索的工作流程中。

将更改推送到功能分支,然后合并到 develop。在管道结束时,将发送一个新的 Slack 通知,如图 10.32 所示。

图片

图 10.32 Jenkins Slack 通知

虽然这可行,但我们还希望在管道失败时收到通知。这就是 try-catch 块发挥作用来处理管道阶段抛出的错误的地方;请参见以下列表。

列表 10.19 Jenkins 中的 Slack 通知

node('workers'){
    try {
        stage('Checkout'){
            checkout scm
            notifySlack('STARTED')
        }

        stage('Unit Tests'){}
        stage('Build'){}
        stage('Push'){}
        stage('Deploy'){}
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
    }
}

这次,使用了 notifySlack() 方法,该方法根据管道构建状态发送不同颜色的通知,如以下列表所示。

列表 10.20 自定义 Slack 通知消息颜色

def notifySlack(String buildStatus){
    buildStatus =  buildStatus ?: 'SUCCESSFUL'
    def colorCode = '#FF0000'

    if (buildStatus == 'STARTED') {                         ❶
        colorCode = '#546e7a'                               ❶
    } else if (buildStatus == 'SUCCESSFUL') {               ❶
        colorCode = '#2e7d32'                               ❶
    } else {                                                ❶
        colorCode = '#c62828c'                              ❶
    }                                                       ❶
    slackSend (color: colorCode.
message: "${env.JOB_NAME} build status: ${buildStatus}")    ❷
}

❶ 在消息的左侧边框上着色

❷ 使用 env.JOB_NAME 发送带有作业名称的 Slack 消息,并使用 buildStatus 变量发送构建状态

根据您的构建结果,代码将发送如图 10.33 所示的 Slack 通知。

图片

图 10.33 构建状态通知

让我们通过抛出错误来模拟构建失败,将以下指令添加到 Build 阶段:

error "Build failed"

将更改推送到 GitHub。在 Build 阶段,管道将失败(见图 10.34)。

图片

图 10.34 在 Jenkins 管道中抛出错误

在 Slack 频道中,这次我们将收到构建状态设置为失败的通知,如图 10.35 所示。

图片

图 10.35 构建失败 Slack 通知

在以下列表中,我们将进一步扩展。我们将在通知中添加更多信息,例如推送事件的作者、Git 提交 ID 和消息。

列表 10.21 自定义 Slack 通知消息属性

def notifySlack(String buildStatus){
    buildStatus =  buildStatus ?: 'SUCCESSFUL'
    def colorCode = '#FF0000'
    def subject = "Name: '${env.JOB_NAME}'\n
Status: ${buildStatus}\nBuild ID: ${env.BUILD_NUMBER}"       ❶
    def summary = "${subject}\nMessage: ${commitMessage()}
\nAuthor: ${commitAuthor()}\nURL: ${env.BUILD_URL}"          ❷

    if (buildStatus == 'STARTED') {
        colorCode = '#546e7a'
    } else if (buildStatus == 'SUCCESSFUL') {
        colorCode = '#2e7d32'
    } else {
        colorCode = '#c62828c'
    }
    slackSend (color: colorCode, message: summary)
}

❶ 显示作业的名称、状态和构建号

❷ 保存主题值和 Git 信息(作者、提交消息)以及构建 URL

notifySlack() 方法将调用 commitAuthor()commitMessage() 来获取适当的信息。commitAuthor() 方法将通过执行 git show 命令返回提交作者的名称,如以下列表所示。

列表 10.22 获取作者的 Git 辅助函数

def commitAuthor(){
    sh 'git show -s --pretty=%an > .git/commitAuthor'           ❶
    def commitAuthor = readFile('.git/commitAuthor').trim()     ❷
    sh 'rm .git/commitAuthor'
    commitAuthor
}

❶ 使用 git show 命令显示提交消息的作者,并将输出保存到 commitAuthor 文件中

❷ 读取 commitAuthor 文件并删除额外空格

commitMessage() 方法将使用带有 HEAD 标志的 git log 命令来获取提交消息描述;请参见以下列表。

列表 10.23 获取提交消息的 Git 辅助函数

def commitMessage() {
    sh 'git log --format=%B -n 1 HEAD > .git/commitMessage'     ❶
    def commitMessage = readFile('.git/commitMessage').trim()   ❷
    sh 'rm .git/commitMessage'
    commitMessage
}

❶ 显示最后提交的消息描述,并将输出保存到 commitMessage 文件中

❷ 读取 commitMessage 内容并删除额外空格

如果我们推送更改,在 CI/CD 管道的末尾,Slack 通知应包含 Jenkins 作业名称、构建 ID 和其状态、作者姓名和提交描述,如图 10.36 所示。

图片

图 10.36 带有 Git 提交详细信息的 Slack 通知

对 movies-store、movies-marketplace 和 movies-parser 的 Jenkinsfiles 应用相同的更改。

注意:第十一章介绍了如何使用 Jenkins Slack 通知插件发送带有更改日志附件的通知。

10.4 使用 Jenkins 处理代码升级

维护多个 Swarm 集群环境在将代码升级到生产时是有意义的,可以避免在生产中破坏东西。同时,拥有类似生产的环境可以帮助您在生产中运行应用程序的镜像,并在预发布环境中重现问题,而不会影响您的客户。但这是有代价的。

注意:您可以通过在正常工作时间之外关闭实例来降低沙箱和预发布环境的成本。

话虽如此,在专用预发布 VPC 中创建一个新的 Swarm 集群用于预发布环境,该 VPC 使用 10.2.0.0/16 CIDR 块,或者将其部署在 Jenkins 部署的同一管理 VPC 中,如图 10.37 所示。

图片

图 10.37 在同一 VPC 内部署沙箱和预发布 Swarm 集群以及 Jenkins

通过运行以下命令在 watchlist-deployment GitHub 仓库上创建一个预生产分支:

git checkout -b preprod

创建一个使用 preprod 标签的 docker-compose.yml 文件,并将 SQS URL 更新为使用预发布队列,如下所示。

列表 10.24 预发布部署的 Docker Compose

version: "3.3"
services:
  movies-loader:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-loader:preprod
    environment:
      - AWS_REGION=eu-west-3
      - SQS_URL=https://sqs.REGION.amazonaws.com/ID/movies_to_parse_staging
  movies-parser:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-parser:preprod

使用用于部署 Swarm 预发布集群的 SSH 密钥对创建一个 Jenkins 凭据,类型为 SSH 用户名和私钥。给它命名为 swarm-staging,如图 10.38 所示。

图片

图 10.38 Swarm 预发布集群 SSH 凭据

创建一个类似于 develop 分支中的 Jenkinsfile,如下所示。更新 swarmManager 变量以引用预发布的 IP 或 DNS 记录。同时更新 SSH 代理凭据以使用 Swarm 预发布凭据。

列表 10.25 用于预发布部署的 Jenkinsfile

def swarmManager = 'manager.staging.domain.com'                  ❶
def region = 'AWS REGION'                                        ❷

node('master'){
    stage('Checkout'){
        checkout scm
    }

     sshagent (credentials: ['swarm-staging']){
         stage('Copy'){
            sh "scp -o StrictHostKeyChecking=no
docker-compose.yml ec2-user@${swarmManager}:/home/ec2-user"      ❸
        }

        stage('Deploy stack'){
            sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager}
'\$(\$(aws ecr get-login --no-include-email --region ${region})).
|| true"                                                         ❹
            sh "ssh -oStrictHostKeyChecking=no
ec2-user@${swarmManager}
docker stack deploy --compose-fil.
docker-compose.yml --with-registry-auth watchlist"               ❹
        }
     }
}

❶ Swarm 管理器 DNS 别名记录或私有 IP 地址

❷ 创建 ECR 存储库的 AWS 区域

❸ 通过 SSH 将 docker-compose.yml 复制到 Swarm 管理实例

❹ 使用 ECR 进行身份验证并通过 SSH 重新部署应用程序堆栈

将更改推送到预生产分支。在 Jenkins 上,当推送事件发生时,应在 watchlist-deployment 项目上触发一个新的预生产嵌套作业,如图 10.39 所示。

图片

图 10.39 预发布上的堆栈部署

在管道的末尾,应用程序堆栈将被部署到 Swarm 预发布。同样,要访问应用程序,使用 Terraform 部署一个用于市场的公共负载均衡器和商店 API 的公共负载均衡器。

最后,为了在 preprod 上触发自动部署,我们需要为每个项目更新 Jenkinsfile,以触发 preprod 上的 watchlist-deployment——例如,对于 movies-loader Jenkinsfile。我们构建并推送带有 preprod 标签的 Docker 镜像,如下一列表所示。

列表 10.26 基于 Git 分支标记 Docker 镜像

stage('Push'){
    sh "\$(aws ecr get-login --no-include-email --region ${region}) || true" ❶
    docker.withRegistry("https://${registry}") {
        docker.image(imageName).push(commitID())                             ❷
        if (env.BRANCH_NAME == 'develop') {                                  ❸
            docker.image(imageName).push('develop')                          ❸
        }                                                                    ❸
        if (env.BRANCH_NAME == 'preprod') {                                  ❸
            docker.image(imageName).push('preprod')                          ❸
        }                                                                    ❸
    }
}

❶ 使用 AWS CLI 通过 ECR 进行认证

❷ 使用当前的 Git 提交 ID 标记镜像并将其存储在 ECR 中

❸ 基于当前 Git 分支名称,Docker 镜像被标记为唯一的标签。

在以下列表中,我们更新 Deploy 阶段的 if 子句条件,以在分支名称是 preprod 时触发外部作业的部署。

列表 10.27 触发外部部署作业

stage('Deploy'){
    if(env.BRANCH_NAME == 'develop' || env.BRANCH_NAME == 'preprod'){
        build job: "watchlist-deployment/${env.BRANCH_NAME}"
    }
}

将更改推送到 develop 分支。然后,在 Jenkins 发布关于 develop 变更的构建状态(如图 10.40)后,创建一个拉取请求以合并 develop 到 preprod 分支。

图片

图 10.40 拉取请求构建状态

当发生合并时,应该在 preprod 分支上触发一个新的构建,如图 10.41 中的 Blue Ocean 视图所示。

图片

图 10.41 preprod 分支上的构建触发

一旦执行了 Push 阶段,应该将带有 preprod 标签的新镜像推送到 Docker 仓库(如图 10.42)。

图片

图 10.42 存储在 ECR 中的带有 preprod 标签的 Docker 镜像

然后,preprod 分支上的部署作业将被执行,以在 Docker Swarm 阶段环境中部署更改(如图 10.43)。

图片

图 10.43 自动触发的阶段部署

对其他微服务进行相同的更改,除了 movies-marketplace。对于 movies-marketplace,我们需要更新构建阶段,如以下列表所示,以注入适当的环境和将前端指向正确的 API URL。

列表 10.28 在构建过程中注入 API URL

stage('Build'){
    switch(env.BRANCH_NAME){
        case 'develop':
            docker.build(imageName, '--build-arg ENVIRONMENT=sandbox .')  ❶
            break
        case 'preprod':
            docker.build(imageName, '--build-arg ENVIRONMENT=staging .')
            break
        default:
            docker.build(imageName)                                       ❷
    }
}

❶ 如果分支名称是 develop,我们将环境设置为 sandbox,因此会加载 sandbox 设置。

❷ 如果分支名称不匹配 develop 或 preprod,则默认加载 sandbox 设置。

将更改推送到 GitHub。这次,Docker 构建过程将以 ENVIRONMENT 参数设置为 staging(当当前分支是 preprod)的方式执行,如图 10.44 所示。这将用 environment .staging.ts 的值替换 environment.ts 文件。

图片

图 10.44 以环境作为参数的 Docker 构建

10.5 实现 Jenkins 交付管道

最后,要将我们的应用程序堆栈部署到生产环境,您需要为生产环境启动一个新的 Swarm 集群。再次,我选择将生产工作负载隔离在专用的生产 VPC 中,该 VPC 使用 10.3.0.0/16 CIDR 块,并在管理 VPC(Jenkins 所在位置)和生产 VPC(Swarm 生产部署位置)之间设置 VPC 对等连接。图 10.45 总结了已部署的架构。

图 10.45 多个 Swarm 集群 VPC 的 VPC 对等连接。部署 Jenkins 集群的 VPC 可以访问沙盒、测试和生产 VPC。

注意:VPC 对等连接不支持传递对等连接。生产、测试和沙盒环境是完全隔离的,例如,数据包不能直接从沙盒路由到生产环境,通过管理 VPC 进行路由。

在 watchlist-deployment 仓库的主分支上创建一个 docker-compose .yml 文件。这次,我们使用 latest 标签为生产环境中的服务,如下所示。

列表 10.29 生产部署的 Docker Compose。

version: "3.3"
services:
  movies-loader:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-loader:latest
    environment:
      - AWS_REGION=eu-west-3
      - SQS_URL=https://sqs.REGION.amazonaws.com/ID/movies_to_parse_production
  movies-parser:
    image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-parser:latest

创建一个 Jenkins 凭据,使用用于部署生产环境 Swarm 集群的 SSH 密钥,并将其命名为 swarm-production,如图 10.46 所示。

图 10.46 Swarm 生产集群 SSH 凭据。

然后,创建一个 Jenkinsfile,如下所示,以远程上传 docker-compose.yml 文件到管理机器。执行 docker stack deploy 命令以部署应用程序。

列表 10.30 生产部署的 Jenkinsfile。

def swarmManager = 'manager.production.domain.com'
def region = 'AWS REGION'
node('master'){
    stage('Checkout'){...}                          ❶

     sshagent (credentials: ['swarm-production']){
         stage('Copy'){...}                         ❷

        stage('Deploy stack'){...}                  ❸
     }
}

❶ 克隆 GitHub 仓库——有关说明,请参阅 10.25 列表。

❷ 通过 SSH 将 docker-compose.yml 复制到 Swarm 管理器——有关说明,请参阅 10.25 列表。

❸ 通过 SSH 重新部署 Docker Compose 堆栈——有关说明,请参阅 10.25 列表。

将更改推送到主分支。GitHub 仓库应类似于图 10.47。

图 10.47 存储在 GitHub 仓库中的部署文件。

Jenkins 管道将在主分支上触发。一旦管道完成,应用程序堆栈将被部署到生产环境,如图 10.48 所示。

图 10.48 在主分支中触发的部署。

要在 CI 管道末尾触发生产部署,请更新 GitHub 仓库以触发部署作业,如果当前分支是 master。例如,更新 movies-loader 的 Jenkinsfile 以构建生产镜像,并使用 latest 标签将结果推送到 Docker 仓库,如下所示。

列表 10.31 标记生产镜像。

stage('Push'){
    sh "\$(aws ecr get-login --no-include-email --region ${region}) || true"
    docker.withRegistry("https://${registry}") {
        docker.image(imageName).push(commitID())
        if (env.BRANCH_NAME == 'develop') {
            docker.image(imageName).push('develop')
        }
        if (env.BRANCH_NAME == 'preprod') {
            docker.image(imageName).push('preprod')
        }
        if (env.BRANCH_NAME == 'master') {
            docker.image(imageName).push('latest')
        }
    }
}

对于部署部分,我们可以简单地更新 if 子句以支持在主分支上部署:

stage('Deploy'){
            if(env.BRANCH_NAME == 'develop.
|| env.BRANCH_NAME == 'preprod.
|| env.BRANCH_NAME == 'master'){
                build job: "watchlist-deployment/${env.BRANCH_NAME}"
            }
}

然而,我们希望在部署到生产之前要求手动验证,以模拟产品/业务验证(或 QA 团队在生产批准前运行测试)在部署发布到生产之前。

要这样做,您可以使用输入步骤插件暂停管道执行,并允许用户交互和控制部署过程到生产环境,如下面的列表所示。

列表 10.32 在生产部署前要求用户批准

stage('Deploy'){
   if(env.BRANCH_NAME == 'develop' || env.BRANCH_NAME == 'preprod'){
      build job: "watchlist-deployment/${env.BRANCH_NAME}"
   }
   if(env.BRANCH_NAME == 'master'){
      timeout(time: 2, unit: "HOURS") {
         input message: "Approve Deploy?", ok: "Yes"
      }
      build job: "watchlist-deployment/master"
   }
}

在这里,我们将超时设置为 2 小时,以给开发者足够的时间验证发布。当 2 小时超时到达时,管道将被终止。

注意:为了避免 Jenkins 工作节点在 2 小时内无所事事,您可以将Deploy阶段移出节点块。您还可以在等待用户输入时发送 Slack 提醒。

将更改推送到功能分支,并在功能分支成功构建并被 Jenkins 批准后(图 10.49),提出合并更改到 develop 分支的拉取请求。

图片

图 10.49 将功能分支合并到 develop 分支

将更改合并到 develop 分支并删除功能分支。应在 develop 分支上触发新的构建,这将部署镜像到 Swarm 沙箱集群;见图 10.50。

图片

图 10.50 触发沙箱部署

接下来,提出合并 develop 到 preprod 分支的拉取请求(图 10.51)。

一旦 PR 合并,预生产分支将触发新的构建,在 CI/CD 管道的末尾。更改将被部署到 Swarm 预发布集群,如图 10.52 所示。

图片

图 10.51 将 develop 分支合并到 preprod

图片

图 10.52 触发到预发布集群的部署

最后,创建一个拉取请求将预生产合并到主分支(图 10.53)。

图片

图 10.53 将 preprod 分支合并到 master

当合并发生时,Jenkins 将在 movies-loader 服务的 master 分支上触发构建,如图 10.54 所示。然而,这次,一旦达到部署阶段,将弹出部署确认对话框。

图片

图 10.54 主分支上的 CI/CD 管道执行

正如图 10.55 所示,交互式输入将询问我们是否批准部署。

图片

图 10.55 部署用户输入对话框

如果我们点击是,管道将被恢复,并在 master 上触发部署作业,如图 10.56 所示。

图片

图 10.56 生产部署批准

在部署过程结束时,新的堆栈将被部署到 Swarm 生产环境中,并且会向配置的 Slack 频道发送通知(图 10.57)。

图片

图 10.57 生产部署成功通知

在完成生产部署后,您已经了解了如何将容器化的微服务应用程序部署到多个环境,以及如何在 CI/CD 管道中处理代码升级。然而,因为我们只管理三个环境(沙盒、预发布和生产),我们将通过定义正则表达式将部署作业的发现行为限制为三个主要分支,如图 10.58 所示。

图片

图 10.58 基于 regular expression 的 Jenkins 发现行为

因此,只有当三个主要分支之一发生变化时,Jenkins 才会发现并触发;见图 10.59。

图片

图 10.59 部署多分支作业

因此,现在如果我们对我们的应用程序进行任何更改,CI/CD 管道将被触发,并且将执行 docker stack deploy,这将更新任何从上一个版本更改的服务。

注意:如果部署目标是单个主机,则不需要 Swarm。本章中解释的相同的 docker-compose.yml 和程序足以在单主机部署环境中持续部署您的应用程序。

摘要

  • 可以使用 S3 存储桶或分布式一致性的键值存储,如 etcd、Consul 或 ZooKeeper,作为服务发现,使节点自动加入 Swarm 集群。

  • 通过在 Swarm 管理器上通过 SSH 执行 docker stack deploy,可以在 Swarm 集群上实现容器的持续部署。

  • 在 CI/CD 管道中添加 Slack 通知可以加快产品交付。团队成员越早意识到构建、集成或部署失败,他们就能越快采取行动。

  • 在部署生产版本之前模拟业务/产品验证,Jenkins 输入步骤插件可以在部署前提示用户进行手动验证。

11 在 K8s 上 Docker 化微服务

本章涵盖

  • 使用 Terraform 在 AWS 上设置 Kubernetes 集群

  • 使用 Jenkins 管道自动化 Kubernetes 上的应用程序部署

  • 打包和版本化 Kubernetes Helm 图表

  • 使用 Kompose 将 Compose 文件转换为 Kubernetes 清单

  • 在 CI/CD 管道中运行部署后测试和健康检查

  • 发现 Jenkins X 并设置无服务器 CI/CD 管道

前一章介绍了如何从头开始为在 Docker Swarm 中运行的容器化应用程序设置 CI/CD 管道(图 11.1)。本章介绍了如何在 Kubernetes(K8s)中部署相同的应用程序并自动化部署。此外,您还将学习如何使用 Jenkins X 来简化在 Kubernetes 中运行的本地区域应用程序的工作流程。

图 11.1 当前 CI/CD 管道工作流程

Docker Swarm 可能是初学者和较小工作负载的好解决方案。然而,对于大型部署和一定规模的工作负载,您可能需要考虑转向 Kubernetes。

对于那些 AWS 高级用户来说,Amazon Elastic Kubernetes Service (EKS) 是一个自然的选择。其他云服务提供商也提供托管 Kubernetes 解决方案,包括 Azure Kubernetes Service (AKS) 和 Google Kubernetes Engine (GKE)。

11.1 设置 Kubernetes 集群

如我所说,AWS 提供了 Amazon Elastic Kubernetes Service (aws.amazon.com/eks)。EKS 集群将在多个私有子网中的自定义 VPC 内部署。EKS 在多个 AWS 可用区运行 Kubernetes 控制平面,以消除单点故障,如图 11.2 所示。

图 11.2 AWS EKS 架构由部署在私有子网中的节点组组成。

一些工具(包括 AWS CloudFormation、eksctl 和 kOps)允许您快速在 EKS 上启动和运行。在本章中,我们选择了 Terraform,因为我们已经在 AWS 上使用它来管理我们的 Jenkins 集群。

要开始,请为沙箱环境配置一个新的 VPC 并将其划分为两个私有子网。Amazon EKS 至少需要两个可用区的子网。VPC 是为了隔离 Kubernetes 工作负载而创建的。为了使 EKS 能够发现 VPC 子网并管理网络资源,我们使用 kubernetes.io/cluster/<cluster-name> 标记它们。《cluster-name》的值与 EKS 集群的名称匹配,即 sandbox。创建一个名为 vpc.tf 的文件,其内容如下所示。

列表 11.1 Kubernetes 自定义 VPC

resource "aws_vpc" "sandbox" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  tags = {
    Name   = var.vpc_name
    Author = var.author
    "kubernetes.io/cluster/${var.cluster_name}" = "shared"
  }
}

然后,定义子网并设置适当的路由表。有关完整源代码,请参阅 chapter11/eks/vpc.tf,或返回 chapter 10 以获取在 AWS 上部署自定义 VPC 的分步指南。

接下来,我们创建一个新的 eks_masters.tf 文件,并定义 sandbox EKS 集群,这是一个托管的 K8s 控制平面,如下所示。

列表 11.2 EKS 沙箱集群

resource "aws_eks_cluster" "sandbox" {
  name            = var.cluster_name
  role_arn        = aws_iam_role.cluster_role.arn
  vpc_config {
    security_group_ids = [aws_security_group.cluster_sg.id]
    subnet_ids         = [for subnet in aws_subnet.private_subnets : subnet.id]
  }
  depends_on = [
    aws_iam_role_policy_attachment.cluster_policy,
    aws_iam_role_policy_attachment.service_policy,
  ]
}

管理控制平面使用具有 AmazonEKSClusterPolicy 和 AmazonEKServicePolicy 策略的 IAM 角色。这些附加项授予集群它需要自行管理的权限。

现在是时候启动一些工作节点了。节点是一个简单的 EC2 实例,它运行 Kubernetes 对象(pods、deployments、services 等)。主机的自动调度会考虑每个节点上的可用资源。在 eks_workers.tf 中定义 EKS 节点组资源,如下所示。

列表 11.3 Kubernetes 节点组资源

resource "aws_eks_node_group" "workers_node_group" {
  cluster_name    = aws_eks_cluster.sandbox.name
  node_group_name = "${var.cluster_name}-workers-node-group"
  node_role_arn   = aws_iam_role.worker_role.arn
  subnet_ids      = [for subnet in aws_subnet.private_subnets : subnet.id]
  scaling_config {
    desired_size = 2
    max_size     = 5
    min_size     = 2
  }
  depends_on = [
    aws_iam_role_policy_attachment.worker_node_policy,
    aws_iam_role_policy_attachment.cni_policy,
    aws_iam_role_policy_attachment.ecr_policy,
  ]
}

我们还创建了一个 IAM 角色,工作节点将假定该角色。我们授予 AmazonEKSWorkerNodePolicy、AmazonEKS_CNI_Policy 和 AmazonEC2ContainerRegistryReadOnly 策略。有关完整源代码,请参阅 chapter11/eks/eks_workers.tf。

注意:本节假设你熟悉通常的 Terraform 计划/应用工作流程;如果你是 Terraform 的新手,请首先参考第五章。

最后,在variables.tf文件中定义表 11.1 中列出的变量。

表 11.1 EKS Terraform 变量

变量 类型 描述
region 字符串 None 部署 EKS 集群的区域名称,例如eu-central-1
shared_credentials_file 字符串 ~/.aws/credentials 共享凭据文件的路径。如果未设置且指定了配置文件,则将使用~/.aws/credentials
aws_profile 字符串 profile 在共享凭据文件中设置的 AWS 配置文件名称
author 字符串 None EKS 集群的所有者名称。标记你的 AWS 资源以跟踪按所有者或环境划分的月度成本是可选的,但建议这么做。
availability_zones 列表 None 启动 VPC 子网的可用区
vpc_name 字符串 sandbox VPC 的名称
cidr_block 字符串 10.1.0.0/16 VPC CIDR 块
cluster_name 字符串 sandbox EKS 集群的名称
public_subnets_count 数字 2 要创建的公共子网数量
private_subnets_count 数字 2 要创建的私有子网数量

然后,执行terraform init命令以初始化工作目录并下载 AWS 提供者插件。在你的初始化目录中,运行terraform plan以审查计划中的操作。你的终端输出应指示计划正在运行以及将要创建的资源。这应包括 EKS 集群、VPC 和 IAM 角色。

如果你对执行计划感到满意,请使用terraform apply确认运行。此配置过程可能需要几分钟。部署成功后,将为沙箱环境部署一个新的 EKS 集群,并在 AWS EKS 控制台中可用,如图 11.3 所示。

图 11.3 EKS 沙箱集群

现在您已经配置了 EKS 集群,您需要配置 kubectl。这是一个用于与集群 API 服务器通信的命令行实用程序。在撰写本书时,我使用的是版本 v1.18.3。

注意:kubectl 工具在许多操作系统包管理器中可用;有关安装说明,请参阅官方文档(kubernetes.io/docs/tasks/tools/)。

要授予 kubectl 对 K8s API 的访问权限,我们需要生成一个 kubeconfig 文件(位于您家目录下的.kube/config)。您可以使用 AWS CLI 的update-kubeconfig命令创建或更新 kubeconfig 文件。运行以下命令以获取您集群的访问凭证:

aws eks update-kubeconfig --name sandbox --region AWS_REGION

要验证您的集群配置正确且正在运行,请执行以下命令。

kubectl get nodes

输出将列出集群中的所有节点以及每个节点的状态:

图片

注意:为了优化 K8s 成本,您可以使用 EC2 Spot 实例,因为它们比按需实例便宜约 30-70%。然而,这需要一些特殊考虑,因为它们可能只有 2 分钟的警告就被终止。

到目前为止,您应该能够使用 Kubernetes。在下一节中,我们将按照 PaC 方法使用 Jenkins 自动化第七章中描述的 Watchlist 应用程序的部署到 K8s 集群。

11.2 使用 Jenkins 自动化持续部署流程

要从 Jenkins 完成 Kubernetes 部署,我们只需要 K8s 部署文件,这些文件将包含对 Docker 镜像的引用,以及配置设置(例如端口、网络名称、标签和约束)。要运行此文件,我们需要执行kubectl apply命令。

在 watchlist-deployment GitHub 仓库的 develop 分支上,创建一个 deployments 文件夹。在其内部,使用您喜欢的文本编辑器或 IDE 创建一个 movies-loader-deploy.yaml 文件,内容如下所示。该部署指令指导 Kubernetes 如何创建和更新 movies-loader 服务。

列表 11.4 Movie loader 部署资源

apiVersion: apps/v1
kind: Deployment
metadata:
  name: movies-loader
  namespace: watchlist
spec:
  selector:
    matchLabels:
      app: movies-loader
  template:
    metadata.
      labels.
        app: movies-loader
    spec:
      containers:
      - name: movies-loader
        image: ID.dkr.ecr.REGION.amazonaws.com/USER/movies-loader:develop
        env:
        - name: AWS_REGION
          value: REGION
        - name: SQS_URL
          value: https://sqs.REGION.amazonaws.com/ID/movies_to_parse_sandbox

注意:作为提醒,movies-loader 和 movies-store 服务分别使用 Amazon SQS 加载和消费电影条目。为了授予这些服务与 SQS 交互的权限,您需要将 AmazonSQSFullAccess 策略分配给 EKS 节点组。

movies-loader 服务可以通过部署资源部署到 Kubernetes。部署定义使用 movies-loader Docker 镜像的develop标签,并定义了一组环境变量,例如 SQS URL 和 AWS 区域。MongoDB 资源也可以使用以下列表中的 mongodb-deploy.yaml 文件进行部署。

列表 11.5 MongoDB 部署资源

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mongodb
  namespace: watchlist
spec:
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata.
      labels.
        app: mongodb
    spec:
      containers:
      - name: mongodb
        image: bitnami/mongodb:latest
        env:
        - name: MONGODB_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongodb-access
              key: username
        - name: MONGODB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongodb-access
              key: password
        - name: MONGODB_DATABASE
          valueFrom:
            secretKeyRef:
              name: mongodb-access
              key: database

这个部署定义中最有趣的部分是环境变量部分。我们不是使用硬编码的 MongoDB 凭据,而是使用 K8s 机密。我们正在创建秘密存储认证凭据,以便只有 Kubernetes 可以访问它们。

在我们创建 Kubernetes 机密之前,我们需要在 Kubernetes 集群中维护一个空间,以便我们可以查看我们用于构建和运行应用程序的 pods、服务和部署的列表。我们将使用以下命令创建一个专用命名空间来关联所有我们的 Kubernetes 对象:

kubectl create namespace watchlist

然后,在您的本地机器上执行以下 Kubernetes 命令以创建 MongoDB 凭据机密:

kubectl create secret generic mongodb-access --from-literal=database='watchlist' 
--from-literal=username='root.
--from-literal=password='PASSWORD' -n watchlist

为其余服务创建部署文件:movies-store、movies-parser 和 movies-marketplace。部署文件夹结构应如下所示:

mongodb-deploy.yaml
movies-store-deploy.yaml
movies-loader-deploy.yaml
movies-parser-deploy.yaml
movies-marketplace-deploy.yaml

所有源代码都可以从 GitHub 仓库下载,位于 chapter11/deployment/kubectl/deployments 文件夹下。

要使用 Jenkins 部署应用程序,请在 watchlist-deployment 项目的顶级目录中创建一个 Jenkinsfile.eks 文件,如下所示。Jenkinsfile 将使用 aws eks update-kubeconfig 命令配置 kubectl。然后它发出一个 kubectl apply 命令来部署部署资源。kubectl apply 命令将部署文件夹作为参数。

列表 11.6 Jenkinsfile 部署阶段

def region = 'AWS REGION'                                                   ❶
def accounts = [master:'production', preprod:'staging', develop:'sandbox']

node('master'){
    stage('Checkout'){
        checkout scm
    }

    stage('Authentication'){
        sh "aws eks update-kubeconfig 
        --name ${accounts[env.BRANCH_NAME]} --region ${region}"             ❷
    }

    stage('Deploy'){
        sh 'kubectl apply -f deployments/'                                  ❸
    }
}

❶ 部署 EKS 集群的 AWS 区域

❷ 配置 kubectl 以连接到 Amazon EKS 集群

❸ 将新更改部署到 EKS

在将 Jenkinsfile 和部署文件推送到 Git 远程仓库之前,我们需要在 Jenkins 主机上安装 kubectl 命令行。此外,我们需要通过 IAM 角色提供对 EKS 的访问权限。为了授予 Jenkins 主机与 K8s 集群交互的权限,我们必须编辑 Kubernetes 中的 aws-auth ConfigMap。在您的本地机器上,运行以下命令:

kubectl edit -n kube-system configmap/aws-auth

将打开一个文本编辑器;将 Jenkins 实例的 IAM 角色添加到 mapRoles 部分。然后保存文件并退出文本编辑器。使用以下命令检查 ConfigMap 是否配置正确:

kubectl describe -n kube-system configmap/aws-auth

一旦配置了 ConfigMap,安装 aws-iam-authenticator,这是一个用于管理 Kubernetes 访问的 AWS IAM 凭据的工具。有关安装指南,请参阅 AWS 文档mng.bz/AOWW。然后,使用 AWS CLI 的 update-kubeconfig 命令生成 kubeconfig。命令应创建一个没有警告的 /home/ec2-user/.kube/config 文件。现在我们可以发出 kubectl get nodes 命令:

现在,我们已经准备好将 Jenkinsfile 和 Kubernetes 部署文件推送到 develop 分支下的 Git 仓库:

git add .
git commit -m "k8s deployment files"
git push origin develop

在推送 K8s 部署文件后,GitHub 仓库内容应类似于图 11.4。

图 11.4 Git 仓库中的 Kubernetes 部署文件

一旦提交更改,我们在第 7.6 节中创建的 GitHub webhook 将在 develop 分支的嵌套作业上触发 watchlist-deployment 多分支作业的构建;见图 11.5。

图片

图 11.5 kubectl apply命令的输出。

Deploy阶段,将执行kubectl apply命令以部署应用程序部署资源。在您的本地机器上,运行此命令以列出在沙盒 K8s 集群中运行的部署:

kubectl get deployments --namespace=watchlist

我们应用程序的四个组件(加载器、解析器、存储器和市场)将与 MongoDB 服务器一起部署:

图片

这些部署资源正在引用存储在 Amazon ECR 中的 Docker 镜像。在部署 EKS 集群时,我们已经授予 K8s 集群与 ECR 交互的权限。然而,如果您的 Docker 镜像托管在需要用户名/密码身份验证的远程仓库中,您需要使用以下命令创建一个 Docker Registry 秘密:

kubectl create secret docker-registry registry 
--docker-username=USERNAME
--docker-password=PASSWOR.
--namespace watchlist

然后,您需要在部署文件中的spec部分引用此秘密,如下所示:

spec:
  containers:
  - name: movies-loader
    image: REGISTRY_URL/USER/movies-loader:develop
  imagePullSecrets:
  - name: registry

我们的应用程序已部署。要访问它,我们需要为市场和服务创建 K8s 服务,如下所示列表。在根仓库中创建一个服务目录,然后创建一个名为 movies-store 的服务,称为 movies-store.svc.yaml。该服务创建一个云网络负载均衡器(例如,AWS Elastic Load Balancer)。这提供了一个外部可访问的 IP 地址,用于访问电影存储 API。

列表 11.7 电影存储服务资源

apiVersion: v1
kind: Service
metadata:
  name: movies-store
  namespace: watchlist
spec:
  ports:
  - port: 80
    targetPort: 3000
  selector:
    app: movies-store
  type: LoadBalancer

此外,我们创建另一个服务以公开电影市场(UI)。将以下列表中的内容添加到 movies-marketplace.svc.yaml 中。

列表 11.8 电影市场服务资源

apiVersion: v1
kind: Service
metadata:
  name: movies-marketplace
  namespace: watchlist
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: movies-marketplace
  type: LoadBalancer

movies-store 和 movies-parser 服务将电影元数据存储在 MongoDB 服务中。因此,我们需要通过 Kubernetes 服务公开 MongoDB 部署,以允许 MongoDB 接收传入的操作。该服务暴露在集群的内部 IP 上。ClusterIP关键字使服务仅可在集群内部访问。由服务针对的 MongoDB pod 由LabelSelector确定。将以下 YAML 块添加到 mongodb-svc.yaml。

列表 11.9 电影市场服务资源

apiVersion: v1
kind: Service
metadata:
  name: mongodb
  namespace: watchlist
spec:
  ports:
    - port: 27017
  selector:
    app: mongodb
    tier: mongodb
  clusterIP: None

最后,我们将列表 11.6 中的 Jenkinsfile 更新为通过将服务文件夹作为参数提供给kubectl apply命令来部署 Kubernetes 服务:

stage('Deploy'){
        sh 'kubectl apply -f deployments/'
        sh 'kubectl apply -f services/'
}

将更改推送到 develop 分支。将触发新的构建,并将服务部署,如图 11.6 所示。

图片

图 11.6 kubectl apply命令的输出

在您的本地机器上输入以下命令:

kubectl get svc -n watchlist

它应该显示三个 K8s 服务的负载均衡器:

图片

在 AWS 管理控制台中,应在 EC2 仪表板中创建两个面向公众的负载均衡器(mng.bz/Zx7Z),如图 11.7 所示。

图片

图 11.7 电影商店和市场 ELB

注意:确保在电影市场项目的 environment.sandbox.tf 文件中设置负载均衡器的 FQDN。API URL 将在构建市场 Docker 镜像时注入。有关更多详细信息,请参阅第 9.1.2 节。

要保护对商店 API 的访问,我们可以通过更新电影商店服务并应用以下列表中详细说明的更改来在公共负载均衡器上启用 HTTPS 监听器。

列表 11.10 HTTPS 监听器配置

apiVersion: v1
kind: Service
metadata:
  name: movies-store
  namespace: watchlist
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http   ❶
    service.beta.kubernetes.io/aws-load-balancer-ssl-cert:                ❶
     arn:aws:acm:{region}:{user id}:certificate/{id}                      ❶
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "https"       ❶
spec:
  ports:
  - name: http
    port: 80
    targetPort: 3000
  - name: https                                                           ❷
    port: 443                                                             ❷
    targetPort: 3000                                                      ❷
  selector:
    app: movies-store
  type: LoadBalancer

❶ 用于在监听器后面指定后端(pod)所使用的协议

❷ 公开端口 443(HTTPS)并将请求内部转发到电影商店 pod 的端口 3000

将更改推送到远程仓库。Jenkins 将部署更改并更新负载均衡器监听器配置,以在端口 443(HTTPS)上接受传入流量,如图 11.8 所示。

图片

图 11.8 负载均衡器 HTTP/HTTPS 监听器

这不是必需的,但你可以在 Amazon Route 53 中创建一个指向负载均衡器 FQDN 的 A 记录,并将 environment.sandbox.ts 更新为使用友好域名而不是负载均衡器 FQDN;请参阅以下列表。

列表 11.11 市场 Angular 环境变量

export const environment = {
  production: false,
  apiURL: 'https://api.sandbox.domain.com',
};

如果你将浏览器指向市场 URL,它应该调用电影商店 API 并列出从 IMDb 页面爬取的电影,如图 11.9 所示。DNS 传播和市场出现可能需要几分钟。

图片

图 11.9 观看列表市场应用程序

现在,每次你更改四个微服务中的任何一个的源代码时,管道都会被触发,更改将被部署到沙盒 Kubernetes 集群中,如图 11.10 所示。

图片

图 11.10 电影市场 CI/CD 工作流程。

最后,为了可视化我们的应用程序,我们可以在终端会话中运行以下命令来部署 Kubernetes 仪表板:

kubectl apply -f https://github.com/kubernetes-sigs/
metrics-server/releases/latest/download/components.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes/
dashboard/v2.0.5/aio/deploy/recommended.yaml

这些命令将在 kube-system 命名空间下部署 metrics-server 和 K8s 仪表板 v2.0.5。收集来自 Kubelet 的资源指标的 metrics-server 必须在集群中运行,以便在 Kubernetes 仪表板中可用指标和图形。

要从 K8s 仪表板授予对集群资源的访问权限,我们需要创建一个 eks-admin 服务账户和集群角色绑定,以具有管理员权限安全地连接到仪表板。创建一个 eks-admin.yaml 文件,其中包含以下列表中的内容(ClusterRoleBinding 资源的 apiVersion 可能在不同版本的 Kubernetes 中有所不同)。

列表 11.12 Kubernetes 仪表板服务账户

apiVersion: v1
kind: ServiceAccount
metadata:
  name: eks-admin
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: eks-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: eks-admin
  namespace: kube-system

然后,使用以下命令创建一个服务账户:

kubectl apply -f eks-admin.yaml

现在,创建一个代理服务器,它将允许你从本地机器上的浏览器导航到仪表板。这将一直运行,直到你通过按 Ctrl-C 停止进程。发出kubectl proxy命令,仪表板应该可以通过localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/login访问。

打开此 URL 将带我们到 Kubernetes 仪表板的账户认证页面。为了访问仪表板,我们需要认证我们的账户。使用以下命令检索 eks-admin 服务账户的认证令牌:

kubectl -n kube-system describe secret 
$(kubectl -n kube-system get secre.
| grep eks-admi.
| awk '{print $1}')

现在复制令牌并将其粘贴到登录屏幕上的“输入令牌”字段。点击“登录”按钮,就这样。你现在已作为管理员登录。

Kubernetes 仪表板,如图 11.11 所示,提供了用户友好的功能来管理和调试已部署的应用程序。太棒了!你已经在 K8s 中成功构建了一个云原生应用的 CI/CD 管道。

图 11.11 Kubernetes 仪表板

11.2.1 使用 Kompose 将 Docker Compose 迁移到 K8s 清单

创建部署文件的另一种方式是将第十章列表 10.12 中定义的 docker-compose.yml 文件转换为开源工具 Kompose。有关安装指南,请参阅项目的官方 GitHub 仓库(github.com/kubernetes/kompose)。

一旦安装了 Kompose,请对第十章提供的 docker-compose.yml 文件(chapter10/deployment/sandbox/docker-compose.yml)运行以下命令:

kompose convert -f docker-compose.yml

这应该会根据 docker-compose.yml 中指定的设置和网络拓扑创建 Kubernetes 部署和服务:

你可以将这些文件推送到远程 Git 仓库,Jenkins 将发出kubectl apply -f命令来部署服务和部署。

然而,为所有必需的 Kubernetes 对象编写和维护 Kubernetes YAML 清单可能是一项耗时且繁琐的任务。对于最简单的部署,你需要至少三个包含重复和硬编码值的 YAML 清单。这就是像 Helm (helm.sh/)这样的工具发挥作用来简化此过程并创建一个可以推广到你的集群的单个包的地方。

11.3 漫步通过持续交付步骤

Helm 是 Kubernetes 的有用包管理器。它有两个部分:客户端(CLI)和服务器(在 Helm 3 中被移除,称为 Tiller)。客户端位于你的本地机器上,服务器位于 Kubernetes 集群中以执行所需操作。

要完全掌握 Helm,你需要熟悉这三个概念。

  • 图表——一组预配置的 Kubernetes 资源

  • 发布—使用 Helm 部署到集群的图表的特定实例

  • Repository—一组已发布的图表,可以通过远程仓库提供给其他人

查看入门页面以获取有关下载和安装 Helm 的说明:helm.sh/docs/intro/install/.

注意 Helm 假设与 n-3 版本的 Kubernetes 兼容。请参阅 Helm 版本支持策略文档,以确定与您的 K8s 集群兼容的 Helm 版本。

在撰写本书时,正在使用 Helm v3.6.1。安装 Helm 后,在 watchlist-deployment 项目的顶级目录中创建一个名为 watchlist 的新图表:

helm create watchlist

这将创建一个名为 watchlist 的目录,包含以下文件和文件夹:

  • Values.yaml—定义了我们想要注入到 Kubernetes 模板中的所有值

  • Chart.yaml—可以用来描述我们正在打包的图表版本

  • .helmignore—类似于 .gitignore 和 .dockerignore,包含在打包 Helm 图表时排除的文件和文件夹列表

  • templates**/—包含实际的清单,如 Deployments、Services、ConfigMaps 和 Secrets

接下来,在模板文件夹内为每个微服务定义模板文件。模板文件描述了如何在 Kubernetes 上部署每个服务:

例如,movies-loader 模板文件夹使用我们在列表 11.4 中定义的相同部署文件,但它引用了在 values.yaml 中定义的变量。

deployment.yaml 文件负责根据 movies-loader Docker 镜像部署部署对象。此定义从 Docker 仓库拉取构建的 Docker 镜像,并在 Kubernetes 中创建一个新的部署;请参见以下列表。

列表 11.13 电影加载器部署

apiVersion: apps/v.
kind: Deploymen.
metadata.
  name: movies-loader
  namespace: {{ .Values.namespace }}
  labels.
    app: movies-loader
    tier: backen.
spec:
  selector.
    matchLabels.
      app: movies-loader
  template.
    metadata.
      name: movies-loader
      labels.
        app: movies-loader
        tier: backen.
      annotations:
        jenkins/build: {{ .Values.metadata.jenkins.buildTag | quote }}
        git/commitId: {{ .Values.metadata.git.commitId | quote }}
    spec:
      containers.
        - name: movies-loader
          image: "{{ .Values.services.registry.uri }}/
mlabouardy/movies-loader:{{ .Values.deployment.tag }}&quot.
          imagePullPolicy: Always
          envFrom:
            - configMapRef:
                name: {{ .Values.namespace }}-movies-loader
            - secretRef:
                name: {{ .Values.namespace }}-secrets
          {{- if .Values.services.registry.secret }}
          imagePullSecrets:
          - name: {{ .Values.services.registry.secret }}
          {{- end }}

Helm 图表使用 {{}} 进行模板化,这意味着其中的内容将被解释以提供输出值。我们还可以使用管道机制将两个或多个命令组合起来进行脚本编写和过滤。

movies-loader 容器引用的环境变量,如 AWS_REGIONSQS_URL,在 configmap.yaml 中定义,如下列表所示。

列表 11.14 电影加载器 ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Values.namespace }}-movies-loader
  namespace: {{ .Values.namespace }}
  labels.
    app: {{ .Values.namespace }}-movies-loader
data:
  AWS_REGION: {{ .Values.services.aws.region }}
  SQS_URL: https://sqs.{{ .Values.services.aws.region }}
.amazonaws.com/{{ .Values.services.aws.account }}/
movies_to_parse_{{ .Values.environment }}

部署文件还引用了敏感信息,例如 MongoDB 凭据。这些凭据存储在 Kubernetes 机密中,如下列表所示。

列表 11.15 应用程序密钥

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Values.namespace }}-secrets
  namespace: {{ .Values.namespace }}
data:
  MONGO_URI: {{ .Values.services.mongodb.uri | b64enc }}
  MONGO_DATABASE : {{ .Values.mongodb.mongodbDatabase | b64enc }}
  MONGODB_USERNAME : {{ .Values.mongodb.mongodbUsername | b64enc }.
  MONGODB_PASSWORD : {{ .Values.mongodb.mongodbPassword | b64enc }}

Helm 图表使得在 values.yaml 文件中设置可覆盖的默认值变得容易,允许我们定义基本设置。我们可以将尽可能多的变量从模板移动到 values.yaml 文件中。这样,我们可以在安装时轻松更新和注入新值:

namespace: 'watchlist'
services:
  registry:
    uri: ''
    secret: ''
deployment:
  tag: ''
  workers:
    replicas: 2

这允许我们创建一个可移植的包,在运行时可以通过覆盖值进行自定义:

此外,请注意在部署文件中使用自定义注释或元数据。我们将在 Helm 图表的构建过程中注入 Jenkins 构建 ID 和 Git 提交 ID。这可以用于调试和解决正在运行的 Kubernetes 部署的问题:

annotations:
        jenkins/build: {{ .Values.metadata.jenkins.buildTag | quote }}
        git/commitId: {{ .Values.metadata.git.commitId | quote }}

MongoDB 提供了一个稳定且官方的 Helm 图表,可用于在 Kubernetes 上直接安装和配置。我们在 Chart.yaml 下的 dependencies 部分将 MongoDB 图表定义为依赖项:

dependencies:
  - name: mongodb
    version: 7.8.10
    repository: https://charts.bitnami.com/bitnami
    alias: mongodb

现在我们已经定义了图表,在你的终端会话中,输入以下命令通过我们刚刚创建的 Helm 图表安装监视列表应用程序:

helm install watchlist ./watchlist -f values.override.yaml

命令接受 values.override.yaml 文件,其中包含在运行时覆盖的值,例如环境名称和 MongoDB 用户名和密码:

environment: 'sandbox'
mongodb:
  mongodbUsername: 'watchlist'
  mongodbPassword: 'watchlist'
deployment:
  tag: 'develop'
  workers:
    replicas: 2

通过检查部署和 Pod 的状态来检查安装进度。输入 kubectl get pods -n watchlist 以显示正在运行的 Pod:

注意:要检查发布生成的清单而不安装图表,请使用 --dry-run 标志以返回渲染的模板。

我们现在可以更新 Jenkinsfile(chapter11/Jenkinsfile.eks),使用 Helm 命令行而不是 kubectl。由于我们的应用程序图表已经安装,我们将使用 helm upgrade 命令来升级图表。此命令接受覆盖的参数值,并设置来自 Jenkins 环境变量 BUILD_TAGcommitID() 方法的注释值,如下所示。

列表 11.16 在 Jenkins 管道中升级 Helm

stage('Deploy'){
        sh """
            helm upgrade --install watchlis.
./watchlist -f values.override.yaml \
                --set metadata.jenkins.buildTag=${env.BUILD_TAG} \
                --set metadata.git.commitId=${commitID()}
        """
}

Helm 尝试执行最不侵入性的升级。它将只更新自上次发布以来已更改的内容。

将更改推送到开发分支。GitHub 仓库应类似于图 11.12。

图 11.12 监视列表 Helm 图表

在 Jenkins 上,将触发一个新的构建。在 Deploy 阶段结束时,将执行 helm upgrade 命令;输出如图 11.13 所示。

图 11.13 Helm 升级输出

现在,开发分支上的每次更改都将构建一个新的 Helm 图表并在沙盒集群上创建一个新的发布。如果 Docker 镜像已更改,Kubernetes 滚动更新提供了在 0% 停机时间内部署更改的功能。

注意:如果在发布过程中出现计划外的情况,可以使用 helm rollback 命令轻松回滚到之前的发布。

对于将代码提升到预发布环境,我们只需更新 .override.yaml 文件,将环境值设置为 staging 并使用 preprod 镜像标签,如下所示。

列表 11.17 预发布变量。

environment: 'staging'
mongodb:
  mongodbUsername: 'watchlist'
  mongodbPassword: 'watchlist'
deployment:
  tag: 'preprod'
  workers:
    replicas: 2

如果你将更改推送到预生产分支,应用程序将被部署到 Kubernetes 预发布集群,如图 11.14 所示。

图 11.14 预生产分支上的 CI/CD 工作流程

我们可以通过输入以下命令来验证预生产版本已被部署:

kubectl describe deployment movies-marketplace -n watchlist

movies-marketplace 部署具有 git/commitId 等于触发 Jenkins 作业的 GitHub 提交 ID 的注解,jenkins/build 注解的值是触发部署的 Jenkins 作业的名称(图 11.15)。

图片

图 11.15 Movies Marketplace 部署描述

对于生产部署,使用以下列表更新 values.override.yaml 中的适当值。在此示例中,我们将镜像标签设置为latest,环境设置为production,并配置了五个 movies-parser 服务的副本。

列表 11.18 生产变量

environment: production
mongodb:
  mongodbUsername: 'watchlist'
  mongodbPassword: 'watchlist'
deployment:
  tag: 'latest'
  workers:
    replicas: 5

将新文件推送到 master 分支。在流水线结束时,堆栈将被部署到 K8s 生产集群。

现在如果在任何一个四个微服务的 master 分支上发生推送事件,CI/CD 流水线将被触发,并请求用户批准,如图 11.16 所示。

图片

图 11.16 生产部署的用户批准

如果部署得到批准,watchlist-deployment 作业将被触发,并执行 master 嵌套作业。因此,将在生产环境中创建 watchlist 应用程序的新 Helm 发布,如图 11.17 所示。

图片

图 11.17 生产中的应用程序部署

部署过程完成后,将向预先配置的 Slack 频道发送 Slack 通知,如图 11.18 所示。

图片

图 11.18 生产部署 Slack 通知

运行kubectl get pods命令。这应该会基于 movies-parser Docker 镜像显示五个 Pod:

图片

要查看市场仪表板,在kubectl get services -n watchlist输出的EXTERNAL-IP列中找到负载均衡器的外部 IP:

图片

在您的浏览器中导航到该地址,应该会显示 Movies Marketplace UI,如图 11.19 所示。

图片

图 11.19 市场生产环境

在生产环境中,您应将负载均衡器的 FQDN 替换为 Route 53 中的别名。有关说明,请参阅官方 AWS 文档:mng.bz/Rq8P

11.4 使用 Helm 打包 Kubernetes 应用程序

到目前为止,您已经看到了如何为基于微服务应用程序创建单个图表,以及如何在新的 Git 提交后使用 Jenkins 创建新发布。另一种打包应用程序的方法是为每个微服务创建单独的图表,然后在主图表中引用这些图表作为依赖项(类似于 MongoDB 图表)。图 11.20 说明了 Helm 图表如何在 CI/CD 流水线中打包。

图片

图 11.20 使用 Helm 的容器化应用程序的 CI/CD

在推送事件上,将触发 Jenkins 构建,以构建 Docker 图像并将新版本打包到 Helm 图表中。从那里,新的图表被部署到相应的 Kubernetes 环境。在此过程中,将发送 Slack 通知以通知开发者有关管道状态。

在电影市场项目上,通过输入以下命令在顶级目录中创建一个新的 Helm 图表:

helm create chart

它应该创建一个名为 chart 的新文件夹,具有以下结构:

图片

如前所述,Helm 图表由用于帮助描述应用程序、定义对所需的最小 Kubernetes 和/或 Helm 版本的约束以及管理图表版本元数据组成。所有这些元数据都位于 Chart.yaml 文件中(chapter11/microservices/movies-marketplace),如下所示。

列表 11.19 电影加载器图表

apiVersion: v2
name: movies-marketplace
description: UI to browse top 100 IMDb movies
type: application
version: 1.0.0
appVersion: 1.0.0

为了能够从主 watchlist 图表中引用此图表,我们需要将其存储在某个地方。有许多开源解决方案可用于存储 Helm 图表。GitHub 可以用作 Helm 图表的远程注册库。创建一个名为 watchlist-charts 的新 GitHub 仓库,并创建一个空的 index.yaml 文件。此文件将包含有关存储库中可用图表的元数据。

注意 Nexus Repository OSS 也支持 Helm 图表。您可以将图表发布到 Nexus 托管的 Helm 仓库。

然后,通过执行以下命令将此文件推送到主分支。

git clone https://github.com/mlabouardy/watchlist-charts.git
cd watchlist-charts
touch index.yaml
git add index.yaml
git commit -m "add index.yaml"
git push origin master

GitHub 仓库将看起来像图 11.21。

图片

图 11.21 Helm 图表 GitHub 仓库

Helm 仓库是一个具有文件 index.yaml 和所有您的图表文件的 HTTP 服务器。为了将 GitHub 仓库变成 HTTP 服务器,我们将启用 GitHub Pages。

点击设置选项卡。向下滚动到 GitHub Pages 部分,并选择 master 分支作为源,如图 11.22 所示。

图片

图 11.22 启用 GitHub 页面

私有 Helm 仓库准备就绪并可供使用后,让我们打包并发布我们的第一个 Helm 图表。在 movies-marketplace 项目中,更新 Build 阶段以使用并行构建来构建 Docker 图像和 Helm 图表。Build 阶段应如下所示。(完整的 Jenkinsfile 可在 chapter11/pipeline/movies-marketplace/Jenkinsfile 中找到。)

列表 11.20 构建 Docker 图像和 Helm 图表

stage('Build') {
 parallel(
  'Docker Image': {
   switch (env.BRANCH_NAME) {
    case 'develop':
       docker.build(imageName, '--build-arg ENVIRONMENT=sandbox .')     ❶
     break
    case 'preprod':
       docker.build(imageName, '--build-arg ENVIRONMENT=staging .')     ❶
     break
    ...
   }
  },
  'Helm Chart': {
     sh 'helm package chart'                                            ❷
  }
 )
}

❶ 通过注入目标环境设置构建适当的 Docker 图像

❷ 将应用程序打包到 Helm 图表中

helm package 命令,如其名称所示,将图表目录打包到图表存档(movies-marketplace-1.0.0.tgz)。最后,更新 Push 阶段以使用并行步骤,如下所示。

列表 11.21 在私有注册表中存储 Docker 图像

stage('Push') {
 parallel(
  'Docker Image': {
    sh "\$(aws ecr get-login --no-include-email --region ${region}) || true" ❶
    docker.withRegistry("https://${registry}") {                             ❷
        docker.image(imageName).push(commitID())                             ❷
        if (env.BRANCH_NAME == 'develop') {                                  ❷
            docker.image(imageName).push('develop')                          ❷
        }                                                                    ❷
        ...                                                                  ❷
    }                                                                        ❷
  },
  'Helm Chart': {                                                            ❸
    ...
  }
 )
}

❶ 通过 ECR 进行身份验证,以便之后推送 Docker 图像

❷ 标记并存储图像到 ECR

❸ 将 Helm 图表发布到 GitHub——有关完整说明,请参阅列表 11.22。

Helm Chart阶段将使用git clone命令克隆 watchlist-charts GitHub 仓库,并使用helm repo index命令将新打包的 Helm 图表的元数据添加到 index.yaml 中。然后它将 index.yaml 和存档图表推送到 Git 仓库;请参阅以下列表。

列表 11.22 将 Helm 图表发布到 GitHub

'Helm Chart': {
    sh 'helm repo index --url https://mlabouardy.github.io/watchlist-charts/ .' ❶
    sshagent(['github-ssh']) {                                                  ❷
      sh 'git clone git@github.com:mlabouardy/watchlist-charts.git.
      sh 'mv movies-marketplace-1.0.0.tgz watchlist-charts/'
      dir('watchlist-charts'){                                                  ❸
          sh 'git add index.yaml movies-marketplace-1.0.0.tg.
&& git commit -m "movies-marketplace&quot.
&& git push origin master'                                                      ❹
      }
     }
  }

❶ 根据包含打包图表的目录生成索引文件

❷ 通过 ssh-agent 为构建提供 SSH 凭据

❸ 将当前目录更改为 watchlist-charts 文件夹

❹ 将存档和索引文件提交并推送到 GitHub

如果你将新的 Jenkinsfile 推送到 Git 远程仓库,将触发一个新的流水线,如图 11.23 所示。在构建阶段,将打包 movies-marketplace Docker 镜像和 Helm 图表。接下来,将执行推送阶段,将 Docker 镜像推送到 Docker 私有仓库,将 Helm 图表推送到 GitHub 仓库。

图 11.23 使用 Helm 和 Docker 的 CI/CD 工作流程

CI/CD 管道完成后,GitHub 仓库中将出现一个新的存档图表,如图 11.24 所示。

图 11.24 打包 Movies Marketplace 图表

index.yaml 文件将在entries部分引用新构建的 Helm 图表,如图 11.25 所示。

图 11.25 Helm 仓库元数据

你可以通过在打包 Helm 图表时使用--version标志提供新版本来覆盖 Chart.yaml 中设置的图表版本:

sh 'helm package chart --app-version ${appVersion} --version ${chartVersion}'

对其他仓库重复相同的步骤以为每个服务创建一个 Helm 图表。完成后,Helm 图表仓库应包含四个存档文件(图 11.26)。

图 11.26 存储在 GitHub 仓库中的应用图表

接下来,我们配置 GitHub 仓库作为 Helm 仓库:

helm repo add watchlist https://mlabouardy.github.io/watchlist-charts

最后,我们可以在以下列表中dependencies部分下的 watchlist Chart.yaml 文件中引用这些图表,如下所示。

列表 11.23 观察列表应用图表

apiVersion: v2
name: watchlist
description: Top 100 iMDB best movies in history
type: application
version: 1.0.0
appVersion: 1.0.0
maintainers:
    - name: Mohamed Labouardy
      email: mohamed@labouardy.com
dependencies:
  - name: mongodb
    version: 7.8.10
    repository: https://charts.bitnami.com/bitnami
    alias: mongodb
  - name: movies-loader
    version: 1.0.0
    repository: https://mlabouardy.github.io/watchlist-charts
  - name: movies-parser
    version: 1.0.0
    repository: https://mlabouardy.github.io/watchlist-charts
  - name: movies-store
    version: 1.0.0
    repository: https://mlabouardy.github.io/watchlist-charts
  - name: movies-marketplace
    version: 1.0.0
    repository: https://mlabouardy.github.io/watchlist-charts

现在所有组件都在一起运行,我们已经检查了核心功能,让我们验证该解决方案是否适用于典型的 GitFlow 开发流程。

11.5 运行部署后烟雾测试

微服务已部署。但这并不意味着这些服务已正确配置并且正确执行了它们应该执行的所有任务。

你希望有一个健康检查,指示你的服务的当前健康操作。你可以通过实现一个对服务 URL 的 HTTP 请求并检查响应代码是否为 200 来设置一个简单的检查。

例如,让我们为 movies-store 服务实现一个健康检查。更新 movies-store 项目的 Jenkinsfile(chapter11/pipeline/movies-store/Jenkinsfile),添加以下列表中显示的功能。

列表 11.24 返回 API URL 的 Groovy 函数

def getUrl(){
    switch(env.BRANCH_NAME){
        case 'preprod':
            return 'https://api.staging.domain.com'
        case 'master':
            return 'https://api.production.domain.com'
        default:
            return 'https://api.sandbox.domain.com'
    }
}

该函数根据当前的 Git 分支名称返回服务 URL。最后,我们在管道的末尾添加一个 Healthcheck 阶段,对服务 URL 执行 cURL 命令:

stage('Healthcheck'){
    sh "curl -m 10 ${getUrl()}"
}

-m 标志用于设置 10 秒的超时时间,以便在检查服务健康状态之前,给 Kubernetes 足够的时间拉取最新构建的镜像并将更改部署到集群中。

一旦你将更改推送到 Git 远程仓库,就会触发一个新的构建。CI/CD 管道完成后,将执行一个 cURL 命令,对服务 URL 进行 GET 请求,如图 11.27 所示。

图片

图片

如果服务在超时时间之前响应,cURL 命令将返回成功的退出代码。否则,将抛出错误,使管道失败。

然而,如果服务正在响应,这并不意味着它运行正确,或者服务的新版本已经成功部署。

为了能够对服务 URL 发出高级 HTTP 请求,我们将从 Jenkins 插件页面安装 Jenkins HTTP 请求插件(www.jenkins.io/doc/pipeline/steps/http_request/),如图 11.28 所示。

图片

图片 Jenkins HTTP 请求插件

我们现在可以更新 movies-store 的 Jenkinsfile。该插件提供了一个 httpRequest DSL 对象,可以用来调用远程 URL。在下面的列表中,httpRequest 返回一个响应对象,通过 content 属性公开响应体。然后,我们使用 JsonSlurper 类将响应解析为 JSON 对象。更新的 Healthcheck 阶段在下面的列表中显示。

列表 11.25 电影商店健康检查阶段

stage('Healthcheck'){
     def response = httpRequest getUrl()
     def json = new JsonSlurper().parseText(response.content)
     def version = json.get('version')

     if version != '1.0.0' {
        error "Expected API version 1.0.0 but got ${version}"
     }
}

服务返回在 Kubernetes 中部署的版本号。这个值在服务源代码中是固定的,但在构建服务的 Docker 镜像时,你可以将 Jenkins 构建 ID 注入为版本号,并在 Healthcheck 阶段检查返回的版本是否等于 Jenkins 构建 ID。

图片 显示了在 Kubernetes 中运行的每个微服务的 CI/CD 管道的最终结果。

图片

图片

当你选择使用 Jenkins 来构建运行在 Kubernetes 中的云原生应用时,你需要创建大量的配置,并且需要花费相当多的时间去学习和使用所有必要的插件来实现这一目标。幸运的是,Jenkins X 出现了,它提供了简单易用的模板。

11.6 发现 Jenkins X

Jenkins X (jenkins-x.io/)是 Kubernetes 上现代云应用的 CI/CD 解决方案。它用于简化配置,并让你利用 Jenkins 2.0 的力量。它还让你能够使用像 Helm、Artifact Hub、ChartMuseum、Nexus 和 Docker Registry 这样的开源工具,轻松构建云原生应用。

Jenkins X 为 Jenkins 添加了缺失的功能:对持续交付的全面支持,以及管理将项目提升到运行在 Kubernetes 中的预览、测试和生产环境的推广。它使用 GitOps 来管理部署到每个环境的 Kubernetes 资源的配置和版本。因此,每个环境都有自己的 Git 仓库,其中包含所有 Helm 图表、它们的版本以及在该环境中运行的应用程序的配置。

在遵循此方法时,Git 是基础设施代码和应用代码的唯一真相来源。所有对期望状态的变化都是 Git 提交。因此,很容易看到谁在何时进行了更改,更重要的是,如果更改导致不良后果,那么很容易回滚这些更改。

话虽如此,让我们动手实践,了解 Jenkins X 是如何工作的。要开始,请安装 Jenkins X CLI,并选择适合你操作系统的最合适说明:mng.bz/20ZX。运行jx version --short以确保你使用的是最新稳定版本。我在撰写本书时使用的是版本 2.1.71。

Jenkins X 运行在 Kubernetes 集群上。如果你在主要的云服务提供商(Amazon EKS、GKE 或 AKS)上运行,Jenkins X 提供了多种创建此集群的方法:

jx create cluster eks --cluster-name=watchlist
Jx create cluster aks --cluster-name=watchlist
Jx create cluster gke --cluster-name=watchlist
Jx create cluster iks --cluster-name=watchlist

注意:你可以通过参考官方指南在现有的 EKS 集群上运行 Jenkins X,官方指南请见jenkins-x.io/v3/admin/setup/operator/

通过在终端会话中运行以下命令,在 K8s 集群上安装 Jenkins X:

jx boot

你将需要回答一系列问题来配置安装,如图 11.30 所示。

安装完成后,你将看到有用的链接和 Jenkins X 相关服务的密码。不要忘记将其保存在某处以备将来使用。

Jenkins X 还部署了一系列支持服务,包括 Jenkins 仪表板、Docker Registry、ChartMuseum 和 Artifact Hub 来管理 Helm 图表,以及 Nexus,它作为 Maven 和 npm 仓库。

图片

图 11.30 Jenkins X 安装输出

以下是在kubectl get svc命令的输出:

图片

将你的浏览器指向安装过程中打印的 Jenkins URL,并使用图 11.30 中显示的管理员用户名和密码登录。图 11.31 中的仪表板应该被提供。

图片

图 11.31 Jenkins 网页仪表板

在安装 Jenkins X 的同时,你可以以无服务器模式运行 Jenkins。然后,你不需要运行持续消耗 CPU 和内存资源的 Jenkins 网页仪表板,而只需在你需要时运行 Jenkins。

Jenkins X 安装还默认创建了两个 Git 仓库:一个用于你的暂存环境,一个用于生产环境,如图 11.32 所示:

  • 暂存—在项目主分支上执行的任何合并都将自动作为新版本部署到暂存环境(自动提升)。

  • 生产—你必须手动使用 jx promote 命令将你的暂存应用程序版本提升到生产环境。

图 11.32 应用部署环境

Jenkins X 使用这些仓库来管理每个环境的部署,并且通过 Git pull 请求进行提升。每个仓库都包含一个 Helm 图表,指定要部署到相应环境的应用程序。每个仓库还有一个 Jenkinsfile 来处理提升。

现在你已经安装了 Jenkins X 的运行集群,我们将创建一个可以使用 Jenkins X 构建和部署的应用程序。为了清晰起见,我使用 Go 创建了一个 RESTful API,它提供了一个包含前 100 部 IMDb 电影的 HTTP 端点列表。我们将使用以下命令将此项目导入 Jenkins:

jx import

如果你希望导入一个已经存在于远程 Git 仓库中的项目,你可以使用 --url 参数:

jx import --url https://github.com/mlabouardy/jx-movies-store

以下为导入命令的输出:

Jenkins X 将检查代码并根据项目使用的编程语言选择正确的默认构建包。我们的项目是用 Go 开发的,所以它将是一个 Go 构建包。Jenkins X 将根据项目的运行环境生成 Jenkinsfile、Dockerfile 和 Helm 图表。导入命令将在 GitHub 上创建一个远程仓库,注册一个 webhook,并将代码推送到远程仓库,如图 11.33 所示。

图 11.33 应用 GitHub 仓库

Jenkins X 还会自动为项目创建一个 Jenkins 多分支管道作业,并且管道将被触发。你可以使用以下命令检查管道的进度:

jx get activity -f jx-movies-store -w

你也可以通过点击项目作业从 Jenkins 仪表板跟踪管道的进度;图 11.34 显示了结果。

图 11.34 应用构建管道

管道阶段在我们在前面配置的 Kubernetes 集群中运行的 Kubernetes pod 上执行,如图 11.35 所示。

图 11.35 基于 Kubernetes pod 的 Jenkins 工作节点

执行的管道将克隆仓库,构建 Docker 镜像,并将其推送到 Docker 仓库,如下面的列表所示。

列表 11.26 主分支上发生事件时的构建阶段

stage('Build Release') {
      when {
        branch 'master'
      }
      steps {
        container('go') {
          dir('/home/jenkins/agent/go/src/
github.com/mlabouardy/jx-movies-store') {
            checkout scm
            sh "git checkout master"
            sh "git config --global credential.helper store"
            sh "jx step git credentials"
            sh "echo \$(jx-release-version) > VERSION"
            sh "jx step tag --version \$(cat VERSION)"
            sh "make build"
            sh "export VERSION=`cat VERSION.
&& skaffold build -f skaffold.yaml"
            sh "jx step post build --image $DOCKER_REGISTRY/$ORG/$APP_NAME:\$(cat VERSION)"
          }
        }
      }
}

Helm 图表将被打包并推送到 ChartMuseum 仓库,同时在项目的 GitHub 仓库中发布一个新版本,如图 11.36 所示。Jenkins X 使用语义版本控制进行标记。

图 11.36 发布应用程序版本

版本将自动推广到预发布环境,如图 11.37 所示。

图 11.37 主分支上的 Jenkins 管道

在推广阶段,Jenkins X 将创建一个新的 PR 来将新版本部署到预发布环境。这个 PR 将在我们的 Git 仓库中的 env/requirements.yaml 文件中添加我们的应用程序及其版本,如图 11.38 所示。

图 11.38 将应用程序推广到预发布环境

现在您可以看到,多分支 jx-movies-store 管道被触发以处理拉取请求。它将检出 PR,执行 helm build,并在环境中执行测试,包括代码审查和批准。成功后,它将合并 PR 到主分支,见图 11.39。

图 11.39 将应用程序部署到预发布环境

一旦应用程序部署完成,我们可以输入 jx get applications 来获取应用程序的访问 URL,如图 11.40 所示。

图 11.40 应用程序整体健康状况

现在我们将更新我们的应用程序并看看会发生什么!让我们创建一个新的功能分支:

git checkout -b feature/readme
git add README.md
git commit -m "update readme"
git push origin feature/readme

Jenkins X 在导入我们的应用程序期间创建了一个 GitHub webhook。这意味着我们只需提交一个更改,我们的应用程序就会自动更新,如图 11.41 所示。

图 11.41 构建 GitHub 拉取请求

Jenkins X 在导入我们的应用程序期间创建了一个 GitHub webhook。这意味着我们只需提交一个更改,我们的应用程序就会自动更新,如图 11.41 所示。

Jenkins X 为应用程序的更改创建了一个预览环境,并显示了评估新功能的链接,如图 11.42 所示。

图 11.42 拉取请求预览环境

每当对仓库进行更改时,都会创建一个预览环境,允许任何相关用户验证或评估功能、错误修复或安全热修复。如果我们点击预览环境的 URL,我们应该可以访问服务 REST API,如图 11.43 所示。

图 11.43 电影商店 API

一旦新更改得到验证,我们可以通过一个 /approve 注释来确认代码和功能更改,如图 11.44 所示。这个简单的注释会将代码更改合并回主分支,并在主分支上启动构建。

图 11.44 Git PR 中的 ChatOps 命令

Jenkins X 提供了多个在管理拉取请求时可以使用的命令。每个命令都会触发特定的操作。表 11.2 总结了最常用的命令。

在主分支构建完成后,将发布一个新的版本,如图 11.45 所示。

表 11.2 ChatOps 命令

命令 描述
/approve 此 PR 可以合并。此命令必须来自仓库 OWNERS 文件中的人。
/retest 重新运行此 PR 的任何失败的测试管道上下文。
/assign USER 将 PR 分配给指定的用户。
/lgtm 这个 PR 看起来不错。此命令可以来自任何有权访问仓库的人。

图 11.45 新应用程序发布

当您对应用程序满意时,您可以使用 jx CLI 通过 GitOps 方法将应用程序提升到不同的环境。例如,我们可以使用以下命令将我们的应用程序提升到生产环境:

jx promote --app jx-movies-store --version 0.0.3 --env production

将创建一个新的 PR,但这次是在我们的生产仓库中,并且触发了环境监视列表生产作业,如图 11.46 所示。

图 11.46 将应用程序提升到生产环境

一旦拉取请求得到验证,生产管道运行 Helm,部署环境,从 ChartMuseum 拉取 Helm 图表和从 Docker 仓库拉取 Docker 镜像。Kubernetes 创建项目的资源,通常是 pod、服务和 ingress。

Jenkins X 使用 Git 分支模式来确定哪些分支名称会自动设置为 CI/CD。默认情况下,主分支以及任何以 PR-feature 开头的分支将被扫描。您可以使用以下命令设置自己的分支发现机制:

jx import --branches "develop|preprod|master|PR-.*"

注意:如果您完成了您的 Amazon EKS 集群,您应该删除它及其资源,以免产生额外费用。发出 terraform destroy 命令以删除 AWS 资源。

摘要

  • Kubernetes 通过帮助操作员部署、扩展、更新和维护他们的服务,并提供服务发现机制,在节点集群上管理容器化应用程序。

  • kubectl apply 命令可以从 Jenkins 管道中使用,以在 K8s 集群上执行部署。

  • Helm 图表封装了 Kubernetes 对象定义,并提供了一种在部署时进行配置的机制。

  • GitHub 页面内置了对从 HTTP 服务器安装 Helm 图表的支持。

  • Jenkins X 为每个启动的代理创建一个 Kubernetes 容器,由运行 Docker 镜像定义,并在每次构建后停止它。

  • Jenkins X 预览环境用于在更改合并到主分支之前对应用程序的更改获取早期反馈。

  • Jenkins X 并不旨在取代 Jenkins,而是通过最佳开源工具构建在它之上。这是一个包含电池的 CI/CD 的绝佳方式,无需组装任何东西。

12 基于 Lambda 的无服务器函数

本章涵盖

  • 从零开始实现基于无服务器的应用程序的 CI/CD 管道

  • 使用 AWS Lambda 设置持续部署和交付

  • 分离多个 Lambda 部署环境

  • 使用 Lambda 别名和阶段变量实现 API 网关的多阶段部署

  • 在 CI/CD 管道完成后发送带有附件的电子邮件通知

在前面的章节中,你学习了如何为在 Docker Swarm 和 Kubernetes 上运行的可容器化应用程序编写 CI/CD 管道。在本章中,你将学习如何部署使用不同架构编写的相同应用程序。

无服务器 是目前增长最快的架构运动。它允许开发者通过将底层基础设施的全部管理责任委托给云服务提供商,更快地开发可扩展的应用程序。然而,采用无服务器架构也带来了一些关键挑战,其中之一就是持续集成/持续部署(CI/CD)。

12.1 基于 Lambda 的应用程序部署

目前有多个无服务器提供商,但为了简化,我们将使用 AWS——具体来说,是 AWS Lambda (aws.amazon.com/lambda/),这是目前在无服务器领域最知名且最成熟的服务。AWS Lambda 于 2014 年 AWS re:Invent 上推出,是第一个无服务器计算的实施。用户可以将他们的代码上传到 Lambda,Lambda 然后代表用户执行操作和扩展活动。

该服务遵循事件驱动架构。这意味着部署在 Lambda 中的代码可以响应来自像 Amazon API Gateway (aws.amazon.com/api-gateway/) 这样的服务发出的 HTTP 请求等事件。

在进一步详细介绍如何为无服务器应用程序创建 CI/CD 管道之前,我们将查看相应的架构。图 12.1 展示了像 Amazon API Gateway、Amazon DynamoDB、Amazon S3 和 AWS Lambda 这样的无服务器服务如何融入应用程序架构。

图片

图 12.1 基于无服务器架构的 Watchlist 应用程序。每个 Lambda 函数负责单个 API 端点。端点通过 API Gateway 管理,并由托管在 S3 存储桶上的 Marketplace 服务消费。

AWS Lambda 推动了微服务开发。也就是说,每个端点都会触发不同的 Lambda 函数。这些函数彼此独立,可以用不同的语言编写。因此,这导致了函数级别的扩展、更简单的单元测试和松散耦合。所有来自客户端的请求首先通过 API Gateway。然后根据需要将传入的请求路由到正确的 Lambda 函数。这些函数是无状态的,因此 DynamoDB 就在这里发挥作用,以管理 Lambda 函数之间的数据持久性。Amazon S3 存储桶用于提供市场静态 Web 应用程序。最后,使用 Amazon CloudFront 分发(可选)从全球边缘缓存位置交付静态资产,如层叠样式表(CSS)或 JavaScript 文件。

要部署 Lambda 函数,我们需要创建一个 AWS Lambda 资源和一个 IAM 执行角色,该角色列出了 Lambda 函数在运行时可以访问的 AWS 资源。例如,Lambda 函数 MoviesStoreListMovies 在 DynamoDB 表上执行 Scan 操作以获取电影列表。因此,Lambda 执行角色应授予对 DynamoDB 表的访问权限。

为了避免代码重复并提供创建 Lambda 函数的轻量级抽象,我们将使用 Terraform 模块。模块是用于一起使用的一组多个资源的容器。

注意:您可以使用 Terraform 注册表 (registry.terraform.io/) 下载社区构建的经过良好测试的模块或远程发布您自己的模块。

负责创建 AWS Lambda 资源的模块位于模块文件夹(chapter12/terraform/modules)下。为每个 Lambda 函数创建一个新的 lambda.tf 文件,其中包含模块块,如下所示。模块资源通过 source 参数引用自定义模块,并覆盖默认变量,例如 Lambda 运行时环境和环境变量。

列表 12.1 使用 Terraform 模块创建 Lambda 函数

module "MoviesLoader" {
  source = "./modules/function"
  name = "MoviesLoader"
  handler = "index.handler"
  runtime = "python3.7"
  environment = {
    SQS_URL = aws_sqs_queue.queue.id
  }
}

module "MoviesParser" {
  source = "./modules/function"
  name = "MoviesParser"
  handler = "main"
  runtime = "go1.x"
  environment = {
    TABLE_NAME = aws_dynamodb_table.movies.id
  }
}

module "MoviesStoreListMovies" {
  source = "./modules/function"
  name = "MoviesStoreListMovies"
  handler = "src/movies/findAll/index.handler"
  runtime = "nodejs14.x"
  environment = {
    TABLE_NAME = aws_dynamodb_table.movies.id
  }
}

module "MoviesStoreSearchMovies" {
  source = "./modules/function"
  name = "MoviesStoreSearchMovies"
  handler = "src/movies/findOne/index.handler"
  runtime = "nodejs14.x"
  environment = {
    TABLE_NAME = aws_dynamodb_table.movies.id
  }
}

module "MoviesStoreViewFavorites" {
  source = "./modules/function"
  name = "MoviesStoreViewFavorites"
  handler = "src/favorites/findAll/index.handler"
  runtime = "nodejs14.x"
  environment = {
    TABLE_NAME = aws_dynamodb_table.favorites.id
  }
}

module "MoviesStoreAddToFavorites" {
  source = "./modules/function"
  name = "MoviesStoreAddToFavorites"
  handler = "src/favorites/insert/index.handler"
  runtime = "nodejs14.x"
  environment = {
    TABLE_NAME = aws_dynamodb_table.favorites.id
  }
}

此代码将基于 Python 3.7 运行时环境部署一个 MoviesLoader Lambda 函数,一个基于 Go 运行时的 MoviesParser 函数,以及一个基于 Node.js 环境的 MoviesStoreListMovies 函数。

接下来,我们将使用 Amazon API Gateway 部署一个 RESTful API,并定义 HTTP 端点,以便在传入的 HTTP/HTTPS 请求上触发 Lambda 函数。列表 12.2 中的 Terraform 代码在 /movies 资源上公开了一个 GET 方法。当在 /movies 端点上调用 GET 方法时,MoviesStoreListMovies Lambda 函数将被触发,以返回存储在 DynamoDB 表上的 IMDb 电影列表。将以下列表中的代码添加到 apigateway.tf 中。

列表 12.2 GET /movies 端点定义

resource "aws_api_gateway_resource" "path_movies" {
   rest_api_id = aws_api_gateway_rest_api.api.id
   parent_id   = aws_api_gateway_rest_api.api.root_resource_id
   path_part   = "movies"
}
module "GetMovies" {
  source = "./modules/method"
  api_id = aws_api_gateway_rest_api.api.id
  resource_id = aws_api_gateway_resource.path_movies.id
  method = "GET"
  lambda_arn = module.MoviesStoreListMovies.arn
  invoke_arn = module.MoviesStoreListMovies.invoke_arn
  api_execution_arn = aws_api_gateway_rest_api.api.execution_arn
}

注意:除了为 Lambda 函数提供统一的入口点外,API 网关还提供了强大的功能,如缓存、跨源资源共享(CORS)配置、安全和身份验证。

定义剩余的 API 端点,或者从第十二章/terraform/apigateway.tf 下载完整的 apigateway.tf 文件。

电影市场的所有内容——包括 HTML、CSS、JavaScript、图片和其他文件——将托管在 Amazon S3 存储桶中。最终用户将通过使用 Amazon S3 提供的公共网站 URL 访问应用程序。因此,我们不需要运行任何像 NGINX 或 Apache 这样的 Web 服务器来使 Web 应用程序可用。以下列表(s3.tf)中的 Terraform 代码创建了一个 S3 存储桶并启用了网站托管。

列表 12.3 S3 网站托管配置

resource "aws_s3_bucket" "marketplace" {
  bucket = "marketplace.${var.domain_name}"
  acl    = "public-read"
  website {
    index_document = "index.html"
    error_document = "index.html"
  }
}

存储桶的访问控制列表(ACL)必须设置为 public-readwebsite 块是我们定义应用程序索引文档的地方。此外,我们通过附加存储桶策略来授予对静态内容的访问权限。存储桶策略授予所有主体对存储桶中任何对象的 s3:GetObject 权限。

注意:除非您想通过 S3 存储桶 URL 访问市场,否则您可以使用 CloudFront 在 S3 上通过使用自定义域名和 SSL 提供应用程序内容。

使用 terraform init 命令安装本地模块,并运行 terraform apply 以配置 AWS 资源。创建整个基础设施可能需要几秒钟。创建步骤完成后,API 和市场 URL 将在 输出 部分显示,如图 12.2 所示。

图片

图 12.2 API 网关和 S3 网站 URL

api 变量包含由 API 网关提供的 RESTful API URL,而 marketplace 变量是市场应用程序的 S3 网站 URL。如果您访问 AWS Lambda 控制台(mng.bz/10Qg),图 12.3 中的 Lambda 函数应该已经部署。

图片

图 12.3 Watchlist 应用程序的 Lambda 函数

将您喜欢的浏览器指向 API 网关 URL,并导航到 /movies 端点。HTTP 请求应触发负责列出电影的 MoviesStoreListMovies Lambda 函数。图 12.4 中的错误消息将被显示。

图片

图 12.4 MoviesStoreListMovies HTTP 响应

目前,Lambda 函数中没有部署任何代码,所以看不到任何内容。要列出电影,我们需要将函数的代码部署到 Lambda 资源。在下一节中,我们将在 Jenkins 中创建 CI/CD 管道来自动化 Lambda 函数的部署。图 12.5 展示了目标 CI/CD 工作流程。

图片

图片

每当您对应用程序的源代码进行更改时,都会触发一个流水线。Jenkins 主节点将在可用的 Jenkins 工作节点之一上安排构建。工作节点将执行位于应用程序 Git 仓库根目录中的 Jenkinsfile 中描述的阶段。第八章中提供了 CheckoutTests 阶段。Build 阶段将编译源代码,安装所需的依赖项,并生成部署包(zip 归档)。接下来,Push 阶段将 zip 文件存储在远程 S3 桶中,最后执行 Deploy 阶段以更新 Lambda 函数的代码,并应用最新的更改。

12.2 创建部署包

在将无服务器应用程序集成到 Jenkins 之前,我们需要将 Lambda 函数的源代码存储在集中式远程仓库中以便进行版本控制。对于无服务器应用程序,最常用的两种策略是将函数组织到仓库中:

  • 单仓库—所有内容都放入同一个仓库;协同工作以提供业务功能的函数被组合在同一个仓库下。

  • 每个服务一个仓库—每个 Lambda 函数都有自己的 Git 仓库,并拥有自己的 CI/CD 流水线。

本节不深入探讨哪种方法更好,而是展示如何使用两种方法构建 CI/CD 流水线。

12.2.1 单仓库策略

由单个用 Python 编写的 Lambda 函数组成的 MoviesLoader 服务,负责将电影列表加载到消息队列中。为 movies-loader Lambda 函数创建一个 GitHub 仓库,如图 12.6 所示,然后将书中仓库(chapter12/functions)中可用的源代码推送到 develop 分支。

图 12.6 MoviesLoader Lambda 函数 GitHub 仓库

Jenkinsfile(位于 chapter12/functions/movies-loader/Jenkinsfile)存储在根仓库中。它与第八章的列表 8.3 提供的类似。在推送事件发生时,它将检出函数源代码,并在 Docker 容器内运行单元测试。适当的单元测试可以保护 Lambda 代码的后续更新。以下列表中显示的定义文件必须提交到 Lambda 函数的代码仓库。

列表 12.4 在 Docker 容器内运行函数单元测试

def imageName = 'mlabouardy/movies-loader'
node('workers'){
    try {
        stage('Checkout'){
            checkout scm
            notifySlack('STARTED')           ❶
        }
        stage('Unit Tests'){
            def imageTest= docker.build("${imageName}-test", "-f Dockerfile.test .")
            imageTest.inside{
                sh "python test_index.py"
            }
        }
    } catch(e){
        currentBuild.result = 'FAILED'       ❷
        throw e
    } finally {
        notifySlack(currentBuild.result)     ❸
    }
}

❶ 通过使用自定义的 notifySlack 方法在构建开始时发送 Slack 通知

❷ 当发生错误时,它将被缓存在此处,并将 currentBuild.result 变量设置为 FAILED,以便之后发送正确的 Slack 通知。

❸ 当流水线完成(成功或失败)时,将发送 Slack 通知,以引起对流水线状态的注意。

在列表 12.5 中,我们创建了一个部署包,这是一个包含 Python 代码及其运行所需的任何依赖项的 zip 文件。构建阶段生成一个 zip 文件,并使用 Git 提交 ID 作为名称。最后,我们将 zip 文件推送到 S3 桶中以进行版本控制,并删除文件以节省空间。

列表 12.5 生成部署包

def functionName = 'MoviesLoader'
def imageName = 'mlabouardy/movies-loader'
def bucket = 'deployment-packages-watchlist'                                ❶
def region = 'AWS REGION'

node('workers'){
    try {
        stage('Checkout'){...}                                              ❷

        stage('Unit Tests'){...}                                            ❸

        stage('Build'){
            sh "zip -r ${commitId}.zip index.py movies.json"                ❹
        }

        stage('Push'){
            sh "aws s3 cp ${commitId}.zip s3://${bucket}/${functionName}/"  ❺
        }
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
        sh "rm -rf ${commitId}.zip "                                        ❻
    }
}

❶ 存储部署包(zip 文件)的 S3 桶的名称

❷ 克隆 Git 仓库。为了简洁,省略了说明;请参阅第十二章/函数/movies-loader/Jenkinsfile 中的命令。

❸ 在 Docker 容器内运行单元测试。有关说明,请参阅第十二章/函数/movies-loader/Jenkinsfile。

❹ 创建一个包含函数入口点(index.py)和电影 JSON 数组的存档(zip 文件)。使用 commitId 函数根据当前的 Git 提交 ID 创建存档的唯一 ID。

❺ 将存档存储到 S3 桶中

❻ 在管道结束时删除存档以节省硬盘空间

注意:我们使用 Git 提交 ID 作为部署包的名称,为每个发布提供一个有意义的名称,并在出现问题时能够回滚到特定的提交。

在 Jenkins 上,为 MoviesLoader Lambda 函数创建一个新的多分支管道作业(请参阅第七章以获取逐步指南)。Jenkins 将发现 develop 分支,并开始一个新的构建;请参阅图 12.7。

图 12.7 MoviesLoader Lambda 函数管道

您可以深入查看 UI 上的步骤,这些步骤与 Jenkinsfile 中的步骤相匹配。当 Jenkins 执行每个阶段时,您可以看到活动。您可以看到作为 单元 测试 阶段一部分运行的测试(图 12.8)。如果测试成功,将生成一个 zip 文件并将其存储在 S3 桶中。

图 12.8 管道执行日志

打开 S3 控制台并单击用于包存储的管道使用的桶。应有一个新的部署包可用,其键名与 Git 提交 ID 相同,如图 12.9 所示。

图 12.9 部署包存储的 S3 桶

对于 movies-parser 函数,同样将函数源代码推送到一个专门的 GitHub 仓库,如图 12.10 所示。

图 12.10 MoviesParser Lambda 函数 GitHub 仓库

在 Git 仓库的顶级目录中创建一个 Jenkinsfile(chapter12/functions/movies-parser/Jenkinsfile),其阶段与第八章的列表 8.8 类似;请参阅以下列表。

列表 12.6 并行运行函数预集成测试

def imageName = 'mlabouardy/movies-parser'

node('workers'){
    try{
        stage('Checkout'){
            checkout scm
        }

        def imageTest= docker.build("${imageName}-test".
"-f Dockerfile.test .")
        stage('Pre-integration Tests'){
            parallel(
                'Quality Tests': {
                    imageTest.inside{
                        sh 'golint'
                    }
                },
                'Unit Tests': {
                    imageTest.inside{
                        sh 'go test'
                    }
                },
                'Security Tests': {
                    imageTest.inside('-u root:root'){
                       sh 'nancy /go/src/github/mlabouardy/
movies-parser/Gopkg.lock'
                    }
                }
            )
        }
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
    }
}

由于该函数是用 Go 编写的,因此我们需要使用 Docker 多阶段构建功能构建一个二进制文件,如第 9.2 节所述。然后,从 Docker 容器中复制构建的二进制文件并生成一个 zip 包。最后,将部署包推送到 S3 桶中,如下所示。

列表 12.7 构建 Go 基础 Lambda 部署包

def functionName = 'MoviesParser'
def imageName = 'mlabouardy/movies-parser'
def region = 'eu-west-3'

node('workers'){
    try{
     stage('Checkout'){...}                  ❶
     stage('Pre-integration Tests'){...}     ❶

     stage('Build'){
       sh """
        docker build -t ${imageName} .
        docker run --rm ${imageName}
        docker cp ${imageName}:/go/src/github.com/mlabouardy/
movies-parser/main main
        zip -r ${commitID()}.zip main
       """
      }

      stage('Push'){
        sh "aws s3 cp ${commitID()}.zip s3://${bucket}/${functionName}/"
      }
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
        sh "rm ${commitID()}.zip"
    }
}

❶ 请参考列表 12.6 中的说明。

将更改推送到 movies-parser Git 仓库,并为 movies-parser 创建一个新的多分支管道作业。管道阶段应该被执行。完成后,管道在 Blue Ocean 视图中应类似于图 12.11。

图 12.11 MoviesParser Lambda 函数工作流程

图 12.12 显示了 Push 阶段的控制台输出。函数部署包将被存储在 MoviesParser 子文件夹下。

图 12.12 将部署包发布到 S3

多仓库模式的明显对应模式是单仓库方法。在这种模式中,单个仓库包含按业务能力分组的 Lambda 函数集合。

12.2.2 多仓库策略

Movies Store API 被拆分为多个 Lambda 函数(MoviesStoreListMoviesMoviesStoreSearchMovieMoviesStoreViewFavoritesMoviesStoreAddToFavorites)。在这些函数之间共享代码的最简单方法是让它们都在单个仓库中。创建一个新的 GitHub 仓库(chapter12/functions/movies-store),如图 12.13 所示。

根目录下的 src/ 文件夹由一系列服务组成。每个服务处理相对较小且自包含的函数。例如,movies/findAll 文件夹负责从 DynamoDB 表中提供电影列表。package.json 文件位于仓库的根目录。然而,在服务目录中有一个单独的 package.json 是相当常见的。

图 12.13 单个仓库中存储的多个 Lambda 函数

在 movies-store 仓库中,使用您喜欢的文本编辑器或 IDE 创建一个 Jenkinsfile(chapter12/functions/movies-store/Jenkinsfile),内容如下一列表所示。有关实现阶段的更多详细信息,请参考列表 8.14。

列表 12.8 运行质量测试和生成代码覆盖率报告

def imageName = 'mlabouardy/movies-store'
node('workers'){
    try {
        stage('Checkout'){
            checkout scm
            notifySlack('STARTED')
        }

        def imageTest= docker.build("${imageName}-test".
"-f Dockerfile.test .")

        stage('Tests'){
            parallel(
                'Quality Tests': {
                    sh "docker run --rm ${imageName}-test npm run lint"
                },
                'Unit Tests': {
                    sh "docker run --rm ${imageName}-test npm run test"
                },
                'Coverage Reports': {
                    sh "docker run --r.
-v $PWD/coverage:/app/coverage ${imageName}-tes.
npm run coverage"
                    publishHTML (target: [
                        allowMissing: false,
                        alwaysLinkToLastBuild: false,
                        keepAll: true,
                        reportDir: "$PWD/coverage",
                        reportFiles: "index.html",
                        reportName: "Coverage Report"
                    ])
                }
            )
        }
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
    }
}

接下来,我们从 Node.js 基础镜像运行 Docker 容器,通过执行 npm install 命令来安装外部依赖。然后,我们将运行容器中的 node_modules 文件夹复制到当前工作区,并创建一个 zip 文件,如下一列表所示。部署包的大小将影响函数的冷启动。为了保持部署包的大小较小,我们通过将 --prod=only 传递给 npm install 命令,只安装运行时依赖。

列表 12.9 构建 Node.js 基础 Lambda 部署包

stage('Build'){
    sh """
      docker build -t ${imageName} .
      containerName=\$(docker run -d ${imageName})
      docker cp \$containerName:/app/node_modules node_modules
      docker rm -f \$containerName
      zip -r ${commitID()}.zip node_modules src
    """
}

注意:动态预配的一个缺点是称为 冷启动 的现象。本质上,一段时间内未使用的函数启动和响应第一个请求需要更长的时间。

然后,在以下列表中,我们将生成的 zip 文件推送到 S3 桶,使用循环遍历每个函数名称,并将 zip 文件保存在 S3 桶下的函数文件夹中。您可以使用 Serverless 框架(www.serverless.com)为每个函数创建一个 zip 文件,并排除未使用的依赖项和文件。

列表 12.10 将 Node.js 部署包发布到 S3

def functions = ['MoviesStoreListMovies'.
'MoviesStoreSearchMovie'.
'MoviesStoreSearchMovie'.
'MoviesStoreAddToFavorites']
def bucket = 'deployment-packages-watchlist'

node('workers'){
    try {
        stage('Checkout'){...}
        stage('Tests'){...}
        stage('Build'){...}
        stage('Push'){
            functions.each { function ->
                sh "aws s3 cp ${commitID()}.zip s3://${bucket}/${function}/"
            }
        }
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
        sh "rm --rf ${commitID()}.zip"
    }
}

返回 Jenkins 仪表板,为 movies-store 项目创建一个新的多分支管道作业,并将更改提交到 develop 分支。几秒钟后,应该会在 develop 分支的 movies-store 作业上触发一个新的构建。在结果页面上,您将看到 develop 分支管道的阶段视图,如图 12.14 所示。

图 12.14 MoviesStore Lambda 函数 CI 工作流程

对于常见情况,构建和推送阶段可能会占用 CI/CD 执行时间的大部分。因此,我们可以使用以下列表中的 parallel 键,以并行运行推送阶段,以保持管道周转时间短。

列表 12.11 带有映射结构的并行指令

stage('Push'){
    def fileName = commitID()                                       ❶
    def parallelStagesMap = functions.collectEntries {              ❷
      ["${it}" : {                                                  ❷
         stage("Lambda: ${it}") {                                   ❷
           sh "aws s3 cp ${fileName}.zip s3://${bucket}/${it}/"     ❷
         }                                                          ❷
      }]                                                            ❷
    }                                                               ❷
    parallel parallelStagesMap                                      ❸
}

❶ 将存档的名称设置为 Git 提交 ID

❷ 并行指令期望一个映射结构,因此我们正在构建一个。我们遍历函数列表,并创建相应的命令将存档文件存储到适当的 S3 文件夹中。

❸ 并行运行阶段

parallel 指令接受一个字符串和闭包的映射。字符串是并行执行的显示名称(函数名称),闭包是实际的 aws s3 cp 指令,用于将部署包复制到 S3 中相应的函数文件夹。因此,每个函数的部署包存储将并行运行,如图 12.15 所示。

图 12.15 MoviesStore CI 工作流程

一旦管道执行完成,在 S3 桶中,应该为每个 Lambda 函数存储一个部署包,如图 12.16 所示。

图 12.16 Lambda 函数部署包

到目前为止,部署包已存储在 S3 桶中,因此我们可以继续更新 Lambda 函数的源代码,使用构建的 zip 文件。

12.3 更新 Lambda 函数代码

对于 MoviesLoaderMoviesParser Lambda 函数,将以下 Deploy 阶段添加到它们的 Jenkinsfile 中(第十二章/函数/movies-loader/Jenkinsfile 和第十二章/函数/movies-parser/Jenkinsfile)。该阶段使用 AWS Lambda CLI 发出 update-function-code 命令来更新函数代码,该代码存储在之前 S3 桶中,如下列表所示。

列表 12.12 使用 AWS CLI 更新 Lambda 函数的代码

stage('Deploy'){
  sh "aws lambda update-function-code --function-name ${functionName.
          --s3-bucket ${bucket} --s3-key ${functionName}/${commitID()}.zi.
          --region ${region}"
}

该命令将作为参数接受存储 zip 文件的 S3 桶的名称以及部署包的 Amazon S3 密钥。

一旦将更改推送到 Git 远程仓库,Jenkins 将使用update-function-code命令更新 Lambda 函数的代码。图 12.17 中的输出确认了这一点。

图 12.17 UpdateFunction-Code操作日志

MoviesLoaderMoviesParser函数的 CI/CD 流水线应包含图 12.18 中显示的阶段。

图 12.18 基于 Python 和 Go 的 Lambda 函数 CI/CD 流水线

注意:Serverless 框架(serverless.com/)或 AWS Serverless 应用程序模型(SAM)也可以用于在 Jenkins 流水线中编写和部署 Lambda 函数。

类似地,将相同的阶段添加到MoviesStore Lambda 函数中——这次,我们将使用for循环将update-function-code命令包装在同一个 GitHub 仓库内的每个函数版本中;请参阅以下列表。

列表 12.13 更新多个 Lambda 函数

stage('Deploy'){
  functions.each { function ->
    sh "aws lambda update-function-cod.
--function-name ${function.
--s3-bucket ${bucket.
--s3-key ${function}/${commitID()}.zi.
--region ${region}"
  }
}

当新阶段被提交时,在推送事件触发后,流水线将被触发,图 12.19 中的 CI/CD 阶段将被执行。

图 12.19 MoviesStore CI/CD 流水线

在我们自动化部署市场之前,我们需要将一些数据加载到 DynamoDB 表中。从 AWS 管理控制台触发MoviesLoader函数,或者从您的终端会话中执行以下命令:

aws lambda invoke --function-name MoviesLoader --payload '{}' response.json

注意:确保将AWSLambda_FullAccess策略分配给配置了您的 AWS CLI 的 IAM 用户。

之前的命令将调用MoviesLoader函数并将函数的输出保存到 response.json 文件中。该函数将电影加载到 SQS 并触发MoviesParser Lambda 函数,该函数将爬取电影的 IMDb 页面并将信息存储在图 12.20 所示的 Movies DynamoDB 表中。

图 12.20。

图 12.20 Movies DynamoDB 表

SQS 中的每条消息都会调用MoviesParser函数;一旦队列变空,DynamoDB 表应包含前 100 部 IMDb 电影。

12.4 在 S3 上托管静态网站

电影市场是一个单页应用程序(SPA),使用 TypeScript 编写,采用 Angular 框架。该应用程序提供静态内容(HTML、JavaScript 和 CSS 文件),这对于 S3 网站托管功能来说可能是一个不错的选择。

让我们自动化将市场部署到 S3 存储桶,如下所示。首先,创建一个 GitHub 项目以版本控制市场源代码。然后,编写一个 Jenkinsfile 以运行质量、单元测试和静态代码分析,使用 SonarQube。有关更多详细信息,请参阅第八章。

列表 12.14 将 Angular 应用程序与 Jenkinsfile 集成

def imageName = 'mlabouardy/movies-marketplace'
def region = 'AWS REGION'

node('workers'){
    try{
        stage('Checkout'){
            checkout scm
            notifySlack('STARTED')
        }

        def imageTest= docker.build("${imageName}-test".
"-f Dockerfile.test .")                                             ❶
        stage('Quality Tests'){
            sh "docker run --rm ${imageName}-test npm run lint"     ❷
        }
        stage('Unit Tests'){
            sh "docker run --r.
-v $PWD/coverage:/app/coverag.
${imageName}-test npm run test"                                     ❸
            publishHTML (target: [                                  ❹
                allowMissing: false,                                ❹
                alwaysLinkToLastBuild: false,                       ❹
                keepAll: true,                                      ❹
                reportDir: "$PWD/coverage/marketplace",             ❹
                reportFiles: "index.html",                          ❹
                reportName: "Coverage Report"                       ❹
            ])                                                      ❹
        }
        stage('Static Code Analysis'){
            withSonarQubeEnv('sonarqube') {                         ❺
                sh 'sonar-scanner'                                  ❺
            }                                                       ❺
        }
        stage("Quality Gate"){
            timeout(time: 5, unit: 'MINUTES') {                     ❻
                def qg = waitForQualityGate()                       ❻
                if (qg.status != 'OK') {                            ❻
                    error "Pipeline aborted due to                  ❻
quality gate failure: ${qg.status}"                                 ❻
                }                                                   ❻
            }                                                       ❻
        }
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)
    }
}

❶ 基于 Dockerfile.test 构建 Docker 镜像以运行自动化测试

❷ 运行代码检查过程

❸ 运行单元测试并生成覆盖率报告

❹ 使用 Jenkins Publish HTML 插件消费覆盖率报告

❺ 使用 SonarQube 运行代码质量检查

❻ 如果 SonarQube 检查超过 5 分钟,则中断检查

添加一个 Build 阶段来创建一个 Docker 容器,安装 npm 依赖项,并将依赖项文件夹以及生成的静态 Web 应用程序文件复制到当前工作区,如下所示。注意使用 --build-arg 参数在构建时注入 API 网关 URL。

列表 12.15 构建 Angular 应用程序

stage('Build'){
            sh """
                docker build -t ${imageName} --build-arg ENVIRONMENT=sandbox .
                containerName=\$(docker run -d ${imageName})
                docker cp \$containerName:/app/dist dist
                docker rm -f \$containerName
            """
 }

然后,在以下列表中,使用 AWS CLI 将生成的静态 Web 应用程序推送到启用了网站托管功能的 S3 存储桶。

列表 12.16 将 Angular 静态应用程序存储到 S3

stage('Push'){
    sh "aws s3 cp --recursive dist/ s3://${bucket}/"     ❶
}

❶ 递归地将本地文件复制到 S3

将更改推送到 develop 分支。应该触发一个新的管道,并且图 12.21 中的阶段将成功执行。

Labouardy

图 12.21 市场 CI/CD 工作流程

你可以通过 Amazon S3 存储桶仪表板验证文件是否已成功存储,或者通过在终端会话中运行 aws s3 ls 命令。图 12.22 显示了市场 S3 存储桶的内容。

Labouardy

图 12.22 市场 S3 存储桶内容

如果你访问 S3 网站 URL(http://BUCKET.s3-website-REGION.amazonaws.com/),它应该显示市场 UI,如图 12.23 所示。

那太好了!然而,当你构建无服务器应用程序时,你必须将部署环境分开,以测试新更改而不影响生产。因此,在构建无服务器应用程序时拥有多个环境是有意义的。

Labouardy

图 12.23 在沙盒环境中运行的 Marketplace 仪表板

12.5 维护多个 Lambda 环境

AWS Lambda 允许你发布一个版本,它代表了函数代码和配置在时间上的状态。默认情况下,每个 Lambda 函数都有一个 $LATEST 版本,指向部署到函数的最新更改。

$LATEST 版本发布新版本时,需要更新 Jenkinsfile(chapter12/functions/movies-loader/Jenkinsfile),添加一个新阶段以发布新的 Lambda 函数版本,如下所示。

列表 12.17 发布新的 Lambda 版本

stage('Deploy'){
   sh "aws lambda update-function-code --function-name ${functionName}
           --s3-bucket ${bucket} --s3-key ${functionName}/${commitID()}.zip
           --region ${region}"

   sh "aws lambda publish-version --function-name ${functionName.
           --description ${commitID()} --region ${region}"
}

当你发布 Lambda 函数的新版本时,你应该给它一个有意义的版本名称,这样你就可以通过其开发周期跟踪对函数所做的不同更改。在列表 12.17 中,我们使用 Git 提交 ID 作为版本方案。然而,你可以使用更高级的版本机制,如语义版本化(semver.org/)。

当管道执行时,在 Deploy 阶段将执行前面的命令。图 12.24 展示了它们的执行日志。

Labouardy

图 12.24 在部署阶段执行的更新和发布命令

注意版本是不可变的:一旦创建,就无法更新它们的代码或设置(内存、执行时间、VPC 配置等)。

在 MoviesLoader Lambda 仪表板上,将基于 develop 分支的源代码发布新版本,如图 12.25 所示。

图 12.25 MoviesLoader Lambda 新发布的版本

将 MoviesStore API 的 Lambda 版本发布并行进行,以减少管道的执行时间;见图 12.26。

因此,您可以在开发工作流程中与 Lambda 函数的不同变体一起工作。

图 12.26 并行运行发布命令

目前,API Gateway 根据 $LATEST 版本触发 MoviesStore Lambda 函数,因此每次发布新版本时,都需要更新 API Gateway 以指向最新版本(图 12.27)——这是一个繁琐且不方便的任务。

图 12.27 GET /favorites 集成请求

幸运的是,有 Lambda 别名 的概念。别名是一个指向特定版本的指针,允许您将函数从一个环境提升到另一个环境(例如从预发布到生产)。与不可变的版本不同,别名是可变的。现在,您不再需要在 API Gateway 集成请求中直接分配 Lambda 函数版本,而是可以分配 Lambda 别名,其中别名是一个变量。该变量将在运行时解析其值。

也就是说,使用 AWS 命令行创建指向最新发布的版本的沙盒、预发布和生产环境的别名:

aws lambda create-alias --function-name MoviesStoreViewFavorites --name sandbox    --version 1

一旦创建,新的别名应添加到“Qualifiers”下拉列表下的“别名”列表中(图 12.28)。

图 12.28 使用多个别名引用不同的环境

我们可以更新 Jenkinsfile 以直接更新别名。更新 Deploy 阶段,使用下一列表中的代码。它更新 Lambda 函数代码,发布新版本,并将对应于当前 Git 分支的别名(master 分支 = 生产别名,preprod 分支 = 预发布别名,develop 分支 = 沙盒别名)指向新部署的版本。

列表 12.18 更新 Lambda 别名为指向最新版本

sh "aws lambda update-function-code --function-name ${it}
        --s3-bucket ${bucket} --s3-key ${it}/${fileName}.zip
        --region ${region}"

def version = sh(
    script: "aws lambda publish-version --function-name ${it}
                 --description ${fileName}
--region ${region} | jq -r '.Version'",
    returnStdout: true
).trim()

if (env.BRANCH_NAME in ['master','preprod','develop']){
    sh "aws lambda update-alias  --function-name ${it}
            --name ${environments[env.BRANCH_NAME]}
--function-version ${version}
            --region ${region}"
}

publish-version 操作返回包含已部署版本号作为属性的 JSON 输出。使用 jq 命令解析 Version 属性并将其值存储在 version 变量中。然后,根据当前的 Git 分支,相应的别名将指向发布的版本号。

将更改推送到 develop 分支。函数代码将被更新,将创建一个新版本,并且沙盒别名将指向最新发布的版本,如图 12.29 所示。

图 12.29 更新 Lambda 别名为已部署版本

例如,在MoviesStoreListMovies Lambda 中,沙箱别名应指向包含 develop 分支源代码的版本,如图 12.30 所示。

图片

图 12.30 沙箱别名指向新的 Lambda 版本

现在你已经看到了如何在 Jenkins 管道中创建别名并切换其值,让我们配置 API Gateway 以使用这些别名和阶段变量。

阶段变量是环境变量,可以在 API Gateway 的每个部署阶段的运行时更改方法的行为。

在 API Gateway 控制台中,导航到 Movies API,单击实例的 GET 方法,并将目标 Lambda 函数更新为使用阶段变量而不是硬编码的 Lambda 函数版本,如图 12.31 所示。

图片

图 12.31 配置 API 集成请求时使用阶段变量

在 Lambda 函数字段中,${stageVariables.environment}告诉 API Gateway 在运行时从阶段变量中读取此字段的值。

当你保存配置时,会出现一个新的提示,要求你授权 API Gateway 调用你的 Lambda 函数别名。此时,我们需要部署我们的 API 以使其公开可用。

从操作下拉菜单中选择部署 API。选择“新建部署阶段”选项,输入sandbox作为阶段名称,并部署它。或者使用列表 12.19 中的 Terraform 代码。沙箱阶段将设置环境阶段变量为sandbox。因此,如果用户在沙箱部署的任何端点上发起 HTTP 请求,将触发具有沙箱别名的相应 Lambda 函数。

列表 12.19 使用别名阶段变量进行 API 部署

resource "aws_api_gateway_deployment" "sandbox" {
   depends_on = [
     module.GetMovies,
     module.GetOneMovie,
     module.GetFavorites,
     module.PostFavorites
   ]

   variables = {
    "environment" = "sandbox"
  }

   rest_api_id = aws_api_gateway_rest_api.api.id
   stage_name  = "sandbox"
}

为预发布和生产环境创建额外的部署阶段。在完成terraform apply命令后,将显示三个部署阶段 URL,如图 12.32 所示。

图片

图 12.32 API Gateway 部署 URL

如果你打开 API 在 https://id.execute-api.region.amazonaws.com/sandbox/movies,你将获得 Lambda MoviesStoreListMovies的别名sandbox的响应。

要将无服务器应用程序部署到预发布环境,创建一个合并 develop 分支到 preprod 分支的 pull request。Jenkins 将在 PR 上发布 develop 作业的构建状态(图 12.33)。然后,将 develop 合并到 preprod。

图片

图 12.33 Jenkins 在 GitHub PR 上的构建状态

一旦 PR 合并,preprod 分支将触发新的构建。在 CI/CD 管道的末尾,预发布别名将指向新部署的版本,如图 12.34 所示。

图片

图 12.34 将 Lambda 函数部署到预发布环境

现在,为了在多个环境中部署市场,我们将根据当前分支名称注入环境名称;请参阅以下列表。

列表 12.20 在构建过程中注入环境名称

stage('Build'){
  sh """
     docker build -t ${imageName}
--build-arg ENVIRONMENT=${environments[env.BRANCH_NAME]} .
     containerName=\$(docker run -d ${imageName})
     docker cp \$containerName:/app/dist dist
     docker rm -f \$containerName
  """
}

然后,在列表 12.21 中,我们将 aws s3 cp 指令更新为将静态文件推送到 S3 桶下以环境名称命名的文件夹。您也可以为每个环境创建一个 S3 桶,但为了简单起见,我们使用单个 S3 来存储市场 place 的不同环境。

列表 12.21 将静态文件推送到 S3 桶

if (env.BRANCH_NAME in ['master','preprod','develop']){
  stage('Push'){
   sh "aws s3 cp --recursive dist/ s3://${bucket}/${environments[env.BRANCH_NAME]}/"
  }
}

将这些更改推送到功能分支。然后发起一个拉取请求以合并到 develop 分支。当合并发生时,图 12.35 中的新管道将被执行。

图片

图 12.35 市场 place 新 CI/CD 管道

将更改合并到 preprod 以将应用程序部署到阶段。然后,从 preprod 合并到 master 分支以进行生产部署。结果,S3 桶应包含三个文件夹。每个文件夹包含市场 place 的不同运行环境,如图 12.36 所示。

图片

图 12.36 具有多个环境的 S3 桶

如果您指向 S3 桶的网站 URL 并添加 /staging 端点,它应提供市场 place 的阶段环境,如图 12.37 所示。

图片

图 12.37 市场 place 阶段环境

现在,要将 Lambda 函数部署到生产环境,通过发起拉取请求将 preprod 分支合并到 master 分支,如图 12.38 所示。

图片

图 12.38 将 movies-store Lambda 函数的 preprod 分支合并到 master

当合并发生时,管道将在 master 分支上触发;见图 12.39。

图片

图 12.39 部署 Lambda 函数到生产

movies-store 函数将进行更新,将创建一个新版本,并且生产别名将指向新部署的版本。

您可以使用 Jenkins Input Step 插件在实际上线部署之前请求开发者授权;参见以下列表。当达到 Deploy 阶段时,将弹出输入对话框以进行部署确认。

列表 12.22 在生产部署之前请求用户批准

if (env.BRANCH_NAME == 'preprod' || env.BRANCH_NAME == 'develop'){
   sh "aws lambda update-alias  --function-name ${it}
           --name ${environments[env.BRANCH_NAME]}
--function-version ${version}
           --region ${region}"
}

if(env.BRANCH_NAME == 'master'){
   timeout(time: 2, unit: "HOURS") {
        input message: "Deploy to production?", ok: "Yes"
   }
   sh "aws lambda update-alias  --function-name ${it}
           --name ${environments[env.BRANCH_NAME]}
--function-version ${version}
           --region ${region}"
}

交互式输入将询问我们是否批准部署。如果我们点击是,管道将继续,并且生产别名将指向新部署的版本,如图 12.40 所示。

图片

图 12.40 Jenkins 管道中的生产部署确认

因此,现在如果我们对我们的无服务器应用程序进行任何更改,CI/CD 管道将被触发,新发布的 Lambda 函数代码将被升级到生产环境。部署作业状态也会发送 Slack 通知,如图 12.41 所示。

图片

图 12.41 生产部署 Slack 通知

在管道触发和进度上发送通知有助于在团队成员之间沟通工作。到目前为止,我们已经用它来发送启动、完成和失败通知。但 Slack 也可以用于从聊天窗口执行操作或命令,例如确认生产部署或触发 Jenkins 作业的构建。

另一种提高作业构建状态意识并报告测试结果的方法是通过电子邮件通知。

12.6 在 Jenkins 中配置电子邮件通知

在 Jenkins 中,可以通过 Email Extension 插件(plugins.jenkins.io/email-ext/) 来实现电子邮件通知。此插件包含了一系列在 Jenkins 上安装的必备插件。

要启用电子邮件通知,您需要配置一个 SMTP 服务器。转到“管理 Jenkins”,然后配置系统。滚动到“扩展电子邮件通知”部分。如果您使用的是 Gmail,请输入您的 SMTP 凭据,然后输入 smtp.gmail.com 作为 SMTP 服务器,并输入您的 Gmail 用户名和密码。选择使用 SSL,并输入端口号为 465。

要发送电子邮件,您需要配置一个收件人地址列表。然后,点击如图 12.42 所示的“应用”和“保存”按钮。

图 12.42 扩展电子邮件通知配置

您可以通过输入收件人电子邮件地址并点击“测试配置”来测试配置。如果一切正常,您将看到消息“电子邮件已成功发送”。

现在插件已配置,请在您的 Jenkinsfile 中输入以下列表,以定义一个根据作业构建状态发送具有可定制属性的电子邮件的功能。

列表 12.23 发送电子邮件以报告作业构建状态

def sendEmail(String buildStatus){
    buildStatus =  buildStatus ?: 'SUCCESSFUL'
    emailext body: "More info at: ${env.BUILD_URL}",
             subject: "Name: '${env.JOB_NAME}' Status: ${buildStatus}",
             to: '$DEFAULT_RECIPIENTS'
}

最后,您可以通过在 finally 块上调用 sendEmail() 方法来在 CI/CD 管道完成后调用该功能。在下面的列表中,只有当主分支上正在运行构建时才会发送电子邮件通知,以避免垃圾邮件。

列表 12.24 在生产部署发生时发送电子邮件

node('workers'){
    try {
        stage('Checkout'){...}
        stage('Tests'){...}
        stage('Build'){...}
        stage('Push'){...}
        stage('Deploy'){...}
    } catch(e){
        currentBuild.result = 'FAILED'
        throw e
    } finally {
        notifySlack(currentBuild.result)

        if (env.BRANCH_NAME == 'master'){
            sendEmail(currentBuild.result)
        }
    }
}

将新的 Jenkinsfile 推送到 GitHub。当主分支上正在执行构建时,将会发送电子邮件。一旦管道完成,您应该能够看到类似于图 12.43 中的电子邮件。

图 12.43 报告作业构建状态的电子邮件通知

电子邮件的主题包含 Jenkins 作业的名称及其构建状态。电子邮件正文包含作业输出的链接。

编写 Jenkinsfiles 的声明式方法提供了一个 post 部分,可以用来放置后执行脚本。您可以通过将其放置在 post 构建部分来调用 sendEmail() 方法,如下面的列表所示。

列表 12.25 Jenkins 声明式管道中的后置步骤

pipeline {
    agent{
        label 'workers'
    }
    stages {
        stage('Checkout'){...}
        stage('Unit Tests'){...}
        stage('Build'){...}
        stage('Push'){...}
    }
    post {
        always {
            if (env.BRANCH_NAME == 'master'){
                sendEmail(currentBuild.currentResult)
            }
        }
    }
}

您还可以通过启用以下列表中的 attachLog 属性来附加作业构建日志。

列表 12.26 在通知邮件中附加日志文件

def sendEmail(String buildStatus){
    buildStatus =  buildStatus ?: 'SUCCESSFUL'
    emailext body: "More info at: ${env.BUILD_URL}",
             subject: "Name: '${env.JOB_NAME}' Status: ${buildStatus}",
             to: '$DEFAULT_RECIPIENTS',
             attachLog: true
}

因此,Jenkins 发送的电子邮件现在将包含作业状态以及完整的控制台输出作为附件,如图 12.44 所示。

图片

图 12.44 将作业日志作为电子邮件通知附件发送

摘要

  • Terraform 模块允许您更好地组织您的基础设施配置代码,并使资源可重用。

  • 当将无服务器应用程序作为一组 Lambda 函数构建时,您需要决定是单独将每个函数推送到其自己的 Git 仓库,还是将它们全部捆绑在一起作为一个单一仓库。

  • AWS Lambda 支持别名,它们是对特定版本的命名指针。这使得使用单个 Lambda 函数用于沙箱、预发布和生产环境变得容易。

  • API 网关阶段变量功能使您能够动态访问不同的 Lambda 函数环境。

  • 电子邮件扩展插件允许您配置电子邮件通知的各个方面。您可以自定义何时发送电子邮件,谁应该接收它,以及电子邮件的内容。

第四部分. 管理和扩展 Jenkins

这最后一部分是关于将你所学的一切结合起来,并更进一步。你将学习如何监控和故障排除正在运行的 Jenkins 集群。我们将从使用 Prometheus 公开 Jenkins 指标并使用 Grafana 构建交互式仪表板开始。接下来,我将演示如何将 Jenkins 日志流式传输到基于 ElasticSearch、Logstash 和 Kibana(ELK)堆栈的集中式日志平台。最后,我将分享有关如何安全和维护 Jenkins 的技巧和最佳实践。

13 收集持续交付指标

本章涵盖

  • 有效监控 Jenkins 及其作业

  • 将 Jenkins 构建日志转发到集中式日志平台

  • 将 Jenkins 日志解析成结构化和可查询的形式

  • 使用 Prometheus 暴露 Jenkins 内部指标

  • 使用 Grafana 构建交互式仪表板

  • 为 Jenkins 创建基于指标的警报

在前几章中,你学习了如何使用自动化工具从头开始设计、构建和部署 Jenkins 集群;你还学习了如何为几个云原生应用设置一个完全工作的 CI/CD 流水线。在本章中,我们将深入探讨高级 Jenkins 主题:监控运行中的 Jenkins 服务器以及检测异常和资源耗尽。在这个过程中,我们将介绍如何为 Jenkins 日志构建一个集中式日志平台。

13.1 监控 Jenkins 集群健康

我们在第五章中构建的集群由一个 Jenkins 主节点和多个工作节点组成,每个节点都在 EC2 实例内部运行。图 13.1 显示了一个典型的 Jenkins 节点配置。

图 13.1 Jenkins 分布式构建架构

到目前为止,Jenkins 集群运行得如预期。然而,你永远不应该将你的 IT 基础设施视为理所当然。你的 Jenkins 主节点或工作节点总有一天会出故障并需要更换。那么,如果你不监控它,你怎么知道你的 Jenkins 集群是否在有效运行呢?

监控 Jenkins 应该成为你 IT 管理的关键部分。监控可以帮助你寻找异常并发现集群中运行的实例上的问题,通过最小化网络中断来节省金钱,并提高效率。

在 AWS 中,你可以使用 Amazon CloudWatch (aws.amazon.com/cloudwatch) 监控 Jenkins 实例。该平台消耗来自所有 AWS 服务的数据,并允许用户可视化、查询并对数据进行操作。默认情况下,Amazon EC2 将指标数据发送到 CloudWatch。

注意:如果你想在 Azure 或 GCP 环境中监控 Jenkins 实例的整体健康和性能,可以使用 Azure Monitor (mng.bz/wQYQ) 或 Google Cloud 的操作 (cloud.google.com/monitoring/quickstart-lamp)。

导航到 Amazon CloudWatch 控制台并跳转到所有指标选项卡。然后,在 EC2 下,通过在搜索栏中输入它们的实例 ID 来查找运行集群的实例,如图 13.2 所示。

图 13.2 EC2 监控的关键指标

你将看到一份相当长的报告指标列表,针对你的 Jenkins EC2 实例。你可以滚动并选择一个或多个指标来显示(例如,EC2 实例 CPU 利用率),并创建一个图形小部件来显示它们,如图 13.3 所示。

图 13.3 当前在 Jenkins 实例上使用的已分配 EC2 计算单元的百分比

默认情况下,EC2 以 5 分钟间隔向 CloudWatch 报告度量。然而,如果您的 Jenkins 集群被广泛使用(例如,托管多个作业和调度许多 CI/CD 管道),您可以在每个实例上启用增强监控功能(mng.bz/GOZR)以获取 1 分钟间隔的度量(尽管会产生额外的费用)。

CloudWatch 也提供仪表板,可以快速查看您的实例性能,同时在数据可视化方面具有极大的灵活性——例如,放大或缩放。

您可以自定义仪表板并添加额外的图表,例如,显示所有网络接口接收和发送的字节数,或磁盘使用情况(从所有实例存储卷写入和读取的字节数),如图 13.4 所示。

图 13.4 构建 CloudWatch 仪表板以监控 Jenkins 实例

现在您已经知道如何使用 CloudWatch 监控 Jenkins 实例。然而,为所有 Jenkins 实例设置 CloudWatch 监控可能会出错且繁琐(以及记住为用于扩展事件的 Jenkins 工作节点执行此操作)。此外,一些度量通过 CloudWatch 不可用(例如,内存使用)。因此,我们将使用高级监控堆栈。

注意:Amazon CloudWatch 代理可以安装在 EC2 实例上以报告额外的有用度量。这个功能很少使用,但了解它的存在是好的。有关说明,请参阅官方指南mng.bz/q5J2

许多工具,从开源到商业级别,都可以帮助您监控您的基础设施并在出现任何故障时通知您。(第 13.3 节介绍了如何设置几乎实时通知您的警报。)好事是,由于维护它的开源社区,一个强大的开源监控解决方案可用。图 13.5 总结了我们将要实施的开源解决方案。

图 13.5 Telegraf 将收集度量,将它们存储在 InfluxDB 中,然后我们可以在 Grafana 中可视化它们。

此监控解决方案可以分为三个部分:

  • Telegraf—一个度量收集代理,安装在每一个 Jenkins 实例上。它收集内部度量并将它们发送到时间序列数据库。

  • InfluxDB—一个开源的时间序列数据库(TSDB),针对快速、高可用性存储进行了优化。它消耗来自 Telegraf 代理的遥测数据。

  • Grafana—一个开源的可视化平台,用于基于存储在 InfluxDB 中的数据构建动态和交互式仪表板。

现在架构已经清晰,我们需要在 EC2 实例上部署一个 InfluxDB 服务器。请查看 InfluxDB 官方文档mng.bz/7lJy,以获取如何安装和配置 InfluxDB 的分步指南。

一旦实例启动并运行,通过 SSH 连接到 InfluxDB 实例,并在终端中输入 influx 命令。influx CLI 是包含在所有 InfluxDB 软件包中的,它是一种轻量级且简单的方式与数据库交互。我们需要创建两个数据库:

  • instances——用于存储有关资源使用的指标,例如 CPU 利用率、内存、网络流量、磁盘使用率等。

  • containers——用于存储在 Jenkins 工作节点中运行的容器的指标。容器基本上是安排给 Jenkins 工作节点的构建作业。

使用 CREATE DATABASE Influx 查询语言 (InfluxQL) 语句创建数据库:

CREATE DATABASE containers;
CREATE DATABASE instances;

也可以通过向端口 8086 上的 InfluxDB API 发送原始 HTTP 请求来创建数据库(见 mng.bz/m1z2)。

现在我们有了数据库,InfluxDB 准备接受查询和写入。要收集 Jenkins 实例指标,我们需要在每个服务器上安装一个 Telegraf 代理。一种方法是在现有实例上安装 Telegraf,但这个解决方案无法扩展,因为每次部署新的 Jenkins 工作节点时,我们都需要安装和配置一个 Telegraf 代理。因此,最好的方法是将在 Jenkins AMI 中打包 Telegraf。再次使用 Packer 将 Jenkins 主节点和工作节点 AMI 打包,其中预安装并配置了 Telegraf 代理。

将下一列表中的代码添加到第四章提供的 setup.sh(chapter13/telegraf/setup.sh)脚本中。此代码将安装 Telegraf 的最新稳定版本(在撰写本书时,版本为 1.19.0)。

列表 13.1 使用 Yum 工具安装 Telegraf 代理

wget https://dl.influxdata.com/telegraf/releases/telegraf-1.19.0-1.x86_64.rpm
yum localinstall telegraf-1.19.0-1.x86_64.rpm
systemctl enable telegraf
systemctl restart telegraf

接下来,我们告诉 Telegraf 要收集哪些指标,通过在 /etc/telegraf/telegraf.conf 创建一个配置文件。该配置文件由 inputs(指标来源)和 outputs(指标去向)组成。以下列表指定了三个输入(CPU 内存使用和 Docker),并将 InfluxDB 指定为输出。Docker 输入读取 Docker 守护进程的指标,然后将这些数据输出到 InfluxDB。

列表 13.2 Telegraf 配置文件,包含各种输入

[global_tags]
hostname="Jenkins"                         ❶

[[inputs.cpu]]                             ❷
  percpu = false
  totalcpu = true
  fieldpass = [ "usage*" ]
  name_suffix = "_vm"

[[inputs.disk]]                            ❸
  fielddrop = [ "inodes*" ]
  Mount_points = ["/"]
  name_suffix = "_vm"

[[inputs.mem]]                             ❹
  name_suffix = "_vm"

[[inputs.swap]]                            ❺
  name_suffix = "_vm"

[[inputs.system]]                          ❻
  name_suffix = "_vm"

[[inputs.docker]]                          ❼
  endpoint = "unix:///var/run/docker.sock"
  container_names = []
  name_suffix = "_docker"

[[outputs.influxdb]]                       ❽
  database = "instances"
  urls = ["http://INFLUXDB_IP:8086"]
  namepass = ["*_vm"]

[[outputs.influxdb]]                       ❾
  database = "containers"
  urls = ["http://INFLUXDB_IP:8086"]
  namepass = ["*_docker"]

❶ 覆盖默认主机名;如果为空,则使用 os.Hostname()

❷ 收集系统 CPU 的指标

❸ 收集磁盘使用情况的指标。默认情况下,统计信息会收集所有挂载点的信息,设置 Mount_points 将限制统计信息仅限于根卷。

❹ 收集系统内存指标

❺ 收集系统交换空间指标

❻ 收集系统负载、运行时间和登录用户数的通用统计信息。它与 Unix 的 uptime 命令类似。

❼ 使用 Docker 引擎 API 收集运行中的 Docker 容器的指标

❽ 将系统指标写入 InfluxDB 实例数据库

❾ 将 Docker 指标写入 InfluxDB 容器数据库

确保将 INFLUXDB_IP 变量替换为运行 InfluxDB 服务器的实例的 IP 地址。

按照第 5.3 节中描述的步骤制作一个新的 Jenkins AMI,并使用新构建的镜像重新部署 Jenkins 集群。一旦新的 Jenkins 集群启动并运行,Telegraf 将开始收集指标并将它们流式传输到 InfluxDB 以进行存储和索引。

要探索指标,我们将使用 Grafana。您可以从 Yum 仓库安装 Grafana 或通过运行 Docker 镜像来安装。(有关更多详细信息,请参阅 Grafana 官方文档mng.bz/5ZY1。一旦安装了 Grafana,请将浏览器导航到 HOST_IP:3000。在登录页面,输入 admin 作为用户名和密码。

在创建用于监控 Jenkins 实例整体健康状况的仪表盘之前,我们需要将 InfluxDB 数据库链接到 Grafana。为此,我们需要为每个 InfluxDB 数据库创建一个数据源。

在侧面板中,点击齿轮图标,然后点击配置 > 数据源。点击图 13.6 所示的添加数据源按钮。然后在设置页面填写以下值:

  • 名称—数据源名称。(这是您在查询中引用数据源的方式。)

  • URL—您的 InfluxDB API 的 HTTP、IP 地址和端口号。(默认情况下,InfluxDB API 端口为 8086。)

  • 数据库—InfluxDB 数据库的名称(实例容器数据库)。

图片

图 13.6 在 Grafana 中配置基于 InfluxDB 的数据源

配置好 InfluxDB 连接后,使用 Grafana 和 InfluxQL 查询并可视化存储在 InfluxDB 中的时序数据。从左侧面板点击仪表盘。从顶部菜单点击首页以获取仪表盘列表。点击底部的新建按钮创建一个新的仪表盘。要添加图表,只需在面板过滤器中点击图表按钮。在查询部分,输入以下 InfluxQL 语句:

SELECT mean("used_percent") FROM "mem_vm" 
WHERE $timeFilte.
GROUP BY time($__interval), "host" fill(null)

此查询从 mem_vm 测量中选择内存使用情况,并按 Jenkins 节点分组结果。查询结果如图 13.7 所示。

图片

图 13.7 构建内存利用率仪表图

要监控 Jenkins 作业的构建时间,可以使用以下语句:

SELECT mean("uptime_ns") FROM "docker_container_status_docker" 
WHERE ("hostname" = 'Jenkins') AND $timeFilte.
GROUP BY time($__interval), "container_name" fill(null)

此查询从 docker_container_status_docker 测量中选择容器在线和运行的时间(即在线时间)值,并按容器名称分组结果(如图 13.8 所示)。

图片

图 13.8 监控 CI/CD 管道内构建的容器

返回 Grafana,您可以为 Jenkins 集群的各个指标创建多个图表进行监控:

  • Jenkins 节点(主节点和工作节点)的 CPU 使用率

  • 网络流量(进出字节数)

  • 每个 Jenkins 节点的内存利用率

  • 运行构建作业的数量

  • 总体健康状态和工作者数量

图 13.9 显示了 Jenkins 集群的宿主级详细信息。完整的仪表板可以从 JSON 文件(chapter13/grafana/dashboard/influxdb.json)中导入。有关说明,请参阅 mng.bz/6mGD

图 13.9 Jenkins 宿主指标

如前所述,监控实例的状态对于保持 Jenkins 集群健康至关重要,通过使用 Telegraf 提供的上述指标(以及许多其他指标),你可以相对容易地实现这一点。

到目前为止,你已经看到了如何监控 Jenkins 实例(服务器端)。现在让我们探索如何监控 Jenkins 服务器本身(应用端)。正如你可能已经猜到的,一个 Jenkins 监控插件可以提供关于 Jenkins 内部发生的事情以及 Jenkins 执行的任务的大量数据。例如,指标插件 (plugins.jenkins.io/metrics/) 通过在 Jenkins 服务器上的 $JENKINS_URL/metrics 端点公开 API 来提供健康检查。该 API 提供以下信息:

  • HTTP 会话和当前 HTTP 请求

  • 按周期详细统计构建时间和构建步骤

  • 线程、操作系统进程列表和堆转储

例如,图 13.10 中的 API 调用返回了 Jenkins 可用执行器的统计信息。

图 13.10 指标 API 与健康检查端点

要创建基于这些指标的仪表板,我们可以编写一个自定义脚本来定期将这些值保存到 InfluxDB,或者使用 Prometheus 指标插件 (plugins.jenkins.io/prometheus/) 来公开一个端点(默认为 /prometheus),Prometheus 服务器可以抓取这些指标。

Prometheus (prometheus.io/) 是一个具有维度数据模型、灵活的查询语言、高效的时序数据库和现代警报方法的开源监控系统。

注意:用于烘焙和部署 Prometheus 服务器的 Packer 模板文件和 Terraform HCL 文件位于 chapter13/prometheus 文件夹中。

首先,从“管理插件”部分安装 Prometheus 指标插件 (plugins.jenkins.io/prometheus/)。安装完成后,你可以在 JENKINS _URL/prometheus(图 13.11)中看到插件的输出。

图 13.11 Prometheus 端点提供指标列表

然后,你需要配置一个 Prometheus 服务器以从 Jenkins 抓取指标。编辑 /etc/prometheus/prometheus.yml 配置文件(列表 13.3)。在 scrape_configs 部分添加一个针对 Jenkins 服务器的作业。此配置文件的编写格式可以在 mng.bz/o8Vr 找到。

列表 13.3 配置 Prometheus 从 Jenkins 抓取指标

global:
  scrape_interval: 10s

scrape_configs:
  - job_name: 'prometheus_master'
    scrape_interval: 5s
    static_configs:
      - targets: ['localhost:9090']
  - job_name: 'jenkins'
    metrics_path: '/prometheus/'
    scheme: https
    static_configs:
       - targets: ['JENKINS_URL']

在 Prometheus 控制台(默认端口为 9090)中,您可以探索从 Jenkins 收集的指标。您将看到图 13.12 中的屏幕。

图片

图 13.12 从 Prometheus 控制台探索 Jenkins 指标

收集的指标如果没有可视化则不太有用。通过创建新的数据源将 Prometheus 与 Grafana 连接起来。要在 Grafana 中创建 Prometheus 数据源,请按照以下步骤操作:

  1. 点击侧面板中的齿轮图标以打开配置菜单。

  2. 点击数据源。

  3. 点击“添加数据源”。

  4. 选择 Prometheus 作为类型。

  5. 设置适当的 Prometheus 服务器 URL 为 prometheus:9090

  6. 点击“保存并测试”以保存新的数据源。

然后,根据可用的指标创建一个仪表板。仪表板包括应用级指标(跟踪队列中作业的总数、待处理作业的数量以及卡住或延迟的作业数量),接着是内部操作指标(JVM),最后是系统级指标(磁盘 I/O、网络、内存等)。图 13.13 展示了仪表板的一部分。

图片

图 13.13 作业和构建的全面 Jenkins 监控摘要

完整的仪表板可以从以下 JSON 文件导入:chapter13/grafana/dashboard/prometheus.json。

另一个流行的 Jenkins 监控解决方案是监控插件(之前称为 JavaMelody)。此插件生成关于 Jenkins 状态的全面 HTML 报告,包括 CPU 和系统负载、平均响应时间和内存使用情况;有关更多详细信息,请参阅 plugins.jenkins.io/monitoring/。此外,报告由 Jenkins 控制台提供,如图 13.14 所示。

图片

图 13.14 JavaMelody 监控的统计信息

太好了!现在您应该能够监控在生产环境中运行的 Jenkins 集群。为了进一步了解您的 Jenkins 环境,您可以收集和分析实时系统和安全事件的 Jenkins 日志,并将它们与性能和服务器指标相关联,以识别和解决问题。

13.2 使用 ELK 对 Jenkins 日志进行集中式记录

默认情况下,Jenkins 日志位于 /var/log/jenkins/jenkins.log。要查看这些日志,使用堡垒主机 SSH 到 Jenkins 主实例,然后执行以下命令:

tail -f -n 100 /var/log/jenkins/jenkins.log

图 13.15 显示了命令输出。

图片

图 13.15 查看 /var/log/jenkins/jenkins.log 中的 Jenkins 日志

您也可以从网络控制台(图 13.16)查看这些日志。转到 Jenkins 控制台,并在管理 Jenkins 页面上选择系统日志。

图片

图 13.16 从 Jenkins 控制台查看 Jenkins 日志

默认情况下,Jenkins 将每个 INFO 日志记录到 stdout,但您可以通过创建自定义日志记录器来配置 Jenkins 记录特定 Jenkins 插件的日志。从系统日志页面,点击“添加新日志记录器”按钮并选择一个有意义的名称。图 13.17 中的示例创建了一个 Slack 插件的日志记录器(Java 包位于 jenkins.plugins.slack)。

图 13.17 使用自定义日志记录器捕获 Slack 插件的登录信息

现在,如果 Jenkins 管道发送任何 Slack 通知,应该会捕获如图 13.18 所示的日志。

图 13.18 Slack 插件的日志显示

您还可以通过导航到仪表板中的作业项并点击“控制台输出”,或查看 \(JENKINS_HOME/jobs/\)JOB_NAME/builds/$BUILD_NUMBER/log 中的日志文件内容来查看特定作业的构建日志。

根据日志轮转配置,日志可能会保存 X 次构建(或天数等),这意味着旧的工作日志可能会丢失。这就是为什么您需要将日志持久化到集中式日志平台,以便进行审计和潜在的故障排除。

注意:您可以在每个项目或作业配置页面中启用“丢弃旧构建”插件 (plugins.jenkins.io/discard-old-build/) 来配置保留旧构建的间隔(例如,每月一次,每 10 次构建一次等)。

此外,分析 Jenkins 日志可以提供大量信息,有助于排查管道作业失败的根本原因。构建日志包含完整的记录,如构建名称、编号、执行时间等。然而,要分析这些日志,您需要将它们发送到外部日志平台。这就是像 ELK 堆栈(Elasticsearch、Logstash 和 Kibana)这样的平台发挥作用的地方。

13.2.1 使用 Filebeat 流式传输日志

Filebeat (www.elastic.co/beats/filebeat),一个轻量级代理,将安装在 Jenkins 主实例上,并将日志发送到 Logstash (www.elastic.co/logstash) 进行处理和聚合。从那里,日志将被存储在 Elasticsearch (www.elastic.co/elasticsearch) 中,并通过交互式仪表板在 Kibana (www.elastic.co/kibana) 中可视化。图 13.19 总结了整个工作流程。

图 13.19 使用 Filebeat 将 Jenkins 日志发送到 ELK 平台

要部署此架构,我们需要为每个组件创建一个机器镜像。您可以使用 Packer 来烘焙 AMI(图 13.20)。Packer 模板可在 GitHub 仓库的 chapter13/COMPONENT_NAME/packer/template.json 中找到。

一旦创建了 AMI,你就可以使用 Terraform 来部署 ELK 堆栈。模板资源可在 GitHub 仓库 chapter13/COMPONENT_NAME/terraform/*.tf 中找到。

图 13.20 使用 Packer 构建的 Logstash、Kibana 和 Elasticsearch AMI

在配置过程结束时,应该创建了三个 EC2 实例,如图 13.21 所示。

图 13.21 在 AWS 上部署的 ELK 堆栈

当日志平台准备好消费传入的 Jenkins 日志时,我们需要在 Jenkins 主实例上安装 Filebeat。通过 SSH 连接到 Jenkins 服务器,并运行以下列表中的命令来安装 Filebeat 的最新稳定版本(在撰写本书时,版本为 7.13.2)。

列表 13.4 在 Jenkins 服务器上安装 Filebeat 代理

curl -L -O https://artifacts.elastic.co/downloads/beats/
filebeat/filebeat-7.13.2-x86_64.rpm
sudo rpm -vi filebeat-7.13.2-x86_64.rpm

接下来,我们需要设置我们想要转发到 ELK 的日志文件的路径。这里我们希望将日志转发到 /var/log/jenkins/jenkins.log。转到 /etc/filebeat 下的 Filebeat 配置目录,并使用以下列表更新 filebeat.yml。

列表 13.5 Filebeat 输入配置

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /var/log/jenkins/jenkins.log                     ❶
  fields:                                              ❷
    type: jenkins                                      ❷
  multiline.pattern: '[0-9]{4}-[0-9]{2}-[0-9]{2}'      ❸
  multiline.negate: true                               ❸
  multiline.match: after                               ❸
output.logstash:                                       ❹
  hosts: ["LOGSTASH_HOST"]                             ❹

processors:                                            ❺
 - add_host_metadata: ~                                ❺
 - add_cloud_metadata: ~                               ❺
 - add_docker_metadata: ~                              ❺
 - add_kubernetes_metadata: ~                          ❺

❶ 从 /var/log/jenkins/jenkins.log 文件中提取行

❷ 在输出中添加一个名为 type 的字段,以便我们能够轻松识别来自 Jenkins 的日志

❸ 配置 Filebeat 以处理多行消息

❹ 直接将日志发送到 Logstash

❺ 使用主机机的相关元数据注释每个日志事件

Jenkins 日志中常见的多行消息,尤其是包含 Java 栈跟踪的日志消息。以下是一个 Java 栈跟踪的示例:

2020-10-22 20:06:58.217+0000[id=124635] FATAL: Ping failed.
                        java.util.concurrent.TimeoutException:
                        at hudson.remoting.PingThread.ping(PingThread.java:134)
                        at hudson.remoting.PingThread.run(PingThread.java:90)

为了正确处理这些多行消息,我们使用 multiline 设置来指定哪些行是单个日志消息的一部分。

LOGSTASH_HOST 变量替换为 Logstash 服务器的 IP 地址。然后使用以下命令重新启动 Filebeat 代理:

systemctl restart filebeat

前往 Kibana 仪表板(在 KIBANA_IP:5601),跳转到管理标签,然后到索引模式。我们必须创建一个新的索引模式。创建索引模式意味着将 Kibana 与 Elasticsearch 索引映射。由于 Logstash 将传入的 Jenkins 日志存储到一系列格式为 jenkins-YYYY.MM.DD 的索引中,因此我们将创建一个索引模式 jenkins-* 来探索所有日志,如图 13.22 所示。

点击下一步选项。从时间过滤器字段名称下拉菜单中选择 @timestamp。然后点击创建索引模式按钮。

现在,要查看日志,请转到发现页面。你可以看到你的索引数据正在传入(如图 13.23 所示)。

图 13.22 将 Elasticsearch 索引连接到 Kibana

图 13.23 从 Kibana 可视化 Jenkins 日志

现在,你已经有一个可以读取 Jenkins 日志的工作管道。然而,你会注意到日志消息的格式并不理想。你希望解析日志消息以从日志中创建特定的命名字段。以下是一个 Jenkins 日志的示例:

2020-06-02 15:21:56.990+0000 INFO  o.j.p.workflow.job.WorkflowRun#finish: movies-loader/develop #7 completed: SUCCESS

行首的时间戳很容易定义为日志级别(INFOWARNINGDEBUG 等)。要解析行,我们可以编写一个 Grok 表达式。

Grok 通过解析文本模式,使用正则表达式,并将它们分配给一个标识符来工作。语法是 %{PATTERN:IDENTIFIER}。我们可以编写一系列 Grok 模式并将前一条日志消息的各个部分分配给不同的标识符,如下面的列表所示。

列表 13.6 解析 Jenkins 日志消息的 Grok 表达式

%{TIMESTAMP_ISO8601:createdAt} %{LOGLEVEL:level}%{SPACE}%{JAVACLASS:class}%{DATA:state}:%{SPACE}%{JOBNAME:project} #%{NUMBER:buildNumber} %{DATA:execution}: %{WORD:status}

Grok 随带自己的模式字典,你可以直接使用。但你可以始终定义自己的自定义模式,如下面的列表所示。

列表 13.7 Grok 自定义模式定义

JAVACLASS (?:[a-zA-Z0-9-]+\.)+[A-Za-z0-9$]+
JOBNAME [a-zA-Z0-9\-\/]+

你可以使用 Kibana Grok Debugger 控制台来调试表达式。此功能在 Kibana 中自动启用,位于 DevTools 选项卡上。

在“样本数据”字段中输入日志消息,在“Grok 模式”字段中输入 Grok 表达式。然后点击模拟。你将看到应用 Grok 模式后产生的模拟事件(图 13.24)。

图 13.24 使用 Grok Debugger 工具模拟 Grok 解析

注意:Grok 模式引用了 JAVACLASSJOBNAME 自定义模式。它们在自定义模式部分定义。每个模式定义都在其自己的行上。

注意:如果发生错误,你可以继续迭代自定义模式,直到输出匹配你期望的事件。

Grok 表达式正在工作,但我们希望解析机制在将日志存储到 Elasticsearch 之前完成。这就是为什么我们将更新 Logstash 配置(chapter13/logstash/packer/jenkins.conf)以解析来自 Filebeat 的传入日志。filter 部分将尝试匹配来自 Jenkins 的消息与之前定义的 Grok 表达式,如下面的列表所示。

列表 13.8 在 Logstash 层解析 Jenkins 日志

filter {
  if [type] == "jenkins" {
    grok {
      patterns_dir => ["/etc/logstash/patterns"]
      match => {
        "message" => "%{TIMESTAMP_ISO8601:createdAt}%{SPACE}\[id=%{INT:buildId}\]
%{SPACE}%{LOGLEVEL:level}%{SPACE}%{JAVACLASS:class}
%{DATA:state}:%{SPACE}%{JOBNAME:project.
#%{NUMBER:buildNumber} %{DATA:execution}: %{WORD:status}"
      }
    }
  }
}

此代码将 Filebeat 收集的 Jenkins 日志解析成字段,并将字段发送到 Elasticsearch。pattern_dir 设置告诉 Logstash 你的自定义模式目录在哪里。你可以通过添加更多处理来自定义解析机制,例如删除未使用的字段或重命名字段。有关更多信息,请参阅 mng.bz/J6Av 上的 Mutate Filter 插件。

重新启动 Logstash 以重新加载配置。你的 Jenkins 日志将被收集并结构化成字段(图 13.25)。目前里面没有多少内容,因为你只收集了 Jenkins 日志。在这里,你可以搜索和浏览你的日志。

图 13.25 将 Jenkins 日志结构化成可查询的字段

来自 Jenkins 的每个日志消息都将匹配并产生表 13.1 中列出的字段。

表 13.1 Elasticsearch 中的 Jenkins 索引字段

字段 描述
time 消息的数据和时间,以 UTC 格式表示
level 日志消息级别(INFO、WARNING、DEBUG、FATAL、ERROR)
project Jenkins 作业的构建名称
buildNumber 作业的构建号,用于标识 Jenkins 运行此构建过程的次数
status 构建状态(失败或成功)
execution 构建当前状态(运行中、挂起、已终止或已完成)

您可以根据status字段在一段时间内显示失败与成功构建数量的堆叠柱状图;见图 13.26。

图片

图 13.26 基于 Jenkins 结构化字段构建交互式小部件

您可以将柱状图保存为小部件并导入到仪表板中。使用仪表板,您可以将多个可视化组合到单个页面上,然后通过提供搜索查询或通过在可视化中单击元素来选择过滤器来过滤它们。仪表板在您想要获取 Jenkins 日志的概览并在不同可视化之间建立关联时非常有用;见图 13.27。

图片

图 13.27 从 Kibana 仪表板分析 Jenkins 日志

完整的仪表板可以从以下 JSON 文件导入:chapter13/kibana/dashboard/jenkins.json。

就这样!您已成功创建了一个使用 Filebeat 将 Jenkins 日志作为输入,将这些日志转发到 Logstash 进行解析,并将解析后的数据写入 Elasticsearch 服务器的管道。

13.2.2 使用 Logstash 插件流式传输日志

您可以通过 Jenkins 上的 Logstash 插件(plugins.jenkins.io/logstash/)直接将 Jenkins 日志发送到 Elasticsearch 实例来跳过 Filebeat 和 Logstash 的配置。如果您尚未使用外部 Logstash 代理将您的基础设施或应用程序日志流式传输到 Elasticsearch,并且不需要使用自定义 Grok 表达式丰富日志解析机制,则此解决方案非常理想。此外,Logstash 插件可以将 Jenkins 实例的日志数据流式传输到任何索引解决方案(包括 Redis、RabbitMQ 和 Elasticsearch)。在当前场景中,我们将使用 Elasticsearch。

在 Jenkins 仪表板的全球配置中成功安装 Logstash 插件后,我们需要使用目标索引器配置该插件。配置 URI,即 Elasticsearch 服务器运行的地址,如图 13.28 所示。

图片

图 13.28 配置 Logstash 插件将日志流式传输到 Elasticsearch 服务器

在 Logstash 配置中配置 Elasticsearch 端点后,您可以在您的管道中添加以下块。这样,所有在logstash步骤中产生的日志都将流式传输到 Elasticsearch:

logstash {
     echo "Job:${env.JOB_NAME}"
}

您可以通过访问如图 13.29 所示的 Kibana 仪表板来查看流式日志。

图片

图 13.29 发送到 Elasticsearch 的日志消息示例

现在我们可以将 Jenkins 实例的日志数据流式传输到 Elasticsearch,最终传输到 Kibana。

13.3 基于指标创建警报

我们可以将日志和监控解决方案进一步扩展并设置警报。最常见的用例是 DevOps 团队接收事件通知,例如当失败构建率显著高于正常水平时。不用说,这个问题可能会对新功能的发布产生重大影响,从而影响业务和用户体验。

您可以使用 Kibana 在指定的条件下定义一个有意义的警报;参见图 13.30。例如,您可以定义一个警报来定期检查失败构建率。对于通知通道,您可以使用 Slack、OpsGenie 或简单的电子邮件通知。

图 13.30 在 Kibana 上配置警报

您还可以通过使用 Grafana 警报功能,根据 Prometheus 或 Telegraf 收集的指标创建警报。

注意:虽然设置和使用 Grafana 警报很简单,但在将警报规则应用于您的指标查询方面更为有限。如果您正在寻找一个高级解决方案,请选择 Prometheus Alertmanager (prometheus.io/docs/alerting/latest/alertmanager/)。

在创建监控警报之前,我们需要添加将通过其接收通知的通知通道。在这里,我们将添加 Slack 作为通知通道。

要设置 Slack,您需要配置一个传入的 Slack webhook URL。通过访问 api.slack.com/apps/new 创建 Slack 应用程序。创建应用程序后,您将被重定向到新应用的设置页面(图 13.31)。从那里,通过切换单选按钮到“开启”来启用“传入 webhook”功能。

图 13.31 在 Slack 应用程序上启用传入 webhook

现在,启用传入 webhook 后,设置页面应该刷新,并出现一些额外选项。其中之一将是一个非常有用的按钮,标记为“添加新 webhook 到 Workspace”,您应该点击它。

请继续选择 Grafana 将要发布消息的 Slack 频道,然后点击“授权您的应用”。您将被返回到您的应用设置页面,在那里您现在应该在“您的 Workspace”部分的 webhook URLs 下看到一个新条目,其中包含一个 webhook URL。复制它。

在创建 webhook URL 后,您需要在 Grafana 中创建一个通知通道。在 Grafana 侧边栏中,将鼠标悬停在“警报”图标上,然后单击“通知通道”,如图 13.32 所示。按照以下步骤创建 Slack 通知通道:

  1. 输入频道的名称。

  2. 将类型更改为 Slack 并输入您已创建的 webhook URL。

图 13.32 配置新的 Slack 通知通道

你可以通过点击底部的发送测试按钮来测试设置。设置所有字段后,只需点击保存按钮。

现在,让我们创建警报。选择你想要创建警报的面板。例如,我们可以在内存使用指标上创建警报。点击警报选项卡,然后点击创建警报。这将打开一个配置警报的表单,你可以设置以下选项:

  • 评估间隔—你希望警报规则评估的时间间隔。对于本例,我们可以将选项设置为每 1 分钟评估 1 分钟。这意味着 Grafana 将每分钟评估一次规则。如果指标违反规则,Grafana 将等待 1 分钟。如果 1 分钟后指标没有恢复,Grafana 将触发警报。

  • 条件—我们可以使用 avg() 函数,因为我们想验证我们的规则与平均内存利用率。

当平均内存利用率超过 90% 时,将触发此警报,如图 13.33 所示。

图 13.33 定义内存使用警报规则

此外,我们需要添加需要发送警报的通知渠道以及警报消息。如果警报被触发,你将在 Slack 频道上看到图 13.34 中的消息。

图 13.34 超过内存阈值时的 Slack 通知

创建一个发送到类似 Slack 的消息应用的警报非常有用。这确保了如果发生错误,你和你的团队成员会立即收到通知。你可以提及你的团队 Slack 群组或使用 @here@channel 确保你的团队收到消息。

摘要

  • 你可以使用 Telegraf、InfluxDB 和 Grafana 构建一个监控堆栈,以收集、存储和可视化 Jenkins 实例指标。

  • 你可以通过编写 Grok 表达式将 Jenkins 日志收集和解析到结构化字段中。

  • Prometheus 插件可用于在 Jenkins 中公开内部和客户端指标。

  • Logstash 插件是集成 Jenkins 日志与 ELK 堆栈的简单方法。

  • Filebeat 可以作为代理安装在 Jenkins 主实例上,以便将日志发送到 Logstash 进行解析。从那里,日志将被存储在 Elasticsearch 中,并在 Kibana 的交互式仪表板中进行分析。

14 Jenkins 管理和最佳实践

本章涵盖

  • 在 CI/CD 管道中共享通用代码和步骤

  • 授予用户作业权限

  • 使用 GitHub 进行认证信息以保护 Jenkins 实例

  • 备份和恢复 Jenkins 插件和作业

  • 将 Jenkins 用作 cron 作业的调度器

  • 将构建作业迁移到新的 Jenkins 实例

第十三章介绍了如何监控 Jenkins 集群,以及如何配置警报并将 Jenkins 日志和指标关联起来以识别问题和避免停机。在本章中,您将学习如何通过为登录用户设置基于角色的访问控制 (RBAC) 以实现细粒度访问来加强 Jenkins 的安全性,以及如何通过使用 GitHub 认证机制添加额外的安全层。

我们还将讨论一些在维护 Jenkins 实例时可能觉得有用的技巧和窍门。我们将探讨如何备份、恢复和存档构建作业,或者将它们从一个服务器迁移到另一个服务器。

14.1 探索 Jenkins 安全性和 RBAC 授权

当前 Jenkins 的配置允许未登录用户具有读取访问权限,而登录用户几乎可以访问一切。要覆盖此默认行为,请从管理 Jenkins(图 14.1)转到配置全局安全性部分。

图 14.1 在 Jenkins 中启用安全性

禁用允许匿名读取访问并启用允许用户注册,然后您将被重定向到登录页面。此选项允许用户通过图 14.2 中显示的创建账户链接自行创建账户。

图 14.2 Jenkins 登录页面

点击创建账户链接。您将被提示添加新用户。在图 14.3 中,我们正在设置一个开发者账户。

图 14.3 设置开发者账户

创建新账户后,登录。您会注意到它对 Jenkins 具有完全控制权。让登录用户做任何事情当然很灵活,也许这对一个小团队来说就足够了。对于更大的团队或多个团队,或者当 Jenkins 在开发环境之外使用时,通常需要更安全的方法。

注意 默认情况下,如果用户创建账户,Jenkins 不使用 CAPTCHA 验证。如果您想启用 CAPTCHA 验证,请安装支持插件,例如 Jenkins JCaptcha 插件 (plugins.jenkins.io/jcaptcha-plugin/)。

14.1.1 矩阵授权策略

要为登录用户设置细粒度访问,我们可以使用 Jenkins 矩阵授权策略插件 (plugins.jenkins.io/matrix-auth/)。此插件允许您通过指定用户控制每个项目的作业权限,这些用户可以在该作业上执行某些操作。

一旦安装了矩阵授权策略插件,请转到配置全局安全。在授权部分,启用基于项目的矩阵授权策略。Jenkins 将显示一个包含授权用户和对应于你可以分配给这些用户的各种权限的复选框的表格(图 14.4)。

图片

图 14.4 基于矩阵的安全配置

权限被组织到几个组中,例如这些。

  • 总体—涵盖基本的系统级权限。

  • 凭据—涵盖管理 Jenkins 凭据。

  • 代理—涵盖与构建节点或工作者的权限(添加或删除 Jenkins 节点)。

  • 作业—涵盖与作业相关的权限(创建新的构建作业、更新或删除现有的构建作业)。

  • 运行—涵盖与构建历史中特定构建相关的权利。

  • 查看—涵盖管理视图。Jenkins 中的视图允许我们将作业和内容组织到标签页分类中。

  • 源代码管理(SCM)—涵盖与版本控制系统(如 Git 或 SVN)相关的权限。

矩阵控制用户可以执行的操作(读取作业、执行构建、安装插件等)。我们有几个内置的授权需要考虑:

  • 匿名用户—任何未登录的用户

  • 认证用户—任何已登录的用户

你可以通过点击添加用户或组来为特定用户配置权限。添加两个用户:一个管理员(例如,mlabouardy/admin)和一个普通用户(例如,developer)。

用户旁边的所有复选框都是用于设置全局权限的。选择所有复选框以授予管理员完全权限。对于开发者(即 John Doe),我们在作业下选择读取权限。这样,开发者现在将有权查看我们在前几章中创建的所有作业;请参阅图 14.5。

点击保存,如果你使用开发者凭据登录,则会打开登录页面。在此模式下,开发者账户只有读取权限,如图 14.6 所示(例如,开发者无法触发构建或配置作业设置)。

图片

图 14.5 精细调整用户权限

图片

图 14.6 Jenkins 只读访问

到目前为止,你已经看到了如何创建和管理 Jenkins 用户,以及如何为这些用户授予细粒度的访问权限。然而,在一个大型组织中,为多个用户分配细粒度的权限可能会很繁琐。幸运的是,你可以在 Jenkins 中创建具有适当权限的不同角色,并将它们分配给不同的用户。

14.1.2 基于角色的授权策略

要管理不同的角色,请从插件管理页面安装基于角色的授权策略插件(plugins.jenkins.io/role-strategy/)。然后从管理全局安全页面激活基于角色的策略选项,如图 14.7 所示。

图片

图 14.7 启用基于角色的授权策略插件

然后,你可以在管理 Jenkins 页面上通过选择管理并分配角色选项(图 14.8)来定义全局角色。请注意,只有当你正确安装了插件时,管理并分配角色才会可见。

图 14.8 定义自定义角色

点击管理角色选项以添加新角色。创建三个具有适当权限的自定义角色:

  • 管理员—将被分配给 Jenkins 管理员以获得对 Jenkins 的完全访问权限

  • 开发者—将被分配给开发者以获得构建作业和查看其日志和状态的权限

  • 质量保证—将被分配给软件质量保证工程师以查看作业状态/健康的权限

然后,从分配角色屏幕(图 14.9)将这些角色分配给特定的用户。在这些设置中,我们将管理员角色分配给管理员账户,将开发者角色分配给开发团队成员,将质量保证角色分配给软件质量保证人员。

图 14.9 管理和分配角色

如果你在组织内部使用 Jenkins,创建和管理用户访问可能是一项繁琐的任务。你可以使用 GitHub 作为认证机制。

注意:你可以使用 Jenkins 配置许多 OAuth2 认证服务,包括 GitLab、Google 和 OpenID。

14.2 配置 Jenkins 的 GitHub OAuth

Jenkins 支持多个认证插件,除了内置的用户名和密码认证之外。如果你在组织内部使用 GitHub 作为版本控制系统,你也可以使用 GitHub OAuth 服务进行用户认证和权限管理。

在 Jenkins 上,从管理插件中安装 GitHub 认证插件(plugins.jenkins.io/github-oauth/)。安装完成后,前往你的 GitHub 账户并创建一个新的应用程序(github.com/settings/applications/new),命名为 Jenkins,并使用图 14.10 中的设置。

图 14.10 配置 GitHub OAuth 应用

授权回调 URL 必须是 JENKINS_URL/securityRealm/finishLogin。点击注册应用程序按钮。将生成客户端 ID 和密钥,如图 14.11 所示。保持页面打开以进行应用程序注册,以便可以将此信息复制到你的 Jenkins 配置中。

图 14.11 应用程序客户端 ID 和客户端密钥

返回 Jenkins,在全局安全配置中,将安全领域选项设置为 GitHub 认证插件。然后设置客户端 ID、客户端密钥和 OAuth 范围,如图 14.12 所示。

图 14.12 配置 Jenkins 客户端设置以进行 OAuth

点击保存和应用按钮以重新加载配置。你现在可以使用你的 GitHub 账户登录,如图 14.13 所示。

图 14.13 授权 Jenkins 访问您的 GitHub 账户

与经典的用户名和密码身份验证类似,您可以使用基于项目的矩阵授权策略来确定每个 GitHub 账户的 Jenkins 权限。

另一个选项是使用 GitHub 提交者授权策略。如果您选择此选项,可以使用 GitHub 仓库权限来确定每个 Jenkins 项目的权限。如果项目的 GitHub 仓库是公开的,所有经过身份验证的用户都将具有只读访问权限,而项目协作者可以构建、编辑、配置、取消或删除 Jenkins 作业。然而,如果项目的 GitHub 仓库是私有的,只有协作者才能管理 Jenkins 作业。

要根据 GitHub 访问权限确定 Jenkins 访问权限,请从“管理 Jenkins”转到“配置全局安全”部分(图 14.14)。

图片

图 14.14 配置 GitHub 授权设置

注意:我们已经授权使用 /github-webhook 回调 URL 接收来自 GitHub 的 post-commit 钩子。

14.3 跟踪 Jenkins 用户操作

除了配置用户账户和访问权限之外,跟踪个别用户操作也可能很有用:换句话说,谁对您的 Jenkins 配置做了什么。这种审计跟踪功能在许多组织中对于安全合规性甚至是必需的。

Audit Trail 插件(plugins.jenkins.io/audit-trail/)会跟踪一组滚动日志文件中的主要用户操作。要设置此插件,请转到插件管理器页面,并在可用插件列表中选择 Audit Trail 插件。然后,像往常一样,在插件下载完成后,点击安装并重启 Jenkins。

要启用审计日志记录,请从主 Jenkins 配置页面配置插件。选择“日志文件”作为记录器;这样,插件将生成系统风格的日志文件。然后,设置日志位置(日志文件要写入的目录),如图 14.15 所示。当然,您需要确保运行您的 Jenkins 实例的用户有权写入此目录。

图片

图 14.15 配置 Audit Trail 插件

默认情况下,审计日志中记录的详细信息相对较少——它们有效地记录了执行的关键操作,例如创建、修改或删除作业配置或视图,以及执行这些操作的用户。日志还显示了单个构建作业是如何启动的。图 14.16 显示了默认日志的摘录。

图片

图 14.16 查看授权用户活动审计日志

您还可以配置要维护的日志文件数量以及每个文件的最大大小。在之前的配置中,我们将日志文件计数设置为 10;在这种情况下,Jenkins 将写入名为 jenkins-audit.log.0、jenkins-audit.log.1 ... jenkins-audit.log.9 的日志文件。现在,您可以访问整个服务器的配置历史,包括系统配置更新以及每个项目配置的更改。

注意:您可以将前面的配置进一步扩展,将这些日志文件流式传输到集中的 ELK 平台,并设置对未经授权用户活动的警报。有关分步指南,请返回第十三章。

14.4 使用共享库扩展 Jenkins

在整本书中,您已经学习了如何为多个应用程序编写 CI/CD 管道,在实现这些管道步骤时,我们已经调用了多个自定义函数。以下列表中显示的这些函数在多个 Jenkinsfiles 中重复出现。

列表 14.1 Git 和 Slack 的辅助函数

def commitAuthor(){
    sh 'git show -s --pretty=%an > .git/commitAuthor'
    def commitAuthor = readFile('.git/commitAuthor').trim()
    sh 'rm .git/commitAuthor'
    commitAuthor
}

def commitID() {}
def commitMessage() {}
def notifySlack(String buildStatus){}

因此,我们在不同的管道中有些共同的代码。为了避免在不同的管道中复制粘贴相同的代码,并减少冗余,我们可以在 Jenkins 中的共享库中集中这些共同代码。这样,我们就可以在所有管道中引用相同的代码。

共享库是一组存储在 Git 仓库中的独立 Groovy 脚本。这意味着您可以进行版本控制、打标签,并执行您习惯使用 Git 的所有操作。在我们编写第一个 Jenkins 共享库之前,我们需要创建一个 GitHub 仓库,其中将存储 Groovy 脚本。

在仓库内部,创建一个 vars 文件夹,并为每个函数编写一个 Groovy 脚本。例如,创建一个名为 commitAuthor.groovy 的文件,并定义一个名为call的函数。当调用commitAuthor指令时,将执行函数的主体,如下所示。

列表 14.2 在共享库中定义全局变量

#!/usr/bin/env groovy                                        ❶

def call() {                                                 ❷
 sh 'git show -s --pretty=%an > .git/commitAuthor'           ❸
 def commitAuthor = readFile('.git/commitAuthor').trim()     ❸
 sh 'rm .git/commitAuthor'                                   ❸
 commitAuthor                                                ❸
}

❶ 在您的路径中搜索 Groovy 以执行脚本

❷ 允许以类似于步骤的方式调用全局变量

❸ 打印 Git 提交作者

注意,Groovy 脚本必须实现call方法。在花括号{}内编写您的自定义代码。您还可以向您的函数添加参数。对其他函数也这样做,并将更改推送到远程仓库。最终,您的仓库应该看起来像图 14.17 所示。

图 14.17 共享库自定义全局变量

现在您已经使用自定义步骤创建了您的库,您需要通知 Jenkins。要添加共享库,请转到作业配置。在 Pipeline Libraries 下,添加具有以下设置的库:

  • 名称—在管道脚本中将使用的简短标识符

  • 默认版本—可以是 Git 能理解的任何内容,例如分支、标签或提交 ID 哈希

接下来,从图 14.18 所示的 GitHub 仓库的 master 分支加载库。

图 14.18 从 GitHub 加载共享库

注意:你还可以在“管理 Jenkins”>“配置系统”>“全局管道库”中全局定义共享库。这样,所有管道都可以使用在此库中实现的功能。

要在管道中加载共享库,你需要使用管道定义顶部的 @Library 注解来导入它。然后通过其名称调用目标函数,如下面的列表所示。

列表 14.3 在脚本管道中导入共享库

@Library('utils')_      ❶

node('workers'){
 stage('Checkout'){
    checkout scm
    notifySlack 'STARTED'
 }
}

❶ 如果紧跟在 @Library 注解之后的行不是导入语句,则需要下划线。

下划线不是打字错误或错误;如果紧跟在 @Library 注解之后的行不是导入语句,你需要这个下划线。你可以使用 @Library('id@version') 注解覆盖为库定义的默认版本。

如果你使用的是声明式管道,你需要将库名称包裹在库部分中,如下面的列表所示。

列表 14.4 在声明式管道中导入共享库

libraries {
     lib('utils')
 }
 pipeline {
     // Your pipeline would go here....
 }

当使用库时,你也可以指定以下格式的版本:

 libraries {
     lib('utils@VERSION')
 }

运行前面的管道,输出应该类似于图 14.19。

图 14.19 在管道中从 Git 加载共享库。

编写库的另一种方法是定义 Groovy 类中的函数。在 src/com/labouardy/utils 中创建 Git.groovy 类,如下面的列表所示。

列表 14.5 编写共享库

#!/usr/bin/env groovy
package com.labouardy.utils

class Git {
   Git(){}

   def commitAuthor() {
       sh 'git show -s --pretty=%an > .git/commitAuthor'
       def commitAuthor = readFile('.git/commitAuthor').trim()
       sh 'rm .git/commitAuthor'
       commitAuthor
   }

   def commitID() {
       sh 'git rev-parse HEAD > .git/commitID'
       def commitID = readFile('.git/commitID').trim()
       sh 'rm .git/commitID'
       commitID
   }

   def commitMessage() {
       sh 'git log --format=%B -n 1 HEAD > .git/commitMessage'
       def commitMessage = readFile('.git/commitMessage').trim()
       sh 'rm .git/commitMessage'
       commitMessage
   }
}

你可以通过选择它们的完全限定名称来加载库中定义的类:

@Library('utils') import com.labouardy.utils.Git
this.commitAuthor()

或者你可以创建一个对象构造函数,然后从对象中调用方法:

def gitUtils = new Git(this)
gitUtils.commitAuthor

注意:通过使用 @Grab 注解,你可以从受信任的库代码中使用 Maven Central (search.maven.org/) 中找到的第三方 Java 库。有关详细信息,请参阅 Grape 文档 (mng.bz/nrxg)。

14.5 备份和恢复 Jenkins

备份数据是一种普遍推荐的做法,你的 Jenkins 服务器不应例外。幸运的是,备份 Jenkins 相对容易。在本节中,我们将探讨几种备份的方法。

在 Jenkins 中,所有设置、构建日志和归档的工件都存储在 $JENKINS_HOME 目录下。你可以手动备份该目录,或者使用像 ThinBackup (plugins.jenkins.io/thinBackup/) 这样的插件。该插件提供了一个简单的用户界面,你可以使用它来备份和恢复你的 Jenkins 配置和数据。

安装插件后,您需要配置备份目录,如图 14.20 所示。指定备份目录为/var/lib/backups。确保 Jenkins 有写入权限!

图 14.20 配置 ThinBackup 插件

现在,您可以通过点击“立即备份”选项来测试备份是否正常工作。它将在设置中指定的备份目录中创建 Jenkins 数据的备份:

要恢复以前的配置,只需转到“恢复”页面并选择您希望恢复的配置日期,如图 14.21 所示。一旦配置已恢复到之前的状态,您需要从磁盘重新加载 Jenkins 配置或重启 Jenkins。

图 14.21 恢复以前的配置

由于备份,在数据损坏或人为事件发生的情况下,您可以从较早的时间点恢复 Jenkins。

注意:ThinBackup 插件将备份存储在本地以供生产使用。强烈建议将备份存储在远程服务器上或挂载外部数据存储。

如果您不喜欢插件,您可以在 Jenkins 上设置一个 cron 作业(下一节将提供更多详细信息)来安排定期备份。它将备份位于/var/lib/jenkins 的所有内容到远程存储库,如以下列表所示。

列表 14.6 将$JENKINS_HOME 文件夹备份到 S3 存储桶

cd $JENKINS_HOME
BACKUP_TIME=$(date +'%m.%d.%Y')
zip -r backup-${BACKUP_TIME} .
aws s3 cp  backup-${BACKUP_TIME} s3://BUCKET/

有时您需要将 Jenkins 构建作业从一个 Jenkins 实例移动或复制到另一个实例,而不复制整个 Jenkins 配置。例如,您可能正在将构建作业迁移到全新实例上的 Jenkins 服务器。

您可以通过简单地复制或移动构建作业目录到新的 Jenkins 实例来在项目实例之间复制或移动构建作业。我已经构建了一个名为 Butler 的开源 CLI(github.com/mlabouardy/butler),以便轻松导入/导出 Jenkins 作业和插件。

要开始,找到适合您系统的适当软件包并下载它。以下是 Linux 的命令:

wget https://s3.us-east-1.amazonaws.com/butlercli/1.0.0/linux/butler
chmod +x butler
cp butler /usr/local/bin/

通过打开新的终端会话并检查 Butler 是否可用来验证安装是否成功。要导出 Jenkins 插件,您需要提供 Jenkins URL:

butler jobs export --server JENKINS_URL --username USERNAME --password PASSWORD

每次在 Jenkins 中创建一个新的工作目录,每个工作都会有一个自己的配置文件,config.xml。

要导入插件,请运行butler plugins export命令。Butler 将列出已安装的插件到 stdout,并生成一个新文件,plugins.txt,其中包含已安装的 Jenkins 插件的名称和版本对,如图 14.22 所示。

图 14.22 已安装 Jenkins 插件的列表

您可以使用 butler plugins/jobs import 命令导入导出的作业和插件。Butler 将使用导出的文件向目标 Jenkins 实例发出 API 调用来导入插件和作业。

因此,总的来说,在 Jenkins 实例之间迁移构建作业并不那么困难——您只需要知道一些针对特殊情况的小技巧,并且如果您知道在哪里查找,Jenkins 提供了一些很好的工具来使过程更顺畅。

如果您希望 $JENKINS_HOME 内容即使在 Jenkins 主实例重启或关闭后也能在磁盘上持久化,您可以在 $JENKINS_HOME 文件夹上挂载一个远程文件系统。

如果您在 AWS 上运行 Jenkins,可以使用 AWS 的一项服务,称为 Amazon Elastic File System(EFS)。通过单击创建文件系统按钮(图 14.23)在 EFS 上创建一个文件系统。

图 14.23 创建 Amazon EFS 文件系统

一旦文件系统创建并处于可用状态,请在 /var/lib/jenkins 目录下挂载 EFS 文件系统,这样所有配置都将保存在 EFS 中:

sudo mount -t nfs4 
-o nfsvers=4.1,rsize=1048576,wsize=1048576,
hard,timeo=600,retrans=2,noresvpor.
EFS_ID.efs.REGION.amazonaws.com:/ /var/lib/jenkins/

如果您想测试它,终止您的 EC2 实例,将自动启动一个新的具有相同配置的实例(确保在烘焙 Jenkins 主 AMI 时将挂载命令添加到 Packer 模板中)。

14.6 使用 Jenkins 设置 cron 作业

Jenkins 提供了一个类似于 cron 的功能,用于定期构建项目。此功能主要用于运行计划构建,如夜间/周构建或运行测试。例如,您可能希望在用户不访问测试的后端时,在夜间运行 Android 或 iOS 发布的性能测试或集成测试。

要配置一个在特定日期和时间运行的计划夜间构建,请转到 Jenkins 仪表板。创建一个新的作业并选择 Freestyle Project。根据图 14.24 中的作业详情进行相应配置。

图 14.24 创建 Freestyle 项目

通过在构建触发器选项卡中编写图 14.25 中所示的 cron 语法,并选择构建周期性选项,来安排您的构建。填写一个 cron 类似的值,以触发管道执行的所需时间。

图 14.25 定义 cron 作业表达式

Jenkins 使用 cron 表达式,字段如下。

  • MINUTES — 一小时中的分钟数(0–59)

  • HOURS — 一天中的小时数(0–23)

  • DAYMONTH — 一个月中的某一天(1–31)

  • MONTH — 一年中的月份(1–12)

  • DAYWEEK — 周中的某一天(0–7),其中 0 和 7 是周日

例如(图 14.26),要在周日午夜触发构建,cron 值 H 12 * * 7 就可以完成这项工作。

注意:您应该知道时区是相对于您的 Jenkins 虚拟机运行的位置而言的。此示例使用协调世界时(UTC)。

图 14.26 备份 $JENKINS_HOME 文件夹的 Shell 脚本

构建你的作业以测试一切是否按预期工作。你的构建结果应类似于图 14.27。

图 14.27 手动触发 cron 作业

下次,你的作业将在午夜 12:00 自动执行,因为你已经使用 cron 语法安排了在此时间运行。

Jenkins 作业可以通过程序方式运行,使用 API 调用或 Jenkins CLI。这为通过集成外部服务(如 AWS Lambda)根据不同事件调用 Jenkins 构建作业以实现复杂的计划构建提供了机会;参见图 14.28。

图 14.28 从 Lambda 函数触发 Jenkins 作业

此图说明了如何通过 Jenkins RESTful API 从 Lambda 函数触发 Jenkins 构建作业。Lambda 函数在即将到来的 CloudWatch 事件规则(云管理的 cron 作业)或来自 API Gateway 的 HTTPS 请求上被调用。

14.7 在本地作为 Docker 容器运行 Jenkins

如果你需要调试 Jenkins 或测试新的插件,你可以在你的机器上本地部署 Jenkins 并将其作为 Docker 容器运行。这样,你可以轻松创建和销毁 Jenkins 服务器。

你可以使用来自 DockerHub 仓库的官方 Jenkins Docker 镜像(hub.docker.com/_/jenkins)。该镜像包含 Jenkins 的当前 LTS 版本(在撰写本文时为 v2.60.3)。

要开始,在你的终端中,使用以下命令在 Docker 中创建一个桥接网络:

docker network create jenkins

我们需要 Docker 守护进程来动态配置 Jenkins 工作节点。这就是为什么我们将基于 Docker 镜像部署一个 Docker 容器:

docker run -d --name docker --privileged 
--network jenkins --network-alias docke.
--env DOCKER_TLS_CERTDIR=/cert.
--volume jenkins-docker-certs:/certs/clien.
--volume jenkins-data:/var/jenkins_hom.
--publish 2376:2376 docker:dind

为了避免暴露在主机机器上运行的 Docker 守护进程(/var/run.docker.sock),我们将运行一个提供自助和短暂 Docker 引擎的 Docker 容器,Jenkins 将使用它而不是工作机的 Docker 引擎。这种模式被称为 Docker in Docker,或 嵌套容器化

我们将覆盖 Jenkins 官方镜像以安装 Docker CLI 和 Jenkins 所需的插件。创建一个包含以下列表内容的 Dockerfile。

列表 14.7 构建自定义 Jenkins 镜像的 Dockerfile

FROM jenkins/jenkins:lts
MAINTAINER mlabouardy <mohamed@labouardy.com>

USER root
RUN apt-get update && apt-get install -y apt-transport-https \
      ca-certificates curl gnupg2 \
      software-properties-common
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
RUN apt-key fingerprint 0EBFCD88
RUN add-apt-repository \
      "deb [arch=amd64] https://download.docker.com/linux/debian \
      $(lsb_release -cs) stable"
RUN apt-get update && apt-get install -y docker-ce-cli      ❶
USER jenkins                                                ❷
RUN jenkins-plugin-cl.
--plugins blueocean:1.24.3 workflow-aggregator:2..
github:1.32.0 docker-plugin:1.2.1                           ❸

❶ 安装 Docker 社区版(CE)客户端

❷ 切换到 Jenkins 用户,以避免默认以特权模式运行容器

❸ 安装 Jenkins 插件

此 Dockerfile 执行以下操作:

  • 安装 Docker 社区版 CLI

  • 安装 Jenkins 插件,包括以下内容:

    • Blue Ocean—对 CD 管道的高级可视化,快速直观地理解软件管道状态

    • Workflow—一套插件,允许你将管道作为代码(Jenkinsfiles)编写

    • GitHub—GitHub API 集成和对 Git 操作的支持

  • Docker—允许你在 Docker 容器上配置 Jenkins 工作节点

从此 Dockerfile 构建一个新的 Docker 镜像,并为镜像分配一个有意义的名称:

docker build -t jenkins-custom:lts .

然后,使用以下docker run命令基于构建的镜像部署容器:

docker run -d --name jenkins --network jenkins 
--env DOCKER_HOST=tcp://docker:2376
--env DOCKER_CERT_PATH=/certs/clien.
--env DOCKER_TLS_VERIFY=.
--publish 8080:8080 --publish 50000:5000.
--volume jenkins-data:/var/jenkins_hom.
--volume jenkins-docker-certs:/certs/client:r.
jenkins-custom:lts

此命令将 Docker 卷映射到/var/jenkins_home 文件夹。如果你需要重新启动或恢复 Jenkins 实例,所有状态都存储在 Docker 卷中。

你也可以通过编写 docker-compose.yml 文件来构建和部署所有服务,如下所示。

列表 14.8 Grok 自定义模式定义

version: "3.8"

services:
 docker:
   image: docker:dind
   ports:
     - "2376:2376"
   networks:
     jenkins:
         aliases:
           - docker
   environment:
     - DOCKER_TLS_CERTDIR=/certs
   volumes:
     - jenkins-docker-certs:/certs/client
     - jenkins-data:/var/jenkins_home
   privileged: true

 jenkins:
   build: .
   ports:
     - "8080:8080"
     - "50000:50000"
   networks:
     - jenkins
   environment:
     - DOCKER_HOST=tcp://docker:2376
     - DOCKER_CERT_PATH=/certs/client
     - DOCKER_TLS_VERIFY=1
   volumes:
     - jenkins-data:/var/jenkins_home
     - jenkins-docker-certs:/certs/client:ro

volumes:
 jenkins-docker-certs: {}
 jenkins-data: {}

networks:
 jenkins:

运行docker-compose up,Docker Compose 将启动并运行 Jenkins。

访问 localhost:8080;你应该看到登录页面。作为 Jenkins 设置的一部分,我们需要查看容器实例内的密码;使用容器 ID(或名称)并运行docker exec命令:

docker container exec ID sh -c "cat /var/jenkins_home/secrets/initialAdminPassword"

运行命令后,你应该看到代码。将其复制并粘贴到仪表板上以解锁 Jenkins;见图 14.29。

图片

图 14.29 Jenkins 服务器在 Docker 容器内运行

要设置工作者,选择“管理 Jenkins”和“系统配置”。然后在云部分点击“配置”标签。Docker 选项将可用。将 Docker URI 设置为tcp://docker:2376,如图 14.30 所示。点击“测试”按钮以检查连接。

图片

图 14.30 在 Jenkins 上配置 Docker 远程 API

Docker API 应该返回一个错误:“服务器向 HTTPS 客户端返回 HTTP 响应”。你需要配置客户端 TLS 证书以连接到 Docker 守护进程。证书可以在 Jenkins 容器内的/certs/client 文件夹中找到。

创建一个新的 Jenkins 凭据,类型为证书,并设置以下设置:

  • 客户端密钥—/certs/client/key.pem 内容

  • 客户端证书—/certs/client/cert.pem 内容

  • 服务器 CA 证书—/certs/client/ca.pem 内容

凭据设置应类似于图 14.31 中的设置。

图片

图 14.31 Jenkins 服务器在本地 Docker 容器内部署

然后,我们需要定义一个代理模板,如图 14.32 所示;此模板是启动 Jenkins 工作者的蓝图。你需要一个可以用来运行 Jenkins 代理运行时的 Docker 镜像。你可以使用 jenkins/ssh-agent (hub.docker.com/r/jenkins/ssh-agent)作为 Jenkins 工作者的基础。该镜像已安装了 SSHD(当你尝试通过 SSH 连接时,它会监听传入的连接)。

图片

图 14.32 配置新的 Docker 代理模板

你还可以构建一个包含所有依赖项和构建项目所需的软件包的自定义 Docker 代理镜像。为了测试它,创建一个新的 Jenkins 管道,内容如图 14.33 所示。

图片

图 14.33 新的内联管道

通过点击左侧导航菜单中的“立即构建”链接来触发管道;作业将启动一个容器并执行管道(图 14.34)。

图片

图 14.34 基于 Docker 容器启动 Jenkins 代理

代理将在每次构建后动态配置并停止。

摘要

  • 您可以通过编写 Jenkins 共享库来在多个管道中共享通用代码和步骤。

  • 您可以使用矩阵授权策略插件对每个项目的用户/组权限进行细粒度控制。

  • 您还可以使用角色策略插件创建一个具有权限列表的自定义角色,并将该角色分配给用户,而不是为每个用户分配适当的权限。

  • 在您的 Jenkins 实例中实现身份验证时,请使用 GitHub 自己的身份验证方案。

  • Docker 插件将在 Docker 容器内运行动态 Jenkins 代理。

总结

我们在这本书中的旅程即将结束。您了解了 Jenkins 和代码化管道方法。您发现了针对云原生应用的几个 CI/CD 实现,例如 Kubernetes 中的容器化应用和无服务器应用。您设计和部署了云上的 Jenkins 集群以实现可扩展性,并精通了 Jenkins 的监控和故障排除。

技术变化迅速,因此拥有一些可以获取最新新闻和信息资源的渠道是非常好的。每周的 DevOps Bulletin (devopsbulletin.com) 特集了一系列关于 PaC 和 DevOps 领域最新奇的文章。我还建议关注 DevOps World (www.devopsworld.com),在那里您可以受到专家和同行的启发,并获得您在组织内部和整体上塑造软件交付未来的工具。

我希望您喜欢这本书,并从中有所收获。PaC 仍然很新,但意识正在迅速增长。在未来几年里,您将看到许多大小组织都将采用 PaC 以实现更快发布并缩短反馈循环。

posted @ 2025-11-22 09:01  绝不原创的飞龙  阅读(8)  评论(0)    收藏  举报