在软件开发领域,有一整套工具链可以帮助我们快速构建起一套自动化的工作环境,在这种环境中工作久了,我们自己也会形成一种自动化的工作习惯,以及思维模式。自动化指的就是将重复的工作,通过一定的方法让计算机代劳的过程。具体的方法有,在互联网、应用商店里找一个已有的程序或者网站,写一段脚本并调用,写一个程序并执行它等等。

下面,我们来看看在整个应用程序的开发周期中,一个擅长自动化的工程师是如何把尽量多的工作进行自动化的。

环境准备

在一个产品的开发之初,团队首要做的是组建一个共同的工作环境。实际上,如果有多个项目的经验就会知道,其实这种工作环境在很多团队是十分类似的。所以每次手动去构建这样的环境也十分重复,应该让机器帮助我们高效地完成这项工作。回想一下,一个团队需要什么样的工作环境呢?源代码管理软件,工作进度可视化工具,文件共享平台,聊天软件,持续集成软件这些各一份,以及若干台开发人员工作站。

有不少自动化环境配置工具可以帮助将这个环境进行脚本化,下面的代码在指定的服务器上部署一个网站:

 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
Configuration WebsiteTest {

    # Import the module that contains the resources we're using.
    Import-DscResource -ModuleName PsDesiredStateConfiguration

    # The Node statement specifies which targets this configuration will be applied to.
    Node 'localhost' {

        # The first resource block ensures that the Web-Server (IIS) feature is enabled.
        WindowsFeature WebServer {
            Ensure = "Present"
            Name   = "Web-Server"
        }

        # The second resource block ensures that the website content copied to the website root folder.
        File WebsiteContent {
            Ensure = 'Present'
            SourcePath = 'c:\test\index.htm'
            DestinationPath = 'c:\inetpub\wwwroot'
        }
    }
}

. .\WebsiteTest.ps1
WebsiteTest
Start-DscConfiguration .\WebsiteTest

上面的代码是 Windows 内置的 PowerShell DSC 脚本,除此之外还有社区的 AnsibleChefVagrant 等软件都能够达到类似的效果,容器技术天然提供了用代码将环境描述出来的能力。重点是,它们能够以代码的形式来描述环境的状态,既能很好地重复运行,又能有效地跟踪变更记录。

项目脚手架

当我们新建一个项目时,通常要新建源代码(src)、单元测试(test),发布物生成(dist)等多个目录,还有一些子目录(比如 Web 项目中的 wwwroot)才能完成项目的初始化。如果你用 Visual Studio(VS) 的话,它已经帮我们建好了。但当 IDE 中的项目模板不够好用的时候(VS 的模板可以定制),或者团队中有的人不用 VS 的时候(那就没办法了),我们就需要一种统一的方式来创建项目结构了。

这个过程叫 scafollding,即搭建项目脚手架的过程。始于 Web 项目的脚手架命令行工具 Yeoman 就是在这种背景下产生的项目。除了 Web 项目,它现在还支持了大量语言和平台的项目创建,包括 .NET Core 项目模板。用下面的命令即可轻松地构建一个典型的 ASP.NET Core 项目:

1
yo aspnet

image

通过 Yeoman 还可以很方便地定制自己的模板,在别人的模板基础上,添加团队特有的一些目录和文件(比如放置团队统一的配置文件、在生成的代码中自动插入版权标识等),因而是一个不错的自动化构建团队项目结构的工具。

开发环境

在日常开发期间,我们手边经常会放一些顺手的软件。Windows 提供了一个“快速访问栏”,以及开始菜单中的固定项可以将常用的软件固定在显眼的位置。如果是命令行工具,则要考虑将它们所在的路径添加到 PATH 中,以便随时取用。比如 msbuild,nuget 这类常用软件等。作为一个团队,团队成员之间还需要共享这些工具。那么可以将这些命令行工具签入到一个专用的代码库中,在团队成员电脑上统一导入这些文件的路径到 PATH 即可。这样顺便还能实现一些脚本工具的版本化管理,非常好用。具体的设置方法,请参考我的 定制自己的命令行环境 一文。

比如,我们曾在整个团队的电脑上创建“gs”用于显示 git status,用 “gc” 用于快速做一次提交(git add . + git commit ),以及用 pr 创建一次 Pull Request。而且由于所有人的电脑环境都是统一构建出来的,大家都将 gs、gc 脚本导入到了 PATH,所以即使交换结对到了别人电脑上,环境依然是熟悉的配方,有一种团队感。

如果团队都在开发同一个产品,最好使用一样的环境。包括操作系统版本,IDE 及设置,以及 CLR/JVM 运行时的版本等。这时,可以使用 Chef 或 Vagrant 等工具来维护一个用于构建开发环境的脚本。定期(比如每二周)生成操作系统镜像,供开发团队用。如果性能允许,可以直接使用这些镜像构建虚拟机用作开发环境。

代码开发

在开发期间,经常不放心一段代码的逻辑是否正确,所以需要启动调试,跟踪一下代码的运行状况是否如预期一样工作:把代码部署到 IIS 或者 Weblogic 里,去网页上登录、创建数据,按照流程重现,然后查看网页结果和日志输出。如果没有按照预期的方式工作,则回到 IDE,改代码,然后编译之后再重复刚才的过程,直到代码运行正确为止。这样的过程其实是十分低效的!原因是刚才的过程是需要多次重复的,而重复是不产生价值的,也就是说从第二次调试开始,我们就在浪费时间。

更高效的做法是,将这段需要测试的代码视作一个单元,并使用单元测试工具对其进行测试。用写好的测试代码来对它进行测试,就可以快速而轻松地验证代码逻辑的正确性。下面这段简单的测试代码,可以帮助我们验证 Execute 方法的正确性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Fact]
public void ShouldExecuteOnValidRobot()
{
    var robot = new Robot();
    robot.PlaceAt(new Coordinate(1, 2), Direction.North);

    var result = new ReportCommand().Execute(robot);
    
    Assert.Equal("1,2,NORTH", result);
}

如果 Report 是一个 MVC 的 Controller 的话,要调试它就得把网站启动起来,并查看网页上的输出,因此在能够人工测试之前,要提前写好网页接口并部署到 IIS 中。而在单元测试里,能直接使用类上的方法来调用,立即断言其结果,显得非常轻盈。轻到了什么程度呢,Visual Studio 甚至提供了一个功能叫即时单元测试(Live Unit Testing),在编辑代码期间,几乎同时 IDE 就会告诉我们哪些测试能够通过,哪些不能通过。类似的功能,在 VS 之外,npm-watch 等工具使用文件改动监控来也能实现类似的能力:

image

任务运行器

在开发期间,每次修改了 TypeScript 文件,或者 es6 文件,如果想去浏览器里看一下效果,都需要把它们编译成 .js 文件。对于 CSS,如果习惯了用 SCAA、LESS 等预编译技术,就也有类似的需求。在前端领域,有不少工具可以将这类重复性工作声明成脚本,然后每次要用,运行一次就可以了。比如,早一点的 Grunt、Gulp 以及现在不少人用的 Webpack 等,这类工具称为任务运行器,或者构建工具。在 C# 技术栈,有 Cake、Psake 可供选择,而 Java 开发则有 Maven 和 Gradle 两板斧。

下面的代码声明了 compile-css 任务,运行它就可以将 scss 目录中的文件编译为 css,并将生成的 css 的文件体积缩小:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var gulp = require('gulp');
var sass = require('gulp-sass');
var cssmin = require('gulp-cssmin');

require('gulp-awaitable-tasks')(gulp);

 gulp.task('compile-css', function*() {
    yield gulp.src('./scss/**/*.scss',  { base: "./" })
        .pipe(sass())
        .pipe(gulp.dest('./styles'));

    yield gulp.src('./styles/**/*.css',  { base: "./" })
        .pipe(cssmin())
        .pipe(gulp.dest('.'));
 });

包管理:依赖和制品仓库

包管理现在已经深入人心了,它解决的主要是代码重用的问题。我们经常需要在工程之间重用代码,在过去我们要么将对方的源代码包含在自己的解决方案里,要么将对方的 dll 下载到本地项目,并手动引进项目里。这些方法不是造成项目之间过多耦合,就是会导致引用的项目版本混乱。NuGet 是 .NET 平台的包管理工具,只要在 packages.config 文件中声明需要哪些包,需要时还原包即可将他人发布的包(其中包含别人的代码生成物,即 dll)下载到本地就可以继续开发。如果对方(发布方)的代码发生了更新,他就发布一个新版本。使用方检测到新版本之后,更新本地的 packages.config、指定要使用新版本,再次还原包,即可很轻松地更新依赖。在 .NET Core 中,包管理被内置到了 dotnet 命令行工具中,提供了更原生的体验。

通过包管理,我们不需要把项目所依赖的第三方组件签入到代码库中(因为它们不会变化),在需要时使用包管理工具还原包即可让这些依赖再次就绪。作为一种重用代码的方法,包管理最早是在 Linux 平台上用于安装和依赖的一种方式,但现在已经在绝大多数编程语言和平台上作为一种默认开发方式;而且大量的社区也使用类似的机制来共享中间件模块,比如 PowerShell GalleryAnsible Galaxy 以及 Kubernetes Helm 等。

数据库迁移:表结构变更

一个迭代过去,新版本的应用即将发布了。我们发现新版本的程序中对数据库引入了一些新数据表,还将一个 int 列改成了 varchar。在开发期间大家都是口头说一下,对方在本地开发数据库上直接就把数据库结构做了对应的修改。但发布到生产环境时,如果还手动来改,就可能造成比较长的停机时间;如果改错了,再想还原回来就更麻烦了!回想一下,是不是几乎每次发布都多少有一些数据库变更需要做?

那么,让机器来吧!在 Ruby on Rails 上首先得到运用的数据库迁移脚本的功能,其实很早在 Entity Framework(EF)中也提供了,如果不用 EF,像 FluentMigratorFlyway 等工具也很好用。

下面的迁移代码描述了一次数据库变更记录,无论是在开发环境,还是生产环境,通过顺序地执行这些迁移代码,都可以快速而准确地执行数据库变更了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 [Migration(20180430121800)]
 public class AddLogTable : Migration
 {
    public override void Up()
    {
       Create.Table("Log")
          .WithColumn("Id").AsInt64().PrimaryKey().Identity()
          .WithColumn("Text").AsString();
    }

    public override void Down()
    {
        Delete.Table("Log");
    }
 }

版本发布

发布新版本时,如何将新版本的程序更新到服务器上?在之前的 DevOps 渊源:角色消融 一文中我描述了一个混乱的版本变更过程:工程师将生成物(比如 .dll)手动复制到服务器上,接着快速回归一下,如果发现问题可能直接在服务器上修改配置。

这中间有很多风险和错误。比如,如果有多台服务器,复制过程将十分缓慢;在某台服务器上做的修改,其他服务器可能没有同步修改;即使只有一台服务器,在服务器上修改的内容如果多了,自己都可能忘记了细节,很容易忘记同步到本地代码库中——下次还是会出现类似的问题!如果部署中遇到严重的问题,想回滚版本的话,那就是另一个故事了……对于频繁发布的互联网产品来说,这种“小米加步枪”式的工作方式还是难以满足需求的。

我们需要自动化执行变更的能力:它能在多个服务器上按要求执行部署过程,在失败时帮助回滚版本,能对大批量服务器做到分批滚动部署…… Octopus DeployElectricFlow 就是这样的工具,它们能以可视化的方式来定制变更过程,并对多环境的部署流程进行跟踪:

image

这类工具往往起到的是对部署任务进行调度、运行和监控的作用,而部署过程中具体需要执行的步骤(比如,数据库变更、启动应用)的脚本还是需要我们自己去准备。一般来说,具体的脚本仍会是上面介绍过的 PowerShell DSC、Chef、Ansible 和本机脚本(bash 或者 PowerShell 脚本)。既然具体代码也是脚本代码,那么上层调度怎么能不去用代码描述和驱动呢?GoCD 允许用代码的方式来描述一个部署流水线,并且可视化地跟踪部署过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pipeline2:
  group: group1
  label_template: "foo-1.0-${COUNT}"
  lock_behavior: none
  tracking_tool:
    link: "http://your-trackingtool/yourproject/${ID}"
    regex: "evo-(\\d+)"
  timer:
    spec: "0 15 10 * * ? *"
  environment_variables:
    DEPLOYMENT: testing
  secure_variables:
    ENV_PASSWORD: "s&Du#@$xsSa"
  materials:
    ...
  stages:
    ...

image

运维监控

上线之后?版本发布之后,我们要观察用户行为,要了解应用程序的运行是否良好,要了解服务器使用率如何……相信很多人早年间就有用站长网(cnzz)来为网站添加流量监控的经验。除了流量,用户的切换标签、点击视频播放等其他行为该怎么监控呢?很多年前,各大统计网站(包括 cnzz、百度和谷歌)都增加了自定义行为的跟踪功能。

不过,这需要我们在开发期间,在应用程序代码的恰当位置放置调用脚本来监控。下面的代码用谷歌分析跟踪一个“自动播放”事件:

1
2
3
4
5
gtag('event', 'video_auto_play_start', {
  'event_label': 'My promotional video',
  'event_category': 'video_auto_play',
  'non_interaction': true
});

监控设置好了之后,数据又怎么看呢?每次都转到统计网站上吗?——那是业务和运营部门的同事该做的事。作为自动化运维的践行者,我们要做的是用一些方法将统计网站上的数据同步回来供分析用。比如,使用对方的 API 读取数据,或者在跟踪事件时同时发送一份到自己的统计服务里,比如使用 Matomo(即 Piwik),或者直接使用 Application Insights 这些托管的服务。当数据就绪之后,就可以动用 SplunkELK 之类的分析工具去分析这些统计工具收集到的数据。生产环境服务器资源使用率(磁盘、CPU 等),以及日志监控和分析都可以使用它们。

下图是一个 Kibana 视图,使用查询语句分析了指定的对应 URL 访问的情况,并以可视化的方式展示出结果:

image


上面简单列举了在整个应用程序开发的生命周期当中,各种常见任务是如何用自动化的方式去解决的。可以看出,平常我们需要不断重复去做的很多工作都有自动化的方法可以提供帮助。

与此同时,可能有读者会有疑虑,难道每个团队中都要引入这么多的工具和方法?那不是没有时间来开发业务功能了?实际的情况并不是这样。并不是要把它们全部搭建完毕才能开始业务的开发。一般来说,都是在一定的重复的、手工的工作已经进行了一段时间之后,团队里的同事发现了其中的浪费才在团队里分析并总结出要采用的自动化策略,然后再根据需求引入了对应的工具的。此外,上面一些段落列举了多个工具,并不是说这些工具都要采用,而是希望大家可以多去了解,然后根据团队的实际情况去采纳最为适合的工具。

简单来说,自动化也是一个演进的过程。没有必要为了追求自动化而去激进地对所有过程都自动化。