什么是 CI、CD
CI(Continuous Integration)持续集成,CD(Continuous Deployment)持续部署(也包含了持续交付的意思)。
CI 指的是一种开发过程的的自动化流程,在我们提交代码的时候,一般会做以下操作:
- lint 检查,检查代码是否符合规范
- 自动运行测试,检查代码是否能通过测试
这个过程我们可以称之为 CI,也就是持续集成,这个过程是自动化的,也就是说我们不需要手动去执行这些操作,只需要提交代码,这些操作就会自动执行。
CD 指的是在我们 CI 流程通过之后,将代码自动发布到服务器的过程,这个过程也是自动化的。 在有了前面 CI 的一些操作之后,说明我们的代码是可以安全发布到服务器的,所以就可以进行发布的操作。
为什么要使用 CI、CD
实际上,就算没有 CI、CD 的这些花里胡哨的概念,对于一些重复的操作,我们也会尽量想办法会让它们可以自动化实现的,只不过可能效率上没有这么高,但是也是可以的。(比如我们自己写脚本shell).
原来我们 本地代码更新 => 本地打包项目 => 清空服务器相应目录 => 上传项目包至相应目录几个阶段.
CI、CD 相比其他方式的优势在于:
- 构建环境的统一
- 一次配置,多次使用:我们需要做的所有操作都通过配置固定下来了,每次提交代码我们都可以执行相同的操作。
- 可观测性:我们可以通过 CI、CD 的日志来查看每次操作的执行情况,而且每一次的 CI、CD 执行的日志都会保留下来,这样我们就可以很方便地查看每一次操作的执行情况。
- 自动化:我们不需要手动去执行 CI、CD 的操作,只需要提交代码,CI、CD 就会自动执行。
- 少量配置:一般的代码托管平台都会提供 CI、CD 的功能,我们只需要简单的配置一下就可以使用了。同时其实不同平台的 CI、CD 配置也是有很多相似之处的,所以我们只需要学习一种配置方式,就可以在不同平台上使用了。
gitlab CI、CD
- 在开发人员提交代码之后,会触发 gitlab 的 CI 流水线。也就是上图的 CI PIPELINE,也就是中间的部分。
- 在 CI 流水线中,我们可以配置多个任务。比如上图的 build、unit test、integration tests 等,也就是构建、单元测试、集成测试等。
- 在 CI 流水线都通过之后,会触发 CD 流水线。也就是上图的 CD PIPELINE,也就是右边的部分。
- 在 CD 流水线中,我们可以配置多个任务。比如上图的 staging、production 等,也就是部署到测试环境、部署到生产环境等。
在 CD 流程结束之后,我们就可以在服务器上看到我们的代码了。
先来了解一下 gitlab CI、CD 中的一些基本概念
- pipeline:流水线,也就是 CI、CD 的整个流程,包含了多个 stage,每个 stage 又包含了多个 job。
- stage: 一个阶段,一个阶段中可以包含多个任务(job),这些任务会并行执行,但是下一个 stage 的 job 只有在上一个 stage 的 job 执行通过之后才会执行。
- job:一个任务,这是 CI、CD 中最基本的概念,也是最小的执行单元。一个 stage 中可以包含多个 job,同时这些 job 会并行执行。
- runner:执行器,也就是执行 job 的机器,runner 跟 gitlab 是分离的,runner 需要我们自己去安装,然后注册到 gitlab 上(不需要跟 gitlab 在同一个服务器上,这样有个好处就是可以很方便实现多个机器来同时处理 gitlab 的 CI、CD 的任务)。 一个gitlab可以有多个runner
- tags: job 需要指定标签,job 可以指定一个或多个标签(必须指定,否则 job 不会被执行),这样 job 就只会在指定标签的 runner 上执行。
- cache: 缓存,可以缓存一些文件,这样下次流水线执行的时候就不需要重新下载了,可以提高执行效率。
- artifacts: 这代表这构建过程中所产生的一些文件,比如打包好的文件,这些文件可以在下一个 stage 中使用,也可以在 pipeline 执行结束之后下载下来。
- variables:变量,可以在 pipeline 中定义一些变量,这些变量可以在 pipeline 的所有 stage 和 job 中使用。
- services:服务,可以在 pipeline 中启动一些服务,比如 mysql、redis 等,这样我们就可以在 pipeline 中使用这些服务了(常常用在测试的时候模拟一个服务)。
- script: 脚本,可以在 job 中定义一些脚本,这些脚本会在 job 执行的时候执行。
CI、CD 的工作模型
我们以下面的配置为例子,简单说明一下 pipeline、stage、job 的工作模型,以及 cache 和 artifacts 的作用:
# 定义一个 `pipeline` 的所有阶段,一个 `pipeline` 可以包含多个 `stage`,每个 `stage` 又包含多个 `job`。
# stage 的顺序是按照数组的顺序来执行的,也就是说 stage1 会先执行,然后才会执行 stage2。
stages:
- stage1 # stage 的名称
- stage2
# 定义一个 `job`,一个 `job` 就是一个任务,也是最小的执行单元。
job1:
stage: stage1 # 指定这个 `job` 所属的 `stage`,这个 `job` 只会在 `stage1` 执行。
script: # 指定这个 `job` 的脚本,这个脚本会在 `job` 执行的时候执行。
- echo "hello world" > "test.txt"
tags: # 指定这个 `job` 所属的 `runner` 的标签,这个 `job` 只会在标签为 `tag1` 的 `runner` 上执行。
- tag1
# cache 可以在当前 `pipeline` 后续的 `job` 中使用,也可以在后续的 `pipeline` 中使用。
cache: # 指定这个 `job` 的缓存,这个缓存会在 `job` 执行结束之后保存起来,下次执行的时候会先从缓存中读取,如果没有缓存,就会重新下载。
key: $CI_COMMIT_REF_SLUG # 缓存的 key
paths: # 缓存的路径
- node_modules/
artifacts: # 指定这个 `job` 的构建产物,这个构建产物会在 `job` 执行结束之后保存起来。可以在下一个 stage 中使用,也可以在 pipeline 执行结束之后下载下来。
paths:
- test.txt
job2:
stage: stage1
script:
- cat test.txt # 这里读取不到
tags:
- tag1
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
# 指定这个 `job` 的缓存策略,只会读取缓存,不会写入缓存。默认是既读取又写入,在 job 开始的时候读取,在 job 结束的时候写入。
# 但是实际上,只有在安装依赖的时候是需要写入缓存的,其他 job 都使用 pull 即可。
policy: pull
# job3 和 job4 都属于 stage2,所以 job3 和 job4 会并行执行。
# job3 和 job4 都指定了 tag2 标签,所以 job3 和 job4 只会在标签为 tag2 的 runner 上执行。
# 同时,在 job1 中,我们指定了 test.txt 作为构建产物,所以 job3 和 job4 都可以使用 test.txt 这个文件。
job3:
stage: stage2
script:
- cat test.txt
tags:
- tag2
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
policy: pull
job4:
stage: stage2
script:
- cat test.txt
tags:
- tag2
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
policy: pull
这里一定是双双并行执行的
为什么缓存node_modules会快
上面这个流程还有几个问题:
- 比如出现错误, 我要执行对应job怎么做?
- 现在都是按照stage并行执行的, 如果我有个场景就是job1结束, 立马执行job3, 不用管job2的执行状态?
when
前面说过,stages关键字可以控制每个任务的执行顺序,且后一个stage会等待前一个stage执行成功后才会执行,那如果我们想要达到前一个stage失败了,后面的stage仍然能够执行的效果
- on_success:只有前面stages的所有工作成功时才执行,这是默认值。
- on_failure:当前面stages中任意一个jobs失败后执行
- always:无论前面stages中jobs状态如何都执行
- manual:手动执行
- delayed:延迟执行
# 官方示例
stages:
- build
- cleanup_build
- test
- deploy
- cleanup
build_job:
stage: build
script:
- make build
# 如果build_job任务失败,则会触发该任务执行
cleanup_build_job:
stage: cleanup_build
script:
- cleanup build when failed
when: on_failure
test_job:
stage: test
script:
- make test
deploy_job:
stage: deploy
script:
- make deploy
when: manual
# 总是执行
cleanup_job:
stage: cleanup
script:
- cleanup after jobs
when: always
needs
可无序执行job,无需按照stage顺序运行某些job,可以让多个stage同时运行
stages:
- build
- test
- deploy
module-a-build:
stage: build
script:
- echo "hello3a"
- sleep 10
module-b-build:
stage: build
script:
- echo "hello3b"
- sleep 10
module-a-test:
stage: test
script:
- echo "hello3a"
- sleep 10
module-b-test:
stage: test
script:
- echo "hello3b"
- sleep 10
运行上面的流水线
但现在我们可能需要,当 module-a-build 运行完成之后就运行 module-a-test,当 module-b-build 运行完成之后运行 module-b-test。这时候就需要 needs 了
stages:
- build
- test
module-a-build:
stage: build
script:
- echo "hello3a"
- sleep 10
module-b-build:
stage: build
script:
- echo "hello3b"
- sleep 30
module-a-test:
stage: test
script:
- echo "hello3a"
- sleep 10
needs: ["module-a-build"] # 当 module-a-build 运行完成之后就运行 module-a-test
module-b-test:
stage: test
script:
- echo "hello3b"
- sleep 10
needs: ["module-b-build"] # 当 module-b-build 运行完成之后就运行 module-b-test
运行流水线
可以查看作业依赖项
image
该关键字指定一个任务(job)所使用的docker镜像,例如image: python:latest使用Python的最新镜像。
image: harbor.duowan.com/shopline-ads/node18_pnpm8_n9:v11
stages
指定任务执行的先后顺序
stages关键字有两个特性:
1.如果两个任务对应的stage名相同,则这两个任务会并行运行
2.下一个stage关联的任务会等待上一个stage执行成功后才继续运行,失败则不运行
image: python:3.6
stages:
- build
- test
- deploy
# build-job1和build-job2会并行执行
build-job1:
stage: build
script:
- echo $PWD
build-job2:
stage: build
script:
- echo $PWD
# 这个任务会在build-job1和build-job2执行成功后再运行
test-job:
stage: test
script:
- echo $PWD
# 注意定义的stage都需要用到
deploy-job:
stage: deploy
script:
- echo $PWD
注:如果想要控制某一个stage在最开始,或者最后执行,可以使用.pre 和 .post 关键字 (这两个默认会添加)
build-job2:
stage: .pre
script:
- echo $PWD
sp可以看到这个: git.duowan.com/webs/ecom-o…
only / except
为了更好地说明,我们还需引入一个Pipelines的概念,Pipelines就是我们在Gitlab的UI界面看到一行行记录:
每个Pipelines内部就是我们定义的任务的执行情况
前面提到,通过stages关键字可以控制任务执行的先后顺序,而通过only/except关键字控制的是任务的触发条件。
image: python:3.6
# 第一种方式
job1:
only:
- master
script:
- echo $PWD
# 第二种方式
job2:
only:
- /^(test2|sp-optimize-public)$/
script:
- echo $PWD
# 第三种方式
job3:
only:
variables:
- $CI_COMMIT_REF_NAME == "master"
# $CI_COMMIT_REF_NAME 这样的变量默认就是存在的 比如这样的 $CI_JOB_ID job的id
script:
- echo $PWD
# 第四种方式 当打了tag才会触发Pipelines
job4:
only:
- tags
script:
- echo $PWD
tags
该关键字指定了使用哪个Runner(哪个机器)去执行我们的任务
default
使用 default 可以定义每个 job 的参数,如果 job 里有,会覆盖 default 里的,例如下面的代码
default: # 定义了一个默认的参数
tags: # 如果 job 里没有 tages,就使用这个 tags
- build
retry: # 如果 job 里没有 retry,就使用这个 tags
max: 2
before_script: # 如果 job 里没有 before_script,就使用这个 tags
- echo "before_script"
stages:
- build
- test
build:
stage: build
before_script:
- echo "我是 job 里的"
script:
- echo "我是 build 的 job"
test:
stage: test
script:
- echo "test 的 job"
运行流水线,查看 build 的日志
在来看下 test
cache
该关键字指定了需要缓存的文件夹或者文件,目的是为了加快执行速度。
官方: docs.gitlab.com/ee/ci/yaml/…
- cache 是用来缓存依赖的,比如 node_modules 文件夹,它可以加快后续 pipeline 的执行流程,因为避免了重复的依赖安装。
cache 可以在当前 pipeline 后续的 job 中使用,也可以在后续的 pipeline 中使用。
cache:
key:
files:
- pnpm-lock.yaml # 表示在lock文件发生变化时重新生成缓存
paths:
- node_modules
- packages/*/node_modules
policy: pull-push # 只拉取 cache,在 job 执行完毕之后不上传 cache
gitlab CI 的 cache 有一个 policy 属性,它的值默认是 pull-push,也就是在 job 开始执行的时候会拉取缓存,在 job 执行结束的时候会将缓存指定文件夹的内容上传到 gitlab 中。
但是在实际使用中,我们其实只需要在安装依赖的时候上传这些缓存,其他时候都只是读取缓存的。所以我们在安装依赖的 job 中使用默认的 policy,而在后续的 job 中,我们可以通过 policy: pull 来指定只拉取缓存,不上传缓存。
如果不指定key, 会有缓存覆盖问题
如果不使用key,不同stage的缓存都会存在default下,生成cache.zip覆盖原来的缓存。
artifacts
类似cache关键字,也可以缓存文件或者文件夹,不同的是,这些文件可以在Gitlab的UI界面中下载,比如可用来存储Android打包生成的apk。只在当前pipelines 生效
artifacts 只会在当前 pipeline 后续的 stage 中共享,不会在 pipeline 之间共享。
因为我们的 artifacts 有时候只是生成一些需要部署到服务器的东西,然后在下一个 stage 使用,所以是不需要长期保留的。所以我们可以通过 expire_in 来指定一个比较短的 artifacts 的过期时间。
artifacts:
paths:
- ./packages/sp-admin/map
- ./packages/sp-admin/.version
expire_in: 1 hour
variables
- Gitlab-CI的变量有几种,分别为预先定义的环境变量, 比如CI_COMMIT_SHA, CI_COMMIT_MESSAGE;
我们可以获取分支名,提交的消息,从而判断任务是否需要执行
- 通过Web界面Settings/CI-CD设置的变量
而Web界面Settings/CI-CD设置的变量,则非常适合用于存储像公私钥,账号密码这些不便公开的数据。因为只有Owner和Maintainer能够查看。
- 自己在.gitlab-ci.yml定义的变量。 ---- 使用方便
cache vs artifacts
初次使用的人,可能会对这个东西有点迷惑,因为它们好像都是缓存,但是实际上,它们的用途是不一样的。
- cache 是用来缓存依赖的,比如 node_modules 文件夹,它可以加快后续 pipeline 的执行流程,因为避免了重复的依赖安装。
- artifacts 是用来缓存构建产物的,比如 build 之后生成的静态文件,它可以在后续的 stage 中使用。表示的是单个 pipeline 中的不同 stage 之间的共享。
需要特别注意的是:cache 是跨流水线共享的,而 artifacts 只会在当前流水线的后续 stage 共享