GitLab CI/CD 在 Node.js 项目中的实践

近期在根据营业分别项目时,我们组被分了许多的项目过来,大批的是基于
Node.js 的,也是我们组延续在运用的言语。

现有流程中的一些题目

在保护多个项目标时刻,会暴露出一些题目:

  1. 怎样有用的运用 测试用例
  2. 怎样有用的运用 ESLint
  3. 布置上线还能再快一些吗

    1. 运用了 TypeScript 今后带来的分外本钱

测试用例

首先是测试用例,最初我们设想在了 git hooks 里边,在实行 git commit 之前会举行搜检,在当地运转测试用例。
这会带来一个时候上的题目,假如是一样平常开辟,这么操纵照样没什么题目标,但假如是线上 bug 修复,实行测试用例的时候根据项目大小可以会延续几分钟。
而为了修复 bug,可以会采纳 commit 的时刻增添 -n 选项来跳过 hooks ,在修复 bug 时这么做无可厚非,然则纵然人人在一样平常开辟中都采纳commit -n 的体式格局来跳过烦琐的测试历程,这个也是没有办法管控的,毕竟是在当地做的这个校验,是不是遵照这个划定规矩,端赖人人自发。

所以一段时候后发明,经由过程这类体式格局实行测试用例来躲避一些风险的作用可以并非很有用。

ESLint

然后就是 ESLint,我们团队基于airbnbESLint 划定规矩自定义了一套更相符团队习气的划定规矩,我们会在编辑器中引入插件用来协助高亮一些毛病,以及举行一些自动格式化的操纵。
同时我们也在 git hooks 中增添了对应的处置惩罚,也是在 git commit 的时刻举行搜检,假如不相符范例则不许可提交。
不过这个与测试用例是雷同的题目:

  1. 编辑器是不是装置 ESLint 插件无从得知,纵然装置插件、是不是人肉疏忽毛病提醒也无从得知。
  2. git hooks 可以被绕过

布置上线的体式格局

之前团队的布置上线是运用shipit周边套件举行布置的。
布置环境强依托当地,由于须要在当地竖立堆栈的临时目次,并经过屡次ssh XXX "command"的体式格局完成 布置 + 上线 的操纵。
shipit供应了一个有用的回滚计划,就是在布置后的途径增添多个汗青布置版本的纪录,回滚时将当前运转的项目目次指向之前的某个版本即可。_不过有一点儿坑的是,很难去挑选我要回滚到谁人节点,以及保存汗青纪录须要占用分外的磁盘空间_
不过正由于如此,shipit在布置多台效劳器时会碰到一些使人不太惬意的处所。

假如是多台新增的效劳器,那末可以经由过程在shipit设置文件中传入多个目标效劳器地点来举行批量布置。
然则假定某天须要上线一些小流量(比方四台机械中的一台),由于前边提到的shipit回滚战略,这会致使单台机械与其他三台机械的汗青版本时候戳不一致(由于这几台机械不是同一时候上线的)
提到了这个时候戳就别的提一嘴,这个时候戳的天生是基于实行上线操纵的那台机械的当地时候,之前有碰到过同事在当地测试代码,将时候调解为了几天前的时候,后时候没有改回准确的时候时举行了一次布置操纵,代码涌现题目后却发明回滚失利了,缘由是该同事布置的版本时候戳太小,shipit 找不到之前的版本(shipit 可以设置保存汗青版本的数目,当时最早的一次时候戳也是大于本次出题目标时候戳的)

也就是说,哪怕有一次举行太小流量上线,那末今后就用不了批量上线的功用了 (没有去细致研讨shipit官方文档,不晓得会不会有相似--force之类的疏忽汗青版本的操纵)

基于上述的状况,我们的布置上线耗时变为了: (__机械数目__)X(__基于当地网速的堆栈克隆、屡次 ssh 操纵的耗时总和__)。 P.S. 为了保证堆栈的有用性,每次实行 shipit 布置,它都邑删除之前的副本,从新克隆

尤其是效劳端项目,偶然紧要的 bug 修复多是在非事情时候,这意味着可以当时你所处的收集环境并非很稳固。
我曾晚上接到过同事的微信,让我帮他上线项目,他家的 Wi-Fi 是某博士的,下载项目依托的时刻出了些题目。
另有过运用挪动装备开热门的体式格局举行上线操纵,有一次非前后星散的项目上线后,直接就收到了联通的短信:「您本月流量已超越XXX」(当时还在用合约套餐,一月就800M流量)。

TypeScript

在客岁下半年最先,我们团队就一直在推进 TypeScript 的运用,由于在大型项目中,具有明白范例的 TypeScript 显著保护性会更高一些。
然则人人都晓得的, TypeScript 终究须要编译转换为 JavaScript(也有 tsc 那种的不天生 JS 文件,直接运转,不过这个更多的是在当地开辟时运用,线上代码的运转我们照样愿望变量越少越好)。

所以之前的上线流程还须要分外的增添一步,编译 TS
而且由于shipit是在当地克隆的堆栈并完成布置的,所以这就意味着我们必需要把天生后的 JS 文件也放入到堆栈中,最直观的,从堆栈的概览上看着就很丑(50% TS、50% JS),同时这进一步增添了上线的本钱。

总结来说,现有的布置上线流程过于依托当地环境,由于每个人的环境差别,这相当于给布置流程增添了许多不可控因素。

怎样处理这些题目

上边我们所碰到的一些题目,实在可以分为两块:

  1. 有用的束缚代码质量
  2. 疾速的布置上线

所以我们就最先寻觅处理计划,由于我们的源码是运用自建的 GitLab 堆栈来举行治理的,首先就找到了 GitLab CI/CD
在研讨了一番文档今后发明,它可以很好的处理我们如今碰到的这些题目。

要运用 GitLab CI/CD 是异常简朴的,只须要分外的运用一台效劳器装置 gitlab-runner,并将要运用 CI/CD 的项目注册到该效劳上就可以了。
GitLab 官方文档中有异常细致的装置注册流程:

install | runner
register | runner
group register | repo 注册 Group 项目时的一些操纵

上边的注册挑选的是注册 group ,也就是悉数 GitLab 某个分组下一切的项目。

主要目标是由于我们这边项目数目太多,单个注册太甚烦琐(还要登录到 runner 效劳器去实行敕令才可以注册)

装置时须要注重的处所

官网的流程已很细致了,不过照样有一些处所可以做一些小提醒,防止踩坑

sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner

这是 Linux 版本的装置敕令,装置须要 root (治理员) 权限,后边跟的两个参数:

  • --userCI/CD 实行 job (后续一切的流程都是基于 job 的)时所运用的用户名
  • --working-directoryCI/CD 实行时的根目次途径 个人的踩坑履历是将目次设置为一个空间大的磁盘上,由于 CI/CD 会天生大批的文件,尤其是假如运用 CI/CD 举行编译 TS 文件而且将其天生后的 JS 文件缓存;如许的操纵会致使 innode 不足发生一些题目

--user 的意义就是
CI/CD 实行运用该用户举行实行,所以假如要编写剧本之类的,发起在该用户登录的状况下编写,防止涌现无权限实行
sudo su gitlab-runner

注册时须要注重的处所

在根据官网的流程实行时,我们的 tag 是留空的,临时没有找到什么用处。。
以及 executor 这个比较主要了,由于我们是从手动布置上线照样往这边挨近的,所以稳妥的体式格局是一步步来,也就是说我们挑选的是 shell ,最通例的一种实行体式格局,对项目标影响也是比较小的(官网示例给的是 docker

.gitlab-ci.yml 设置文件

上边的环境已悉数装好了,接下来就是须要让 CI/CD 真正的跑起来
runner 以哪一种体式格局运转,就靠这个设置文件来形貌了,根据商定须要将文件安排到 repo 堆栈的根途径下。
当该文件存在于堆栈中,实行 git push 敕令后就会自动根据设置文件中所形貌的行动举行实行了。

上边的两个链接里边信息异常完整,包含种种可以设置的选项。

平常来说,设置文件的构造是如许的:

stages:
  - stage1
  - stage2
  - stage3

job 1:
  stage: stage1
  script: echo job1

job 2:
  stage: stage2
  script: echo job2

job 3:
  stage: stage2
  script:
    - echo job3-1
    - echo job3-2

job 4:
  stage: stage3
  script: echo job4

stages 用来声明有用的可被实行的 stage,根据声明的递次实行。
下边的那些 job XXX 名字不主要,这个名字是在 GitLab CI/CD Pipeline 界面上展现时运用的,主要的是谁人 stage 属性,他用来指定当前的这一块 job 隶属于哪一个 stage
script 则是详细实行的剧本内容,假如要实行多行敕令,就像job 3那种写法就好了。

假如我们将上述的 stagejob 之类的换成我们项目中的一些操纵install_dependenciestesteslint之类的,然后将script字段中的值换成相似npx eslint之类的,当你把这个文件推送到远端效劳器后,你的项目就已最先自动运转这些剧本了。
而且可以在Pipelines界面看到每一步实行的状况。

P.S. 默许状况下,上一个 stage 没有实行完时不会实行下一个 stage 的,不过也可以经由过程分外的设置来修正:

allow failure

when

设置仅在特定的状况下触发 CI/CD

上边的设置文件存在一个题目,由于在设置文件中并没有指定哪些分支的提交会触发 CI/CD 流程,所以默许的一切分支上的提交都邑触发,这必定不是我们想要的效果。
CI/CD 的实行会占用体系的资本,假如由于一些开辟分支的实行影响到了骨干分支的实行,这是一件得不偿失的事变。

所以我们须要限制哪些分支才会触发这些流程,也就是要用到了设置中的 only 属性。

运用only可以用来设置哪些状况才会触发 CI/CD,平常我们这边经常使用的就是用来指定分支,这个是要写在详细的 job 上的,也就是大抵是如许的操纵:

详细的设置文档

job 1:
  stage: stage1
  script: echo job1
  only:
    - master
    - dev

单个的设置是可以如许写的,不过假如 job 的数目变多,这么写就意味着我们须要在设置文件中大批的反复这几行代码,也不是一个很悦目的事变。
所以这里可以会用到一个yaml的语法:

这是一步可选的操纵,只是想在设置文件中削减一些反复代码的涌现

.access_branch_template: &access_branch
  only:
    - master
    - dev

job 1:
  <<: *access_branch
  stage: stage1
  script: echo job1

job 2:
  <<: *access_branch
  stage: stage2
  script: echo job2

一个相似模版继承的操纵,官方文档中也没有提到,这个只是一个削减冗余代码的体式格局,无足轻重。

缓存必要的文件

由于默许状况下,CI/CD在实行每一步(job)时都邑清算一下当前的事情目次,保证事情目次是清洁的、不包含一些之前使命留下的数据、文件。
不过这在我们的 Node.js 项目中就会带来一个题目。
由于我们的 ESLint、单元测试 都是基于 node_modules 下边的种种依托来实行的。
而现在的状况就相当于我们每一步都须要实行npm install,这显著是一个不必要的糟蹋。

所以就提到了另一个设置文件中的选项:cache

用来指定某些文件、文件夹是须要被缓存的,而不能消灭:

cache:
  key: ${CI_BUILD_REF_NAME}
  paths:
    - node_modules/

大抵是如许的一个操纵,CI_BUILD_REF_NAME是一个 CI/CD 供应的环境变量,该变量的内容为实行 CI/CD 时所运用的分支名,经由过程这类体式格局让两个分支之间的缓存互不影响。

布置项目

假如基于上边的一些设置,我们将 单元测试、ESLint 对应的剧本放进去,他就已可以完成我们想要的效果了,假如某一步实行失足,那末使命就会停在那边不会继承向后实行。
不过现在来看,后边已没有过剩的使命供我们实行了,所以是时刻将 布置 这一步操纵接过来了。

布置的话,我们现在挑选的是经由过程 rsync 来举行同步多台效劳器上的数据,一个比较简朴高效的布置体式格局。

P.S. 布置须要分外的做一件事变,就是竖立从
gitlab runner地点机械
gitlab-runner用户到目标布置效劳器对运用户下的机械信托关联。

有 N 多种要领可以完成,最简朴的就是在
runner机械上实行
ssh-copy-id 将公钥写入到目标机械。

或许可以像我一样,提早将
runner 机械的公钥拿出来,须要与机械竖立信托关联时就将这个字符串写入到目标机械的设置文件中。

相似如许的操纵:
ssh 10.0.0.1 "echo \"XXX\" >> ~/.ssh/authorized_keys"

大抵的设置以下:

variables:
  DEPLOY_TO: /home/XXX/repo # 要布置的目标效劳器项目途径
deploy:
  stage: deploy
  script:
    - rsync -e "ssh -o StrictHostKeyChecking=no" -arc --exclude-from="./exclude.list" --delete . 10.0.0.1:$DEPLOY_TO
    - ssh 10.0.0.1 "cd $DEPLOY_TO; npm i --only=production"
    - ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;"

同时用到的另有
variables,用来提出一些变量,在下边运用。

ssh 10.0.0.1 "pm2 start $DEPLOY_TO/pm2/$CI_ENVIRONMENT_NAME.json;",这行剧本的用处就是重启效劳了,我们运用pm2来治理历程,默许的商定项目途径下的pm2文件夹存放着个个环境启动时所需的参数。

固然了,现在我们在用的没有这么简朴,下边会一致提到

而且在布置的这一步,我们会有一些分外的处置惩罚

这是比较主要的一点,由于我们可以会更想要对上线的机遇有主动权,所以 deploy 的使命并非自动实行的,我们会将其修正为手动操纵还会触发,这用到了另一个设置参数:

deploy:
  stage: deploy
  script: XXX
  when: manual  # 设置该使命只能经由过程手动触发的体式格局运转

固然了,假如不须要,这个移除就好了,比方说我们在测试环境就没有设置这个选项,仅在线上环境运用了如许的操纵

更轻易的治理 CI/CD 流程

假如根据上述的设置文件举行编写,实际上已有了一个可用的、包含完整流程的 CI/CD 操纵了。

不过它的保护性并非很高,尤其是假如 CI/CD 被运用在多个项目中,想做出某项修改则意味着一切的项目都须要从新修正设置文件并上传到堆栈中才见效。

所以我们挑选了一个更天真的体式格局,终究我们的 CI/CD 设置文件是大抵如许子的(省略了部份不相干的设置):

variables:
  SCRIPTS_STORAGE: /home/gitlab-runner/runner-scripts
  DEPLOY_TO: /home/XXX/repo # 要布置的目标效劳器项目途径

stages:
  - install
  - test
  - build
  - deploy_development
  - deploy_production

install_dependencies:
  stage: install
  script: bash $SCRIPTS_STORAGE/install.sh

unit_test:
  stage: test
  script: bash $SCRIPTS_STORAGE/test.sh

eslint:
  stage: test
  script: bash $SCRIPTS_STORAGE/eslint.sh

# 编译 TS 文件
build:
  stage: build
  script: bash $SCRIPTS_STORAGE/build.sh

deploy_development:
  stage: deploy_development
  script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.1
  only: dev     # 零丁指定见效分支

deploy_production:
  stage: deploy_production
  script: bash $SCRIPTS_STORAGE/deploy.sh 10.0.0.2
  only: master  # 零丁指定见效分支

我们将每一步 CI/CD 所须要实行的剧本都放到了 runner 那台效劳器上,在设置文件中只是实行了谁人剧本文件。
如许当我们有什么战略上的调解,比方说 ESLint 划定规矩的变动、布置体式格局之类的。
这些都完整与项目之间举行解耦,后续的操纵基础都不会让正在运用 CI/CD 的项目从新修正才可以支撑(部份须要新增环境变量的导入之类确实实须要项目标支撑)。

接入钉钉关照

实际上,当 CI/CD 实行胜利或许失利,我们可以在 Pipeline 页面中看到,也可以设置一些邮件关照,但这些都不是时效性很强的。
鉴于我们现在在运用钉钉举行事情沟通,所以就研讨了一波钉钉机械人。
发明有支撑 GitLab 机械人,不过功用并不实用,只能处置惩罚一些 issues 之类的, CI/CD 的一些关照是缺失的,所以只好本身基于钉钉的音讯模版完成一下了。

由于上边我们已将各个步骤的操纵封装了起来,所以这个修正对同事们是无感知的,我们只须要修正对应的剧本文件,增添钉钉的相干操纵即可完成,封装了一个简朴的函数:

function sendDingText() {
  local text="$1"

  curl -X POST "$DINGTALK_HOOKS_URL" \
  -H 'Content-Type: application/json' \
  -d '{
    "msgtype": "text",
    "text": {
        "content": "'"$text"'"
    }
  }'
}

# 详细发送时传入的参数
sendDingText "proj: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME\ndeploy success\n$CI_PIPELINE_URL\ncreated by: $GITLAB_USER_NAME\nmessage: $CI_COMMIT_MESSAGE"

# 某些 case 失利的状况下 是不是须要更多的信息就看本身自定义咯
sendDingText "error: $CI_PROJECT_NAME[$CI_JOB_NAME]\nenv: $CI_ENVIRONMENT_NAME"

上述用到的环境变量,除了DINGTALK_HOOKS_URL是我们自定义的机械人关照地点之外,其他的变量都是有 GitLab runenr所供应的。

种种变量可以从这里找到:predefined variables

回滚处置惩罚

聊完了一般的流程,那末也该提一下出题目时刻的操纵了。
人非圣贤孰能无过,很有可以某次上线一些没有考虑到的处所就会致使效劳涌现异常,这时刻首要使命就是让用户还可以照旧接见,所以我们会挑选回滚到上一个有用的版本去。
在项目中的 Pipeline 页面 或许 Enviroment 页面(这个须要在设置文件中某些 job 中手动增添这个属性,平常会写在 deploy 的那一步去),可以在页面上挑选想要回滚的节点,然后从新实行 CI/CD 使命,即可完成回滚。

不过这在 TypeScript 项目中会有一些题目,由于我们回滚平常来说是从新实行上一个版本 CI/CD 中的 deploy 使命,在 TS 项目中,我们在 runner 中缓存了 TS 转换 JS 以后的 dist 文件夹,而且布置的时刻也是直接将该文件夹推送到效劳器的(TS项目标源码就没有再往效劳器上推过了)。

而假如我们直接点击 retry 就会带来一个题目,由于我们的 dist 文件夹是缓存的,而 deploy 并不会管这类事儿,他只会把对应的要推送的文件发送到效劳器上,并重启效劳。

而实际上 dist 照样末了一次(也就是失足的那次)编译出来的 JS 文件,所以处理这个题目有两种要领:

  1. deploy 之前实行一下 build
  2. deploy 的时刻举行推断

第一个计划肯定是不可行的,由于严峻依托于操纵上线的人是不是晓得有这个流程。
所以我们主如果经由过程第二种计划来处理这个题目。

我们须要让剧本在实行的时刻晓得,dist 文件夹里边的内容是不是是本身想要的。
所以就须要有一个 __标识__,而做这个标识最简朴有用探囊取物的就是,git commit id
每个 commit 都邑有一个唯一的标识标记,而且我们的 CI/CD 实行也是依托于新代码的提交(也就意味着肯定有 commit)。
所以我们在 build 环节将当前的commit id也缓存了下来:

git rev-parse --short HEAD > git_version

同时在 deploy 剧本中增添分外的推断逻辑:

currentVersion=`git rev-parse --short HEAD`
tagVersion=`touch git_version; cat git_version`

if [ "$currentVersion" = "$tagVersion" ]
then
    echo "git version match"
else
    echo "git version not match, rebuild dist"
    bash ~/runner-scripts/build.sh  # 分外的实行 build 剧本
fi

如许一来,就防止了回滚时照样布置了毛病代码的风险。

关于为何不将
build 这一步操纵与
deploy 兼并的缘由是如许的:

由于我们会有许多台机械,同时
job 会写许多个,相似
deploy_1
deploy_2
deploy_all,假如我们将
build 的这一步放到
deploy

那就意味着我们每次
deploy,纵然是一次布置,但由于我们挑选一台台机械零丁操纵,它也会从新天生屡次,这也会带来分外的时候本钱

hot fix 的处置惩罚

CI/CD 运转了一段时候后,我们发明偶然处理线上 bug 照样会比较慢,由于我们提交代码后要守候完整的 CI/CD 流程走完。
所以在研讨后我们决议,针对某些特定状况hot fix,我们须要跳过ESLint、单元测试这些流程,疾速的修复代码并完成上线。

CI/CD 供应了针对某些 Tag 可以举行差别的操纵,不过我并不想这么搞了,缘由有两点:

  1. 这须要修正设置文件(一切项目)
  2. 这须要开辟人员熟习对应的划定规矩(打 Tag

所以我们采纳了另一种取巧的体式格局来完成,由于我们的分支都是只吸收Merge Request那种体式格局上线的,所以他们的commit title实际上是牢固的:Merge branch 'XXX'
同时 CI/CD 会有环境变量通知我们当前实行 CI/CDcommit message
我们经由过程婚配这个字符串来搜检是不是相符某种划定规矩来决议是不是跳过这些job

function checkHotFix() {
  local count=`echo $CI_COMMIT_TITLE | grep -E "^Merge branch '(hot)?fix/\w+" | wc -l`

  if [ $count -eq 0 ]
  then
    return 0
  else
    return 1
  fi
}

# 运用要领

checkHotFix

if [ $? -eq 0 ]
then
  echo "start eslint"
  npx eslint --ext .js,.ts .
else
  # 跳过该步骤
  echo "match hotfix, ignore eslint"
fi

如许可以保证假如我们的分支名为 hotfix/XXX 或许 fix/XXX 在举行代码兼并时, CI/CD 会跳过过剩的代码搜检,直接举行布置上线。 没有跳过装置依托的那一步,由于 TS 编译照样须要这些东西的

小结

现在团队已有凌驾一半的项目接入了 CI/CD 流程,为了轻易同事接入(主如果编辑 .gitlab-ci.yml 文件),我们还供应了一个脚手架用于疾速天生设置文件(包含自动竖立机械之间的信托关联)。

相较之前,布置的速率显著的有提拔,而且不再对当地收集有种种依托,只如果可以将代码 push 到长途堆栈中,后续的事变就和本身没有什么关联了,而且可以轻易的举行小流量上线(布置单台考证有用性)。

以及在回滚方面则是更天真了一些,可在多个版本之间疾速切换,而且经由过程界面的体式格局,操纵起来也越发直观。

终究可以说,假如没有 CI/CD,实际上开辟形式也是可以忍耐的,不过当运用了 CI/CD 今后,再去运用之前的布置体式格局,则会显著的感觉到不温馨。(没有对照,就没有危险😂)

完整的流程形貌

  1. 装置依托
  2. 代码质量搜检

    1. ESLint 搜检

      1. 搜检是不是为 hotfix 分支,假如是则跳过本流程
    2. 单元测试

      1. 搜检是不是为 hotfix 分支,假如是则跳过本流程
  3. 编译 TS 文件
  4. 布置、上线

    1. 推断当前缓存 dist 目次是不是为有用的文件夹,假如不是则从新实行第三步编译 TS 文件
    2. 上线终了后发送钉钉关照

后续要做的

接入 CI/CD 只是第一步,将布置上线流程一致后,可以更轻易的做一些其他的事变。
比方说在顺序上线后可以考证一下接口的有用性,假如发明有毛病则自动回滚版本,从新布置。
或许说接入 docker, 这些调解在肯定程度上对项目保护者都是通明的。

参考资料

    原文作者:贾顺名
    原文地址: https://segmentfault.com/a/1190000019341766
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞