DockOne微信分享(一七八):基于Pipeline的CI/CD在趣头条的应用实践


【编者的话】在今天的趣头条,随着业务需求的快速增长,部署与扩容的需求也越来越多,为更快的响应业务需求,业务容器化也随之加速。本次分享主要介绍在此业务场景下,部署在ECS的服务如何进行容器化的快速接入,使用基于Pipeline的Jenkins实现流程控制及部署,并动态的适配多环境与多集群。

CI/CD 作为业务自动化部署流水线的重要一环,在容器化快速及频繁发布的需求下,迎来了新的挑战。传统项目中,使用Shell或Pipeline脚本,将编译好的代码上传至服务器并启动就算完成了,而在容器环境中,则涉及到更为复杂的流程。

流程概览

在初期,CI究竟是使用基于Shell的自由风格任务还是基于Pipeline的任务,做个简单对比来看看:
img-11.png

基于以上,最终还是选择了已当前流行的Pipeline为主要任务类型,并据此重新设计了任务流程。看图:
‫img1.jpg

代码:
#! /usr/bin/env groovy

  def Controller(){
      Init.ExtraSettings()

      node(NODE_STAGE_INIT) {
          stage('Stage 1: Pre-Process') {
              // Fetch data from CMDB
              CMDB.FetchConfig()

              if (APP_LANG in APP_LANG_NO_COMPILE) {
                  NODE_STAGE_BUILD = "master"
              } else {
                  NODE_STAGE_BUILD =  APP_LANG
              }
          }
      }

      node(NODE_STAGE_GIT) {
          stage('Stage 2: Checkout & SonarQube') {
              Git.Controller()

              // SonarScanner
              Init.SonarAnalyzer()
          }
      }

      node(NODE_STAGE_BUILD) {
          stage('Stage 3: Build Project') {
              // Compile
              Compile.Controller()
          }
      }

      if ( BUILD_LEGACY == false ) {
          node(NODE_STAGE_DOCKER) {
              stage('Stage 4: Build Image') {
                  // Build & Push Docker image
                  Docker.Controller()
              }
          }

          node(NODE_STAGE_K8S) {
              stage('Stage 5: Deploy to k8s') {
                  k8s.Controller()
              }
          }

          next_stage_id = 6

      } else {
          // 兼容发布至非容器环境
          node(NODE_STAGE_BUILD) {
              stage('Stage 4: Deploy') {
                  Deploy.Legacy()
              }
          }

          next_stage_id = 5
      }

      node(NODE_STAGE_INIT) {
          stage("Stage $next_stage_id: Post-Process") {
              // Report
              Notice.Report()

              // Automatic Test
          }
      }
  }

  return this
- Code stages.groovy 


对每一个关键步骤都设置一个开关,便于调试的同时也更大的增加灵活性。
// Triggers
SKIP_DOCKER   = false
SKIP_KUBE     = false
SKIP_COMPILE  = false
SKIP_GIT      = false
SKIP_TEST     = false
SKIP_SONAR    = true
SKIP_REPORT   = false 


可以在任务配置中通过定义变量来改变行为。

任务配置

每一个任务都可以通过变量进行定义,可接受的变量及其作用如下:
img-2.png

img-3.png

是不是非常复杂?看一眼实际任务配置:
img-4.png

img-5.png

任务的配置在开始构建时从CMDB获取,但实际工作中总会有一些看似比较合理的需求需要能支持,因此可以在任务配置中定义变量来覆盖CMDB的配置,如下图:
img-6.png

img-7.png

现在,新建Jenkins任务就非常容易了,模块化的函数配合灵活的变量定义,可以轻松应对大部分需求。

Stage 1:Pre-Process

获取项目配置

项目配置信息直接写在Jenkins任务配置里简单方便,但项目变多时配置维护就变得复杂了,因此需要集中统一管理配置,方便维护的同时,也能将配置权限下放至项目开发者,配置变更也能更快速。

在每次构建的时候才去获取配置,确保拿到最新配置。

拿到配置后,使用readJSON将配置解析为相关变量供后续流程使用。
img-8.png

权限控制

Jenkins内置有多个权限控制方式,但在多项目+多用户下进行权限控制时,情况就变复杂了,其实也不算特别复杂,就是需要勾太多框框,随着项目与用户增多,难度呈指数级上升。

因此,在设计流程时,就完全抛弃了Jenkins内置的那几套,基于CMDB/GitLab进行权限控制,一个很简单的思路:
  • 如果你没权限访问代码,那么你可能也不能发布我的项目
  • 权限检查在任务预处理阶段进行,不通过则直接终止任务
  • 简单方便易于实现(且又能少只锅)


Stage 2:Checkout & SonarQube

Checkout


  • 初始化,项目第一次构建自动进行初始化操作

  • 检查
    • 检查Git Repo是否为空
    • 检查是否选择了版本
    • 检查是否需要在指定目录下进行Checkout
    • 检查发布环境与代码版本规则是否匹配
      img-9.png
    • 最后,输出代码相关信息,方便事后追溯


SonarQube

SonarQube是一个开源的代码质量管理系统,支持25+种语言,可以通过使用插件机制与Eclipse和Jira等其他外部工具集成,从而实现了对代码的质量的全面自动化分析和管理。

集成

SonarQube与Jenkins集成也非常简单,构建时根据当前项目生成配置,再调用SonarScanner就行。

权限控制

SoanrQube中默认情况下用户可以看到所有项目,且可查看源码,这肯定不是我们所期望的,因此也需要进行权限控制,这个思路同上面Jenkins的权限管理一致,依据代码库权限进行控制。

Stage 3:Build Project

这里比较简单,根据项目语言(APP_LANG),自动匹配相关节点进行构建。
  • PHP项目,如检测到项目根目录存在composer.json则调用compoer install
  • NodeJS项目,如未提供编译命令(BUILD_COMMAND),则执行默认指令(npm i
  • GoLang项目,如未提供编译命令(BUILD_COMMAND),则执行默认指令(make


Stage 4:Build Image

终于跟容器有点关系了!
img-10.png

是不是很简单?

确实很简单:
  • 查找并模版文件

  • 在模版文件基础上加入项目信息,如
    • 项目代码
    • 为满足稀奇古怪的需求而特设的指令

  • Build image
  • Push image


img-12.png

看到这里,也许你会明白为何不用插件非要不自量力的自己实现。

Stage 5:Deploy to Kubernetes

部署至Kubernetes相对简单多了,替换deployment.yaml文件, apply 一下就完了。

不信看这里:
img-13.png

等等,注意CronJob函数,难免会有些项目需要利用CronJob完成一些定时任务。

需求多的时候,CronJob的管理相当麻烦为了(偷懒)减少不必要的麻烦,我们针对业务需求写了个小函数(CronJob)自动化处理定时任务。

开发者在自己项目中提供一个文件,命名为 cronjob.json,在项目发布时,Pipeline检测到这个文件即进行处理,文件格式大致如下:
[{"id":0, 
  "time": "00 01 * * *", 
  "name": "test-task", 
  "command": "echo 'hello world'", 
  "description": "Hello world", 
  "notify": "user1@abc.com|user2@abc.com", 
  "when": "failure"
}] 

  • id为编号,从0开始,必填重要项
  • time为执行任务时间,标准Cron格式
  • name为计划任务名称,应与项目相关
  • command为要执行的命令
  • description为备注信息
  • notify为接收通知者邮件地址,多个之间加 '|', 企业微信发送

  • when为发送消息的条件:
    • always
    • never
    • failure
    • success

  • 为避免任务失败后循环执行,所有失败的任务都会先发送失败消息然后再以成功状态结束


如此,计划任务管理在Kubernetes这部分基本无须人工干预了。

Q&A

Q:生成新的镜像怎么自动打新的tag?

A:我们镜像Tag使用本次构建选定的Git版本,如分支名称或者Tag。
Q:你们的Jenkins实例有多少,Jenkins实例是怎么规划的?

A:1个Master节点提供UI界面,几个Agent分别对应不同语言版本和不同环境Kubernetes集群,运行在容器中。规划就是按语言或版本分节点,按集群分节点(Agent)。
Q:SonarQube跟Jenkins的集成,能否详细介绍一下,能否show一下Groovy代码。

A:这个比较简单,构建时将项目信息输入到sonar-project.properties文件中,再调用sonar-scanner命令即可。
Q: 这个Pipeline Jenkinsfile是多个在一起吗? 还是直接写的Groovy文件?

A:多个Groovy文件,按类型分函数,一个功能尽量只写一次。
Q:Jenkins的权限控制能否再细化一下?

A:我们这边权限实际上是在CMDB中完成的。构建时向CMDB发起查询请求,传递当前项目名称、选择的环境、用户名过去,CMDB判断当前用户是否有权限构建选定的环境,允许则返回项目配置信息,否则返回错误代码,这边收到后报错终止。
Q:SonarQube的权限控制及性能当面?

A:权限控制使用SonarQube提供的API,将项目跟GitLab中相应项目权限匹配起来,GitLab中可以查看这个项目代码,那么SonarQube中就能看到这个项目结果和Code。
Q: 你们是直接将SonarQube、GitLab/Jenkins的权限控制到一起了? 怎样做的统一?

A:使用LDAP认证。
Q:Sonar使用的sonar-scanner还是mvn sonar:sonar?

A:使用 SonarScanner。
Q:Kubernetes的services.yaml文件在哪里管理?

A:deployment & service & configmap之类文件都是提供Git进行版本控制,针对语言有模版,构建时进行替换。
Q:Pipeline有回滚机制吗,你们集成覆盖率测试了吗?

A:回滚机制暂时不打算通过Pipeline进行,后续在另外的平台实现。
覆盖率测试方面目前还没做。
Q: 你们的Pipeline触发策略是什么样的?

A:人工触发,因为有必须要人工选择的Git版本。为防止误发布,默认没有选定版本,不选则在预处理时报错终止。
Q:Pipeline这套机制的脚本如果出错了如何调试?

A:echo输出调试(手动滑稽)。
Q:Pipeline语法和使用上有什么参考链接吗?

A:http://www.groovy-lang.org/https://www.w3cschool.cn/groovy/https://jenkins.io/doc/book/pipeline/syntax/
Q:Git Checkout的时候,你们的Git SCM没有考虑隐私安全的事情吗,比如代码权限受限?

A:Jenkins使用了一个最小权限用户去GitLab上拉代码。安全方面,Jenkins所有节点都是可控的。
Q: 你们的各工具间,有没有做集成?比如使用Pipeline来操作Jira相关issue等?或其他问题管理工具。

A:我们这边目前还没集成Jira。如果有这需求肯定会对接起来。
至于其它的则根据需要在不同阶段进行上报。
Q:构建及部署都在容器中?要构建的文件或制品文件怎么存放与管理的?

A:Agent容器启动时挂载了一个目录,里面有全套附属文件及Jenkins home目录。build节点完成自己工作后,其它节点按需接手处理。
以上内容根据2018年6月26日晚微信群分享内容整理。分享人汝林,趣头条容器运维工程师。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册