对于不断演进中的产品,持续交付(CD)使其开发到产品交付的过程更加简单。持续集成(CI)位于持续交付过程的开始阶段,它扮演了这个过程中的重要角色,由它定义软件开发过程。
在书上和网上可以查到很多持续集成工具的资料,但处于持续集成过程中核心的构建作业却没有太多资料。
典型的持续集成过程如下:开发人员在他们自己的机器上手工构建和测试源代码。然后他们会把修改提交到一个源码控制管理系统。随后构建工具将运行作业编译和测试这些代码。然后把构建的工件上传到一个中心资源库,用于接下来的开发和测试。
因此,作业在持续集成工具中的角色是管理源代码不断的修改、运行测试并通过发布通道管理工件传输的物流。
构建工作的作业任务少则数个,多则上千,所有作业都执行不同的功能。幸运的是,我们可以用一种更加有效的方式去管理这些作业。
自动化创建构建作业
那么为什么我们要装置一套自动化创建其他作业的设施?
构建工具在用户手册中描述了如何创建构建作业,但却很少具体解释如何管理这些作业,尽管这些工具完全可以做到这些事情。
通常开发人员要了解他们的应用是如何构建的,然后为创建和配置构建作业取得独占性控制权限。然而,这个过程存在一些缺陷:
- 软件架构约束:由于架构层面的因素,应用很可能有各种不同的构建方式。开发人员可能需要说明他与另一个开发人员在构建应用时的不同,所以一个作业和另一个作业的构建配置总有些细微的差异。因此,如果所有的配置都不同,那么大量的作业就会变得极难维护。
- 人为因素:手工创建作业引入了出错的风险,尤其是采用先复制再修改的方式去创建一个新作业(即先复制一个已有的作业,然后再修改这个副本,从而得到一个新作业)。
- 作业时间轴控制:通常情况下,不会保存每次构建的作业配置,也就是说,如果一个作业的配置被修改了,那么就有可能破坏之前的构建。
所以综合考虑以上几点,如果创建作业时没有一个一致的方法,开发人员就要用自己的设备执行构建了,那么迎接我们的最终结果将是极难维护的持续集成构建系统和令人头痛的管理。这违背了建立持续集成系统的原则:精益和易维护性。
实际上大多数构建工具都具有这样的设施,可以通过 API 脚本自动化创建、维护和备份构建作业。然而,尽管构建工具在用户手册里提到过这些功能,但却经常被忽视而未被采用。
自动化创建构建作业的优点
为简化持续集成构建过程,可以使用一个主构建作业去自动化创建多个作业,这种做法有以下几个优点。
- 通过作业模版或脚本创建所有构建作业的配置。它们也可以放在配置控制之下。使用模版或运行脚本可以非常简单地创建一个新作业。这可以显著地缩减作业创建时间(从若干分钟缩短到几秒种)。今后,也可以更加简单地修改作业配置;修改构建作业模版的配置就可以保证所有在之后新创建的作业都会继承这些修改。
- 所有已有构建作业的配置都保持一致,相比杂乱不一致的系统,这就可以更加简单地通过工具或脚本全局更新所有的配置了。
- 开发人员在构建作业时就不再需要构建工具的详细知识了。
- 自动化创建和拆卸构建作业的能力是敏捷持续集成构建生命周期的一部分。
让我们一起探讨一下下列的几个要点:
要点 1:作业配置的配置控制
让持续集成系统的每个组件都处于配置控制之下是一个好的实践。这些组件不仅仅包括源代码,还包括基础的构建系统和构建作业。
大多数构建工具把作业配置以文件形式保存在主系统上。这些文件设定为普通的 XML 格式,可以直接通过某种 REST API 前端接口正常地存取这些文件。某些构建工具具备一些内置的特性,可以利用这些特性把作业配置以文件形式保存到任意位置。
因为可以把作业配置保存为文件,就能够把作业配置以文件形式保存到配置管理(CM)系统中了。不管是直接修改文件然后再上传到构建工具,还是修改构建工具的作业配置,保存这个文件后再把它手工上传到配置管理系统,用这两种方式针对作业配置所做的任何变更都会被记录下来。实际实施哪种方法取决于从构建工具中直接访问作业配置文件的简易程度。
在配置管理系统中保存作业配置还有一个重要的理由,如果发生灾难性故障,丢失了所有作业配置,构建系统可以比较迅速地恢复,把所有作业、构建日志和构建历史恢复回已知的最近的一次良好状态。
要点 2:作业维护性
不言而喻,维护上千个不同的配置将是一件令人头疼的事,正因为如此,才使得作业配置的标准化格外重要。如果必需要做一次变更,同时它又会影响到大量相似的作业,那么更加简单的做法是编写脚本或创建特定的任务去修改这些作业。
对于持续集成来说,在构建系统中有上千个作业无论如何也不是最好的策略。我们将在下面的要点 4 中对此做更加深入的讨论。
要点 3:开发人员控制
尽管开发人员被鼓励独立自主地工作,但他们也要使用一个集中的构建系统构建应用程序,他们需要依照构建过程按照指导方针工作。
开发人员希望他们的代码变更能得到快速反馈,所以不希望在构建工具和作业配置上浪费过多的时间。假如通过一个按钮为开发人员提供“自助服务”式的解决方案,可以自动化地构建作业,就会帮助他们更快地完成开发任务。
要点 4:精益持续集成系统
尽管软件开发团队在产品研发中采用了敏捷方法,但在他们却不一定在工作中以相同的方式使用构建工具,也就是说已经适当配置过的构建工具不应该包含太多长期的构建作业。理想情况下,应该把作业看作构建持续集成生命周期的一部分,按需来创建。有一种常见的方法是通过任意历史作业配置重新创建构建作业,并保留本次构建痕迹,即日志、工件、测试报告等,然后只在构建工具中保留少量作业。
当然,这一解决方案(即按需创建 - 拆卸作业)或许是一个无法达成的目标,但该要点在此主要强调,应该把构建工具中的作业数保持在一个可管理的级别。
日常开发
构建管理在治理工具和过程时必须要有助于开发人员的日常活动,能够辅助开发人员在日常完成与构建和持续集成相关的活动。工具提供了授权功能,可以指定哪些开发团队可以创建构建作业,虽然工具提供了灵活的作业配置方式,但仍然要通过软件开发最佳实践的治理。
构建管理员在治理工具和过程时,应充分考虑开发人员的实际工作,让他们在日常能够更有效地完成构建和持续集成工作。在软件开发最佳实践治理的范畴内尽可能地为负责创建构建作业的团队提供更多的灵活性。
持续集成作业套件
思考我们考虑一下开发人员在日常开发过程中执行的典型任务:
- 构建一个基线
- 发布基线
- 构建一个发布分支(经常与发布基线结合在一起)
- 创建一个开发分支,构建这个分支
- 运行集成测试
先抛开最佳开发实践不说(比如尽可能多地在基线上开发、今后几乎没有开发分支等),软件开发团队通常在构建工具中运行构建作业套件完成上文所述的任务。
现在,作业的自动化创建为软件开发过程带来了巨大的价值;假设一名开发人员为了管理日常运维,打算为所有开发任务都创建四到五个作业,那么用手工方式创建这些作业将耗费大量的时间。相比之下,自动化创建这些作业只需要花费一点点时间去推一下按钮。
自助服务作业创建解决方案的实现时机
在下列情况下适合实现创建构建作业的自助服务解决方案:
- “新建的”项目的初始化(之前从未构建过)。
- 项目已经存在,但从未给它们建立过任何构建作业。也就是说以前都是从开发人员个人机器上进行的手工构建。
还有一些情况不适合实现自助服务作业创建解决方案:
- 如果一个产品只有几个作业,即大型统一性应用,从商业价值来看,就没有必要花费大量的时间,只为那几个少数项目努力地设计、测试和推动自动化创建作业过程。然而,随着业务的扩展,因为要建立数据流管理额外的负载,产品架构将更为复杂,今后构建工具中的作业数会必然有所上升。
- 已有作业的项目:当在构建工具中处理已有作业时可以考虑以下两个场景。
- 删除已有作业并用作业组件创建工具重新创建它们,这么做会丢失所有历史构建信息和日志。
- 保留已有作业,尝试修改它们的配置以符合作业配置的要求,再由作业套件创建工具创建新的作业。
无论如何,维护遗留的构建作业都难免要花费一些日常管理成本。
在构建工具中装置自助服务作业套件创建设施
我们已经讨论过自动化创建构建作业的优点,那么现在让我们一起讨论一下如何在构建工具中实现这样的特性。任何语言(即 Java、.NET 等)构建应用的原理应该是相同的。
介绍作业创建构建表单
构建表单是一种把信息从构建工具直接传到另一个特性或后端脚本的方法,由这些特性和脚本执行作业创建的任务。
理想情况下,应该尽早提供尽可能多的项目信息,比如像项目名称、源代码的位置和特定构建开关(通常是必需的)等。也就不必为了新建构建作业再来添加额外的配置,从而减少了后期的额外付出。如此,在推动“构建”按钮片刻之后就会创建出作业。
构建工具实现:
我们现在要讨论如何在一个构建工具中实现这样的作业创建机制。我曾经使用 Hudson 和 Anthill Pro 3 这两款工具建立过作业套件创建机制,具备一定的个人经验。
Hudson / Jenkins
为了清晰起见,我们只讨论 Hudson,但讨论的信息对 Jenkins 同样有用。
有些管理者希望尽可能地减少他们的开发资源预算,并且不希望受到高价许可的束缚,于是他们非常广泛地采用了 Hudson 持续集成工具。Hudson 把它的构建作业配置定为 XML 格式。通过顶级页面的作业 url 可以访问标准的作业配置 XML 文件,比如: http://hostname/job-name/config.xml
在配置文件中大多数断落是自解释的。只有在 Hudson 图形用户界面中使用配置文件时,才能使用插件在文件中增加段落。
通过用特定的项目信息替换文件中占位符的方式,可以把这些配置文件“模版化”。各个模版可以用来实现不同的任务,比如从不同的软件配置管理系统(例如 Subversion 或 GIT)中检出源代码。
所以现在我们需要建立创建作业构建表单与脚本的关联。下面的图表展示了工作流程:
Hudson 创建作业的工作流程
构建表单把信息传给脚本,脚本把模版中的占位符替换为相关信息。然后通过 API 把配置文件上传到 Hudson 工具。在浏览器中当前的 url 后添加上“api” 后可以查阅到关于 Hudson API 的更多细节,例如 http://hostname/job-name/api 。
下面展示了用 Hudson API 创建作业上传命令的实例:
curl –H Content-Type:application/xml –s –data @config.xml ${HUDSONURL}/createItem?name=hudsonjobname
Hudson 创建作业表单
可以为作业手工添加一个新视图。
Hudson 作业视图
需要注意以下几点:
- 如果 Hudson 已配置安全方面的内容,要确保“匿名”用户拥有“创建作业”的权限,否则上传无法通过认证。
- 如果 Hudson 选项卡用于查看特定的作业分组,除了通用的“所有”选项卡(或者任何一个使用正则表达式的选项卡)之外,任何最新创建的作业都不可能自动地放在那些选项卡下面。需要升级 Hudson 主配置文件并手工重启 Hudson 界面。创建后的作业可以手工添加到相关的选项卡上(如果你非常希望这么做的话)。
脚本可以用任意语言来编写,比如 ANT、Perl、Groovy 等等。所以,为了实现前文描述的那些步骤而创作执行脚本其实是相对简单的工作。
典型的 ANT 脚本如下所示:
<project name="createjob" default="createHudsonjobsConfigs"> <!—为在 hudson 中创建新作业,更新 hudson 作业 config.xml 模版的 Ant 脚本 --> <!-- 从 Hudson 构建表单中获取外部属性 --> <property name="hudsonjobname" value="${HUDSON.JOB.NAME}" /> <property name="scmpath" value="${SCM.PATH}" /> <property name="mvngoals" value="${MVN.GOALS}" /> <!-- ... 以同样的做法从 Hudson 表单中获取剩余属性 --> ... ... <property name="hudson.createItemUrl" value="http://hudson.server.location/createItem?name=" /> <!—包括 ant 作业扩展 --> <taskdef resource="net/sf/antcontrib/antlib.xml"/> <target name = "createHudsonjobsConfigs" description="creates new config.xml file from input parameters"> <mkdir dir="${hudsonjobname}"/> <!-- 依次循环每一个作业模版文件,替换 Hudson 中作业模版里的属性占位符 --> <for list="ci-build-trunk,RC-build-branch" param="jobName"> <sequential> <delete file="${hudsonjobname}/${configFile}" failonerror="false"></delete> <copy file="../job-templates/@{jobName}-config.xml" tofile="${hudsonjobname} /${configFile}"/> <replace file="${hudsonjobname}/${configFile}" token="$HUDSON.JOB.NAME" value="${hudsonjobname}"/> <replace file="${hudsonjobname}/${configFile}" token="$SCM.PATH" value="${scmpath}"/> <!-- ...do same for rest of tokens in the job template --> ... ... <antcall target="configXMLUpload"> <param name="job" value="@{jobName}"></param> </antcall> </sequential> </for> </target> <!—构造作业配置上传命令 --> <target name="configXMLUpload"> <echo>curl -H Content-Type:application/xml -s --data @config.xml ${hudson.createItemUrl}${hudsonjobname}-${job}</echo> <exec executable="curl" dir="${hudsonjobname}"> <arg value="-H" /> <arg value="Content-Type:application/xml"/> <arg value="-s" /> <arg value="--data" /> <arg value="@config.xml" /> <arg value="${hudson.createItemUrl}${hudsonjobname}-${job}"/> </exec> </target> </project>
典型的含有占位符的 Hudson 作业模版如下所示:
<?xml version='1.0' encoding='UTF-8'?> <project> <actions/> <description>Builds $POM.ARTIFACTID</description> ... ... <scm class="hudson.scm.SubversionSCM"> <locations> <hudson.scm.SubversionSCM_-ModuleLocation> <remote>$SCM.PATH/trunk</remote> <local>.</local> <depthOption>infinity</depthOption> <ignoreExternalsOption>false</ignoreExternalsOption> </hudson.scm.SubversionSCM_-ModuleLocation> </locations> </scm> <assignedNode>$BUILD.FARM</assignedNode> ... ... <builders> <hudson.tasks.Maven> <targets>$MVN.GOALS</targets> <mavenName>$MVN.VERSION</mavenName> <usePrivateRepository>false</usePrivateRepository> </hudson.tasks.Maven> </builders> ... ... </project>
ci-build-trunk-config.xml Hudson 作业模版
Anthill Pro
Anthill Pro 来自于 Urban Code 有限公司,是一款经许可的构建和持续集成工具。通过它的图形用户界面和大量 API 库用户可以基本完成构建作业的定制控制。
uBuild 是来自 UrbanCode 的最新形式,本质上它是 Anthill 的缩减版,上一代产品最初被设计成管理整条持续发布管道,相比而言它更加主要倾向于面向产品的持续集成构建。uBuild 未提供 API(Anthill 有),但它可以创建一个相同作用的插件。
在 Anthill 中把构建作业称为“工作流程”,在本质上它是单个作业任务按特定顺序(串行或并行)执行的管道。
Anthill 的 API 脚本语言是 Beanshell,这门语言基于标准的 Java 方法。
Beanshell 脚本存储在 Anthill 图形用户界面里,可以从作业任务中调用这些脚本。Anthill 的最新版本可以定制插件,这种插件可以使用任意编程语言来开发。
上面提到的工作流程中唯一必须的方法是“constructs”,可以使用 API 调用这个方法创建 Anthill 作业任务。由于不得不用数据填充工作流程的每个方面,所以脚本可能会特别地长。像 Hudson 一样,通过执行主工作流程的构建表单收集构建作业信息,再调用 Beanshell 脚本创建相关的工作流程。
Anthill 作业视图
Anthill 创建工作流表单
典型的 Java Beanshell 脚本如下所示:
private static Project createProject(User user) throws Exception { // 获取 buildlife 属性值 String groupId = getAnthillProperty("GROUPID"); String artifactId = getAnthillProperty("ARTIFACTID"); String build_farm_env_name = "linux-build-farm"; String branchName = "branchName"; Workflow[] workflows = new Workflow[3]; // 设置项目 Project project = new Project(artifactId + "_" + branchName); // 判定项目是否已存在,是否已激活 boolean isProjectActive; try { Project projectTest = getProjectFactoryInstance(artifactId + "_" + branchName); isProjectActive = projectTest.isActive(); } catch (Exception err) { isProjectActive = false; } if (!isProjectActive) { project.setFolder(getFolder(groupId, artifactId)); setLifeCycleModel(project); setEnvironmentGroup(project); setQuietConfiguration(project); String applications = getAnthillProperty("APPS"); // 创建项目属性 createProjectProperty(project, "artifactId", artifactId, false, false); // 设置工作流程服务组,增加环境属性 addPropertyToServerGroup(project, build_farm_env_name, applications); project.store(); ... ... // 创建持续集成构建工作流程 String workflowName = "ci-build"; workflows[0] = createWorkflow( project, workflowName, perforceClientFor(branchName, groupId, artifactId, workflowName) + buildLifeId, perforceTemplateFor(branchName, groupId, artifactId, workflowName), "${stampContext:maven_version}", build_farm_env_name, "ci-build Workflow Definition" ); // 增加触发的 url 提交构建分支 addRepositoryTrigger(workflows[0]); // 创建分支流程 workflowName = "Branch"; workflows[1] = createWorkflow( project, workflowName, perforceClientFor(branchName, groupId, artifactId, workflowName) + buildLifeId, perforceTemplateFor(branchName, groupId, artifactId, workflowName), "From ${stampContext:maven_version}", build_farm_env_name, "Branch Workflow Definition" ); // 创建发布工作流程 workflowName = "Release"; workflows[2] = createWorkflow( project, workflowName, perforceClientFor(branchName, groupId, artifactId, workflowName) + buildLifeId, perforceTemplateFor(branchName, groupId, artifactId, workflowName), "${stampContext:maven_release_version}", build_farm_env_name, "Release Workflow Definition" ); ... ... } else { // 项目已经存在 String msg = "Project " + artifactId + "_" + branchName + " already exists - check project name and pom"; throw new RuntimeException (msg); } return project; }
创建项目 Java Beanshell 脚本示例
注意:它也可能创建一个自包含的 Java Beanshell 脚本(比如放在 Anthill Pro 应用服务器中的文件夹下的 jar 文件)。直接调用 jar 文件中的类可以编写应用内的 shell 任务。这项工作特别有用,因为脚本能在发布到开发运行环境之前就做单元测试。
其他工具
所以,只要具有通过 API 或模版特性创建作业的能力,就可以使用其他类似的构建工具来完成在此描述的实现,比如 Thoughtworks Go、Jetbrains Team Teamcity 和 Atlassian Bamboo 等,不一而足。
构建工具终极敏捷:实现精益构建工具的哲学
本文在早先讨论了按需自动化创建持续集成项目并在完成后拆卸的概念,当时曾经提到过一个优点。那就是这种做法能显著减少构建工具中的构建作业数。
为了在构建工具上自动化构建和拆卸作业,我还有待于实现一个敏捷的精益方法,这就需要突破像“任务还没有发布”之类的技术挑战。然而,在此讨论的每一件事都可能引导此类解决方案的最终实现。实际上 Atlassian 的构建工具 Bamboo 已经具备了相应的特性,该特性可以检测到在主要基线上创建的分支,然后自动化创建一个适当的作业去构建这个分支。
总结
我们已经讨论了如何装置设施去创建其他作业,这是针对企业的持续交付策略的重要构成。
- 在构建工具中自动化创建作业的方法应成为日常软件开发运维的一部分。
- 作业的自动化创建节省了开发人员的时间,使他们可以继续完成更加重要的任务。
- 构建作业一致的配置可以简化作业的维护,并使全公司达成一致的开发实践。
- 自动化创建作业最终会使软件开发到构建工具层都采用正确的敏捷。
关于作者
Martin Peston**** 是 Gamesys 的一名构建和持续集成管理人员, Gamesys 是英国领先的在线和移动赌博的公司,拥有 Jackpotjoy (广受欢迎的宾戈游戏品牌)和 Friendzys(在 Facebook 上非常成功的社交游戏)。
Martin 有 15 年跨行业的 IT 经验,包括太空、防御、卫生和赌博等行业。他把大约一半的职业生涯都奉献给了构建管理,热衷于促进软件开发公司的持续交付最佳实践。
在闲暇的时候,Martin 还是个痴迷的天文爱好者,所著的《Meade LXD55 和 LXD75 望远镜用户指南》在 2007 年由 Springer 出版社出版。
查看英文原文: Managing Build Jobs for Continuous Delivery
感谢臧秀涛对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ ;)或者腾讯微博( @InfoQ ;)关注我们,并与我们的编辑和其他读者朋友交流。
评论