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

b72bac5909f9fd0332b8b90c64086d2f.jpeg

科力普省心购是晨光文具集团在2019年初为了拓展综合办公物资采购业务成立的B2B电商平台。随着云时代容器技术的火热发展,Docker、Kubernetes等容器化技术越来越受到研发团队的喜爱。因此,省心购项目在启动之时就决定拥抱容器全面上云。产品需求不断增多,传统的开发测试运维方式已无法保证项目的快速迭代。同时Git仓库不断地增多,CI/CD配置的“各自为战”也导致了基础组件无法获得及时的更新。本文主要介绍了晨光科力普省心购在DevOps探索过程中基于GitLab CI/CD持续集成服务的实践。简要介绍GitLab CI/CD在晨光科力普项目中的应用

f9b05d307fb2e9469c3c92f1af854f35.png


背景介绍
科力普省心购是晨光文具集团在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分支 Kubernetes集群 运行环境 说明
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部署回滚等阶段配置,这些也是我们下一步计划要做的事情。

00fae260f1f7dc5b6c29f44fa6702299.png


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

  2. stages:

  3.  - compile # 编译阶段

  4.  - docker-build # 镜像构建阶段

  5.  - deployment # 部署阶段


  6. compile:

  7.  stage: compile

  8.  image: golang:1.14.2

  9.  script:

  10.      - go build # 执行编译命令go build或npm ci等

  11.  artifacts:

  12.    paths:

  13.      - bin/ # 编译结果暂存,可通过GitLab Web界面下载,主要是为了传递给镜像构建阶段


  14. docker-build:

  15.  stage: docker-build

  16.  image: docker:19.03.8

  17.  services:

  18.    - docker:19.03.8-dind

  19.  script:

  20.    # 执行镜像构建命令,特殊的镜像命名方式同样需要采用artifacts传递给部署阶段

  21.    - docker build -t registry.*.com/clp-dev/project:${CI_COMMIT_SHORT_SHA}-YYYYMMDDHHmm .


  22. deployment:

  23.  stage: deployment

  24.  image: registry.*.com/kubectl:v1.17.3 # 需要自己构建包含kubectl执行程序的镜像

  25.  script:

  26.    # 执行部署命令

  27.    - kubectl patch deploy K8S_DEPLOYMENT_NAME -p '更新镜像json字符串'


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

e9849c2e6c59e5dad554aa27a002eb67.png


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

f9b05d307fb2e9469c3c92f1af854f35.png


对于各个阶段,start_in延时、timeout超时控制、retry失败重试、interruptible打断、trigger触发器、parallel并行等操作都是支持的。如果需要安排定点上线还可以使用CI/CD->Schedules页面配置构建任务的何时执行。
由于是GitLab官方推出的持续集成服务,许多跟仓库有关的信息都可以在执行构建任务时通过环境变量获取到,并随着版本的更新不断地扩增。比如我们这边打包镜像阶段统一使用CI_COMMIT_SHORT_SHA提交信息短码作为镜像标签。
多项目CI/CD配置管理

69ddad96a5b640fa5f479eaee3359d3b.png


遇到的问题
项目初始情况:
  • A项目基于Go语言,compile阶段image: golang:1.12.8

  • B项目基于Node.js,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为我们提供细致的配置代码模块复用。
文件组合:
 
 
  1. # 文件复用演示

  2. # 镜像构建阶段文件

  3. # 项目 /common/cicd

  4. # 位置 /prepared-docker-build.yaml

  5. job-docker-build:

  6.  stage: docker-build

  7.  script:

  8.      - docker build -t registry.*.com/mygroup/myproject:CI_COMMIT_SHORT_SHA


  9. # 部署阶段文件

  10. # 项目 /common/cicd

  11. # 位置 /prepared-deployment.yml

  12. job-deployment:

  13.  stage: deployment

  14.  script:

  15.    - kubectl patch deploy K8S_DEPLOYMENT_NAME -p '更新镜像json字符串'


  16. # 引用

  17. # 项目 /yourgroup/yourproject

  18. # 位置 /.gitlab-ci.yml

  19. include:

  20.  - project: "common/cicd"

  21.    ref: "master" # v1 v2 branch

  22.    file: "/prepared-docker-build.yml"

  23.  - project: "common/cicd"

  24.    file: "/prepared-deployment.yml"


如上,通过include特性,我们很方便的实现了job-docker-build阶段和job-deployment阶段的配置复用。如果你想要对公共配置进行版本管理,可以通过ref指定分支或者标签。我们团队目前直接使用了默认的master分支进行维护,CI/CD项目的修改会影响到引用项目所有构建,对于我们团队来说,利大于弊。
我们最终的目的是使用common/CICD项目实现所有项目.gitlab-ci.yml配置托管,所以我们会将组合文件同时托管在common/cicd项目中,具体项目只需引入组合文件即可。如下:

 
 
  1. # 项目 /common/cicd

  2. # 位置 /yourgroup/yourproject-ci.yml

  3. include:

  4.  - local: "/prepared-docker-build.yml"

  5.  - local: "/prepared-deployment.yml"

  6.  - local: "/prepared-stages.yml"


  7. # 项目 /yourgroup/yourproject

  8. # 位置 /.gitlab-ci.yml

  9. include:

  10.  - project: "common/cicd"

  11.    file: "/yourgroup/yourproject-ci.yml"

  12. 模块组合

  13. # 项目 /common/cicd


  14. # 位置 /prepared-rule.yml

  15. # 通过Merge Request操作合并时,Merge到目标分支前不允许触发构建

  16. #(此处暂时屏蔽,但它很有用,在真正合并前我们可以做代码规范和能否运行检测等)

  17. .rule-merge_request_event: &rule-merge_request_event

  18.  if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

  19.  when: never # 满足条件 不执行


  20. # 默认规则如果不是合并动作,再检查是否是dev分支,是的话才能执行构建任务。

  21. .rule-default:

  22.  rules:

  23.    - *rule-merge_request_event

  24.    - if: '$CI_COMMIT_REF_NAME == "dev"'

  25.      when: on_success #上个阶段执行成功了,此阶段继续执行


  26. # 位置 /stage-tags.yml

  27. # 分配给持有dev标签的worker运行

  28. .tags-dev:

  29.  tags:

  30.    - dev


  31. # 位置 /prepared-compile.yaml


  32. # Go项目编译动作

  33. job-compile-go:

  34.  extends:

  35.    - .rule-default

  36.    - .tags-dev

  37.  stage: compile

  38.  script:

  39.      - go build .


  40. # Node项目编译动作

  41. job-compile-node:

  42.  extends:

  43.    - .rule-default

  44.    - .tags-dev

  45.  stage: compile

  46.  script:

  47.    - npm ci


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

 
 
  1. # 位置 /prepared-compile.yaml

  2. .compile-default:

  3.  stage: compile

  4.  interruptible: true


  5. .compile-script-go:

  6.  script:

  7.    - go build


  8. # 可供引用者重写

  9. .compile-case:

  10.  variables:

  11.    APP_TYPE: API

  12.  extends:

  13.    - .compile-default


  14. # 位置 /stage-compile.yaml

  15. job-compile:

  16.  extends:

  17.    - .rule-dev

  18.    - .tags-dev

  19.    - .compile-case


  20. # 位置 /yourgroup/yourproject-ci.yml

  21. .compile-case:

  22.  variables:

  23.    APP_TYPE: WEB_API

  24.  extends:

  25.    - .compile-default

  26.    - .compile-script-go


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

  • extends支持多级继承,但是不建议使用三个以上级别。支持的最大嵌套级别为10

  • include总共允许包含100个,重复包含被视为配置错误

  • 尽可能保证相同语言的构建阶段模块内容一致

  • 如果你的项目较为复杂,那么单独管理.gitlab-ci.yml更为合适


并发构建处理

f9b05d307fb2e9469c3c92f1af854f35.png


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


compile编译阶段,往往需要获取依赖包。依Node为例,在执行编译命令num run build前需要执行npm ci或npm 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进行了扩展,增加了files和prefix两个字段。作用是:node_modules目录实际是依赖package.json或package-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放弃环境隔离策略或许是一个更好的选择。


功能探索

f9b05d307fb2e9469c3c92f1af854f35.png


阶段输出

f9b05d307fb2e9469c3c92f1af854f35.png


上图是个失败的演示,仅仅是为了说明近期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就会处理其他所有事情。这使启动新项目变得更加容易,并使整个公司的应用程序设置方式保持一致。
实测过程中在添加Kubernetes集群时需要Kubernetes集群的管理权限,条件不足,暂时放弃验证。依然把它拿出说的原因是它描述了一个非常理想化的情形,通过配置集群连接信息和少量CI/CD配置即可做到自动化持续集成。
Dashboard Prometheus
基于Prometheus的控制面板,位于项目级别Opeartions->Metrics页面,可以配置现有的Prometheus地址。上边提到的,增加了check阶段仅能确认部署是否成功,业务是否能正常的运行还是需要借助一些指标进行确认。可以选择配置在这里方便具体的开发和测试同学跟踪。


Q&A

90982966d7f21144a30e7b9e9e63f21f.png


Q:gitlab-runner在没有CICD任务时也是运行中的么?那这样是否会占用过多资源?是否可以做到类似Jenkins + Kubernetes当有CICD任务的时候才按需启动一个新的slaver容器?A:我们一个组的项目只启动一个runner,注册4个worker,runner依轮询方式监听GitLab构建任务,没任务时就1个容器,有任务时才会启动构建容器,构建容器的资源占用可通过配置文件限制。
Q:我看到有些特性是在新版本的GitLab里面才有的,旧版本应该升级吗?你们采用的什么升级策略?A:如果更新的特性对我们很有吸引力,如include和extends,我们会评估一下升级风险和近期是不是有重要任务发布,都没有的话,我们就做好备份进行升级,版本的话不会跟的特别近,也怕有风险。
Q:和Jenkins比较,你觉得GitLab CI/CD有什么优势,或者说说他们的差别,适用场景?A:Jenkins别的团队也有在用,我个人觉得GitLab的配置更加简单,因为分支策略,我们的合并动作都会在gitlab merge request里进行,合并完直接切到ci cd->pipeline页面,团队每个人都能实时获取构建进度。
Q:问下你们实践的场景规模有多大?多大团队,多少仓库,占公司多少比例的仓库,多少并发和流量的应用,发布频率多少,每天一次吗?A:我们组去年组建的现在有10个左右仓库,别的组项目比较多,整体大概有100多个项目。使用GitLab CI/CD的占一半。我们组已经完成了CI/CD的配置整合,正在向别的团队推广。发布频率的话,dev、test、uat比较频繁,线上环境看测试进度1天1次。
Q:目前遇到一个问题,master对应生产环境,虽然分支做了保护,在deploy步骤设置成when munual ,但是所有组员都可以点击发布。鉴权这方面GitLab CI/CD比较弱,是否有解决?A:Setting->Repository->Protected Branches下可以设置分支可由那些角色进行merge或者push,可通过这里控制团队成员的权限是否能合并到master。我们在master之前会有个uat验收分支,现在权限团队成员都有,后边会拿掉master的,交给测试团队那边。


猜你喜欢

转载自blog.51cto.com/14992974/2551051