写点什么

DevOps 流水线设计的最佳实践

作者:DevOps在路上
  • 2023-07-11
    湖北
  • 本文字数:9359 字

    阅读完需:约 31 分钟

DevOps流水线设计的最佳实践

谈到到 DevOps,持续交付流水线是绕不开的一个话题,相对于其他实践,通过流水线来实现快速高质量的交付价值是相对能快速见效的,特别对于开发测试人员,能够获得实实在在的收益。很多文章介绍流水线,不管是 jenkins,gitlab-ci, 流水线,还是 drone, github action 流水线, 文章都很多,但是不管什么工具,流水线设计的思路是一致的。于此同时,在实践过程中,发现大家对流水像有些误区,不是一大堆流水线,就是一个流水线调一个超级复杂的脚本,各种硬编码和环境依赖,所以希望通过这篇文章能够给大家分享自己对于 Pipeline 流水线的设计心得体会。

概念

  1. 持续集成 (Continuous Integration,CI)


持续集成(CI)是在源代码变更后自动检测、拉取、构建和(在大多数情况下)进行单元测试的过程对项目而言,持续集成(CI)的目标是确保开发人员新提交的变更是好的,不会发生 break build; 并且最终的主干分支一直处于可发布的状态,对于开发人员而言,要求他们必须频繁地向主干提交代码,相应也可以即时得到问题的反馈。实时获取到相关错误的信息,以便快速地定位与解决问题显然这个过程可以大大地提高开发人员以及整个 IT 团队的工作效率,避免陷入好几天得不到好的“部署产出”,影响后续的测试和交付。



  1. 持续交付 (Continuous Delivery,CD)


持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的「预发布环境」(production-like environments)中。交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段 持续交付并不是指软件每一个改动都要尽快部署到产品环境中,它指的是任何的代码修改都可以在任何时候实时部署。强调: 1、手动部署 2、有部署的能力,但不一定部署



  1. 持续部署 (Continuous Deployment, CD)


代码通过评审之后,自动部署到生产环境中。持续部署是持续交付的最高阶段。强调 1、持续部署是自动的 2、持续部署是持续交付的最高阶段 3、持续交付表示的是一种能力,持续部署则是一种方式


流水线的编排设计

参考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html这里非常推荐以版本控制系统为源的构建流水线设计,从每一位开发人员提交代码即可对当前提交代码进行检查编译构建,尽快将错误反馈给每位提交人员。



对于 DevOps 流水线,主要是由各类任务串联起来,而对于任务本身又分为两张类型,一种是自动化任务,一种是人工执行任务。具体如下:


  1. 自动化任务:包括了代码静态检查,构建,打包,部署,单元测试,环境迁移,自定义脚本运行等。

  2. 人工任务:人工任务主要包括了检查审核,打标签基线,组件包制作等类似工作。


而通常我们看到的流水线基本都由上述两类任务组合编排而成,一个流水线可以是完全自动化执行,也可以中间加入了人工干预节点,在人工干预处理后再继续朝下执行。比如流水线中到了测试部署完成后,可以到测试环境人工验证环节,只有人工验证通过再流转到迁移发布到生产环境动作任务。DevOps 流水线实际上和我们原来经常谈到的持续集成最佳实践是相当类似的,较大的一个差异点就在于引入了容器化技术来实现自动化部署和应用托管。至于在 DevOps 实践中,是否必须马上将项目切换到微服务架构框架模式,反而不是必须得。


在整个 DevOps 流水线中,我们实际上强调个一个关键点在于“**一套 Docker 镜像文件+多套环境配置+多套构建版本标签”**做法。以确保我们最终构建和测试通过的版本就是我们部署到生产环境的版本。构建操作只有一次,而后面到测试环境,到 UAT 环境,到生产环境,都属于是镜像的环境迁移和部署。而不涉及到需要再次重新打包的问题。这个是持续集成,也是 DevOps 的基本要求。

流水线任务的标准化/原子化

今天谈 DevOps 流水线编排,主要是对流水线编排本身的灵活性进一步思考。


  • 构建操作:构建我们通常采用 Maven 进行自动化构建,构建完成输出一个或多个 Jar 包或 War 包。


注意常规方式下构建完执行进行部署操作,部署操作一般就是将构建的结果拷贝到我们的测试环境服务器,同时对初始化脚本进行启动等。而在 DevOps 下,该操作会变成两个操作,即一个打包,一个部署。打包是将构建完成的内容制作为镜像,部署是将镜像部署到具体的资源池和指定集群。


  • 打包镜像操作:实际上即基于构建完成的部署包来生成镜像。该操作一般首先基于一个基础镜像文件基础上进行,在基础镜像文件上拷贝和写入具体的部署包文件,同时在启动相应的初始化脚本。


那么首先要考虑构建操作和打包操作如何松耦合开,打包操作简单来就是就是一个镜像制作,需要的是构建操作产生的输出。我们可以对其输出和需要拷贝的内容在构建的时候进行约定。而打包任务则是一个标准化的镜像制作任务,我们需要考虑的仅仅是基于 1)基于哪个基础镜像 2)中间件容器默认目录设置 3)初始化启动命令。即在实际的打包任务设计的时候,我们不会指定具体的部署包和部署文件,这个完全由编排的时候由上游输入。


  • 部署操作:部署操作相当更加简单,重点就是将镜像部署到哪个资源池,哪个集群节点,初始化的节点配置等。具体部署哪个镜像不要指定,而是由上游任务节点输入。


任务节点间松耦合设计的意义这种松耦合设计才能够使流水线编排更加灵活。比如我们在进行了构建打包后,我们希望同时讲打包内容部署到开发环境和测试环境。那么则是打包动作完成后需要对接两个应用部署任务。这两个部署任务都依托上面的打包结果进行自动化部署,可以并行进行。对于测试环境部署完成后,我们需要进行测试人员手工验证测试,如果测试通过,我们打标签后希望能够直接发布到 UAT 环境。而这种操作我们也希望在一个流水线来设计和完成。这样我们更加容易在持续集成看板上看到整个版本构建和迁移的完整过程。如果这是在一个大流水线里面,那么对于 UAT 环境部署任务就需要一直去追溯流水线上的最近的一个打包任务节点,同时取该任务节点产生的输出来进行相应的环境部署操作。在谈 DevOps 的时候,一个重点就是和 QA/QC 的协同,因此在流水线编排的时候一定要考虑各类测试节点,包括静态代码检查,自动化的单元测试,人工的测试验证。同时最好基于持续集成实践,能够将测试过程和整个自动化构建过程紧密结合起来。简单来说,测试人员发现 build1.0.0001 版本 4 个 bug 并提交,那么在下次自动化构建完成并单元测试通过后,测试人员能够很清楚的看到哪些 Bug 已经修改并可以在新构建的版本进行验证。只有这样才能够形成闭环,整个流水线作业才能够更好的发挥协同作用。

流水线中蕴含的工程实践

流水线除了任务步骤的编排,更重要的核心是最佳工程实践的体现。过去传统的思维,自动化就是写个 shell/python 脚本批量执行,在 DevOps/微服务时代,这一招太 out 了,每种工程实践的背后都有需要解决的问题,通过在流水线设计中注入最佳的工程实践,可以让流水线的价值最大化,也让流水线更高级不是嘛。


  1. 版本控制 - 解决的问题:需求和代码的关系,版本变化的跟踪

  2. 最优的分支策略 - 解决的问题:版本发布和团队协作,某些情况会和环境有关系

  3. 代码静态扫描 ** - 解决的问题: 开发规范和安全的问题**

  4. 80%以上的单元测试覆盖率 ** - 解决的问题:代码功能质量的问题,让测试左移**

  5. 漏洞(Vulnerability)扫描 - 解决的问题:部署环境/产品安全的问题

  6. 开源工具扫描 ** - 解决的问题:解决供应链安全问题,别忘了 log4j**

  7. 制品(Artifact)版本控制 - 解决的问题:制品的版本控制,制品的晋级,某些情况下环境的回滚

  8. 环境自动创建 - 解决的问题:解决的是构建/部署环境一致性的问题,开发测的好好的,测试一验证怎么不行啊,容器化/云原生让这个问题更好的解决

  9. 不可变服务器(Immutable Server )- 解决的问题: 可能不好理解,打个比方如果如果你的服务器挂了,或者某次配置更改了服务就起不来了,使用不可变基础设施的主要好处是部署的简单性、可靠性和一致性,服务器可以随时替换上线

  10. 集成测试

  11. 性能测试

  12. 每次提交都触发:构建、部署和自动化测试 ** - 解决的问题:快速失败,避免下游时间的浪费**

  13. 自动化变更请求 ** - 解决问题:某些场景下通过状态变更触发某些动作**

  14. 零停机发布 - 解决的问题:滚动/蓝绿/灰度发布等,用户无感知

  15. 功能开关 - 解决的问题: 主干开发中,如果某个功能没开放完,就通过 on/off 某个特性来让稳定的功能上线;还有一个场景,比如某些面对消费者的广告网站,想看看自己某个功能客户是否细化,通过功能开关看看市场反馈,一般和 A/B 测试配合

基于场景设计流水线

是否需要一条完整的流水线?流水线是越多越好,还是越少越好?建议按照场景来设计,一条流水线通吃所有流程是不现实的,搞了好多流水线(比如一个构建就一个流水线,一个复制操作就一个流水线)这些都是不可取的,维护成本巨大,得不偿失。



流水线按照场景分类如下:


  • 端到端自动化流水线

  • 需求、代码构建、测试、部署环境内嵌自动化能力,每次提交都触发完整流水线

  • 提交阶段流水线(个人级)、

  • 验收阶段流水线(团队级)、

  • 部署阶段流水线(部署/发布)

  • 流水线自动化触发,递次自动化(制品)晋级;

  • 流水线任务按需串行、并行、特殊场景下跳过执行

  • 必要环节人工干预, e.g. 在手工测试、正式发布等环节导入手工确认环节,流水线牵引流动


1)提交流水线

过程如下:


  • 提交即构建

  • 编译单测打包代码质量检查

  • 构建错误第一时间通知提交人


以 Jenkins 实现为例,通过 webhook 触发 CI 构建,首先配置 Jenkins 项目


  • 使用 generic webhook 方式触发项目构建

  • 配置构建触发器参数(获取 gitlab 返回的数据,比如分支、用户等信息)

  • 配置构建触发器中的 token(确保唯一,建议可以用项目名称)

  • 配置触发器中的请求过滤(merge_request,opend)



其次是 Gitlab 的配置


  • 项目-》集成-》新建 webhook

  • 填写 webhook 地址?token=projectName

  • MergeRequest 操作触发



剩下的就是编写 Jenkinsfile 了,下面列出几个关键点 1.获取 gitlab 数据中的分支名称,作为本次构建的分支名称。2.获取 gitlab 数据中的用户邮箱,作为构建失败后通知对象。

2)MR 流水线

过程如下:


  • codereview

  • 配置分支保护

  • 创建合并请求对将代码审查结果在评论区展现

  • 由 assignUser 合并代码


**合并流水线设计:**合并流水线的步骤其实跟提交流水线很类似,但是在代码质量检查的步骤中严格要求检查质量阈的状态,当质量阈状态为错误的时候,需要立即失败并通知发起人。第一次设计


  • 开发人员创建 MR 并指定 AssignUser。

  • CI 工具开始对 MR 中的源分支进行编译构建打包代码检查。

  • 构建成功(代码质量没问题)在 MR 页面评论提示信息。

  • 构建失败在 MR 页面评论失败信息


第二次设计(借助 GitlabCI)- 优化点:加入 MR 构建失败拦截,成功自动合并


  • 项目配置当流水线成功时才能 merge。

  • 开发人员创建 MR 并指定 AssignUser。

  • Jenkins 开始对 MR 中的源分支的最后一次 commit 状态改为 running。

  • 然后进行编译构建打包代码检查。

  • 构建成功,更新最后一次 commit 的状态为 success。

  • 构建失败,更新最后一次 commit 的状态为 faild。

3)SQL 发布流水线


除了代码有版本,其实 SQL 也有“版本的”,SQL 脚本的版本对于产品的升级回滚至关重要。一般对 SQL 的集成,会包含如下要素


  1. 构建环节,对 SQL 语法进行检查,避免打进包里语法是错的;某些情况下,多个开发会写不同的增量脚本,最后发布时候需要做脚本的合并

  2. SQL 脚本的版本,某些情况下产品自身要用表来记录自身业务脚本的版本,通过产品版本来判断某些脚本是否应该被执行。


当然,也有其他数据库版本管理工具,比如 flyway 和 liquibase;


  • Flyway 是独立于数据库的应用、管理并跟踪数据库变更的数据库版本管理工具。用通俗的话讲,Flyway 可以像 Git 管理不同人的代码那样,管理不同人的 sql 脚本,从而做到数据库同步。

  • liquibase 只是在功能上和 Flyway 有差异


不管怎么样,它们底层的原理都是用另外的表记录 SQL 脚本的版本,升级更新是比较版本差异,来决定是否执行。python 自带的 model 模块 python manage.py makemigrations 同样在做类似 的事情数据库版本管理


流水线的关键元素

不管你用什么 CI/CD 平台,开源的 Jenkins, GitLab CI, Teckton, Drone,还是商用的 Azure,阿里云效等,不管是代码化,还是可视化,流水线包含的元素基本都差不多,下面通过不同的示例来说明这些元素的作用和含义。参考:



Agent&Runner(执行代理)

image: "registry.example.com/my/image:latest" #gitlab-ci
复制代码


pool:  vmImage: ubuntu-latest  #auzure
复制代码


agent { label 'linux' }  //jenkins
agent { docker { image 'maven:3-alpine' label 'Ubuntu' args '-v /root/.m2:/root/.m2' }}
复制代码

Parameter(参数变量)

  • **流水线级别参数 **(全局参数),范围限于整个流水线运行时,可被整个流水线其他任务使用

  • 内置全局参数 - 一般称为 built-in(预定义) variable, 有的平台成为环境变量


export CI_JOB_ID="50"export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"export CI_COMMIT_SHORT_SHA="1ecfd275"export CI_COMMIT_REF_NAME="main"export CI_REPOSITORY_URL="https://gitlab-ci-token:[masked]@example.com/gitlab-org/gitlab-foss.git"
1. BUILD_ID : 当前build的id2. BUILD_NUM : 当前build的在pipeline中的build num3. PIPELINE_NAME : pipeline 名称4. PIPELINE_ID: pipeline Id5. GROUP: pipeline 所属的group 名称6. TRIGGER_USER: 触发build的user(event触发的为触发gitlab event的user)7. STAGE_NAME: 当前运行的stage的名称8. STAGE_DISPLAY_NAME : 当前运行的stage的显示名称9. PIPELINE_URL : pipeline在ui中的网页的链接10. BUILD_URL: build 在ui的网页链接11. WORKSPACE: 当前stage运行的工作目录,通常用作拼接绝对路径
复制代码


  • 非内置全局参数


 environment {                 HARBOR_ACCESS_KEY = credentials('harbor-userpwd-pair')             SERVER_ACCESS_KEY = credentials('deploy-userpwd-pair')              GITLAB_API_TOKEN = credentials('gitlab_api_token_secret')           }
复制代码


  • 外部参数 - 一般作为运行时参数


variables:  TEST_SUITE:    description: "The test suite that will run. Valid options are: 'default', 'short', 'full'."    value: "default"  DEPLOY_ENVIRONMENT:    description: "Select the deployment target. Valid options are: 'canary', 'staging', 'production', or a stable branch of your choice."
复制代码


parameters([         separator(name: "PROJECT_PARAMETERS", sectionHeader: "Project Parameters"),        string(name: 'PROJECT_NAME', defaultValue: 'vue-app', description: '项目名称') ,        string(name: 'GIT_URL', defaultValue: 'git@git.xxxx.com.cn:devopsing/vuejs-docker.git', description: 'Git仓库URL') ,])
复制代码


  • 步骤任务参数 (局部参数) - 一般作为某个插件任务的输入参数,也可以使用上个任务的输出作为参数,范围仅限于该任务内

  • 加密变量 - 对特殊变量进行加密处理


secrets:  DATABASE_PASSWORD:    vault: production/db/password@ops  # translates to secret `ops/data/production/db`, field `password`
复制代码

Step(步骤)

参考: https://docs.drone.io/pipeline/overview/


---kind: pipelinetype: dockername: default
steps:- name: backend image: golang commands: - go build - go test
- name: frontend image: node commands: - npm install - npm run test
...
复制代码

Stage(阶段)

一般用于对多个任务(step)进行分组归类,便于管理


 stage('Pull code') {            steps {                echo 'Pull code...'                script {                    git branch: '${Branch_Or_Tags}', credentialsId: 'gitlab-private-key', url: 'git@git.xxxx.com.cn:xxxx/platform-frontend.git'                }            }}
复制代码

Trigger(触发器)

trigger:- master- releases/*
复制代码


trigger_pipeline:stage: deployscript:- 'curl --fail --request POST --form token=$MY_TRIGGER_TOKEN --form ref=main "https://gitlab.example.com/api/v4/projects/123456/trigger/pipeline"'rules:- if: $CI_COMMIT_TAGenvironment: production
复制代码


trigger:  event:  - promote  target:  - production
trigger: type: cron cron: '*/5 * * * *' #每5分钟执行一次
复制代码

制品归档 &缓存 (artifacts&cache)

一般用于 CI 制品的归档,以及 CI 构建的缓存


archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
复制代码


job:  artifacts:    name: "$CI_JOB_NAME"    paths:      - binaries/
cache: &global_cache key: $CI_COMMIT_REF_SLUG paths: - node_modules/ - public/ - vendor/ policy: pull-push
复制代码

集成凭证(Credentials)

参考:



主要用于 CI/CD 流水线对接外部工具,通过 token/pwd/private key 等方式连接外部服务。一般需要在界面做些提前配置,生成 token 或者凭证 ID,将 ID 在 CI/CD yaml 或 jenkinsfile 中使用


withCredentials([usernamePassword(credentialsId: 'amazon', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {// available as an env variable, but will be masked if you try to print it out any which way// note: single quotes prevent Groovy interpolation; expansion is by Bourne Shell, which is what you wantsh 'echo $PASSWORD'// also available as a Groovy variableecho USERNAME// or inside double quotes for string interpolationecho "username is $USERNAME"}
复制代码

Service(服务)

该元素应用于一些复杂的场景,比如需要一种外部(公共)服务为流水线提供某种输入或者结果。您可以将相互依赖的服务用于复杂的作业,例如端到端测试,其中外部 API 需要与自己的数据库通信。例如,对于使用 API 的前端应用程序的端到端测试,并且 API 需要数据库:


end-to-end-tests:  image: node:latest  services:    - name: selenium/standalone-firefox:${FIREFOX_VERSION}      alias: firefox    - name: registry.gitlab.com/organization/private-api:latest      alias: backend-api    - postgres:14.3  variables:    FF_NETWORK_PER_BUILD: 1    POSTGRES_PASSWORD: supersecretpassword    BACKEND_POSTGRES_HOST: postgres  script:    - npm install    - npm test
复制代码

模板(Template)

参考: https://docs.drone.io/template/yaml/某些平台会使用“模板“的概念,其实就是复用的思想,通过加载固定模板实现一些快捷动作


kind: templateload: plugin.yamldata:  name: name  image: image  commands: commands
kind: pipelinetype: dockername: defaultsteps: - name: {{ .input.name }} image: {{ .input.image }} commands: - {{ .input.commands }}
复制代码

执行逻辑控制

参考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html



stage('run-parallel') {  steps {    parallel(      a: {        echo "task 1"      },      b: {        echo "task 2"      }    )  }}
stage('Build') { when { environment name: 'ACTION_TYPE', value: 'CI&CD' } steps { buildDocker("vue") } }
复制代码



stages:  - build  - test  - deploy
image: alpine
build_a: stage: build script: - echo "This job builds something quickly."
build_b: stage: build script: - echo "This job builds something else slowly."
test_a: stage: test needs: [build_a] script: - echo "This test job will start as soon as build_a finishes." - echo "It will not wait for build_b, or other jobs in the build stage, to finish."
test_b: stage: test needs: [build_b] script: - echo "This test job will start as soon as build_b finishes." - echo "It will not wait for other jobs in the build stage to finish."
deploy_a: stage: deploy needs: [test_a] script: - echo "Since build_a and test_a run quickly, this deploy job can run much earlier." - echo "It does not need to wait for build_b or test_b." environment: production
deploy_b: stage: deploy needs: [test_b] script: - echo "Since build_b and test_b run slowly, this deploy job will run much later." environment: production
复制代码

门禁审批

参考:https://learn.microsoft.com/en-us/azure/devops/pipelines/release/deploy-using-approvals?view=azure-devops


pipeline {    agent any    stages {        stage('Example') {            input {                message "Should we continue?"                ok "Yes, we should."                submitter "alice,bob"                parameters {                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')                }            }            steps {                echo "Hello, ${PERSON}, nice to meet you."            }        }    }}
复制代码


pool:    vmImage: ubuntu-latest
jobs:- job: waitForValidation displayName: Wait for external validation pool: server timeoutInMinutes: 4320 # job times out in 3 days steps: - task: ManualValidation@0 timeoutInMinutes: 1440 # task times out in 1 day inputs: notifyUsers: | someone@example.com instructions: 'Please validate the build configuration and resume' onTimeout: 'resume'
复制代码

部署流水线分步骤实施

说了这么多,如果从 0 开始写流水线呢,可以按照下面的步骤,从“点”到“线”结合业务需要串起来,适合自己团队协作开发节奏的流水线才是最好的。


  1. 价值流进行建模并创建简单的可工作流程

  2. 将构建和部署流程自动化

  3. 将单元测试和代码分析自动化

  4. 将验收测试自动化

  5. 将发布自动化



注意的事项开始写流水线需要注意一下几个方面,请考虑进去


  • 确定变量 - 哪些是你每次构建或者部署需要变化的,比如构建参数,代码地址,分支名称,安装版本,部署机器 IP 等,控制变化的,这样保证任务的可复制性,不要写很多 hardcode 进去

  • 变量/命名的规范化,不要为了一时之快,最后换个机器/换个项目,流水线就不能玩了,还要再改

  • 如果可以,最好是封装标准动作成为插件,甚至做成自研平台服务化,让更多团队受益

  • 如果你还在用手动的方式配置流水线,请尽快切换到代码方式,不管是 jenkinsfile,还是 yaml , 一切皆代码 也是 DevOps 提倡的。



流水线案例

案例-1

案例-2


发布于: 刚刚阅读数: 4
用户头像

DevOps Master | 专注于团队工程效能 2019-03-04 加入

多年DevOps企业落地实践经验,热衷于敏捷精益, 研发效能,工具链建设,供应链安全 欢迎关注我的公众号-DevOps在路上

评论

发布
暂无评论
DevOps流水线设计的最佳实践_DevOps_DevOps在路上_InfoQ写作社区