晨光科力普基于GitLab CI/CD持续集成服务的应用

hellojqk

简要介绍GitLab CI/CD在晨光科力普项目中的应用

背景介绍

科力普省心购是晨光文具集团在2019年成立的办公用品采购特惠电商平台,面向中小企业客户和个人客户,拥有小程序、H5和WEB等多个商城入口。省心购项目启动之前,公司其他项目多为企业、政府、事业单位等提供办公用品采购服务,采用定期发版的方式保证系统的稳健运行,一个小的需求也可能要等上一周才会发布。多达五套的运行环境使得我们需要一款能够保证省心购项目快速迭代的CI/CD工具。

为什么选择GitLab CI/CD

首先是公司选择了gitlab作为代码仓库,本身包含协调作业的开源持续集成服务GitLab CI/CD,那么GitLab CI/CD自然成了我们首先调研的对象。gitlab作为服务的提供者,由gitlab-runner注册后依轮询的方式获取服务的指令,执行相应的构建动作,同时将构建进度和结果及时返回给gitlab并在web端仓库侧边栏CI/CD->Pipelines页面中滚动展示出来。Setting->CI/CD模块下的Auto DevOps 自动化DevOps功能、Variables变量配置、Runners执行者等配置项提供了强大的公共配置管理功能。在编写完.gitlab-ci.yml构建配置文件和dockerfile文件即可满足我们的自动化需求。从我们使用的大半年时间来看,官方对于GitLab CI/CD的迭代速度也是非常快的,基本上每个月都会有新特性的加入。

分支与环境介绍

git分支 K8S集群 运行环境 说明
dev dev dev 开发环境(自动)
test test test 测试环境(自动)
uat uat uat 验收环境(自动)
prd prd pre、prd 金丝雀(自动)和生产环境(手动)
feature-* 需求分支,按需合并到环境分支

流程简述:

我们采用合并即发布的策略,push对应环境分支自动部署。其中prd分支的金丝雀环境自动部署,生产环境需手动部署。开发同学基于teambition认领新的需求,创建feature-*分支,按需合并到dev、test、uat分支发布。现在我们的流程仅有三个阶段:compile编译、docker-build镜像构建和deployment部署。从提交代码到部署成功约3分钟时间,除生产分支外零人为干预。也有许多待完善的地方,比如尚未集成commit-check提交检查、test自动测试、deployment-check部署状态检查、deployment-rollback部署回滚等阶段配置,这些也是我们下一步计划要做的事情。
pipeline列表页

GitLab CI/CD的相关介绍

  • gitlab-runner 持续集成服务的执行者,官方提供了多种部署方式,如常见的shell、docker、docker-machine、kubernetes等。基于部署维护和权限方面的考量,我们最终选择基于docker部署。为每个团队启动一个runner容器,容器内按部署环境注册了4个worker分别处理各个分支的构建任务。
  • .gitlab-ci.yml CI/CD持续集成配置文件,配置构建任务的顺序和结构。若使用docker部署,每个阶段需要指定该阶段所需镜像。
#.gitlab-ci.yml示例
stages:
  - compile # 编译阶段
  - docker-build # 镜像构建阶段
  - deployment # 部署阶段

compile:
  stage: compile
  image: golang:1.14.2
  script:
      - go build # 执行编译命令 go build 或 npm ci 等
  artifacts:
    paths:
      - bin/ # 编译结果暂存,可通过gitlab web 界面下载,主要是为了传递给 镜像构建阶段

docker-build:
  stage: docker-build
  image: docker:19.03.8
  services:
    - docker:19.03.8-dind
  script:
    # 执行镜像构建命令,特殊的镜像命名方式同样需要采用 artifacts 传递给 部署阶段
    - docker build -t registry.*.com/clp-dev/project:${CI_COMMIT_SHORT_SHA}-YYYYMMDDHHmm .

deployment:
  stage: deployment
  image: registry.*.com/kubectl:v1.17.3 # 需要自己构建包含kubectl执行程序的镜像
  script:
    # 执行部署命令
    - kubectl patch deploy K8S_DEPLOYMENT_NAME -p '更新镜像json字符串'

对于一些敏感信息,如docker镜像仓库登录密钥和kubectl配置文件,可通过gitlab web端Project级别或Group级别侧边栏Settings->CI/CD->Variables配置页面配置。在构建过程中可通过环境变量获取到这些信息。CI/CD->Pipelines->Status Tag下可以查看到构建任务阶段明细。如下图:

pipeline详情页

在我们现有的项目中使用的还是比较简单的用法。复杂的情形也可以轻松应对,参考官方gitlab-runner的CI/CD构建流程图
gitlab-runner的构建详情页

对于各个阶段,start_in延时、timeout超时控制、retry失败重试、interruptible 打断、trigger触发器、parallel并行等操作都是支持的。如果需要安排定点上线还可以使用CI/CD->Schedules页面配置构建任务的何时执行。

由于是gitlab官方推出的持续集成服务,许多跟仓库有关的信息都可以在执行构建任务时通过环境变量获取到,并随着版本的更新不断地扩增。比如我们这边打包镜像阶段统一使用CI_COMMIT_SHORT_SHA提交信息短码作为镜像标签。

多项目CI/CD配置管理

遇到的问题

项目初始情况

  • A项目基于go语言,compile阶段 image: golang:1.12.8
  • B项目基于nodejs,compile阶段 image:node:v10.8

随着时间推移

  • C项目基于go语言,compile阶段 image: golang:1.13.1
  • D项目...
  • E项目...

每个项目下各自维护的.gitlab-ci.yml配置文件给开发和维护带来了极大的不便,如:

  1. 新开项目从别的项目中复制一份过来用通常是较为简单地做法,但是docker-build阶段和deployment阶段都是冗余的配置,不符合编程理念。
  2. 开发语言、容器基础镜像等存在的BUG或升级需要我们跟进,就算只有1个项目,我们也要创建一个配置升级分支并合并到所有环境分支上,重复劳动。
  3. 配置文件维护也是一个持续的过程,gitlab版本升级引入新特性、构建阶段完善(编译前增加test单元测试,部署后增加check部署状态检查)等都很难推进。

如何解决

gitlab-ci.yml采用YAML数据格式语言,自然不可缺少对于锚点(&)和引用(*)的支持,在一个文件中可以很方便的将阶段公共配置拆分出来。同时可将gitlab-ci.yml按阶段拆分成不同的阶段配置文件,在需要的时候引入并重写。我们可以使用include特性引入local当前仓库, file相同gitlab,template官方模板和remote远程文件(OSS等)从不同位置引入1+个配置好的yaml文件进行文件复用。还可以使用extends为我们提供细致的配置代码模块复用。

文件组合

# 文件复用演示
# 镜像构建阶段文件
# 项目 /common/cicd
# 位置 /prepared-docker-build.yaml
job-docker-build:
  stage: docker-build
  script:
      - docker build -t registry.*.com/mygroup/myproject:CI_COMMIT_SHORT_SHA

# 部署阶段文件
# 项目 /common/cicd
# 位置 /prepared-deployment.yml
job-deployment:
  stage: deployment
  script:
    - kubectl patch deploy K8S_DEPLOYMENT_NAME -p '更新镜像json字符串'

# 引用
# 项目 /yourgroup/yourproject
# 位置 /.gitlab-ci.yml
include:
  - project: "common/cicd"
    ref: "master" # v1 v2 branch
    file: "/prepared-docker-build.yml"
  - project: "common/cicd"
    file: "/prepared-deployment.yml"

如上,通过include特性,我们很方便的实现了job-docker-build阶段和job-deployment阶段的配置复用。如果你想要对公共配置进行版本管理,可以通过ref指定分支或者标签。我们团队目前直接使用了默认的master分支进行维护,cicd项目的修改会影响到引用项目所有构建,对于我们团队来说,利大于弊。

我们最终的目的是使用common/cicd项目实现所有项目.gitlab-ci.yml配置托管,所以我们会将组合文件同时托管在common/cicd项目中,具体项目只需引入组合文件即可。如下:

# 项目 /common/cicd
# 位置 /yourgroup/yourproject-ci.yml
include:
  - local: "/prepared-docker-build.yml"
  - local: "/prepared-deployment.yml"
  - local: "/prepared-stages.yml"

# 项目 /yourgroup/yourproject
# 位置 /.gitlab-ci.yml
include:
  - project: "common/cicd"
    file: "/yourgroup/yourproject-ci.yml"

模块组合

# 项目 /common/cicd

# 位置 /prepared-rule.yml
# 通过Merge Request操作合并时,Merge到目标分支前不允许触发构建
#(此处暂时屏蔽,但它很有用,在真正合并前我们可以做代码规范和能否运行检测等)
.rule-merge_request_event: &rule-merge_request_event
  if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  when: never # 满足条件 不执行

# 默认规则 如果不是合并动作,再检查是否是 dev 分支,是的话才能执行构建任务。
.rule-default:
  rules:
    - *rule-merge_request_event
    - if: '$CI_COMMIT_REF_NAME == "dev"'
      when: on_success #上个阶段执行成功了,此阶段继续执行

# 位置 /stage-tags.yml
# 分配给持有dev标签的worker运行
.tags-dev:
  tags:
    - dev

# 位置 /prepared-compile.yaml

# go项目编译动作
job-compile-go:
  extends:
    - .rule-default
    - .tags-dev
  stage: compile
  script:
      - go build .

# node项目编译动作
job-compile-node:
  extends:
    - .rule-default
    - .tags-dev
  stage: compile
  script:
    - npm ci

上方示例所表达的意思是:在构建任务编译阶段,如果是合并动作发起的构建则不处理,不是合并动作触发的构建还需判断构建任务的触发分支是否来自于dev分支。使用了yml锚点&和引用*extends特性,实现了规则和标签的复用。甚至是支持重写,如下:

# 位置 /prepared-compile.yaml
.compile-default:
  stage: compile
  interruptible: true

.compile-script-go:
  script:
    - go build

# 可供引用者重写
.compile-case:
  variables:
    APP_TYPE: API
  extends:
    - .compile-default

# 位置 /stage-compile.yaml
job-compile:
  extends:
    - .rule-dev
    - .tags-dev
    - .compile-case

# 位置 /yourgroup/yourproject-ci.yml
.compile-case:
  variables:
    APP_TYPE: WEB_API
  extends:
    - .compile-default
    - .compile-script-go

总结

通过以上示例简单演示了我们现在的多项目配置文件管理方式,这种方式为我们多项目构建流程的可持续维护奠定了基础。

  • extends 支持多级继承,但是不建议使用三个以上级别。支持的最大嵌套级别为10
  • include 总共允许包含100个,重复包含被视为配置错误
  • 尽可能保证相同语言的构建阶段模块内容一致
  • 如果你的项目较为复杂,那么单独管理.gitlab-ci.yml更为合适

并发构建处理

我们早期的配置方式只使用了一个runner启动一个worker为团队项目进行构建任务,因为这样配置简单,能避免很多问题,如:build_dir位置问题,先后构建问题等。随着项目的增多,不同的项目和分支上频繁的合并代码,构建任务逐渐出现了堆积的情况,对于并发构建的需求越来越强烈。这些问题驱动着我们对gitlab-runnerci/cd配置持续优化。

concurrent与limit

  • concurrent runner下所有worker最大可以并发执行的任务数
  • limit worker并发执行任务数 默认0不限制数量

这两个参数属于runner配置文件中的配置项,如果我们想要让多个worker并发的执行构建则需要设置为>1。

interruptible

依我们目前的使用需求为例,合并代码到dev分支自动执行构建任务。假设A同学将自己的代码合并到dev分支,正在执行构建任务,此时B同学也往dev分支合并了代码,就会导致同时有两个dev分支的构建任务在进行。对于我们来说说,之前A同学的构建任务已经过时,没有必要再执行,只需要执行B同学的构建任务即可。gitlab 12.3版本引入了interruptible特性,在.gitlab-c.yml阶段配置时使用此特性,那么同分支上后续的构建任务将自动取消前置构建任务。引入后B同学的构建任务将自动取消前边A同学的构建任务。

custom_build_dir

在执行构建作业前,gitlab-runner-helper会先将项目clone到builds_dir目录下相应的文件夹下。在开启并发构建后,可能会导致多个阶段任务在同一个目录上执行,视具体情况而定。参照官方的建议,最保险的做法是对GIT_CLONE_PATH工作目录设置。如下:

variables:
  # CI_BUILDS_DIR 构建根路径 默认:/builds
  # CI_CONCURRENT_ID 单个执行者的执行的唯一ID
  # CI_PROJECT_PATH yourgroup/yourproject
  GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH

cache

compile编译阶段,往往需要获取依赖包。依node为例,在执行编译命令num run build前需要执行npm cinpm i命令获取依赖包。如果不配置缓存策略,那么每次都需要拉取依赖包数据,不仅会占用带宽,同时会拖慢我们的构建速度。

compile:
  cache:
    # 最终会保存在 .../yourgroup/yourproject/key_node_modules/cache.zip
    key: key_node_modules
    paths: # 那些目录需要缓存
      - node_modules

上方是一个简单地缓存策略配置。但依我们的需求为例,希望各个环境的缓存能够被隔离开。那么进阶一点的做法是增加分支名区分,如下:

compile:
  cache:
    # 分支名+node_modules 例如:dev-node_modules
    # 最终会保存在 .../yourgroup/yourproject/dev-node_modules/cache.zip
    key: ${CI_COMMIT_REF_NAME}-node_modules
    paths:
      - node_modules

gitlab 12.5版本对key进行了扩展,增加了filesprefix两个字段。作用是:node_modules目录实际是依赖package.jsonpackage-lock.json中的配置生成的,如果没有变化,那么也就没有必要重新缓存。如下:

compile:
  cache:
    # `key`=`prefix`+`-`+`SHA(files)`
    key:
      # 判定缓存是否需要更新的文件,最多2个,最终生成的路径是根据这两个文件计算出SHA码
      # 最终会保存在 .../yourgroup/yourproject/dev-feef9576d21ee9b6a32e30c5c79d0a0ceb68d1e5/cache.zip
      files:
        - package.json
        - package-lock.json
      # 生成目录的前缀,可以不定义
      prefix: ${CI_COMMIT_REF_NAME}
    paths:
      - node_modules

一旦配置了cache模块,那么在构建任务时就会先获取缓存,并在阶段完成后更新缓存,这种方法能够更好的处理是否需要更新问题。如果不同分支依赖的包相同且很少发生变化,那么取消配置prefix放弃环境隔离策略或许是一个更好的选择。

功能探索

阶段输出

pipeline构建进度

上图是个失败的演示,仅仅是为了说明近期gitlab版本更新对阶段输出界面进行了优化,增加了计时器,可以帮助我们在遇到构建任务较慢时分析原因。

trigger

我们平常的项目都比较简单,一个阶段一个阶段的执行,对于复杂的项目来说,一个构建任务可能同时要执行2+个以上的并行构建。

依官方项目为例,gitlab有ce社区版和ee企业版两个版本,gitlab-runner也为不同的平台提供了独立安装包。针对不同的版本编写不同的构建流程和阶段,在构建时可以通过触发器来控制多个任务同时进行。

依前后端分离的项目为例,在发布API项目的同时需要发布WEB项目,假设WEB项目依赖API项目的版本信息进行负载均衡配置,API在执行构建任务时通过触发器并传递参数(API版本信息)触发WEB的构建任务。

如果你的项目较为复杂,需要动态的生成构建任务配置文件,GitLab 12.9近期更新的一个版本已经支持了这种做法。同时官方提供了触发器的API,还可以将构建动作集成到别的应用当中。

delayed+start_in+retry

在部署完成之后,我们通常会通过kubectl手动确认部署状态,或者是通过http服务暴露的特殊的包含版本信息的连接进行部署确认。这种做法随着项目的增多是一件很累的事,delayed延迟+start_in延迟多久+retry失败重试组合使用是个很好的选择,也是我们团队准备补充的阶段。

Auto DevOps

摘自官方描述:Auto DevOps提供了预定义的CI/CD配置,使您可以自动检测,构建,测试,部署和监视应用程序。借助CI / CD最佳实践和工具,Auto DevOps旨在简化成熟和现代软件开发生命周期的设置和执行。借助Auto DevOps,软件开发过程的设置变得更加容易,因为每个项目都可以使用最少的配置来完成从验证到监视的完整工作流程。只需推送您的代码,GitLab就会处理其他所有事情。这使启动新项目变得更加容易,并使整个公司的应用程序设置方式保持一致。

实测过程中在添加k8s集群时需要k8s集群的管理权限,条件不足,暂时放弃验证。依然把它拿出说的原因是它描述了一个非常理想化的情形,通过配置集群连接信息和少量CI/CD配置即可做到自动化持续集成。

Dashboard Prometheus

基于prometheus的控制面板,位于项目级别Opeartions->Metrics页面,可以配置现有的prometheus地址。上边提到的,增加了check阶段仅能确认部署是否成功,业务是否能正常的运行还是需要借助一些指标进行确认。可以选择配置在这里方便具体的开发和测试同学跟踪。

相关官方资料

以上是基于我们目前实践经历,罗列出的部分GitLab CI/CD介绍,可能存在有误的地方。如果你对它感兴趣,那么官方文档则是更好的阅读选择。

原文地址:https://github.com/hellojqk/note/blob/master/devops/cicd/gitlab-ci.md

关于我们

基于多年为企业、政府、事业单位等客户的采购服务经验,结合专业优质的办公用品渠道供应链优势,为企业市场重新定义真正好货低价的办公用品。欢迎访问我们的网站 b.colipu.com ,合作方式: 15189798580 (微信号同手机号)

微信小程序:搜索“科力普省心购”或扫描下方二维码

科力普省心购

阅读 1.8k
9 声望
0 粉丝
0 条评论
你知道吗?

9 声望
0 粉丝
文章目录
宣传栏