gitlab 或许咱们很常用,CI、CD 也应该早有耳闻,可是或许还没有去真实地了解过,这篇文章便是我对 gitlab CI、CD 的一些了解,以及踩过的一些坑,希望能协助到咱们。
什么是 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 装备也是有许多相似之处的,所以咱们只需求学习一种装备方法,就能够在不同渠道上运用了。
在自己的实践中,都是运用了 docker executor 来履行 CI/CD 脚本,这样就能够确保多个项目都在各自独立的作业空间中打包、构建等,比方,几个前端项目依靠于不同的 node 版别,假如直接在 shell 中履行,总会有时分会呈现古怪的问题。运用 gitlab 的 CI/CD 能够确保不受其他的项目影响,一起,后期也不会呈现一个版别能够另一个版别又不能够的问题,由于在 CI/CD 流程中,假如运用的 docker 镜像版别履行脚本不经过,那直接就失利了,立刻就能够发现了。
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 的 CI、CD 是一个很巨大的论题,一起许多内容或许比较少用,所以本文只是介绍个人在实践中用到的一些内容,其他的东西假如有需求,能够自行查阅官方文档。
指定特定分支才会履行的 job
这个算是根本操作了,咱们能够经过 only
来指定特定分支才会履行的 job
,也有其他办法能够完成,比方 rules
,详细请参考官方文档。
deploy-job:
stage: deploy
# 当时的这个 job 只会在 master 分支代码更新的时分会履行
only:
- "master"
不同 job 之间的依靠
这个也是根本操作,咱们能够经过 needs
来指定不同 job
之间的依靠联系,比方 job1
依靠 job2
,那么 job1
就会在 job2
履行完毕之后才会履行。
job1:
stage: deploy
needs:
- job2
指定履行 job 的 runner
咱们能够经过 tags
来指定 job
履行的 runner
,比方咱们能够指定 job
只能在 api
标签的 runner
上履行。
build-job:
stage: build
tags:
- api
假如咱们没有标签为
api
的runner
,那么这个job
就会一直不会被履行,所以需求确保咱们装备的tag
有对应的runner
。
指定 job 的 docker image
留意:这个只在咱们的
runner
的executor
为docker
的时分才会生效。也便是咱们的runner
是一个docker
容器。
有时分,咱们需求履行一些特定指令,可是咱们大局的 docker
镜像里边没有,或许只需求一个特定的 docker
镜像,这个时分咱们能够经过 image
来指定 job
的 docker
镜像。
deploy-job:
stage: deploy
tags:
- api
# 指定 runner 的 docker image
image: eleven26/rsync:1.3.0
script:
# 下面这个指令只在上面指定的 docker 镜像中存在
- rsync . root@example.com:/home/www/foo
为咱们的集成测验指定一个 service
在咱们的 CI 流程中,或许会有一些集成测验需求运用到一些服务,比方咱们的 mysql
,这个时分咱们能够经过 services
来指定咱们需求的服务。
test_rabbitmq:
# 这会发动一个 rabbitmq 3.8 的 docker 容器,咱们的 job 就能够运用这个容器了。
# 咱们的 job 能够衔接到一个 rabbitmq 的服务,然后进行测验。
# 需求留意的是,这个容器只会在当时 job 履行的时分存在,履行完毕之后就会被删去。所以发生的数据不会被保留。
services:
- rabbitmq:3.8
stage: test
only:
- master
tags:
- go
script:
# 下面的测验指令会衔接到上面发动的 rabbitmq 服务
- "go test -v -cover ./pkg/rabbitmq"
复用 yaml 装备片段
在 yaml
中,有一种机制能够让咱们复用 yaml
装备片段,比方:
# 发布代码的 job
.deploy-job: &release-job
tags:
- api
image: eleven26/rsync:1.3.0
script:
- rsync . root@example.com:/home/www/foo
deploy-release:
<<: *release-job
stage: deploy
only:
- "release"
deploy-master:
<<: *release-job
stage: deploy
only:
- "master"
上面的代码中,咱们界说了一个 release-job
的装备片段,然后在 deploy-release
和 deploy-master
中,咱们都引用了这个装备片段,这样咱们就能够复用这个装备片段了。
等同于下面的代码:
# 发布代码的 job
.deploy-job: &release-job
tags:
- api
image: eleven26/rsync:1.3.0
script:
- rsync . root@example.com:/home/www/foo
deploy-release:
tags:
- api
image: eleven26/rsync:1.3.0
script:
- rsync . root@example.com:/home/www/foo
stage: deploy
only:
- "release"
deploy-master:
tags:
- api
image: eleven26/rsync:1.3.0
script:
- rsync . root@example.com:/home/www/foo
stage: deploy
only:
- "master"
在
yaml
的术语中,这一种机制叫做anchor
。
cache vs artifacts
初度运用的人,或许会对这个东西有点利诱,由于它们好像都是缓存,可是实际上,它们的用处是不一样的。
-
cache
是用来缓存依靠的,比方node_modules
文件夹,它能够加速后续pipeline
的履行流程,由于避免了重复的依靠装置。 -
artifacts
是用来缓存构建产品的,比方build
之后生成的静态文件,它能够在后续的stage
中运用。表明的是单个 pipeline 中的不同 stage 之间的同享。
指定 artifacts 的过期时刻
咱们能够经过 expire_in
来指定 artifacts
的过期时刻,比方:
job1:
stage: build
only:
- "release"
image: eleven26/apidoc:1.0.0
tags:
- api
artifacts:
paths:
- public
expire_in: 1 hour
由于咱们的 artifacts
有时分只是生成一些需求布置到服务器的东西,然后鄙人一个 stage
运用,所以是不需求长时刻保留的。所以咱们能够经过 expire_in
来指定一个比较短的 artifacts
的过期时刻。
cache 只 pull 不 push
gitlab CI 的 cache
有一个 policy
特点,它的值默许是 pull-push
,也便是在 job
开端履行的时分会拉取缓存,在 job
履行完毕的时分会将缓存指定文件夹的内容上传到 gitlab 中。
可是在实际运用中,咱们其实只需求在装置依靠的时分上传这些缓存,其他时分都只是读取缓存的。所以咱们在装置依靠的 job 中运用默许的 policy
,而在后续的 job
中,咱们能够经过 policy: pull
来指定只拉取缓存,不上传缓存。
job:
tags:
- api
image: eleven26/rsync:1.3.0
cache:
key:
files:
- composer.json
- composer.lock
paths:
- "vendor/"
policy: pull # 只拉取 vendor,在 job 履行完毕之后不上传 vendor
cache 的 key 运用文件
这一个特性对错常有用的,在现代软件工程的实践中,往往经过 *.lock
文件来记录咱们运用的额依靠的详细版别,以确保在不同环境中运用的时分保持一致的行为。
所以,相应的,咱们的缓存也能够在 *.lock
这类文件发生变化的时分,从头生成缓存。上面的比方就运用了这种机制。
script 中运用多行指令
在 script
中,咱们能够运用多行指令,比方:
job:
script:
# 咱们能够经过下面这种方法来写多行的 shell 指令,也便是以一个竖线开端,然后换行
- |
if [ "$release_host" != "" ]; then
host=$release_host
fi
CD – 如何同步代码到服务器
假如咱们的项目需求布置到服务器上,那么咱们还需求做一些额定的操作,比方同步代码到服务器上。 假如咱们的 gitlab 是经过容器履行的,或许咱们的 runner 的 executor 是 docker,那么有一种比较常见的办法是经过 ssh 私钥来进行布置。
咱们能够经过以下流程来完成:
- 新建一对 ssh key,比方
id_rsa
和id_rsa.pub
。 - 将
id_rsa.pub
的内容增加到服务器的authorized_keys
文件中。 - 将
id_rsa
上传到 gitlab 中(在项目的 CI/CD 装备中,装备一个变量,变量名为PRIVATE_KEY
,内容为id_rsa
的内容,类型为file
)。 - 在咱们的
ci
装备文件中,增加如下装备即可:
before_script:
- chmod 600 $PRIVATE_KEY
deploy:
stage: deploy
image: eleven26/rsync:1.3.0
script:
# $user 是 ssh 的用户
# $host 是 ssh 的主机
# $port 是 ssh 的端口
# $PRIVATE_KEY 是咱们在 gitlab 中装备的私钥
- rsync -az -e "ssh -o StrictHostKeyChecking=no -p $port -i $PRIVATE_KEY" --delete --exclude='.git' . $user@$host:/home/www
这儿的 rsync
指令中,咱们运用了 -o StrictHostKeyChecking=no
参数,这是为了避免每次都需求手动输入 yes
来承认服务器的指纹。
安全最佳实践:
- 为每一个 project 装备 ssh key 变量,假如是大局变量的话,其他 project 能够在未授权的状况下,访问到这个私钥,这对错常风险的。
- 运用独自的库房来保存 ci 装备文件,防止其他人未经授权就修改 ci 装备文件,这也对错常风险的。(需求库房的权限为 public,假如 gitlab 布置在公网上又不想暴露 ci 装备,需求自行想办法处理)当然直接放项目里边的
.gitlab-ci.yml
也不是不能够,便是在发布的时分需求审核一下它的变动。
有必要严格遵循以上两步,不然会造成严峻的安全问题。由于拿到了私钥,就等于拿到了咱们的服务器暗码。
ERROR: Job failed: exit code xx 处理方案
咱们在运用的时分或许会常常遇到这种过错(在 job
履行的输出里边),假如运气好,在输出里边也有一些额定的过错信息,
这种是最好处理的,它现已告知你过错原因了。还有一种十分坑爹的状况是:job
失利了,只要一个非 0 的退出状况码,可是没有任何的报错信息,这种状况就比较难处理(愈加坑爹的是,偶然呈现这种失利)。
job script 的履行流程
假如咱们了解了 gitab CI/CD 中 job
的履行原理,那么这个问题其实就很好处理了,job
的 script
履行流程如下:
- 拿到
script
中第一条指令,然后履行。 - 查看上一步的退出状况码,假如状况码为 0,继续履行下一条指令。不然,
job
直接失利,然后显现信息ERROR: Job failed: exit code <xx>
,最终的<xx>
便是上一条指令的非 0 的那个退出状况码。 - 按以上两个步骤来一条条履行
script
中的指令。
假如运用的是 bash shell,咱们能够经过 echo $? 来获取上一条指令的退出状况码。状况码方面的约好都是:0 表明成功,非 0 表明不成功。
处理办法
知道了 job
的履行原理之后,问题就很好处理了,咱们只需求在 job
履行日志中找到最终那一条指令即可:
- 先看这个指令是否有履行失利相关的过错输出信息,假如有,那么处理对应过错即可。
- 假如这个履行失利的指令,一点输出都没有。那么咱们能够深化了解一下这个指令的退出状况码什么时分等于咱们
job
的状况码,然后再对症下药。
一个实例
下面是一个 job
日志的最终几行,可是不包括详细的过错信息:
$ if (( $need_restart_queue == 1 )); then ssh $user@$host "supervisorctl restart xx"; fi
Cleaning up project directory and file based variables
ERROR: Job failed: exit code 1, no message
第一行是履行的指令,这个指令中,经过 ssh
履行了一条长途指令,然后退出。第二行是 job
失利后做整理操作输出的日志,最终一行输出 job
失利的过错码。
便是这个过错,困扰了我几天,由于它是偶然失利的。
在这个比方中,比上面提到的要复杂一点,这儿经过了 ssh
来履行长途指令,假如经过 ssh
履行长途指令,那么 ssh
指令的退出状况码便是履行的那个长途指令的退出状况码。
清晰了这一点,咱们就能够把问题定位在那个长途指令 supervisorctl restart xx
上,也便是说咱们的失利是由于这个指令导致的。
后边排查发现,supervisorctl
指令本身就有一定几率失利,针对这种状况,有两种处理方案:
- 重试,能够给
job
指定重试次数,能够是 0~2,也便是说 gitlab 的job
最多能够重试 2 次。 - 疏忽这个过错,运用其他处理方案。(咱们能够在
ssh
指令后边加上|| true
来疏忽,加上这个,指令退出状况码一定是 0 了)
我是采取了后边那一种处理办法,由于服务器上还有一个定时使命来检测对应的进程,假如进程不存在,则会运用
supervisorctl start xx
来发动对应的服务。
总结
最终,总结一下本文中一些比较关键的内容:
- 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
来指定的。