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 cd 不完全指南

  1. 在开发人员提交代码之后,会触发 gitlab 的 CI 流水线。也便是上图的 CI PIPELINE,也便是中心的部分。
  2. 在 CI 流水线中,咱们能够装备多个使命。比方上图的 buildunit testintegration tests 等,也便是构建、单元测验、集成测验等。
  3. 在 CI 流水线都经过之后,会触发 CD 流水线。也便是上图的 CD PIPELINE,也便是右边的部分。
  4. 在 CD 流水线中,咱们能够装备多个使命。比方上图的 stagingproduction 等,也便是布置到测验环境、布置到出产环境等。

在 CD 流程完毕之后,咱们就能够在服务器上看到咱们的代码了。

gitlab CI、CD 中的一些根本概念

在开端之前,咱们先来了解一下 gitlab CI、CD 中的一些根本概念:

  • pipeline:流水线,也便是 CI、CD 的整个流程,包括了多个 stage,每个 stage 又包括了多个 job
  • stage: 一个阶段,一个阶段中能够包括多个使命(job),这些使命会并行履行,可是下一个 stagejob 只要在上一个 stagejob 履行经过之后才会履行。
  • job:一个使命,这是 CI、CD 中最根本的概念,也是最小的履行单元。一个 stage 中能够包括多个 job,一起这些 job 会并行履行。
  • runner:履行器,也便是履行 job 的机器,runner 跟 gitlab 是别离的,runner 需求咱们自己去装置,然后注册到 gitlab 上(不需求跟 gitlab 在同一个服务器上,这样有个好处便是能够很便利完成多个机器来一起处理 gitlab 的 CI、CD 的使命)。
  • tag: runnerjob 都需求指定标签,job 能够指定一个或多个标签(有必要指定,不然 job 不会被履行),这样 job 就只会在指定标签的 runner 上履行。
  • cache: 缓存,能够缓存一些文件,这样下次流水线履行的时分就不需求从头下载了,能够提高履行功率。
  • artifacts: 这代表这构建进程中所发生的一些文件,比方打包好的文件,这些文件能够鄙人一个 stage 中运用,也能够在 pipeline 履行完毕之后下载下来。
  • variables变量,能够在 pipeline 中界说一些变量,这些变量能够在 pipeline 的一切 stagejob 中运用。
  • services:服务,能够在 pipeline 中发动一些服务,比方 mysqlredis 等,这样咱们就能够在 pipeline 中运用这些服务了(常常用在测验的时分模仿一个服务)。
  • script: 脚本,能够在 job 中界说一些脚本,这些脚本会在 job 履行的时分履行。

CI、CD 的作业模型

咱们以下面的装备为比方,简略阐明一下 pipelinestagejob 的作业模型,以及 cacheartifacts 的作用:

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 履行进程能够用下面的图来表明:

gitlab ci cd 不完全指南

阐明:

  1. 上面的图有两个 pipeline 被履行了,可是 pipeline2 没有全部画出来
  2. 其间,在 pipeline 1 中,stage1 中的 job 会先被履行,然后才会履行 stage2 中的 job
  3. stage1 中的 job1job2 是能够并行履行的,这也便是 stage 的本质上的含义,表明了一个阶段中不同的使命,比方咱们做测验的时分,能够一起对不同模块做测验。
  4. job1job2 都指定了 tag1 标签,所以 job1job2 只会在标签为 tag1runner 上履行。
  5. job1 中,咱们创建了一个 test.txt 文件,这个文件会作为 stage1 的构建产品,它能够在 stage2 中被运用,也便是 job3job4 都能够读取到这个文件。一种实际的场景是,前端布置的时分,build 之后会生成能够布置的静态文件,这些静态文件就会被保留到布置相关的 stage 中。需求留意的是,artifacts 只会在当时 pipeline 后续的 stage 中同享,不会在 pipeline 之间同享。
  6. 一起,在 job1 中,咱们也指定了 cache,这个 cache 会在 job1 履行完毕之后保存起来,不同于 artifactscache 是能够在不同的 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 会对应一种特定的 executorexecutor 便是咱们履行 CI/CD 里边 script 的环境。比方假如咱们指定了 executor 类型为 docker,那么咱们 CI/CD 脚本里边的 script 将会在一个独立的 docker 容器中履行。

简略来说,runner 是履行 CI/CD 脚本的机器,这个机器上有不同类型的 executor,一个 executor 代表着一个不同类型的指令行终端,最常见的是 shelldocker,当然也支撑 widnows 的 powershell

咱们能够经过下图来了解一下 gitlab 是怎样跟 runner 合作的:

gitlab 是经过 tags 来找到运行脚本的 runner 的,假如 jobtagsrunnertags 匹配了,就能够将那个 job 放到 runner 上处理。

gitlab ci cd 不完全指南

其他一些在个人实践中的一些经验

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

假如咱们没有标签为 apirunner,那么这个 job 就会一直不会被履行,所以需求确保咱们装备的 tag 有对应的 runner

指定 job 的 docker image

留意:这个只在咱们的 runnerexecutordocker 的时分才会生效。也便是咱们的 runner 是一个 docker 容器。

有时分,咱们需求履行一些特定指令,可是咱们大局的 docker 镜像里边没有,或许只需求一个特定的 docker 镜像,这个时分咱们能够经过 image 来指定 jobdocker 镜像。

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-releasedeploy-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 私钥来进行布置。

咱们能够经过以下流程来完成:

  1. 新建一对 ssh key,比方 id_rsaid_rsa.pub
  2. id_rsa.pub 的内容增加到服务器的 authorized_keys 文件中。
  3. id_rsa 上传到 gitlab 中(在项目的 CI/CD 装备中,装备一个变量,变量名为 PRIVATE_KEY,内容为 id_rsa 的内容,类型为 file)。
  4. 在咱们的 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 的履行原理,那么这个问题其实就很好处理了,jobscript 履行流程如下:

  1. 拿到 script 中第一条指令,然后履行。
  2. 查看上一步的退出状况码,假如状况码为 0,继续履行下一条指令。不然,job 直接失利,然后显现信息 ERROR: Job failed: exit code <xx>,最终的 <xx> 便是上一条指令的非 0 的那个退出状况码。
  3. 按以上两个步骤来一条条履行 script 中的指令。

假如运用的是 bash shell,咱们能够经过 echo $? 来获取上一条指令的退出状况码。状况码方面的约好都是:0 表明成功,非 0 表明不成功。

处理办法

知道了 job 的履行原理之后,问题就很好处理了,咱们只需求在 job 履行日志中找到最终那一条指令即可:

  1. 先看这个指令是否有履行失利相关的过错输出信息,假如有,那么处理对应过错即可。
  2. 假如这个履行失利的指令,一点输出都没有。那么咱们能够深化了解一下这个指令的退出状况码什么时分等于咱们 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 指令本身就有一定几率失利,针对这种状况,有两种处理方案:

  1. 重试,能够给 job 指定重试次数,能够是 0~2,也便是说 gitlab 的 job 最多能够重试 2 次。
  2. 疏忽这个过错,运用其他处理方案。(咱们能够在 ssh 指令后边加上 || true 来疏忽,加上这个,指令退出状况码一定是 0 了)

我是采取了后边那一种处理办法,由于服务器上还有一个定时使命来检测对应的进程,假如进程不存在,则会运用 supervisorctl start xx 来发动对应的服务。

总结

最终,总结一下本文中一些比较关键的内容:

  • gitlab 中的一些根本概念:
    • pipeline:代表了一次 CI 的履行进程,它包括了多个 stage
    • stage:代表了一组 job 的调集,stage 会依照次序履行。
    • job:代表了一个详细的使命,比方 buildtestdeploy 等。
  • 一个 stage 中的多个 job 是能够并行履行的。可是下一个 stagejob 有必要要等到上一个 stage 的一切 job 都履行完毕之后才会履行。
  • cacheartifacts 的区别:
    • cache 是用来缓存依靠的,比方 node_modules 文件夹,它能够加速后续 pipeline 的履行流程,由于避免了重复的依靠装置。
    • artifacts 是用来缓存构建产品的,比方 build 之后生成的静态文件,它能够在后续的 stage 中运用。表明的是单个 pipeline 中的不同 stage 之间的同享
  • cache 在装置依靠的 job 中才需求运用默许的 policy,也便是 pull-push,在其他不需求装置依靠的 job 中运用 pull 就能够了,不需求上传缓存。
  • cachekey 能够指定多个文件,这样在指定的文件变动的时分,缓存会失效,这往往用在依靠相关的文件中。
  • 能够运用 services 关键字来指定需求发动的服务,比方 mysqlredis 等,在 job 中能够衔接到这些 services,从而便利进行测验。
  • 能够运用 yamlanchor 机制来复用一些装备片段,能够少写许多重复的装备。
  • 一个 job 有必要运行在某个 runner 上,jobrunner 的关联是经过 tag 来指定的。