谈到到DevOps,持续交付流水线是绕不开的一个话题,相对于其他实践,通过流水线来实现快速高质量的交付价值是相对能快速见效的,特别对于开发测试人员,能够获得实实在在的收益。很多文章介绍流水线,不管是jenkins,gitlab-ci, 流水线,还是drone, github action 流水线, 文章都很多,但是不管什么工具,流水线设计的思路是一致的。于此同时,在实践过程中,发现大家对流水像有些误区,不是一大堆流水线,就是一个流水线调一个超级复杂的脚本,各种硬编码和环境依赖,所以希望通过这篇文章能够给大家分享自己对于Pipeline流水线的设计心得体会。
持续集成(CI)是在源代码变更后自动检测、拉取、构建和(在大多数情况下)进行单元测试的过程
对项目而言,持续集成(CI)的目标是确保开发人员新提交的变更是好的,不会发生break build; 并且最终的主干分支一直处于可发布的状态,
对于开发人员而言,要求他们必须频繁地向主干提交代码,相应也可以即时得到问题的反馈。实时获取到相关错误的信息,以便快速地定位与解决问题
显然这个过程可以大大地提高开发人员以及整个IT团队的工作效率,避免陷入好几天得不到好的“部署产出”,影响后续的测试和交付。
持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的「预发布环境」(production-like environments)中。交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段 持续交付并不是指软件每一个改动都要尽快部署到产品环境中,它指的是任何的代码修改都可以在任何时候实时部署。
强调: 1、手动部署 2、有部署的能力,但不一定部署
代码通过评审之后,自动部署到生产环境中。持续部署是持续交付的最高阶段。
强调 1、持续部署是自动的 2、持续部署是持续交付的最高阶段 3、持续交付表示的是一种能力,持续部署则是一种方式
参考: https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html
这里非常推荐以版本控制系统为源的构建流水线设计,从每一位开发人员提交代码即可对当前提交代码进行检查编译构建,尽快将错误反馈给每位提交人员。
对于DevOps流水线,主要是由各类任务串联起来,而对于任务本身又分为两张类型,一种是自动化任务,一种是人工执行任务。具体如下:
而通常我们看到的流水线基本都由上述两类任务组合编排而成,一个流水线可以是完全自动化执行,也可以中间加入了人工干预节点,在人工干预处理后再继续朝下执行。比如流水线中到了测试部署完成后,可以到测试环境人工验证环节,只有人工验证通过再流转到迁移发布到生产环境动作任务。
DevOps流水线实际上和我们原来经常谈到的持续集成最佳实践是相当类似的,较大的一个差异点就在于引入了容器化技术来实现自动化部署和应用托管。至于在DevOps实践中,是否必须马上将项目切换到微服务架构框架模式,反而不是必须得。
在整个DevOps流水线中,我们实际上强调个一个关键点在于“一套Docker镜像文件+多套环境配置+多套构建版本标签”做法。以确保我们最终构建和测试通过的版本就是我们部署到生产环境的版本。
构建操作只有一次,而后面到测试环境,到UAT环境,到生产环境,都属于是镜像的环境迁移和部署。而不涉及到需要再次重新打包的问题。这个是持续集成,也是DevOps的基本要求。
今天谈DevOps流水线编排,主要是对流水线编排本身的灵活性进一步思考。
注意常规方式下构建完执行进行部署操作,部署操作一般就是将构建的结果拷贝到我们的测试环境服务器,同时对初始化脚本进行启动等。而在DevOps下,该操作会变成两个操作,即一个打包,一个部署。打包是将构建完成的内容制作为镜像,部署是将镜像部署到具体的资源池和指定集群。
那么首先要考虑构建操作和打包操作如何松耦合开,打包操作简单来就是就是一个镜像制作,需要的是构建操作产生的输出。我们可以对其输出和需要拷贝的内容在构建的时候进行约定。而打包任务则是一个标准化的镜像制作任务,我们需要考虑的仅仅是基于
1)基于哪个基础镜像
2)中间件容器默认目录设置
3)初始化启动命令。
即在实际的打包任务设计的时候,我们不会指定具体的部署包和部署文件,这个完全由编排的时候由上游输入。
任务节点间松耦合设计的意义
这种松耦合设计才能够使流水线编排更加灵活。比如我们在进行了构建打包后,我们希望同时讲打包内容部署到开发环境和测试环境。那么则是打包动作完成后需要对接两个应用部署任务。这两个部署任务都依托上面的打包结果进行自动化部署,可以并行进行。
对于测试环境部署完成后,我们需要进行测试人员手工验证测试,如果测试通过,我们打标签后希望能够直接发布到UAT环境。而这种操作我们也希望在一个流水线来设计和完成。这样我们更加容易在持续集成看板上看到整个版本构建和迁移的完整过程。如果这是在一个大流水线里面,那么对于UAT环境部署任务就需要一直去追溯流水线上的最近的一个打包任务节点,同时取该任务节点产生的输出来进行相应的环境部署操作。
在谈DevOps的时候,一个重点就是和QA/QC的协同,因此在流水线编排的时候一定要考虑各类测试节点,包括静态代码检查,自动化的单元测试,人工的测试验证。同时最好基于持续集成实践,能够将测试过程和整个自动化构建过程紧密结合起来。
简单来说,测试人员发现build1.0.0001版本4个bug并提交,那么在下次自动化构建完成并单元测试通过后,测试人员能够很清楚的看到哪些Bug已经修改并可以在新构建的版本进行验证。只有这样才能够形成闭环,整个流水线作业才能够更好的发挥协同作用。
流水线除了任务步骤的编排,更重要的核心是最佳工程实践的体现。过去传统的思维,自动化就是写个shell/python脚本批量执行,在DevOps/微服务时代,这一招太out了,每种工程实践的背后都有需要解决的问题,通过在流水线设计中注入最佳的工程实践,可以让流水线的价值最大化,也让流水线更高级不是嘛。
是否需要一条完整的流水线?流水线是越多越好,还是越少越好?
建议按照场景来设计,一条流水线通吃所有流程是不现实的,搞了好多流水线(比如一个构建就一个流水线,一个复制操作就一个流水线)这些都是不可取的,维护成本巨大,得不偿失。
流水线按照场景分类如下:
过程如下:
以Jenkins实现为例,
通过webhook触发CI构建,首先配置Jenkins项目
其次是Gitlab的配置
剩下的就是编写Jenkinsfile了,下面列出几个关键点
1.获取gitlab数据中的分支名称,作为本次构建的分支名称。
2.获取gitlab数据中的用户邮箱,作为构建失败后通知对象。
过程如下:
合并流水线设计:合并流水线的步骤其实跟提交流水线很类似,但是在代码质量检查的步骤中严格要求检查质量阈的状态,当质量阈状态为错误的时候,需要立即失败并通知发起人。
第一次设计
第二次设计(借助GitlabCI)- 优化点:加入MR构建失败拦截,成功自动合并
除了代码有版本,其实SQL也有“版本的”,SQL脚本的版本对于产品的升级回滚至关重要。
一般对SQL的集成,会包含如下要素
当然,也有其他数据库版本管理工具,比如 flyway 和 liquibase;
不管怎么样,它们底层的原理都是用另外的表记录SQL脚本的版本,升级更新是比较版本差异,来决定是否执行。
python自带的model模块 python manage.py makemigrations 同样在做类似 的事情
数据库版本管理
不管你用什么CI/CD平台,开源的Jenkins, GitLab CI, Teckton, Drone,还是商用的Azure,阿里云效等,不管是代码化,还是可视化,流水线包含的元素基本都差不多,下面通过不同的示例来说明这些元素的作用和含义。
参考:
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'
}
}
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的id
2. BUILD_NUM : 当前build的在pipeline中的build num
3. PIPELINE_NAME : pipeline 名称
4. PIPELINE_ID: pipeline Id
5. 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`
参考: https://docs.drone.io/pipeline/overview/
---
kind: pipeline
type: docker
name: default
steps:
- name: backend
image: golang
commands:
- go build
- go test
- name: frontend
image: node
commands:
- npm install
- npm run test
...
一般用于对多个任务(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:
- master
- releases/*
trigger_pipeline:
stage: deploy
script:
- '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_TAG
environment: production
trigger:
event:
- promote
target:
- production
trigger:
type: cron
cron: '*/5 * * * *' #每5分钟执行一次
一般用于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
参考:
主要用于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 want
sh 'echo $PASSWORD'
// also available as a Groovy variable
echo USERNAME
// or inside double quotes for string interpolation
echo "username is $USERNAME"
}
该元素应用于一些复杂的场景,比如需要一种外部(公共)服务为流水线提供某种输入或者结果。
您可以将相互依赖的服务用于复杂的作业,例如端到端测试,其中外部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
参考: https://docs.drone.io/template/yaml/
某些平台会使用“模板“的概念,其实就是复用的思想,通过加载固定模板实现一些快捷动作
kind: template
load: plugin.yaml
data:
name: name
image: image
commands: commands
kind: pipeline
type: docker
name: default
steps:
- 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
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开始写流水线呢,可以按照下面的步骤,从“点”到“线”结合业务需要串起来,适合自己团队协作开发节奏的流水线才是最好的。
注意的事项
开始写流水线需要注意一下几个方面,请考虑进去