曾经听到过这样一句话,”不要用战术上的勤奋掩盖战略上的懒惰”,所以战术和战略更像是抽象类和具体类,而面向对象设计实际上是现实等级制度的一种映射。因此我们注意到,决策者通常关注的是战略层面的抽象概念,而执行者通常更关注战术层面的具体实现,正如在代码的架构设计中,处在顶层的代码以发送指令为主要使命,处在底层的代码以实现功能为主要使命。面对日新月异的互联网技术,当我们听到越来越多的新名词,譬如微服务、DevOps、单页面应用、前后端分离等等,这些概念曾让我们迷恋于追寻一个又一个风口,一如曾经的 O2O、VR、共享经济和人工智能,那么我们真的懂得如何让这些概念落地吗?在今天这篇文章中,我想和大家一起探讨持续集成相关的话题,并以 Hexo 结合 TravisCI 实现自动化部署为例,聊聊我心目中的 DevOps。

从 DevOps 谈谈持续集成

不知从何时起,DevOps 开始成为大家竞相追捧的概念,同 ThoughtWorks 所倡导的微服务、敏捷开发一样,大家仿佛抓住了一根新的救命稻草一般,那么我们在说 DevOps 的时候,我们到底想要表达什么观点呢?想要搞清楚这个问题,我认为首先要明白,什么是 DevOps?从概念上讲,DevOps 是一个面向 IT 运维的工具流,以 IT 自动化以及持续集成(CI)、持续部署(CD)为基础,目的是优化开发、测试、运维等所有环节,所以 DevOps 本质上是一组部门间沟通协作的流程和方法,其目的是为了协调开发(DEV)、测试(QA)、运维(OPS)这三种角色,使开发运维一体化,通过高度自动化工具和流程,来确保软件构建、测试和发布更加快捷、频繁和稳定。

所以,我们在说 DevOps 的时候,我们想表达的或许是流程和管理、运维和自动化、架构和服务、文化和组织等等的概念,那么在这些观点中,最重要的是什么呢?我认为是持续集成(CI)和持续部署(CD),这是 DevOps 中从始至终贯穿的一条主线。通过 Git 这样的源代码控制工具,我们可以确保项目在一条主干上开发。而自动化测试/部署等周边工具,则为我们提供了实施持续集成/持续部署的必要条件。从公司角度出发,公司普遍更看重项目的交付能力,所以在传统持续集成/部署的基础上,我们时常会听到持续交付这样的声音,这时我们就会意识到,DevOps 实则是持续集成思想的一种延伸,它并不是一个新的概念,事实上我们这个行业,每年都喜欢这种“旧酒换新瓶”的做法,持续集成/部署/交付是 DevOps 的核心技术,如果没有自动化测试和自动化部署,DevOps 就是难以落地的空中楼阁。

由此,我们就引出今天这篇文章的主题,即持续集成。我们提到,DevOps 是是一套面向 IT 的跨部门协作的工作流,它是持续集成思想的一种延伸,所以持续集成首先是一组工具链的集合。从某种意义上来讲,决策者喜欢 DevOps,并不是真正喜欢 DevOps,而是形式上的 DevOps 非常容易实现,因为有形的工具资源的整合是非常容易的,真正困难的是无形的流程资源的整合。你可以让两个陌生人在一起假装情侣,但你永远不可能真正拉近两个人心间的距离。通常而言,我们会用到下列工具:

  • 版本控制和协作开发:Github、GitLab、BitBucket、Coding 等。
  • 自动化构建和测试:Apache Ant、Maven、Selenium、QUnit、NUnit、XUnit、MSBuild 等。
  • 持续集成和交付:Jenkins、TravisCI、Flow.CI 等。
  • 容器/服务部署:Docker、AWS、阿里云等。

从术和道的角度来看待持续集成,我们会发现在术的层面上,我们有非常多的选择空间,所以接下来我们主要从道的层面,来说说持续集成的核心思想。我们提到在实践 DevOps 的时候,需要有一条项目主干,那么持续集成的基本概念,就是指频繁地提交代码到主干分支,这样做的目的是,保证问题被及时发现以及避免分支大幅度偏离主干。

在使用 Git 的场景下来看待持续集成,及时提交代码到主分支,可以避免因为分支改动过大而带来的冲突问题。按照敏捷开发的理论,每个 feature 通过迭代开发来集成到最终产品中,那么持续集成的目的,就是为了让产品可以在快速迭代的同时保证产品质量。在这里产品质量有两层含义,第一,本次 feature 提交通过测试;第二,本次 feature 提交无副作用。我们可以注意到,持续集成的第一个目的,即保证问题被及时发现,对应前者;持续集成的第二个目的,即避免分支大幅度偏离主干,对应后者。

所谓持续集成,是指代码在集成到主干前,必须要通过自动化测试,只要有一个测试用例失败,就不能集成到主干,所以持续集成和自动化测试天生就是紧密联系在一起的。我们不能只看到持续集成/部署/交付,如果连流程上的自动化都无法实现,这些都是无从谈起的,从开发者的角度来看,理想的状态是编译即部署,我们提交的每一行代码,都是可以集成、交付和部署的代码,所以实际上是对开发者的代码质量提高了要求。所有我们觉得美好的事情,其实核心都在于人如何去运作,想到一位前辈说过的话,“软件开发没有银弹”,所有试图通过某种方法论解决软件工程复杂性的想法,都是天真而幼稚的。

Jenkins 持续集成落地实践

博主曾经在公司项目上实践过持续集成,深感持续集成想要真正在团队里落地,受到太多太多的因素制约。我们采取的方案是,使用 Git/Github 作为源代码版本控制工具,使用 Jenkins 作为持续集成工具,使用 MSBuild 作为项目构建工具,使用 MSTest/NUnit 作为单元测试框架,使用 Selenium/UI Automation 作为 UI 自动化测试框架,这些工具可以很好地同 Jenkins 整合起来。在持续集成工具的选择上,我们并没有太多的选择空间,因为公司需要同时支持 Java 和 JavaScript/Nodejs 项目的持续集成,在持续集成落地这件事情上,我们最终选择了妥协,我们不再追求自动化部署,而是选择通过这个过程来快速定位问题,具体原因我们下面来讲。

首先,我们期望的是开发者在提交代码以后,可以触发编译、构建、测试和部署等一系列操作,我们会通过 Git 从远程仓库拉取最新代码,然后通过 MSBuild 来编译整个代码,由于 MSBuild 提供了定制化的脚本,可以对编译、测试和部署等环节进行精准控制,所以我们在 Jenkins 上触发的实际上是一系列动作,而这些都是可以在 Jenkins 上进行配置的,我们通常会将 Jenkins 上的日志以邮件形式发送给开发者,所以在很长一段时间里,每天到公司第一件事情,就是查看邮箱里的邮件,一旦发现有测试用例没有通过测试,我们就需要重复“修改代码“->“提交代码“这个过程,直至所有用例都完全通过测试,理论上通过测试的代码就可以直接部署上线,因为 MSBuild 可以帮助我们生成最终文件,我们只需要将其打包然后上传到服务器即可,可是实际上这是我们假想的一种场景而已,因为现实场景中我们考虑得通常会更多。

一个关键的问题是,我们没有可以量化的标准去评估,本次提交是否可以集成到主干。我知道你一定会说测试,事实是开发者不喜欢写测试,或者是写了不可测的测试,前一种观点认为写测试会占用开发时间,所以在开发时间相对紧张的时候,这就变成了我们不写测试的借口;后一种观点则是不会写可测试代码的表现,典型的表现是代码耦合度高、依赖大量无法 Mock 的对象实例、不会合理使用断言,所以在这种情况下,持续集成是没有意义的,我们不知道何时代码可以集成、交付和部署。我承认自动化测试无法全面替代人工测试,但当我们的关注点放在交付和部署上的时候,是否应该考虑先让持续集成落地,这实在是比 DevOps 更基础、更接地气,因为我相信持续集成是一种思想,它对开发团队中的每一个人都提出了更高的要求,持续集成是为了在保证产品质量的同时快速迭代,如果你心中没有产品质量的概念,DevOps 并不能帮你提高产品质量。

第二个关键的问题是,开发和运维该如何去协作,DevOps 是为了促进部门间沟通协作而提出的一套工作流,自动化是这套机制能够良好运行下去的前提,可是在现实场景中一切并没有那么理想。以我们公司为例,开发组和运维组分属两个不同的部门,运维组在上线、部署等关键环节设置了严格的审批流程,即运维组牢牢地控制着线上生产环境,所以即使我们通过 MSBuild 在 Jenkins 上为程序打好了包,我们依然需要按照运维组的要求,提交上线请求、人工上传程序以及等待部门审批,通常我们上线只有等到每周五,而上线流程所需的东西,我们需要在一周前准备好,所以你可以注意到一个现象,虽然在流程上开发团队和运维团队是结合在一起的,但实际上两者的工作目标依然是分离的。那是不是将两个团队放在一起工作,就能解决这个问题呢?我想合作的前提是相互理解和信任,如果彼此都不愿意去了解对方的工作流程,DevOps 可能仅仅是我们用工具堆积出来的虚幻感。

实现 Hexo 博客的自动化部署

好了,在公司使用 Jenkins 实践持续集成,在现实场景中总会受到各种各样的制约,这并不是因为持续集成这个想法不好,而是在现实面前我们都选择了妥协。有句话说,“如果没有见过光明,我本可忍受黑暗”,我们喜欢一个人或者是一样东西,都是因为我们觉得它是美好的,可以让我们觉得这个世界温暖,那么在公司以外的地方,我想更加自由地做些我喜欢的事情。在公司实践持续集成的时候,因为公司对权限的严格控制,我难以实现那种想象中的持续集成,即在成功地在提交代码以后直接触发编译和部署,我想在公司之外做成这件事情。

为什么想到要给博客做持续集成呢?首先,持续集成和单元测试联系紧密,我自认为我的单元测试刚刚入门,为了写出更好的单元测试,我必须要这样做,来强迫自己努力去写好单元测试;其次,持续集成可以将开发和部署分离,所以我在任何一台计算机上撰写博客,都可以通过 TravisCI 实现编译和部署,对 Hexo 这种静态博客而言,部署其实就是推送页面到 Github 而已,整体难度并没有太高。最后,我平时更新博客都是手动推送页面,因为我不喜欢用 Hexo 提供的部署功能,现在我想让自己专注在内容写作上,而一切都可以在我的控制范围内。这正是我所想,如果能让一切更好一点,我都愿意去尝试和努力。

关于 Hexo 这类静态博客生成器搭建博客的原理,我这里不想在赘述,因为我愿意相信,懂得搭建博客的人,一定是了解 Git、Github Pages 和 Markdown 等等的概念的,关于配置相关的细节大家可以参考官网。这里想着重介绍下 TravisCI,TravisCI 是一个在线的、分布式的持续集成服务,可以用来构建和测试托管在 Github 上的代码,并且其本身就是开源的。TravisCI 提供了主流编程语言如 C#、Java、JavaScript、Ruby、PHP、Node.js 等的支持,相比 Jenkins 而言,它是一个轻量级的持续集成平台,它会在每次提交代码后,根据配置文件来创建一个虚拟机,并执行用户定义的 Build 任务,这个虚拟机提供版本控制(Git)、项目构建(Node.js)等,在此前提下,我们下面着手 Hexo 的自动化部署。

方案设计

Hexo 博客实际上可以分成两部分,即博客源代码和静态页面。其中博客源代码主要是指 Hexo 及其相关模块、博客内容(source)、博客主题(theme),而静态页面由 Hexo 动态生成,通常放置在public目录中。对 Hexo 来讲,我们最终部署需要的是这些静态页面,所以我们设计得一个方案是,将静态页面存放在 master 分支,将博客源代码存放在 blog 分支。当用户提交代码到 blog 分支后,会触发 TravisCI 中定义的一系列操作,它会首先从 blog 分支拉取博客源代码,然后在 TravisCI 中完成静态页面的生成,最后将其提交到 master 分支以完成博客的更新,整个过程非常优雅,终于让我彻底摆脱了手动更新博客的过去,而更重要的是,从此写博客不再受地点的制约,因为写博客就是提交代码,生成静态页面以及部署到 Github Pages,现在全部交给了 TravisCI.

配置 TravisCI

TravisCI 是一个轻量级的持续集成方案,其轻量级主要体现在它的配置文件,即使用 TravisCI 并不需要我们安装任何软件,我们仅仅需要提供一个.travis.yml 文件即可,该文件通常被放置在项目根目录里。和 Jenkins 这样的持续集成工具不同,我们在这个文件中即可定制 Build 任务,下面给出一个基本的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
language: node_js
node_js: stable

# S: Build Lifecycle
install:
- npm install

script:
- hexo clean
- hexo generate

after_script:
- cd ./public
- git init
- git config user.name "qinyuanpei"
- git config user.email "qinyuanpei@163.com"
- git add .
- git commit -m "Update Blog"
- git push --force --quiet "https://${CI_TOKEN}@${GH_REF}" master:master
# E: Build LifeCycle

branches:
only:
- blog

env:
global:
- GH_REF: github.com/qinyuanpei/qinyuanpei.github.io

如果大家熟悉 Jenkins 的使用,就会发现这里定义的 Build 任务似曾相识。在这里我们首先指定了项目构建语言,即这是一个 node.js 的项目,然后我们会通过 npm 安装所有依赖,我们注意到在根目录里有一个 package.json 文件,该文件定义了整个项目依赖的项目。如果你使用过 Nuget,你会发现这一切都是如此的合理。那么当整个环境准备就绪以后,我们就可以着手博客的构建啦,和平时一样,我们会执行 hexo clean 和 hexo generate 命令,这样 Hexo 会帮助我们生成所有的静态页面,现在我们通过 Git 将其推送到 master 分支,通常基于 Github Pages 托管的页面都是存放在 gh-pages 分支的,可是对 Hexo 而言,我们放在 master 分支是没有问题的,这就是 TravisCI 构建整个博客的具体过程。

关联 TravisCI

到目前为止,我们定义好了 TravisCI 将会在虚拟机中执行的 Build 任务。我们知道,这里 TravisCI 是需要访问我们托管在 Github 上的代码仓库的,所以我们必须将这个代码仓库和 Travis 关联起来,这样它就具备了从代码仓库拉取代码(Pull)和向代码仓库推送(Push)代码的能力。印象中公司是给每一个 Jenkins 服务器关联了一个 Github 账户,这样需要持续集成的项目只需要添加这个账号,并为其赋予基本的读写权限即可。在这里是类似的,我们有两种方案来关联 TravisCI,即为 TravisCI 虚拟机添加 SSH-Key 和使用 Github 提供的 Personal Access Token。

前者和我们平时使用 Git 时配置 SSH-Key 是一样的,但考虑到公开密钥产生的安全性问题,TravisCI 建议我们使用官方的一个工具来对密钥进行加密,这是一个基于 Ruby 开发的命令行工具,加密后的内容可以在 TravisCI 中解密,这种方案需要安装 Ruby,博主选择放弃。如果你要问我为什么放弃 Ruby,大概是因为我忘不了曾经被 Jekyll 支配的恐惧感。而后者的原理是将 Github 生成的 Token 作为一个环境变量存储在 TravisCI 中,我们在定义 TravisCI 中的 Build 任务时可以引用这些环境变量,我们只需要在执行 Git 命令时带上这个 Token 就可以了。显然这种方式更合我的胃口,它的缺点是对此 Github 采用了粗放式的权限控制,即这个 Token 时可以访问所有代码仓库的,这一点大家自己可以根据自身情况来决定要使用哪一种方式。

我们在 Github 中的 Setting->Developer Settings 找到 Personal Access Token,然后选择所有 repo 相关的权限,生成这个 token 后将其复制下来备用,因为它只有在这个地方是可见的。接下来我们打开TravisCI,在使用 Github 登录后我们就可以在这里看到所有的项目,如图是我个人的 TravisCI 界面:

TravisCI主界面
TravisCI主界面

大家可以注意到,这里我开启了 qinyuanpei.github.io 这个仓库的持续集成服务,如果大家没有在这里看到项目列表,可以点击”Sync account”按钮进行同步。好了,现在我们继续配置:

配置TravisCI
配置TravisCI

在这里我们配置了名为 CI_TOKEN 的环境变量,该值对应.travis.yml 文件中的${CI_TOKEN}。现在我们在本地提交代码到 blog 分支,就会触发 TravisCI 执行 Build 任务,在这里 Build 任务是从 blog 分支拉取博客内容及主题,通过 npm 安装依赖的 nodejs 模块,最终 Hexo 生成的静态页面会被推送到 master 分支,这样就完成了整个自动化构建的流程。下面是 TravisCI 执行 Build 过程中的日志界面:

TravisCI日志
TravisCI日志

从计划写这样一篇文章,到我一边写博客一篇将它发布在网络上,前后花了大概我 3 天左右的时间。这段时间发生了太多太多的事情,所以写东西受难免受到情绪影响,你现在看到这篇由 TravisCI 自动生成的博客,大概无法想象屏幕前的我有着怎样复杂的心绪,有时候我告诉自己要沉下心来学点什么,有时候我会觉得此时的我和过去没有什么区别。转眼间忙忙碌碌一年到头,可会想起来顿时觉得时间像虚度一般,有人说,当你对未来不再有什么期许的时候,就是你开始衰老的迹象,可我真的老了吗?我不是只有 25 岁吗?好啦,夜深人静,该去睡觉了,这篇文章就是这样子啦。