什么是 CI、CD
CI(Continuous Integration)持续集成,CD(Continuous Deployment)持续部署(也包含了持续交付的意思)。
CI 指的是一种开发过程的的自动化流程,在我们提交代码的时候,一般会做以下操作:
lint
检查,检查代码是否符合规范- 自动运行测试,检查代码是否能通过测试
这个过程我们可以称之为 CI,也就是持续集成,这个过程是自动化的,也就是说我们不需要手动去执行这些操作,只需要提交代码,这些操作就会自动执行。
CD 指的是在我们 CI 流程通过之后,将代码自动发布到服务器的过程,这个过程也是自动化的。
在有了前面 CI 的一些操作之后,说明我们的代码是可以安全发布到服务器的,所以就可以进行发布的操作。
为什么要使用 CI、CD
实际上,就算没有 CI、CD 的这些花里胡哨的概念,对于一些重复的操作,我们也会尽量想办法会让它们可以自动化实现的,只不过可能效率上没有这么高,但是也是可以的。
CI、CD 相比其他方式的优势在于:
- 一次配置,多次使用:我们需要做的所有操作都通过配置固定下来了,每次提交代码我们都可以执行相同的操作。
- 可观测性:我们可以通过 CI、CD 的日志来查看每次操作的执行情况,而且每一次的 CI、CD 执行的日志都会保留下来,这样我们就可以很方便地查看每一次操作的执行情况。
- 自动化:我们不需要手动去执行 CI、CD 的操作,只需要提交代码,CI、CD 就会自动执行。
- 少量配置:一般的代码托管平台都会提供 CI、CD 的功能,我们只需要简单的配置一下就可以使用了。同时其实不同平台的 CI、CD 配置也是有很多相似之处的,所以我们只需要学习一种配置方式,就可以在不同平台上使用了。
gitlab CI、CD
在开始之前,我们可以通过下图来了解一下 CI、CD 的整体流程:
- 在开发人员提交代码之后,会触发 gitlab 的 CI 流水线。也就是上图的
CI PIPELINE
,也就是中间的部分。 - 在 CI 流水线中,我们可以配置多个任务。比如上图的
build
、unit test
、integration tests
等,也就是构建、单元测试、集成测试等。 - 在 CI 流水线都通过之后,会触发 CD 流水线。也就是上图的
CD PIPELINE
,也就是右边的部分。 - 在 CD 流水线中,我们可以配置多个任务。比如上图的
staging
、production
等,也就是部署到测试环境、部署到生产环境等。
在 CD 流程结束之后,我们就可以在服务器上看到我们的代码了。
gitlab CI、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 的任务)。tag
:runner
和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
的作用:
ci
配置文件(也就是一个 pipeline
的所有任务):
# 定义一个 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:
- tag1
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
policy: pull
job4:
stage: stage2
script:
- cat test.txt
tags:
- tag1
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
policy: pull
上面的配置文件的 pipeline
执行过程可以用下面的图来表示:
说明:
- 上面的图有两个
pipeline
被执行了,但是pipeline2
没有全部画出来 - 其中,在
pipeline 1
中,stage1
中的job
会先被执行,然后才会执行stage2
中的job
。 stage1
中的job1
和job2
是可以并行执行的,这也就是stage
的本质上的含义,表示了一个阶段中不同的任务,比如我们做测试的时候,可以同时对不同模块做测试。job1
和job2
都指定了tag1
标签,所以job1
和job2
只会在标签为tag1
的runner
上执行。job1
中,我们创建了一个test.txt
文件,这个文件会作为stage1
的构建产物,它可以在stage2
中被使用,也就是job3
和job4
都可以读取到这个文件。一种实际的场景是,前端部署的时候,build 之后会生成可以部署的静态文件,这些静态文件就会被保留到部署相关的 stage 中。需要注意的是,artifacts
只会在当前
pipeline
后续的stage
中共享,不会在pipeline
之间共享。- 同时,在
job1
中,我们也指定了cache
,这个cache
会在job1
执行结束之后保存起来,不同于artifacts
,cache
是可以在不同的pipeline
之间共享的。一种很常见的使用场景就是我们代码的依赖,比如node_modules
文件夹,它可以加快后续pipeline
的执行流程,因为避免了重复的依赖安装。
需要特别注意的是:
cache
是跨流水线共享的,而artifacts
只会在当前流水线的后续 stage 共享。
gitlab runner 和 executor
gitlab runner
在 CI/CD 中是一个非常重要的东西,因为我们写的 CI/CD 的配置就是在 runner
上运行的,如果我们想要执行 CI/CD 任务,我们必须先安装配置 gitlab-runner
。
其中 runner
是一台执行 CI/CD 脚本的机器(也就是安装了 gitlab-runner
的机器)。这个机器可以部署在 gitlab 服务器以外的任意一台电脑上,当然也可以跟 gitlab 在同一台服务器。
而每一个 runner
会对应一种特定的 executor
,executor
就是我们执行 CI/CD 里面 script
的环境。比如如果我们指定了 executor
类型为 docker
,那么我们 CI/CD 脚本里面的 script
将会在一个独立的 docker 容器中执行。
简单来说,runner
是执行 CI/CD 脚本的机器,这个机器上有不同类型的 executor
,一个 executor
代表着一个不同类型的命令行终端,最常见的是 shell
、docker
,当然也支持 widnows 的 powershell
。
我们可以通过下图来了解一下 gitlab 是怎么跟 runner
配合的:
gitlab 是通过
tags
来找到运行脚本的runner
的,如果job
的tags
跟runner
的tags
匹配了,就可以将那个job
放到runner
上处理。
说明:
- 我们在两台机器上安装了
gitlab-runner
,它们的 IP 是192.168.2.123
和192.168.2.234
。 test-job
的tags
中包含了api
这个tag
,而runner1
的tags
也包含了api
这个tag
,因此test-job
会被runner1
执行。- 同理,
npm-install
这个job
会被runner3
处理。
从上图我们可以看到,其实不同的
runner
是有可能位于不同的机器上的。
总结
最后,总结一下本文中一些比较关键的内容:
- gitlab 中的一些基本概念:
pipeline
:代表了一次 CI 的执行过程,它包含了多个stage
。stage
:代表了一组job
的集合,stage
会按照顺序执行。job
:代表了一个具体的任务,比如build
、test
、deploy
等。
- 一个
stage
中的多个job
是可以并行执行的。但是下一个stage
的job
必须要等到上一个stage
的所有job
都执行完毕之后才会执行。 cache
和artifacts
的区别:cache
是用来缓存依赖的,比如node_modules
文件夹,它可以加快后续pipeline
的执行流程,因为避免了重复的依赖安装。artifacts
是用来缓存构建产物的,比如build
之后生成的静态文件,它可以在后续的stage
中使用。表示的是单个 pipeline 中的不同 stage 之间的共享。
cache
在安装依赖的job
中才需要使用默认的policy
,也就是pull-push
,在其他不需要安装依赖的job
中使用pull
就可以了,不需要上传缓存。cache
的key
可以指定多个文件,这样在指定的文件变动的时候,缓存会失效,这往往用在依赖相关的文件中。- 可以使用
services
关键字来指定需要启动的服务,比如mysql
、redis
等,在 job 中可以连接到这些 services,从而方便进行测试。 - 可以使用
yaml
的anchor
机制来复用一些配置片段,可以少写很多重复的配置。 - 一个
job
必须运行在某个runner
上,job
和runner
的关联是通过tag
来指定的。