曾经听到过这样一句话,”不要用战术上的勤奋掩盖战略上的懒惰”,所以战术和战略更像是抽象类和具体类,而面向对象设计实际上是现实等级制度的一种映射。因此我们注意到,决策者通常关注的是战略层面的抽象概念,而执行者通常更关注战术层面的具体实现,正如在代码的架构设计中,处在顶层的代码以发送指令为主要使命,处在底层的代码以实现功能为主要使命。面对日新月异的互联网技术,当我们听到越来越多的新名词,譬如微服务、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岁吗?好啦,夜深人静,该去睡觉了,这篇文章就是这样子啦。