在上一节中,我们已经在空仓库 vue-project 中完成了 CI/CD 流程,本章节,我们已前端 Vue 项目为例,分享一下我司前端项目实战应用,并讲解其中 Runner 执行流程与原理,从而解放每日因多次部署发版的累的发麻的小手,本章目标就一个做到可独立配置自己前端项目完成 CI/CD 。

4.1 项目准备

本章节我们依然以 vue-project 为例,使用 vue 脚手架命令初始化一个 vue 项目,如果你已经有现成的前端项目也可使用现成的代码仓库,以下是我的仓库代码运行效果图:

images

项目已经在我本地准备好了,接下来我们准备编辑 yml,实现如下目标:

  1. 本地分支代码推送远端后,管理员 code review 且合并(merged)代码到 dev 分支后,自动触发 CI/CD 将开发分支 dev 代码自动部署到测试服;
  2. 发布 Release 版本,dev 分支的代码经过测试后,合并到 master 分支,基于 master 分支上打上新版本 tag ,手动点击(或自动)发布,可自动部署到生产环境;

为了更容易实现这个伟大的共产主义目标,我们先将这个大目标拆解成个上面所说的两个小目标①、②,废话不多说,拔刀吧 (开干吧)_

4.2 CI/CD 执行步骤

通过上一节的了解,我们已经认识到持续集成与部署(CI/CD)的大概原理,但还是不够清晰,这里我们还是不讲原理,以粗鲁的语言描述一下大致细节,目的还是以为应用实战为主,旨在可以理解本章相关的 YML 配置,如有对原理感兴趣我放在第七章再详解。

通过上一节的认识,我们了解到 CI 执行的大致步骤如:

  1. 为仓库(Repo)注册并开启 Runner(用于执行具体的 Job);
  2. 提交到仓库的代码,根据仓库里 YML 中的具体 Job 交给 Runner 执行,如果 Job 中没有设定 only 关键词,意思是所有分支上的代码都会执行此 Job,下文我们会根据 master 和 dev 不同的分支执行不同的 Job,以达到部署到不同环境的目的;
  3. Runner 根据 YML 中指定的 image 启动容器,然后在容器内按顺序执行 Job 中的 script,如果 YML 中没有配置 image 关键词,会以注册 Runner 时填写的镜像来启动容器;

<此处配图>

上一章的 script 很简单,即简单的 echo 输出,本场景会以我司的前端为例,详情展开每一个 Job,来吧,让我们一起战斗吧,比卡丘!!!

4.2 部署到测试环境 为了避免 YML 中关键词有理解歧义,我已重新创建了 frontend Runner 专门用于跑前端项目,我们回想一下人工在部署前端项目时的步骤是什么?:

  1. npm install (安装依赖)
  2. npm build (打包)
  3. rsync xxx (将打包的静态文件上传到服务器)

以上为手动发版步骤,1、2步为 CI 的过程,3步为 CD,我们编写 YML 完成此过程,我们先写个简单.gitlab-ci.yml文件,以下为例:

 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
29
30
31
32
33
34
35
36
37
38
stages:
  - build
  - deploy
  
build-dev:
  image: node:14.15.4
  stage: build
  tags:
    - frontend
  only:
    refs:
      - dev
  script:
    - npm install
    - npm run build
  artifacts:
    name: "$CI_COMMIT_REF_SLUG"
    when: on_success
    expire_in: 1 week
    paths:
      - dist/

deploy-dev:
  image: node:14.15.4
  stage: deploy
  tags:
    - frontend
  when: on_success
  script:
    - echo "rsync code"
  only:
    refs:
      - dev
  dependencies:
    - build-dev
  environment:
    name: dev
    url: http://dev.xxxx.com

配置完成后,将代码推送到 dev 分支,可以看到如下图所示,执行了两个任务 Job,并且打包出了可下载文件。

images

  1. build 任务:执行完成 npm run build完成后打包出了 dist 可下载文件,

images

  1. deploy 任务完成后输出了 rsync code,

images

将打包好的代码,部署到测试服务器,那还剩最后一步,我们使用 rsync 命令将打包出来的静态文件,发布到测试服务器,写法如下(其他忽略):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
deploy-dev:
  image: betterde/rsync:latest
  stage: deploy
  tags:
    - frontend
  when: on_success
  script:
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - rsync -rav -e "ssh -p 22 -o StrictHostKeyChecking=no" --delete dist/ "$PREVIEW_SERVER_USER"@"$PREVIEW_SERVER":"$PREVIEW_PROJECT_PATH"
  only:
    refs:
      - dev
  dependencies:
    - build-dev
  environment:
    name: dev
    url: http://dev.xxxx.com

注🐖意:image: betterde/rsync:latest 使用此镜像,里面已经安装好了 rsync。

script 解释:

  • 8行:mkdir -p ~/.ssh 在容器(betterde/rsync)内创建 .ssh 目录;
  • 9行:echo “$SSH_PRIVATE_KEY” » ~/.ssh/id_rsa 将环境变量 SSH_PRIVATE_KEY内容输出到 id_rsa 文件;
  • 10行:chmod 600 ~/.ssh/id_rsa 设置文件权限
  • 11行:关键的一行,将 dist目录下递归上传到文件,PREVIEW_SERVER 服务器 PREVIEW_PROJECT_PATH 的目录下;
    • -o StrictHostKeyChecking=no 通过 SSH 连接 Linux 服务器,一般首次会询问 Are you sure you want to continue connecting (yes/no)? 提示确认,这个参数可以免交互。
    • –delete 删除目标目录中比源目录多出的文件

写法中,我们设置多个环境变量,以下是我的环境变量配置截图(自己的环境自行调整)如图所示:

images

🌶 注意:添加的环境变量,不要选择 Protected 否则可能会无法读到该变量。

配置完成环境变量,修改 App.vue 文件的欢迎语句代码为 Welcome to Your CI/CD. ,将修改后的代码合并到 dev 并推送到远端dev 分支,这时我们会看到会自动触发 CI/CD 流程,如下图所示:

images

待deploy-dev 任务执行完成后,登录服务器,查看 /usr/wwwroot/vue/frontend 目录下文件,可以看到打包好的代码已经在服务器了,如下:

1
2
3
4
➜  ~ frontend pwd
/usr/wwwwroot/vue/fronted
➜  ~ frontend ls
cssfavicon.ico img index.html js

👄 最后修改你服务器的 Nginx,将 root目录指向/usr/wwwroot/vue/frontend目录,并配置好域名解析,就可以访问了,看到如下界面,那么恭喜你完成了测试环境的CI/CD。

images

4.3 我司开发工作流

完成了测试环境的 CI/CD 配置,撸起袖子,我们再一把劲配置到生产环境,我们先来梳理一下手动发布生产所需要的步骤:

  1. npm install (安装依赖)
  2. npm build (打包)
  3. rsync xxx (将打包的静态文件上传到服务器)

从步骤中可以得知,无论是测试环境还是生产环境,都需要 1.npm install 和 2.npm build 两个过程,所以这里测试和生产就可以共用一个 build Job 任务。

部署到测试环境,触发动作我们期望的是当有代码合并到测试分支 dev 上就自动触发 CI/CD 完成部署,回顾一下生产环境期望的啥来?

再正式回答这个问题前,我们得先弄明白一个流程,先来分享一个不推荐做法,我想可能还有不少同学是这么干的,每次发版为了图方便在生产环境上是通过 git 直接拉取 master 分支代码,试想如果此时 master 分支代码有Bug,如何快速回滚到上一个版本呢?

这不就扯着🥚了,因为压根就没有版本的概念,咋回滚,找到上一次 merged 到 master 分支的 commit ID?太费劲了大胸弟~

为了解决以上问题,每次发版必须要有版本,版本如何定义我就不多啰嗦了,每家公司依据自己的家规来即可,说一下我们日常开发与发版操作流程:

  1. 在开发前,需要在本地创建自己的开发分支,基于本地 dev 或者 master 创建自己的开发分支,如果两者分支代码一致那基于哪个分支创建都无所谓的,但是如果 dev 分支已有别人的代码,且这次你的修改又要早于他们发版,这时你就要基于 master分支创建自己的开发分支,这种情况常见的场景便是热修复,比如这里我创建分支为 hotfix/test-bug,待本地开发完,且自测结束;
  2. 提交自己的开发分支并推送到远端,命令:git push origin hotfix/test-bug,然后提交合并请求,从hotfix/test-bug 到 dev分支的 new merge request 的请求,如图所示;

images

  1. 有仓库管理员权限的开发者,便可以 review 或者 merege 代码,如图所示:

images

  1. 批准后的代码合并到 dev,通过 CI/CD 自动部署到测试环境,测试完成后,接下来进入到正式发版流程;
  2. 这里有两种情况,一种是测试过的整个 dev分支代码发版,第二种情况是仅发版 hotfix/test-bug 这一个热修复分支的代码, a. 情况一创建新的合并请求,将 dev 分支代码合并到 master; b. 情况二创建新的合并请求,再将 hotfix/test-bug分支代码合并到 master;
  3. 无论哪种情况两者分支的代码再次 review 后都进入到了 master 分支,然后基于 master 分支创建一个v1.0.0 版本 tag 下图所示:

images

images

直到这里终于可以回到上文中那个问题了,我们期望生产环境的CI/CD何时触发,以及帮我们做什么?

答:待我创建好版本 tag 以后自动触发 CI 完成 vue 代码的单元测试和打包,打包好后自动触发 CD 流程完成代码的部署流程。

4.4 部署到生产环境

弄清楚了我们要做什么,接下来配置也就是一哆嗦的事,首先生产环境的触发行为是在创建 tag 以后自动执行 CI/CD 任务,以下是我的配置:

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
stages:
  - build
  - deploy
  
build:
  image: node:14.15.4
  stage: build
  tags:
    - frontend
  only:
    refs:
      - dev
      - tags
  script:
    - npm config set registry https://registry.npm.taobao.org
    - npm install
    - npm run build
  artifacts:
    name: "$CI_COMMIT_REF_SLUG"
    when: on_success
    expire_in: 1 week
    paths:
      - dist/

deploy:dev:
  image: betterde/rsync:latest
  stage: deploy
  tags:
    - frontend
  when: on_success
  script:
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - rsync -rav -e "ssh -p 22 -o StrictHostKeyChecking=no" --delete dist/ "$PREVIEW_SERVER_USER"@"$PREVIEW_SERVER":"$PREVIEW_PROJECT_PATH"
  only:
    refs:
      - dev
  dependencies:
    - build
  environment:
    name: dev
    url: http://dev.vue.sjmsdev.com:9100

deploy:prod:
  image: betterde/rsync:latest
  stage: deploy
  tags:
    - frontend
  when: on_success
  script:
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - rsync -rav -e "ssh -p 22 -o StrictHostKeyChecking=no" --delete dist/ "$PRODUCTION_SERVER_USER"@"$PRODUCTION_SERVER":"$PRODUCTION_PROJECT_PATH"
  only:
    refs:
      - tags
  dependencies:
    - build
  environment:
    name: production
    url: http://prod.vue.sjmsdev.com

🐖 注意几个修改点:

  1. build 任务的关键字 only refs 值是 dev 和 tags,是指当有代码合并到 dev 分支或者创建 tag 触发此 Job;
  2. deploy:prod 任务关键字 only refs 值是 tags,是指仅有创建 tag 后才会自动触发此 Job;
  3. 注意不要忘记配置生产环境的环境变量;

以下是我发版新版 v1.0.3,执行的效果图:

4.5 本章小结

本章主要以 vue 项目分享了前端项目从测试环境到生产环境的工作流,以及如何 CI/CD,每家公司的家规可能不尽相同,没必要完全照搬模仿,这里只是分享了我司的情况,未必是最完美的做法,比如以下细节可当做思考题:

  1. CI/CD 中如何添加上语法检测;
  2. deploy 中使用的 image 是否可以自己制作一个;
  3. 如果打包测试环境与生产环境命令不一样,如npm run build:dev和npm run build:prod CI/CD 中又改怎么写呢?