CI/CD,以及DevOps早就不是一个新的概念,但是如果你仔细观察,会发现有相当多的公司,在DevOps上还处于非常低的水准。造成这种现象的原因很多,如管理层缺少软件工程的概念,组织结构存在隔离,缺少合适的工程实践人员等等。

实际上,如果考虑投入/产出比,可能没有比搭建一套DevOps系统对一个软件开发项目的效率提升更大了。很多时候,一个简单的集成、部署工具就能为团队每天节省好几个小时的时间。

但是,对很多团队来说,DevOps是一个相对陌生的东西,有些人觉得DevOps就是维护Jenkins和其上的一堆Job,有些人觉得就是一个把产品放到线上去的脚本,总之,这个事情看起来是一堆的搭建、配置、维护这样的脏活累活,很繁琐,好像也没啥技术含量,做得不好还容易遭埋怨。另外,在大多数小团队里,几乎是清一色的开发人员(甚至确定到Java开发人员这样的组成),而在大家的认知里,DevOps这件事应该是运维工程师来做的,我只要写好业务代码就行了。这种观点是与DevOps的理念南辕北辙的。

DevOps是什么

目前对DevOps没有统一的定义,按照我的理解,DevOps是一种高效拉通各个职能,保证持续快速交付客户价值的方法实践。

DevOps(Develop + Operation)是关系研发流程里所有人的事情,之前有同事对DevOps做过很形象的比喻:敏捷是把产品、开发和测试绑在一起背锅,而DevOps则是把产品、开发、测试和运维都绑在一起背锅。它所代表的不仅仅是工具或是脚本,而是工程实践、研发方式、和交付态度,它将影响到我们所有人。而一旦你开始习惯它,它就会变成水和空气那样的必需品,不注意感知不到,但再也离不开。 另外,DevOps不是飘在空中的一个概念(它现在还没有非常官方的明确定义),它是跟具体落地息息相关的。所谓千人千面,每一个团队都会去寻找一个最适合他们当下情况的DevOps实践。影响的因素有很多,团队成熟度,产品特点,用户特点,现有组织架构等都是需要考虑的因素。

下面我用一个例子来说一下怎么开始。

例子:在线短信网关服务

宇宙短信公司为用户提供短信网关服务,他们的网关服务包含多个模块,如发送、计费、认证等等。同时,他们还提供多种语言的SDK,方便用户集成,如Python、Java、Go等等。公司按服务模块分成了多个组,他们共同维护一些公共模块的代码。

我们来分析一下这家公司的团队情况及他们的业务特点:

  1. 他们提供在线的网关服务,用户通过SDK调用他们的服务,所以他们的服务要对SDK保持兼容,但服务可以向前兼容的方式升级
  2. SDK的升级分为两部分,一是该公司将SDK发布到线上供客户下载,而是客户将新的SDK替换掉旧的,因此,对SDK版本的兼容性就需要保证
  3. 像计费这样的模块,不会被用户直接使用,不需要做接口上的兼容,但是需要保证数据的正确性
  4. 存在一些公共模块的代码,会被所有人使用到,因此对这部分模块的版本管理就相对重要一些

假设这家公司已经搭建了私有的Gitlab服务,每个产品有独立的Group,每个模块有独立的Project,公共模块也是一样,有一个公共模块的名字就叫common。第一步我们要做些什么呢?

从梳理研发流程开始

先不要急着搭建Jenkins或是其它CI/CD工具,也先不去管云原生这些东西,顶住各类DevOps工具的诱惑,很多工具号称可以一键给你带来DevOps能力,它可能可以做到,但此时先放一边。 第一步:Code Less,Think More! 我们先看一下当前是怎么工作的,我们是怎么开发的,建立什么样的分支,各个模块之间怎么协调,流程各个阶段怎么流转,构建在哪做的,打包又是怎么搞的,公共模块修改了之后怎么同步到所有模块的,等等。敏捷里面小步迭代,持续反馈的思想用在这里是非常合适的,我的建议是先梳理出一张当前研发的流程图,它可能很粗,比如下面这样:

dev process v1

很多人会觉得这算什么研发流程?别急,这只是我们的第一个版本,让我们来一步步细化它。

第二步,我们在每一个阶段上添加与之相关的角色。这时候有了下面这张图。

dev process v2

从这张图上我们可以看出:

  1. 有产品经理来梳理用户需求
  2. 测试由开发人员自己完成
  3. 发布也是开发人员自己完成
  4. 部署的事情责任不明确,可能谁都会做

第三步,我们分析每个阶段向下一阶段交付的标的物,并把它标注在图上。注意,这里一定要从被流转阶段的视角去分析,比如部署阶段,分析一些用到了哪些标的物才能完成这一阶段的工作。

dev process v3

宇宙短信公司在开发、测试、发布时候的标的物都是war和conf配置文件,但是在部署的时候多了个SQL。我们可以看出这家公司是以Java为主要技术栈的,那这个SQL是怎么管理的呢?这里可能存在问题。另外这些war和conf文件是以什么方式在各个阶段间流转的呢?通过交流我们发现,有些是在代码中的,如conf配置,有些是开发人员自己打包提供的,如war。这中间存在着很多的沟通环节,还有打包的时候,公共模块的代码集成的是什么版本,这些完全取决于某个开发人员当时的状态。显然,这种做法极易出错,且效率不高。

先搞定模块的构建

请大家想一想,我们在各个阶段间沟通流转,尤其是研发之后的阶段,所说的是什么?不是某段代码,也不是某个文档,而是构建的产物,即某一时刻某一个代码上构建出来的标的物。它往往有一个唯一的标识,叫作版本。有了这两个概念,我们在交流的时候就可以说请把宇宙短信1.0版本的接口文档发给我,或者是这个问题我在宇宙微信2.0版本上遇到了。

构建物究竟是什么?它需要包含哪些东西呢?简而言之,它需要包含后面部署和对外交付相关的所有东西,包括:

  • 编译好的模块,如golang代码编译成二进制程序
  • 相关依赖关系
  • 相关的数据变更脚本和文档,如SQL、systemd配置文件
  • 对外的接口文档
  • 安装、升级、回滚等工具或脚本
  • 其它需要带到下一阶段的内容

怎么开始构建?

  1. 梳理每个模块的构建环境,并标准化(docker是一个好帮手)
  2. 将构建步骤、脚本和代码放在同一个地方
  3. 选择一种构建目标格式,如rpm,docker image,pypi等
  4. 构建目标存放在响应的构建物仓库里,如本地yum源
  5. 自动进行构建(Gitlab-CI就是一个很容易开始的方式)

下面是一个Python模块的.gitlab-ci.yml示例:

stages:
    - test
    - package
    - publish

utest:
    stage: test
    except:
        - tags
    image: python:3-stretch
    script:
        - pip install -r requirements.txt -i https://pypi.douban.com/simple
        - pip install pytest -i https://pypi.douban.com/simple
        - python setup.py install
        - pytest ./tst

python-package:
    stage: package
    image: python:3-stretch
    script:
        - echo "__version__ = '${CI_COMMIT_TAG:-master}'" > src/demo/version.py
        - export PYTHONPATH=$PWD/src; python setup.py sdist
        - cp dist/demo-${CI_COMMIT_TAG:-master}.tar.gz demo-${CI_COMMIT_TAG:-master}.tar.gz
    artifacts:
        expire_in: 7d
        paths:
            - demo-${CI_COMMIT_TAG:-master}.tar.gz

pypi:
    stage: publish
    image: scp.local:3-stretch  # local image that contains scp command
    only:
        - tags
    dependencies:
        - python-package
    script:
        - scp demo-${CI_COMMIT_TAG:-master}.tar.gz test@pypi.local:/home/test/pypi-packages

这个文件中配置了三个stage,分别是test、package和publish,其中publish只在打tag的时候执行。三个stage都是在docker环境下执行,这样就保证了构建的标准化,最后把构建的tar.gz包发布到本地pypi源上。

注意:一个模块尽量保证只构建出一个服务。

版本号怎么确定?

一句话:能保证唯一性的任何标准都可以。

现在软件开发的发布节奏越来越快,通过计划定大版本的方式在很多时候已经不是很有必要了。所以,像宇宙短信这样的公司,我觉得精确到天的时间加上当天第几次发布就挺好,比如20190620.1,表示这个是2019年6月20号发布的第一个版本。

当然,如果你对大版本的节奏比较在意,或者你的产品是要输出给用户安装部署的,比如chrome这样的浏览器,可以采用:主要版本.次要版本.修复版本的格式,如Python 3.7.3。

import sys

sys.version_info  # sys.version_info(major=3, minor=7, micro=3, releaselevel='final', serial=0)

构建完成之后先跑一跑

上面我们完成了第一个stage,构建。现在这个模块每天都有很多个版本被构建出来,那么问题来了,这些版本有的好一点,有的还是半成品,我该选择哪个呢?在一开始的阶段,我们当然没有办法也不可能快速地搭建一套测试体系出来,但有一件事是值得也是必须要做的。

那就是在一个标准环境中将构建产物跑起来。

这里有几个事情需要关注:

  1. 定义运行环境,如CentOS 7
  2. 定义安装方式,如yum install
  3. 确定依赖条件,如需要预先初始化某个数据库
  4. 确定跑起来的标准,如HTTP服务可以响应用户请求

定义产品

上面说的都是模块,通常也就是一个Github上的Project。但大部分情况下,我们所要交付的都是一个产品,特别是面对下游用户的时候,说产品的某个版本更容易交流。那么,怎么定义一个产品,又怎么把产品和模块、产品版本和模块版本联系起来呢?

首先,一个产品通常是:

  1. 许多模块的集合
  2. 以一定的方式和顺序整合这些模块
  3. 对环境的需求,包括硬件如网络、内存,和软件如操作系统版本
  4. 相关的安装部署文档
  5. 使用手册

的集合体。所有这些关系和材料都应该被很好的组织和管理起来。而产品的版本,对应的就是上面这些东西,只是模块有了具体的版本号,比如宇宙一站式通知中心产品2.0版本,包括:

  • 短信平台1.0
  • 微信平台1.2
  • 电话平台2.0

这三个模块构建版本和其它那些文档、工具什么的。具体怎么管理产品和模块的关系,有很多种方法,如维护一个git仓库等等,这里要注意两点:

  1. 产品的版本号一旦确定,内容就应该确定下来,不可再被更改,如果有更改,就增加版本号
  2. 应该能通过产品版本找到每个模块版本,并进而找到每个模块对应这个产品的代码版本,这就是通过版本号进行回溯

Next…

有了上面这些,一个基本的构建加产品发布的框架已经初现雏形了,下一步建议重点关注两点:

  1. 让产品的部署可以自动化地进行
  2. 对产品的可用性有基本的自动化测试

我相信,到这个时候,接下来该做什么,宇宙公司应该有自己的计划和目标了,当然,我们也会在下一篇中继续探讨这个问题。