标签 CI 下的文章

未来的开发工作需要非常精通 CI/CD 流程。

在OpenDev上,掌握CI/CD

在 2017 年启动后,OpenDev 大会现在已是一个年度活动。在去年 9 月的首届活动上,会议的重点是边缘计算。今年的活动,于 5 月 22 - 23 日举行,会议的重点是持续集成和持续发布 (CI/CD),并与 OpenStack 峰会一起在温哥华举行。

基于我在 OpenStack 项目的 CI/CD 系统的技术背景和我近期进入容器下的 CI/CD 方面的经验,我被邀请加入 OpenDev CI/CD 计划委员会。今天我经常借助很多开源技术,例如 Jenkins、GitLab、Spinnaker 和 Artifactory 来讨论 CI/CD 流程。

这次活动对我来说是很激动人心的,因为我们将在这个活动中融合两个开源基础设施理念。首先,我们将讨论可以被任何组织使用的 CI/CD 工具。为此目的,在 讲演 中,我们将听到关于开源 CI/CD 工具的使用讲演,一场来自 Boris Renski 的关于 Spinnaker 的讲演,和一场来自 Jim Blair 的关于 Zuul 的讲演。同时,讲演会涉及关于开源技术的偏好的高级别话题,特别是那种跨社区的和本身就是开源项目的。从Fatih Degirmenci 和 Daniel Farrel 那里,我们将听到关于在不同社区分享持续发布实践经历,接着 Benjamin Mako Hill 会为我们带来一场关于为什么自由软件需要自由工具的分享。

在分享 CI/CD 相对新颖的特性后,接下来的活动是对话、研讨会和协作讨论的混合组合。当从人们所提交的讲座和研讨会中进行选择,并提出协作讨论主题时,我们希望确保有一个多样灵活的日程表,这样任何参与者都能在 CI/CD 活动进程中发现有趣的东西。

这些讲座会是标准的会议风格,选择涵盖关键主题,如制定 CI/CD 流程,在实践 DevOps 时提升安全性,以及更具体的解决方案,如基于容器关于 Kubernetes 的 Aptomi 和在 ETSI NFV 环境下 CI/CD。这些会话的大部分将会是作为给新接触 CI/CD 或这些特定技术的参与者关于这些话题和理念的简介。

交互式的研讨会会持续相对比较长的时间,参与者将会在思想上得到特定的体验。这些研讨会包括 “在持续集成任务中的异常检测”、“如何安装 Zuul 和配置第一个任务”,和“Spinnake 101:快速可靠的软件发布”。(注意这些研讨会空间是有限的,所以设立了一个 RSVP 系统。你们将会在会议的链接里找到一个 RSVP 的按钮。)

可能最让我最兴奋的是协作讨论,这些协作讨论占据了一半以上的活动安排。协作讨论的主题由计划委员会选取。计划委员会根据我们在社区里所看到来选取对应的主题。这是“鱼缸”风格式的会议,通常是几个人聚在一个房间里围绕着 CI/CD 讨论某一个主题。

这次会议风格的理念是来自于开发者峰会,最初是由 Ubuntu 社区提出,接着 OpenStack 社区也在活动上采纳。这些协作讨论的主题包含不同的会议,这些会议是关于 CI/CD 基础,可以鼓励跨社区协作的提升举措,在组织里推行 CI/CD 文化,和为什么开源 CI/CD 工具如此重要。采用共享文档来做会议笔记,以确保尽可能的在会议的过程中分享知识。在讨论过程中,提出行动项目也是很常见的,因此社区成员可以推动和所涉及的主题相关的倡议。

活动将以联合总结会议结束。联合总结会议将总结来自协同讨论的关键点和为即将在这个领域工作的参与者指出可选的职业范围。

可以在 OpenStack 峰会注册页 上注册参加活动。或者可以在温哥华唯一指定售票的会议中心购买活动的入场券,价格是 $199。更多关于票和全部的活动安排见官网 OpenDev 网站。

我希望你们能够加入我们,并在温哥华渡过令人激动的两天,并且在这两天的活动中学习,协作和在 CI/CD 取得进展。


via: https://opensource.com/article/18/5/opendev

作者:Elizabeth K.Joseph 选题:lujun9972 译者:jamelouis 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在软件开发中经常会提到 持续集成 Continuous Integration (CI)和 持续交付 Continuous Delivery (CD)这几个术语。但它们真正的意思是什么呢?

在谈论软件开发时,经常会提到 持续集成 Continuous Integration (CI)和 持续交付 Continuous Delivery (CD)这几个术语。但它们真正的意思是什么呢?在本文中,我将解释这些和相关术语背后的含义和意义,例如 持续测试 Continuous Testing 和 持续部署 Continuous Deployment 。

概览

工厂里的装配线以快速、自动化、可重复的方式从原材料生产出消费品。同样,软件交付管道以快速、自动化和可重复的方式从源代码生成发布版本。如何完成这项工作的总体设计称为“持续交付”(CD)。启动装配线的过程称为“持续集成”(CI)。确保质量的过程称为“持续测试”,将最终产品提供给用户的过程称为“持续部署”。一些专家让这一切简单、顺畅、高效地运行,这些人被称为 运维开发 DevOps 践行者。

“持续”是什么意思?

“持续”用于描述遵循我在此提到的许多不同流程实践。这并不意味着“一直在运行”,而是“随时可运行”。在软件开发领域,它还包括几个核心概念/最佳实践。这些是:

  • 频繁发布:持续实践背后的目标是能够频繁地交付高质量的软件。此处的交付频率是可变的,可由开发团队或公司定义。对于某些产品,一季度、一个月、一周或一天交付一次可能已经足够频繁了。对于另一些来说,一天可能需要多次交付也是可行的。所谓持续也有“偶尔、按需”的方面。最终目标是相同的:在可重复、可靠的过程中为最终用户提供高质量的软件更新。通常,这可以通过很少甚至无需用户的交互或掌握的知识来完成(想想设备更新)。
  • 自动化流程:实现此频率的关键是用自动化流程来处理软件生产中的方方面面。这包括构建、测试、分析、版本控制,以及在某些情况下的部署。
  • 可重复:如果我们使用的自动化流程在给定相同输入的情况下始终具有相同的行为,则这个过程应该是可重复的。也就是说,如果我们把某个历史版本的代码作为输入,我们应该得到对应相同的可交付产出。这也假设我们有相同版本的外部依赖项(即我们不创建该版本代码使用的其它交付物)。理想情况下,这也意味着可以对管道中的流程进行版本控制和重建(请参阅稍后的 DevOps 讨论)。
  • 快速迭代:“快速”在这里是个相对术语,但无论软件更新/发布的频率如何,预期的持续过程都会以高效的方式将源代码转换为交付物。自动化负责大部分工作,但自动化处理的过程可能仍然很慢。例如,对于每天需要多次发布候选版更新的产品来说,一轮 集成测试 integrated testing 下来耗时就要大半天可能就太慢了。

什么是“持续交付管道”?

将源代码转换为可发布产品的多个不同的 任务 task 和 作业 job 通常串联成一个软件“管道”,一个自动流程成功完成后会启动管道中的下一个流程。这些管道有许多不同的叫法,例如持续交付管道、部署管道和软件开发管道。大体上讲,程序管理者在管道执行时管理管道各部分的定义、运行、监控和报告。

持续交付管道是如何工作的?

软件交付管道的实际实现可以有很大不同。有许多程序可用在管道中,用于源代码跟踪、构建、测试、指标采集,版本管理等各个方面。但整体工作流程通常是相同的。单个业务流程/工作流应用程序管理整个管道,每个流程作为独立的作业运行或由该应用程序进行阶段管理。通常,在业务流程中,这些独立作业是以应用程序可理解并可作为工作流程管理的语法和结构定义的。

这些作业被用于一个或多个功能(构建、测试、部署等)。每个作业可能使用不同的技术或多种技术。关键是作业是自动化的、高效的,并且可重复的。如果作业成功,则工作流管理器将触发管道中的下一个作业。如果作业失败,工作流管理器会向开发人员、测试人员和其他人发出警报,以便他们尽快纠正问题。这个过程是自动化的,所以比手动运行一组过程可更快地找到错误。这种快速排错称为 快速失败 fail fast ,并且在抵达管道端点方面同样有价值。

“快速失败”是什么意思?

管道的工作之一就是快速处理变更。另一个是监视创建发布的不同任务/作业。由于编译失败或测试未通过的代码可以阻止管道继续运行,因此快速通知用户此类情况非常重要。快速失败指的是在管道流程中尽快发现问题并快速通知用户的方式,这样可以及时修正问题并重新提交代码以便使管道再次运行。通常在管道流程中可通过查看历史记录来确定是谁做了那次修改并通知此人及其团队。

所有持续交付管道都应该被自动化吗?

管道的几乎所有部分都是应该自动化的。对于某些部分,有一些人为干预/互动的地方可能是有意义的。一个例子可能是 用户验收测试 user-acceptance testing (让最终用户试用软件并确保它能达到他们想要/期望的水平)。另一种情况可能是部署到生产环境时用户希望拥有更多的人为控制。当然,如果代码不正确或不能运行,则需要人工干预。

有了对“持续”含义理解的背景,让我们看看不同类型的持续流程以及它们在软件管道上下文中的含义。

什么是“持续集成”?

持续集成(CI)是在源代码变更后自动检测、拉取、构建和(在大多数情况下)进行单元测试的过程。持续集成是启动管道的环节(尽管某些预验证 —— 通常称为 上线前检查 pre-flight checks —— 有时会被归在持续集成之前)。

持续集成的目标是快速确保开发人员新提交的变更是好的,并且适合在代码库中进一步使用。

持续集成是如何工作的?

持续集成的基本思想是让一个自动化过程监测一个或多个源代码仓库是否有变更。当变更被推送到仓库时,它会监测到更改、下载副本、构建并运行任何相关的单元测试。

持续集成如何监测变更?

目前,监测程序通常是像 Jenkins 这样的应用程序,它还协调管道中运行的所有(或大多数)进程,监视变更是其功能之一。监测程序可以以几种不同方式监测变更。这些包括:

  • 轮询:监测程序反复询问代码管理系统,“代码仓库里有什么我感兴趣的新东西吗?”当代码管理系统有新的变更时,监测程序会“唤醒”并完成其工作以获取新代码并构建/测试它。
  • 定期:监测程序配置为定期启动构建,无论源码是否有变更。理想情况下,如果没有变更,则不会构建任何新内容,因此这不会增加额外的成本。
  • 推送:这与用于代码管理系统检查的监测程序相反。在这种情况下,代码管理系统被配置为提交变更到仓库时将“推送”一个通知到监测程序。最常见的是,这可以以 webhook 的形式完成 —— 在新代码被推送时一个 挂勾 hook 的程序通过互联网向监测程序发送通知。为此,监测程序必须具有可以通过网络接收 webhook 信息的开放端口。

什么是“预检查”(又称“上线前检查”)?

在将代码引入仓库并触发持续集成之前,可以进行其它验证。这遵循了最佳实践,例如 测试构建 test build 和 代码审查 code review 。它们通常在代码引入管道之前构建到开发过程中。但是一些管道也可能将它们作为其监控流程或工作流的一部分。

例如,一个名为 Gerrit 的工具允许在开发人员推送代码之后但在允许进入(Git 远程)仓库之前进行正式的代码审查、验证和测试构建。Gerrit 位于开发人员的工作区和 Git 远程仓库之间。它会“接收”来自开发人员的推送,并且可以执行通过/失败验证以确保它们在被允许进入仓库之前的检查是通过的。这可以包括检测新变更并启动构建测试(CI 的一种形式)。它还允许开发者在那时进行正式的代码审查。这种方式有一种额外的可信度评估机制,即当变更的代码被合并到代码库中时不会破坏任何内容。

什么是“单元测试”?

单元测试(也称为“提交测试”),是由开发人员编写的小型的专项测试,以确保新代码独立工作。“独立”这里意味着不依赖或调用其它不可直接访问的代码,也不依赖外部数据源或其它模块。如果运行代码需要这样的依赖关系,那么这些资源可以用 模拟 mock 来表示。模拟是指使用看起来像资源的 代码存根 code stub ,可以返回值,但不实现任何功能。

在大多数组织中,开发人员负责创建单元测试以证明其代码正确。事实上,一种称为 测试驱动开发 test-driven develop (TDD)的模型要求将首先设计单元测试作为清楚地验证代码功能的基础。因为这样的代码可以更改速度快且改动量大,所以它们也必须执行很快。

由于这与持续集成工作流有关,因此开发人员在本地工作环境中编写或更新代码,并通单元测试来确保新开发的功能或方法正确。通常,这些测试采用断言形式,即函数或方法的给定输入集产生给定的输出集。它们通常进行测试以确保正确标记和处理出错条件。有很多单元测试框架都很有用,例如用于 Java 开发的 JUnit。

什么是“持续测试”?

持续测试是指在代码通过持续交付管道时运行扩展范围的自动化测试的实践。单元测试通常与构建过程集成,作为持续集成阶段的一部分,并专注于和其它与之交互的代码隔离的测试。

除此之外,可以有或者应该有各种形式的测试。这些可包括:

  • 集成测试 验证组件和服务组合在一起是否正常。
  • 功能测试 验证产品中执行功能的结果是否符合预期。
  • 验收测试 根据可接受的标准验证产品的某些特征。如性能、可伸缩性、抗压能力和容量。

所有这些可能不存在于自动化的管道中,并且一些不同类型的测试分类界限也不是很清晰。但是,在交付管道中持续测试的目标始终是相同的:通过持续的测试级别证明代码的质量可以在正在进行的发布中使用。在持续集成快速的原则基础上,第二个目标是快速发现问题并提醒开发团队。这通常被称为快速失败。

除了测试之外,还可以对管道中的代码进行哪些其它类型的验证?

除了测试是否通过之外,还有一些应用程序可以告诉我们测试用例执行(覆盖)的源代码行数。这是一个可以衡量代码量指标的例子。这个指标称为 代码覆盖率 code-coverage ,可以通过工具(例如用于 Java 的 JaCoCo)进行统计。

还有很多其它类型的指标统计,例如代码行数、复杂度以及代码结构对比分析等。诸如 SonarQube 之类的工具可以检查源代码并计算这些指标。此外,用户还可以为他们可接受的“合格”范围的指标设置阈值。然后可以在管道中针对这些阈值设置一个检查,如果结果不在可接受范围内,则流程终端上。SonarQube 等应用程序具有很高的可配置性,可以设置仅检查团队感兴趣的内容。

什么是“持续交付”?

持续交付(CD)通常是指整个流程链(管道),它自动监测源代码变更并通过构建、测试、打包和相关操作运行它们以生成可部署的版本,基本上没有任何人为干预。

持续交付在软件开发过程中的目标是自动化、效率、可靠性、可重复性和质量保障(通过持续测试)。

持续交付包含持续集成(自动检测源代码变更、执行构建过程、运行单元测试以验证变更),持续测试(对代码运行各种测试以保障代码质量),和(可选)持续部署(通过管道发布版本自动提供给用户)。

如何在管道中识别/跟踪多个版本?

版本控制是持续交付和管道的关键概念。持续意味着能够经常集成新代码并提供更新版本。但这并不意味着每个人都想要“最新、最好的”。对于想要开发或测试已知的稳定版本的内部团队来说尤其如此。因此,管道创建并轻松存储和访问的这些版本化对象非常重要。

在管道中从源代码创建的对象通常可以称为 工件 artifact 。工件在构建时应该有应用于它们的版本。将版本号分配给工件的推荐策略称为 语义化版本控制 semantic versioning 。(这也适用于从外部源引入的依赖工件的版本。)

语义版本号有三个部分: 主要版本 major 、 次要版本 minor 和 补丁版本 patch 。(例如,1.4.3 反映了主要版本 1,次要版本 4 和补丁版本 3。)这个想法是,其中一个部分的更改表示工件中的更新级别。主要版本仅针对不兼容的 API 更改而递增。当以 向后兼容 backward-compatible 的方式添加功能时,次要版本会增加。当进行向后兼容的版本 bug 修复时,补丁版本会增加。这些是建议的指导方针,但只要团队在整个组织内以一致且易于理解的方式这样做,团队就可以自由地改变这种方法。例如,每次为发布完成构建时增加的数字可以放在补丁字段中。

如何“分销”工件?

团队可以为工件分配 分销 promotion 级别以指示适用于测试、生产等环境或用途。有很多方法。可以用 Jenkins 或 Artifactory 等应用程序进行分销。或者一个简单的方案可以在版本号字符串的末尾添加标签。例如,-snapshot 可以指示用于构建工件的代码的最新版本(快照)。可以使用各种分销策略或工具将工件“提升”到其它级别,例如 -milestone 或 -production,作为工件稳定性和完备性版本的标记。

如何存储和访问多个工件版本?

从源代码构建的版本化工件可以通过管理 工件仓库 artifact repository 的应用程序进行存储。工件仓库就像构建工件的版本控制工具一样。像 Artifactory 或 Nexus 这类应用可以接受版本化工件,存储和跟踪它们,并提供检索的方法。

管道用户可以指定他们想要使用的版本,并在这些版本中使用管道。

什么是“持续部署”?

持续部署(CD)是指能够自动提供持续交付管道中发布版本给最终用户使用的想法。根据用户的安装方式,可能是在云环境中自动部署、app 升级(如手机上的应用程序)、更新网站或只更新可用版本列表。

这里的一个重点是,仅仅因为可以进行持续部署并不意味着始终部署来自管道的每组可交付成果。它实际上指,通过管道每套可交付成果都被证明是“可部署的”。这在很大程度上是由持续测试的连续级别完成的(参见本文中的持续测试部分)。

管道构建的发布成果是否被部署可以通过人工决策,或利用在完全部署之前“试用”发布的各种方法来进行控制。

在完全部署到所有用户之前,有哪些方法可以测试部署?

由于必须回滚/撤消对所有用户的部署可能是一种代价高昂的情况(无论是技术上还是用户的感知),已经有许多技术允许“尝试”部署新功能并在发现问题时轻松“撤消”它们。这些包括:

蓝/绿测试/部署

在这种部署软件的方法中,维护了两个相同的主机环境 —— 一个“蓝色” 和一个“绿色”。(颜色并不重要,仅作为标识。)对应来说,其中一个是“生产环境”,另一个是“预发布环境”。

在这些实例的前面是调度系统,它们充当产品或应用程序的客户“网关”。通过将调度系统指向蓝色或绿色实例,可以将客户流量引流到期望的部署环境。通过这种方式,切换指向哪个部署实例(蓝色或绿色)对用户来说是快速,简单和透明的。

当新版本准备好进行测试时,可以将其部署到非生产环境中。在经过测试和批准后,可以更改调度系统设置以将传入的线上流量指向它(因此它将成为新的生产站点)。现在,曾作为生产环境实例可供下一次候选发布使用。

同理,如果在最新部署中发现问题并且之前的生产实例仍然可用,则简单的更改可以将客户流量引流回到之前的生产实例 —— 有效地将问题实例“下线”并且回滚到以前的版本。然后有问题的新实例可以在其它区域中修复。

金丝雀测试/部署

在某些情况下,通过蓝/绿发布切换整个部署可能不可行或不是期望的那样。另一种方法是为 金丝雀 canary 测试/部署。在这种模型中,一部分客户流量被重新引流到新的版本部署中。例如,新版本的搜索服务可以与当前服务的生产版本一起部署。然后,可以将 10% 的搜索查询引流到新版本,以在生产环境中对其进行测试。

如果服务那些流量的新版本没问题,那么可能会有更多的流量会被逐渐引流过去。如果仍然没有问题出现,那么随着时间的推移,可以对新版本增量部署,直到 100% 的流量都调度到新版本。这有效地“更替”了以前版本的服务,并让新版本对所有客户生效。

功能开关

对于可能需要轻松关掉的新功能(如果发现问题),开发人员可以添加 功能开关 feature toggles 。这是代码中的 if-then 软件功能开关,仅在设置数据值时才激活新代码。此数据值可以是全局可访问的位置,部署的应用程序将检查该位置是否应执行新代码。如果设置了数据值,则执行代码;如果没有,则不执行。

这为开发人员提供了一个远程“终止开关”,以便在部署到生产环境后发现问题时关闭新功能。

暗箱发布

在 暗箱发布 dark launch 中,代码被逐步测试/部署到生产环境中,但是用户不会看到更改(因此名称中有 暗箱 dark 一词)。例如,在生产版本中,网页查询的某些部分可能会重定向到查询新数据源的服务。开发人员可收集此信息进行分析,而不会将有关接口,事务或结果的任何信息暴露给用户。

这个想法是想获取候选版本在生产环境负载下如何执行的真实信息,而不会影响用户或改变他们的经验。随着时间的推移,可以调度更多负载,直到遇到问题或认为新功能已准备好供所有人使用。实际上功能开关标志可用于这种暗箱发布机制。

什么是“运维开发”?

运维开发 DevOps 是关于如何使开发和运维团队更容易合作开发和发布软件的一系列想法和推荐的实践。从历史上看,开发团队研发了产品,但没有像客户那样以常规、可重复的方式安装/部署它们。在整个周期中,这组安装/部署任务(以及其它支持任务)留给运维团队负责。这经常导致很多混乱和问题,因为运维团队在后期才开始介入,并且必须在短时间内完成他们的工作。同样,开发团队经常处于不利地位 —— 因为他们没有充分测试产品的安装/部署功能,他们可能会对该过程中出现的问题感到惊讶。

这往往导致开发和运维团队之间严重脱节和缺乏合作。DevOps 理念主张是贯穿整个开发周期的开发和运维综合协作的工作方式,就像持续交付那样。

持续交付如何与运维开发相交?

持续交付管道是几个 DevOps 理念的实现。产品开发的后期阶段(如打包和部署)始终可以在管道的每次运行中完成,而不是等待产品开发周期中的特定时间。同样,从开发到部署过程中,开发和运维都可以清楚地看到事情何时起作用,何时不起作用。要使持续交付管道循环成功,不仅要通过与开发相关的流程,还要通过与运维相关的流程。

说得更远一些,DevOps 建议实现管道的基础架构也会被视为代码。也就是说,它应该自动配置、可跟踪、易于修改,并在管道发生变化时触发新一轮运行。这可以通过将管道实现为代码来完成。

什么是“管道即代码”?

管道即代码 pipeline-as-code 是通过编写代码创建管道作业/任务的通用术语,就像开发人员编写代码一样。它的目标是将管道实现表示为代码,以便它可以与代码一起存储、评审、跟踪,如果出现问题并且必须终止管道,则可以轻松地重建。有几个工具允许这样做,如 Jenkins 2。

DevOps 如何影响生产软件的基础设施?

传统意义上,管道中使用的各个硬件系统都有配套的软件(操作系统、应用程序、开发工具等)。在极端情况下,每个系统都是手工设置来定制的。这意味着当系统出现问题或需要更新时,这通常也是一项自定义任务。这种方法违背了持续交付的基本理念,即具有易于重现和可跟踪的环境。

多年来,很多应用被开发用于标准化交付(安装和配置)系统。同样, 虚拟机 virtual machine 被开发为模拟在其它计算机之上运行的计算机程序。这些 VM 要有管理程序才能在底层主机系统上运行,并且它们需要自己的操作系统副本才能运行。

后来有了 容器 container 。容器虽然在概念上与 VM 类似,但工作方式不同。它们只需使用一些现有的操作系统结构来划分隔离空间,而不需要运行单独的程序和操作系统的副本。因此,它们的行为类似于 VM 以提供隔离但不需要过多的开销。

VM 和容器是根据配置定义创建的,因此可以轻易地销毁和重建,而不会影响运行它们的主机系统。这允许运行管道的系统也可重建。此外,对于容器,我们可以跟踪其构建定义文件的更改 —— 就像对源代码一样。

因此,如果遇到 VM 或容器中的问题,我们可以更容易、更快速地销毁和重建它们,而不是在当前环境尝试调试和修复。

这也意味着对管道代码的任何更改都可以触发管道新一轮运行(通过 CI),就像对代码的更改一样。这是 DevOps 关于基础架构的核心理念之一。


via: https://opensource.com/article/18/8/what-cicd

作者:Brent Laster 选题:lujun9972 译者:pityonline 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

容器应用程序平台能够动态地启动具有资源限制的独立容器,从而改变了运行 CI/CD 任务的方式。

现今,由于 Docker 和 Kubernetes(K8S)提供了可扩展、可管理的应用平台,将应用运行在容器中的实践已经被企业广泛接受。近些年势头很猛的微服务架构也很适合用容器实现。

容器应用平台可以动态启动指定资源配额、互相隔离的容器,这是其最主要的优势之一。让我们看看这会对我们运行 持续集成/持续部署 continuous integration/continuous development (CI/CD)任务的方式产生怎样的改变。

构建并打包应用需要一定的环境,要求能够下载源代码、使用相关依赖及已经安装构建工具。作为构建的一部分,运行单元及组件测试可能会用到本地端口或需要运行第三方应用(如数据库及消息中间件等)。另外,我们一般定制化多台构建服务器,每台执行一种指定类型的构建任务。为方便测试,我们维护一些实例专门用于运行第三方应用(或者试图在构建服务器上启动这些第三方应用),避免并行运行构建任务导致结果互相干扰。为 CI/CD 环境定制化构建服务器是一项繁琐的工作,而且随着开发团队使用的开发平台或其版本变更,会需要大量的构建服务器用于不同的任务。

一旦我们有了容器管理平台(自建或在云端),将资源密集型的 CI/CD 任务在动态生成的容器中执行是比较合理的。在这种方案中,每个构建任务运行在独立启动并配置的构建环境中。构建过程中,构建任务的测试环节可以任意使用隔离环境中的可用资源;此外,我们也可以在辅助容器中启动一个第三方应用,只在构建任务生命周期中为测试提供服务。

听上去不错,让我们在现实环境中实践一下。

注:本文基于现实中已有的解决方案,即一个在 Red Hat OpenShift v3.7 集群上运行的项目。OpenShift 是企业级的 Kubernetes 版本,故这些实践也适用于 K8S 集群。如果愿意尝试,可以下载 Red Hat CDK,运行 jenkins-ephemeral 或 jenkins-persistent 模板在 OpenShift 上创建定制化好的 Jenkins 管理节点。

解决方案概述

在 OpenShift 容器中执行 CI/CD 任务(构建和测试等) 的方案基于分布式 Jenkins 构建,具体如下:

  • 我们需要一个 Jenkins 主节点;可以运行在集群中,也可以是外部提供
  • 支持 Jenkins 特性和插件,以便已有项目仍可使用
  • 可以用 Jenkins GUI 配置、运行任务或查看任务输出
  • 如果你愿意编码,也可以使用 Jenkins Pipeline

从技术角度来看,运行任务的动态容器是 Jenkins 代理节点。当构建启动时,首先是一个新节点启动,通过 Jenkins 主节点的 JNLP(5000 端口) 告知就绪状态。在代理节点启动并提取构建任务之前,构建任务处于排队状态。就像通常 Jenkins 代理服务器那样,构建输出会送达主节点;不同的是,构建完成后代理节点容器会自动关闭。

不同类型的构建任务(例如 Java、 NodeJS、 Python等)对应不同的代理节点。这并不新奇,之前也是使用标签来限制哪些代理节点可以运行指定的构建任务。启动用于构建任务的 Jenkins 代理节点容器需要配置参数,具体如下:

  • 用于启动容器的 Docker 镜像
  • 资源限制
  • 环境变量
  • 挂载的卷

这里用到的关键组件是 Jenkins Kubernetes 插件。该插件(通过使用一个服务账号) 与 K8S 集群交互,可以启动和关闭代理节点。在插件的配置管理中,多种代理节点类型表现为多种 Kubernetes pod 模板,它们通过项目标签对应。

这些代理节点镜像以开箱即用的方式提供(也有 CentOS7 系统的版本):

注意:本解决方案与 OpenShift 中的 Source-to-Image(S2I) 构建无关,虽然后者也可以用于某些特定的 CI/CD 任务。

入门学习资料

有很多不错的博客和文档介绍了如何在 OpenShift 上执行 Jenkins 构建。不妨从下面这些开始:

阅读这些博客和文档有助于完整的理解本解决方案。在本文中,我们主要关注具体实践中遇到的各类问题。

构建我的应用

作为示例项目,我们选取了包含如下构建步骤的 Java 项目:

  • 代码源: 从一个 Git 代码库中获取项目代码
  • 使用 Maven 编译: 依赖可从内部仓库获取,(不妨使用 Apache Nexus) 镜像自外部 Maven 仓库
  • 发布成品: 将编译好的 JAR 上传至内部仓库

在 CI/CD 过程中,我们需要与 Git 和 Nexus 交互,故 Jenkins 任务需要能够访问这些系统。这要求参数配置和已存储凭证可以在下列位置进行管理:

  • 在 Jenkins 中: 我们可以在 Jenkins 中添加凭证,通过 Git 插件能够对项目添加和使用文件(使用容器不会改变操作)
  • 在 OpenShift 中: 使用 ConfigMap 和 Secret 对象,以文件或环境变量的形式附加到 Jenkins 代理容器中
  • 在高度定制化的 Docker 容器中: 镜像是定制化的,已包含完成特定类型构建的全部特性;从一个代理镜像进行扩展即可得到。

你可以按自己的喜好选择一种实现方式,甚至你最终可能混用多种实现方式。下面我们采用第二种实现方式,即首选在 OpenShift 中管理参数配置。使用 Kubernetes 插件配置来定制化 Maven 代理容器,包括设置环境变量和映射文件等。

注意:对于 Kubernetes 插件 v1.0 版,由于 bug,在 UI 界面增加环境变量并不生效。可以升级插件,或(作为变通方案) 直接修改 config.xml 文件并重启 Jenkins。

从 Git 获取源代码

从公共 Git 仓库获取源代码很容易。但对于私有 Git 仓库,不仅需要认证操作,客户端还需要信任服务器以便建立安全连接。一般而言,通过两种协议获取源代码:

  • HTTPS:验证通过用户名/密码完成。Git 服务器的 SSL 证书必须被代理节点信任,这仅在证书被自建 CA 签名时才需要特别注意。
git clone https://git.mycompany.com:443/myapplication.git
  • SSH:验证通过私钥完成。如果服务器的公钥指纹出现在 known_hosts 文件中,那么该服务器是被信任的。
git clone ssh://[email protected]:22/myapplication.git

对于手动操作,使用用户名/密码通过 HTTP 方式下载源代码是可行的;但对于自动构建而言,SSH 是更佳的选择。

通过 SSH 方式使用 Git

要通过 SSH 方式下载源代码,我们需要保证代理容器与 Git 的 SSH 端口之间可以建立 SSH 连接。首先,我们需要创建一个私钥-公钥对。使用如下命令生成:

ssh keygen -t rsa -b 2048 -f my-git-ssh -N ''

命令生成的私钥位于 my-git-ssh 文件中(口令为空),对应的公钥位于 my-git-ssh.pub 文件中。将公钥添加至 Git 服务器的对应用户下(推荐使用“服务账号”);网页界面一般支持公钥上传。为建立 SSH 连接,我们还需要在代理容器上配置两个文件:

  • 私钥文件位于 ~/.ssh/id_rsa
  • 服务器的公钥位于 ~/.ssh/known_hosts。要实现这一点,运行 ssh git.mycompany.com 并接受服务器指纹,系统会在 ~/.ssh/known_hosts 文件中增加一行。这样需求得到了满足。

将 id_rsa 对应的私钥和 known_hosts 对应的公钥保存到一个 OpenShift 的 secret(或 ConfigMap) 对象中。

apiVersion: v1
kind: Secret
metadata:
  name: mygit-ssh
stringData:
  id_rsa: |-
    -----BEGIN RSA PRIVATE KEY-----
    ...
    -----END RSA PRIVATE KEY-----
  known_hosts: |-
    git.mycompany.com ecdsa-sha2-nistp256 AAA...

在 Kubernetes 插件中将 secret 对象配置为卷,挂载到 /home/jenkins/.ssh/,供 Maven pod 使用。secret 中的每个对象对应挂载目录的一个文件,文件名与 key 名称相符。我们可以使用 UI(管理 Jenkins / 配置 / 云 / Kubernetes),也可以直接编辑 Jenkins 配置文件 /var/lib/jenkins/config.xml:

<org.csanchez.jenkins.plugins.kubernetes.PodTemplate>
<name>maven</name>
...
  <volumes>
    <org.csanchez.jenkins.plugins.kubernetes.volumes.SecretVolume>
      <mountPath>/home/jenkins/.ssh</mountPath>
      <secretName>mygit-ssh</secretName>
    </org.csanchez.jenkins.plugins.kubernetes.volumes.SecretVolume>
  </volumes>

此时,在代理节点上运行的任务应该可以通过 SSH 方式从 Git 代码库获取源代码。

注:我们也可以在 ~/.ssh/config 文件中自定义 SSH 连接。例如,如果你不想处理 known_hosts 或私钥位于其它挂载目录中:

Host git.mycompany.com
   StrictHostKeyChecking no
   IdentityFile /home/jenkins/.config/git-secret/ssh-privatekey

通过 HTTP 方式使用 Git

如果你选择使用 HTTP 方式下载,在指定的 Git-credential-store 文件中添加用户名/密码:

  • 例如,在一个 OpenShift secret 对象中增加 /home/jenkins/.config/git-secret/credentials 文件对应,其中每个站点对应文件中的一行:
https://username:[email protected]
https://user:[email protected]
  • 在 git-config 配置中启用该文件,其中配置文件默认路径为 /home/jenkins/.config/git/config:
[credential]
  helper = store --file=/home/jenkins/.config/git-secret/credentials

如果 Git 服务使用了自有 CA 签名的证书,为代理容器设置环境变量 GIT_SSL_NO_VERIFY=true 是最便捷的方式。更恰当的解决方案包括如下两步:

  • 利用 ConfigMap 将自有 CA 的公钥映射到一个路径下的文件中,例如 /usr/ca/myTrustedCA.pem)。
  • 通过环境变量 GIT_SSL_CAINFO=/usr/ca/myTrustedCA.pem 或上面提到的 git-config 文件的方式,将证书路径告知 Git。
[http "https://git.mycompany.com"]
    sslCAInfo = /usr/ca/myTrustedCA.pem

注:在 OpenShift v3.7 及早期版本中,ConfigMap 及 secret 的挂载点之间不能相互覆盖,故我们不能同时映射 /home/jenkins 和 /home/jenkins/dir。因此,上面的代码中并没有使用常见的文件路径。预计 OpenShift v3.9 版本会修复这个问题。

Maven

要完成 Maven 构建,一般需要完成如下两步:

  • 建立一个社区 Maven 库(例如 Apache Nexus),充当外部库的代理。将其当作镜像使用。
  • 这个内部库可能提供 HTTPS 服务,其中使用自建 CA 签名的证书。

对于容器中运行构建的实践而言,使用内部 Maven 库是非常关键的,因为容器启动后并没有本地库或缓存,这导致每次构建时 Maven 都下载全部的 Jar 文件。在本地网络使用内部代理库下载明显快于从因特网下载。

Maven Jenkins 代理镜像允许配置环境变量,指定代理的 URL。在 Kubernetes 插件的容器模板中设置如下:

MAVEN_MIRROR_URL=https://nexus.mycompany.com/repository/maven-public

构建好的成品(JAR) 也应该保存到库中,可以是上面提到的用于提供依赖的镜像库,也可以是其它库。Maven 完成 deploy 操作需要在 pom.xml 的分发管理 下配置库 URL,这与代理镜像无关。

<project ...>
<distributionManagement>
 <snapshotRepository>
  <id>mynexus</id>
  <url>https://nexus.mycompany.com/repository/maven-snapshots/</url>
 </snapshotRepository>
 <repository>
  <id>mynexus</id>
  <url>https://nexus.mycompany.com/repository/maven-releases/</url>
 </repository>
</distributionManagement>

上传成品可能涉及认证。在这种情况下,在 settings.xml 中配置的用户名/密码要与 pom.xml 文件中的对应的服务器 id 下的设置匹配。我们可以使用 OpenShift secret 将包含 URL、用户名和密码的完整 settings.xml 映射到 Maven Jenkins 代理容器中。另外,也可以使用环境变量。具体如下:

  • 利用 secret 为容器添加环境变量:
MAVEN_SERVER_USERNAME=admin
MAVEN_SERVER_PASSWORD=admin123
  • 利用 config map 将 settings.xml 挂载至 /home/jenkins/.m2/settings.xml:
<settings ...>
<mirrors>
<mirror>
<mirrorOf>external:*</mirrorOf>
<url>${env.MAVEN_MIRROR_URL}</url>
<id>mirror</id>
</mirror>
</mirrors>
<servers>
<server>
<id>mynexus</id>
<username>${env.MAVEN_SERVER_USERNAME}</username>
<password>${env.MAVEN_SERVER_PASSWORD}</password>
</server>
</servers>
</settings>

禁用交互模式(即,使用批处理模式) 可以忽略下载日志,一种方式是在 Maven 命令中增加 -B 参数,另一种方式是在 settings.xml 配置文件中增加 <interactiveMode>false</interactiveMode> 配置。

如果 Maven 库的 HTTPS 服务使用自建 CA 签名的证书,我们需要使用 keytool 工具创建一个将 CA 公钥添加至信任列表的 Java KeyStore。在 OpenShift 中使用 ConfigMap 将这个 Keystore 上传。使用 oc 命令基于文件创建一个 ConfigMap:

oc create configmap maven-settings --from-file=settings.xml=settings.xml --from-
file=myTruststore.jks=myTruststore.jks

将这个 ConfigMap 挂载至 Jenkins 代理容器。在本例中我们使用 /home/jenkins/.m2 目录,但这仅仅是因为配置文件 settings.xml 也对应这个 ConfigMap。KeyStore 可以放置在任意路径下。

接着在容器环境变量 MAVEN_OPTS 中设置 Java 参数,以便让 Maven 对应的 Java 进程使用该文件:

MAVEN_OPTS=
-Djavax.net.ssl.trustStore=/home/jenkins/.m2/myTruststore.jks
-Djavax.net.ssl.trustStorePassword=changeit

内存使用量

这可能是最重要的一部分设置,如果没有正确的设置最大内存,我们会遇到间歇性构建失败,虽然每个组件都似乎工作正常。

如果没有在 Java 命令行中设置堆大小,在容器中运行 Java 可能导致高内存使用量的报错。JVM 可以利用全部的宿主机内存,而不是使用容器内存现在并相应设置默认的堆大小。这通常会超过容器的内存资源总额,故当 Java 进程为堆分配过多内存时,OpenShift 会直接杀掉容器。

虽然 jenkins-slave-base 镜像包含一个内建脚本设置堆最大为容器内存的一半(可以通过环境变量 CONTAINER_HEAP_PERCENT=0.50 修改),但这只适用于 Jenkins 代理节点中的 Java 进程。在 Maven 构建中,还有其它重要的 Java 进程运行:

  • mvn 命令本身就是一个 Java 工具。
  • Maven Surefire 插件 默认派生一个 JVM 用于运行单元测试。

总结一下,容器中同时运行着三个重要的 Java 进程,预估内存使用量以避免 pod 被误杀是很重要的。每个进程都有不同的方式设置 JVM 参数:

  • 我们在上面提到了 Jenkins 代理容器堆最大值的计算方法,但我们显然不应该让代理容器使用如此大的堆,毕竟还有两个 JVM 需要使用内存。对于 Jenkins 代理容器,可以设置 JAVA_OPTS。
  • mvn 工具被 Jenkins 任务调用。设置 MAVEN_OPTS 可以用于自定义这类 Java 进程。
  • Maven surefire 插件滋生的用于单元测试的 JVM 可以通过 Maven argLine 属性自定义。可以在 pom.xml 或 settings.xml 的某个配置文件中设置,也可以直接在 maven 命令参数 MAVEN_OPS 中增加 -DargLine=…。

下面例子给出 Maven 代理容器环境变量设置方法:

JAVA_OPTS=-Xms64m -Xmx64m
MAVEN_OPTS=-Xms128m -Xmx128m -DargLine=${env.SUREFIRE_OPTS}
SUREFIRE_OPTS=-Xms256m -Xmx256m

我们的测试环境是具有 1024Mi 内存限额的代理容器,使用上述参数可以正常构建一个 SpringBoot 应用并进行单元测试。测试环境使用的资源相对较小,对于复杂的 Maven 项目和对应的单元测试,我们需要更大的堆大小及更大的容器内存限额。

注:Java8 进程的实际内存使用量包括“堆大小 + 元数据 + 堆外内存”,因此内存使用量会明显高于设置的最大堆大小。在我们上面的测试环境中,三个 Java 进程使用了超过 900Mi 的内存。可以在容器内查看进程的 RSS 内存使用情况,命令如下:ps -e -o pid,user,rss,comm,args。

Jenkins 代理镜像同时安装了 JDK 64 位和 32 位版本。对于 mvn 和 surefire,默认使用 64 位版本 JVM。为减低内存使用量,只要 -Xmx 不超过 1.5 GB,强制使用 32 位 JVM 都是有意义的。

JAVA_HOME=/usr/lib/jvm/Java-1.8.0-openjdk-1.8.0.161–0.b14.el7_4.i386

注意到我们可以在 JAVA_TOOL_OPTIONS 环境变量中设置 Java 参数,每个 JVM 启动时都会读取该参数。JAVA_OPTS 和 MAVEN_OPTS 中的参数会覆盖 JAVA_TOOL_OPTIONS 中的对应值,故我们可以不使用 argLine,实现对 Java 进程同样的堆配置:

JAVA_OPTS=-Xms64m -Xmx64m
MAVEN_OPTS=-Xms128m -Xmx128m
JAVA_TOOL_OPTIONS=-Xms256m -Xmx256m

但缺点是每个 JVM 的日志中都会显示 Picked up JAVA_TOOL_OPTIONS:,这可能让人感到迷惑。

Jenkins 流水线

完成上述配置,我们应该已经可以完成一次成功的构建。我们可以获取源代码,下载依赖,运行单元测试并将成品上传到我们的库中。我们可以通过创建一个 Jenkins 流水线项目来完成上述操作。

pipeline {
  /* Which container to bring up for the build. Pick one of the templates configured in Kubernetes plugin. */
  agent {
    label 'maven'
  }

  stages {
    stage('Pull Source') {
      steps {
        git url: 'ssh://[email protected]:22/myapplication.git', branch: 'master'
      }
    }
    stage('Unit Tests') {
      steps {
        sh 'mvn test'
      }
    }
    stage('Deploy to Nexus') {
      steps {
        sh 'mvn deploy -DskipTests'
      }
    }
  }
}

当然,对应真实项目,CI/CD 流水线不仅仅完成 Maven 构建,还可以部署到开发环境,运行集成测试,提升至更接近于生产的环境等。上面给出的学习资料中有执行这些操作的案例。

多容器

一个 pod 可以运行多个容器,每个容器有单独的资源限制。这些容器共享网络接口,故我们可以从 localhost 访问已启动的服务,但我们需要考虑端口冲突的问题。在一个 Kubernetes pod 模板中,每个容器的环境变量是单独设置的,但挂载的卷是统一的。

当一个外部服务需要单元测试且嵌入式方案无法工作(例如,数据库、消息中间件等) 时,可以启动多个容器。在这种情况下,第二个容器会随着 Jenkins 代理容器启停。

查看 Jenkins config.xml 片段,其中我们启动了一个辅助的 httpbin 服务用于 Maven 构建:

<org.csanchez.jenkins.plugins.kubernetes.PodTemplate>
  <name>maven</name>
  <volumes>
    ...
  </volumes>
  <containers>
    <org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
      <name>jnlp</name>
      <image>registry.access.redhat.com/openshift3/jenkins-slave-maven-rhel7:v3.7</image>
      <resourceLimitCpu>500m</resourceLimitCpu>
      <resourceLimitMemory>1024Mi</resourceLimitMemory>
      <envVars>
      ...
      </envVars>        
      ...
    </org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
    <org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
      <name>httpbin</name>
      <image>citizenstig/httpbin</image>
      <resourceLimitCpu></resourceLimitCpu>
      <resourceLimitMemory>256Mi</resourceLimitMemory>
      <envVars/>
      ...
    </org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
  </containers>
  <envVars/>
</org.csanchez.jenkins.plugins.kubernetes.PodTemplate>

总结

作为总结,我们查看上面已描述配置的 Jenkins config.xml 对应创建的 OpenShift 资源以及 Kubernetes 插件的配置。

apiVersion: v1
kind: List
metadata: {}
items:
- apiVersion: v1
  kind: ConfigMap
  metadata:
    name: git-config
  data:
    config: |
      [credential]
          helper = store --file=/home/jenkins/.config/git-secret/credentials
      [http "http://git.mycompany.com"]
          sslCAInfo = /home/jenkins/.config/git/myTrustedCA.pem
    myTrustedCA.pem: |-
      -----BEGIN CERTIFICATE-----
      MIIDVzCCAj+gAwIBAgIJAN0sC...
      -----END CERTIFICATE-----
- apiVersion: v1
  kind: Secret
  metadata:
    name: git-secret
  stringData:
    ssh-privatekey: |-
      -----BEGIN RSA PRIVATE KEY-----
      ...
      -----END RSA PRIVATE KEY-----
    credentials: |-
      https://username:[email protected]
      https://user:[email protected]
- apiVersion: v1
  kind: ConfigMap
  metadata:
    name: git-ssh
  data:
    config: |-
      Host git.mycompany.com
        StrictHostKeyChecking yes
        IdentityFile /home/jenkins/.config/git-secret/ssh-privatekey
    known_hosts: '[git.mycompany.com]:22 ecdsa-sha2-nistp256 AAAdn7...'
- apiVersion: v1
  kind: Secret
  metadata:
    name: maven-secret
  stringData:
    username: admin
    password: admin123

基于文件创建另一个 ConfigMap:

oc create configmap maven-settings --from-file=settings.xml=settings.xml 
--from-file=myTruststore.jks=myTruststore.jks

Kubernetes 插件配置如下:

<?xml version='1.0' encoding='UTF-8'?>
<hudson>
...
  <clouds>
    <org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud plugin="[email protected]">
      <name>openshift</name>
      <defaultsProviderTemplate></defaultsProviderTemplate>
      <templates>
        <org.csanchez.jenkins.plugins.kubernetes.PodTemplate>
          <inheritFrom></inheritFrom>
          <name>maven</name>
          <namespace></namespace>
          <privileged>false</privileged>
          <alwaysPullImage>false</alwaysPullImage>
          <instanceCap>2147483647</instanceCap>
          <slaveConnectTimeout>100</slaveConnectTimeout>
          <idleMinutes>0</idleMinutes>
          <label>maven</label>
          <serviceAccount>jenkins37</serviceAccount>
          <nodeSelector></nodeSelector>
          <nodeUsageMode>NORMAL</nodeUsageMode>
          <customWorkspaceVolumeEnabled>false</customWorkspaceVolumeEnabled>
          <workspaceVolume class="org.csanchez.jenkins.plugins.kubernetes.volumes.workspace.EmptyDirWorkspaceVolume">
            <memory>false</memory>
          </workspaceVolume>
          <volumes>
            <org.csanchez.jenkins.plugins.kubernetes.volumes.SecretVolume>
              <mountPath>/home/jenkins/.config/git-secret</mountPath>
              <secretName>git-secret</secretName>
            </org.csanchez.jenkins.plugins.kubernetes.volumes.SecretVolume>
            <org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume>
              <mountPath>/home/jenkins/.ssh</mountPath>
              <configMapName>git-ssh</configMapName>
            </org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume>
            <org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume>
              <mountPath>/home/jenkins/.config/git</mountPath>
              <configMapName>git-config</configMapName>
            </org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume>
            <org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume>
              <mountPath>/home/jenkins/.m2</mountPath>
              <configMapName>maven-settings</configMapName>
            </org.csanchez.jenkins.plugins.kubernetes.volumes.ConfigMapVolume>
          </volumes>
          <containers>
            <org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
              <name>jnlp</name>
              <image>registry.access.redhat.com/openshift3/jenkins-slave-maven-rhel7:v3.7</image>
              <privileged>false</privileged>
              <alwaysPullImage>false</alwaysPullImage>
              <workingDir>/tmp</workingDir>
              <command></command>
              <args>${computer.jnlpmac} ${computer.name}</args>
              <ttyEnabled>false</ttyEnabled>
              <resourceRequestCpu>500m</resourceRequestCpu>
              <resourceRequestMemory>1024Mi</resourceRequestMemory>
              <resourceLimitCpu>500m</resourceLimitCpu>
              <resourceLimitMemory>1024Mi</resourceLimitMemory>
              <envVars>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  <key>JAVA_HOME</key>
                  <value>/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.i386</value>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  <key>JAVA_OPTS</key>
                  <value>-Xms64m -Xmx64m</value>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  <key>MAVEN_OPTS</key>
                  <value>-Xms128m -Xmx128m -DargLine=${env.SUREFIRE_OPTS} -Djavax.net.ssl.trustStore=/home/jenkins/.m2/myTruststore.jks -Djavax.net.ssl.trustStorePassword=changeit</value>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  <key>SUREFIRE_OPTS</key>
                  <value>-Xms256m -Xmx256m</value>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  <key>MAVEN_MIRROR_URL</key>
                  <value>https://nexus.mycompany.com/repository/maven-public</value>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                <org.csanchez.jenkins.plugins.kubernetes.model.SecretEnvVar>
                  <key>MAVEN_SERVER_USERNAME</key>
                  <secretName>maven-secret</secretName>
                  <secretKey>username</secretKey>
                </org.csanchez.jenkins.plugins.kubernetes.model.SecretEnvVar>
                <org.csanchez.jenkins.plugins.kubernetes.model.SecretEnvVar>
                  <key>MAVEN_SERVER_PASSWORD</key>
                  <secretName>maven-secret</secretName>
                  <secretKey>password</secretKey>
                </org.csanchez.jenkins.plugins.kubernetes.model.SecretEnvVar>
              </envVars>
              <ports/>
              <livenessProbe>
                <execArgs></execArgs>
                <timeoutSeconds>0</timeoutSeconds>
                <initialDelaySeconds>0</initialDelaySeconds>
                <failureThreshold>0</failureThreshold>
                <periodSeconds>0</periodSeconds>
                <successThreshold>0</successThreshold>
              </livenessProbe>
            </org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
            <org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
              <name>httpbin</name>
              <image>citizenstig/httpbin</image>
              <privileged>false</privileged>
              <alwaysPullImage>false</alwaysPullImage>
              <workingDir></workingDir>
              <command>/run.sh</command>
              <args></args>
              <ttyEnabled>false</ttyEnabled>
              <resourceRequestCpu></resourceRequestCpu>
              <resourceRequestMemory>256Mi</resourceRequestMemory>
              <resourceLimitCpu></resourceLimitCpu>
              <resourceLimitMemory>256Mi</resourceLimitMemory>
              <envVars/>
              <ports/>
              <livenessProbe>
                <execArgs></execArgs>
                <timeoutSeconds>0</timeoutSeconds>
                <initialDelaySeconds>0</initialDelaySeconds>
                <failureThreshold>0</failureThreshold>
                <periodSeconds>0</periodSeconds>
                <successThreshold>0</successThreshold>
              </livenessProbe>
            </org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
          </containers>
          <envVars/>
          <annotations/>
          <imagePullSecrets/>
        </org.csanchez.jenkins.plugins.kubernetes.PodTemplate>
      </templates>
      <serverUrl>https://172.30.0.1:443</serverUrl>
      <serverCertificate>-----BEGIN CERTIFICATE-----
MIIC6jCC...
-----END CERTIFICATE-----</serverCertificate>
      <skipTlsVerify>false</skipTlsVerify>
      <namespace>first</namespace>
      <jenkinsUrl>http://jenkins.cicd.svc:80</jenkinsUrl>
      <jenkinsTunnel>jenkins-jnlp.cicd.svc:50000</jenkinsTunnel>
      <credentialsId>1a12dfa4-7fc5-47a7-aa17-cc56572a41c7</credentialsId>
      <containerCap>10</containerCap>
      <retentionTimeout>5</retentionTimeout>
      <connectTimeout>0</connectTimeout>
      <readTimeout>0</readTimeout>
      <maxRequestsPerHost>32</maxRequestsPerHost>
    </org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud>
  </clouds>

</hudson>

尝试愉快的构建吧!

原文发表于 ITNext,已获得翻版授权。


via: https://opensource.com/article/18/4/running-jenkins-builds-containers

作者:Balazs Szeti 选题:lujun9972 译者:pinewall 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

介绍

这篇文章是我在 CI 环境(特别是在 Gitlab 中)的 Docker 容器中构建 Go 项目的研究总结。我发现很难解决私有依赖问题(来自 Node/.NET 背景),因此这是我写这篇文章的主要原因。如果 Docker 镜像上存在任何问题或提交请求,请随时与我们联系。

dep

由于 dep 是现在管理 Go 依赖关系的最佳选择,因此在构建前之前运行 dep ensure。

注意:我个人不会将我的 vendor/ 文件夹提交到源码控制,如果你这样做,我不确定这个步骤是否可以跳过。

使用 Docker 构建的最好方法是使用 dep ensure -vendor-only。 见这里。

Docker 构建镜像

我第一次尝试使用 golang:1.10,但这个镜像没有:

  • curl
  • git
  • make
  • dep
  • golint

我已经创建好了用于构建的镜像(github / dockerhub),我会保持更新,但我不提供任何担保,因此你应该创建并管理自己的 Dockerhub。

内部依赖关系

我们完全有能力创建一个有公共依赖关系的项目。但是如果你的项目依赖于另一个私人 Gitlab 仓库呢?

在本地运行 dep ensure 应该可以和你的 git 设置一起工作,但是一旦在 CI 上不适用,构建就会失败。

Gitlab 权限模型

这是在 Gitlab 8.12 中添加的,这个我们最关心的有用的功能是在构建期提供的 CI_JOB_TOKEN 环境变量。

这基本上意味着我们可以像这样克隆依赖仓库:

git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/myuser/mydependentrepo

然而,我们希望使这更友好一点,因为 dep 在试图拉取代码时不会奇迹般地添加凭据。

我们将把这一行添加到 .gitlab-ci.yml 的 before_script 部分。

before_script:
  - echo -e "machine gitlab.com\nlogin gitlab-ci-token\npassword ${CI_JOB_TOKEN}" > ~/.netrc

使用 .netrc 文件可以指定哪个凭证用于哪个服务器。这种方法可以避免每次从 Git 中拉取(或推送)时输入用户名和密码。密码以明文形式存储,因此你不应在自己的计算机上执行此操作。这实际用于 Git 在背后使用 cURL。 在这里阅读更多。

项目文件

Makefile

虽然这是可选的,但我发现它使事情变得更容易。

配置这些步骤意味着在 CI 脚本(和本地)中,我们可以运行 make lint、make build 等,而无需每次重复步骤。

GOFILES = $(shell find . -name '*.go' -not -path './vendor/*')
GOPACKAGES = $(shell go list ./...  | grep -v /vendor/)

default: build

workdir:
    mkdir -p workdir

build: workdir/scraper

workdir/scraper: $(GOFILES)
    GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o workdir/scraper .

test: test-all

test-all:
    @go test -v $(GOPACKAGES)

lint: lint-all

lint-all:
    @golint -set_exit_status $(GOPACKAGES)

.gitlab-ci.yml

这是 Gitlab CI 魔术发生的地方。你可能想使用自己的镜像。

image: sjdweb/go-docker-build:1.10

stages:
  - test
  - build

before_script:
  - cd $GOPATH/src
  - mkdir -p gitlab.com/$CI_PROJECT_NAMESPACE
  - cd gitlab.com/$CI_PROJECT_NAMESPACE
  - ln -s $CI_PROJECT_DIR
  - cd $CI_PROJECT_NAME
  - echo -e "machine gitlab.com\nlogin gitlab-ci-token\npassword ${CI_JOB_TOKEN}" > ~/.netrc
  - dep ensure -vendor-only

lint_code:
  stage: test
  script:
    - make lint

unit_tests:
  stage: test
  script:
    - make test

build:
  stage: build
  script:
    - make

缺少了什么

我通常会用我的二进制文件构建 Docker 镜像,并将其推送到 Gitlab 容器注册库中。

你可以看到我正在构建二进制文件并退出,你至少需要将该二进制文件(例如生成文件)存储在某处。


via: https://seandrumm.co.uk/blog/building-go-projects-with-docker-on-gitlab-ci/

作者:SEAN DRUMM 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

持续集成 Continuous Integration (CI) 是为项目的每一项变更运行测试的过程,如同这是新的交付项目一样。如果持续执行,这意味着软件随时可以发布。 CI 是整个 IT 行业以及自由开源项目非常成熟的流程。Fedora 在这方面有点落后,但我们正在赶上。阅读以下内容了解进展。

我们为什么需要这个?

CI 将全面改善 Fedora。它通过尽早揭示 bug 提供更稳定和一致的操作系统。它让你在遇到问题时添加测试,以免再次发生(避免回归)。CI 可以运行来自上游的项目测试,还有测试集成在发行版中 Fedora 特定的测试。

最重要的是,一致的 CI 能自动化并减少手工劳动。它释放了我们宝贵的志愿者和贡献者,让他们将更多时间花在 Fedora 的新事物上。

它看起来如何?

对于初学者,我们将对在 Fedora 包仓库 (dist-git) 的每个提交运行测试。这些测试独立于构建时运行的每个软件包的测试。但是,它们在尽可能接近 Fedora 用户运行环境的环境中测试软件包的功能。除了特定的软件包测试外,Fedora 还运行一些发行版测试,例如从 F27 升级到 F28 或者全新安装。

软件包根据测试结果进行“控制”:测试失败会阻止将更新推送给用户。但是,有时由于各种原因,测试会失败。也许测试本身是错误的,或者不是最新的软件。或者可能发生基础架构问题,并阻止测试正常运行。维护人员能够重新触发测试或放弃测试结果,直到测试更新。

最终,当在 https://src.fedoraproject.org 上有合并请求或者更新时,Fedora 的 CI 将运行测试。这将使维护者了解建议的更改对包稳定性的影响,并帮助他们决定如何进行。

我们如今有什么?

目前,CI 管道在 Fedora Atomic Host 的部分软件包上运行测试。其他软件包可以在 dist-git 中进行测试,但它们不会自动运行。发行版特定的测试已经在我们所有的软件包上运行。这些测试结果被用于过滤测试失败的软件包。

我该如何参与?

最好的入门方法是阅读关于 Fedora 持续集成的文档。你应该熟悉标准测试接口,它描述了很多术语以及如何编写测试和使用现有的测试。

有了这些知识,如果你是一个软件包维护者,你可以开始添加测试到你的软件包。你可以在本地或虚拟机上运行它们。 (对于破坏性测试来说后者是明智的!)

标准测试接口使测试保持一致。因此,你可以轻松地将任何测试添加到你喜欢的包中,并在 [仓库]​​3 提交合并请求给维护人员。

在 irc.freenode.net 上与 #fedora-ci 联系,提供反馈,问题或关于 CI 的一般性讨论。


via: https://fedoramagazine.org/continuous-integration-fedora/

作者:Pierre-Yves Chibon 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 fleetster, 我们搭建了自己的 Gitlab 实例,而且我们大量使用了 Gitlab CI。我们的设计师和测试人员也都在用它,也很喜欢用它,它的那些高级功能特别棒。

Gitlab CI 是一个功能非常强大的持续集成系统,有很多不同的功能,而且每次发布都会增加新的功能。它的技术文档也很丰富,但是对那些要在已经配置好的 Gitlab 上使用它的用户来说,它缺乏一个一般性介绍。设计师或者测试人员是无需知道如何通过 Kubernetes 来实现自动伸缩,也无需知道“镜像”和“服务”之间的不同的。

但是,他仍然需要知道什么是“管道”,知道如何查看部署到一个“环境”中的分支。因此,在本文中,我会尽可能覆盖更多的功能,重点放在最终用户应该如何使用它们上;在过去的几个月里,我向我们团队中的某些人包括开发者讲解了这些功能:不是所有人都知道 持续集成 Continuous Integration (CI)是个什么东西,也不是所有人都用过 Gitlab CI。

如果你想了解为什么持续集成那么重要,我建议阅读一下 这篇文章,至于为什么要选择 Gitlab CI 呢,你可以去看看 Gitlab.com 上的说明。

简介

开发者保存更改代码的动作叫做一次 提交 commit 。然后他可以将这次提交 推送 push 到 Gitlab 上,这样可以其他开发者就可以 复查 review 这些代码了。

Gitlab CI 配置好后,Gitlab 也能对这个提交做出一些处理。该处理的工作由一个 运行器 runner 来执行的。所谓运行器基本上就是一台服务器(也可以是其他的东西,比如你的 PC 机,但我们可以简单称其为服务器)。这台服务器执行 .gitlab-ci.yml 文件中指令,并将执行结果返回给 Gitlab 本身,然后在 Gitlab 的图形化界面上显示出来。

开发者完成一项新功能的开发或完成一个 bug 的修复后(这些动作通常包含了多次的提交),就可以发起一个 合并请求 merge request ,团队其他成员则可以在这个合并请求中对代码及其实现进行 评论 comment 。

我们随后会看到,由于 Gitlab CI 提供的两大特性, 环境 environment 与 制品 artifact ,使得设计者和测试人员也能(而且真的需要)参与到这个过程中来,提供反馈以及改进意见。

管道 pipeline

每个推送到 Gitlab 的提交都会产生一个与该提交关联的 管道 pipeline 。若一次推送包含了多个提交,则管道与最后那个提交相关联。管道就是一个分成不同 阶段 stage 的 作业 job 的集合。

同一阶段的所有作业会并发执行(在有足够运行器的前提下),而下一阶段则只会在上一阶段所有作业都运行并返回成功后才会开始。

只要有一个作业失败了,整个管道就失败了。不过我们后面会看到,这其中有一个例外:若某个作业被标注成了手工运行,那么即使失败了也不会让整个管道失败。

阶段则只是对批量的作业的一个逻辑上的划分,若前一个阶段执行失败了,则后一个执行也没什么意义了。比如我们可能有一个 构建 build 阶段和一个 部署 deploy 阶段,在构建阶段运行所有用于构建应用的作业,而在部署阶段,会部署构建出来的应用程序。而部署一个构建失败的东西是没有什么意义的,不是吗?

同一阶段的作业之间不能有依赖关系,但它们可以依赖于前一阶段的作业运行结果。

让我们来看一下 Gitlab 是如何展示阶段与阶段状态的相关信息的。

pipeline-overview

pipeline-status

作业 job

作业就是运行器要执行的指令集合。你可以实时地看到作业的输出结果,这样开发者就能知道作业为什么失败了。

作业可以是自动执行的,也就是当推送提交后自动开始执行,也可以手工执行。手工作业必须由某个人手工触发。手工作业也有其独特的作用,比如,实现自动化部署,但只有在有人手工授权的情况下才能开始部署。这是限制哪些人可以运行作业的一种方式,这样只有信赖的人才能进行部署,以继续前面的实例。

作业也可以建构出 制品 artifacts 来以供用户下载,比如可以构建出一个 APK 让你来下载,然后在你的设备中进行测试; 通过这种方式,设计者和测试人员都可以下载应用并进行测试,而无需开发人员的帮助。

除了生成制品外,作业也可以部署环境,通常这个环境可以通过 URL 访问,让用户来测试对应的提交。

做作业状态与阶段状态是一样的:实际上,阶段的状态就是继承自作业的。

running-job

制品 Artifacts

如前所述,作业能够生成制品供用户下载来测试。这个制品可以是任何东西,比如 Windows 上的应用程序,PC 生成的图片,甚至 Android 上的 APK。

那么,假设你是个设计师,被分配了一个合并请求:你需要验证新设计的实现!

要该怎么做呢?

你需要打开该合并请求,下载这个制品,如下图所示。

每个管道从所有作业中搜集所有的制品,而且一个作业中可以有多个制品。当你点击下载按钮时,会有一个下拉框让你选择下载哪个制品。检查之后你就可以评论这个合并请求了。

你也可以从没有合并请求的管道中下载制品 ;-)

我之所以关注合并请求是因为通常这正是测试人员、设计师和相关人员开始工作的地方。

但是这并不意味着合并请求和管道就是绑死在一起的:虽然它们结合的很好,但两者之间并没有什么关系。

download-artifacts

环境 environment

类似的,作业可以将某些东西部署到外部服务器上去,以便你可以通过合并请求本身访问这些内容。

如你所见, 环境 environment 有一个名字和一个链接。只需点击链接你就能够转至你的应用的部署版本上去了(当前,前提是配置是正确的)。

Gitlab 还有其他一些很酷的环境相关的特性,比如 监控 monitoring ,你可以通过点击环境的名字来查看。

environment

总结

这是对 Gitlab CI 中某些功能的一个简单介绍:它非常强大,使用得当的话,可以让整个团队使用一个工具完成从计划到部署的工具。由于每个月都会推出很多新功能,因此请时刻关注 Gitlab 博客。

若想知道如何对它进行设置或想了解它的高级功能,请参阅它的文档。

在 fleetster,我们不仅用它来跑测试,而且用它来自动生成各种版本的软件,并自动发布到测试环境中去。我们也自动化了其他工作(构建应用并将之发布到 Play Store 中等其它工作)。

说起来,你是否想和我以及其他很多超棒的人一起在一个年轻而又富有活力的办公室中工作呢? 看看 fleetster 的这些招聘职位 吧!

赞美 Gitlab 团队 (和其他在空闲时间提供帮助的人),他们的工作太棒了!

若对本文有任何问题或回馈,请给我发邮件:[email protected] 或者发推给我:-) 你可以建议我增加内容,或者以更清晰的方式重写内容(英文不是我的母语)。

那么,再见吧,

R.

P.S:如果你觉得本文有用,而且希望我们写出其他文章的话,请问您是否愿意帮我买杯啤酒给我 让我进入 鲍尔默峰值?


via: https://rpadovani.com/introduction-gitlab-ci

作者:Riccardo 译者:lujun9972 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出