介绍
CI/CD 是持续集成、持续部署的意思,研发团队的开发流程以代码版本仓库(VCS)为中心,以不同的 branch
或 tag
进行代码提交,之后 VCS 会触发一系列的流水线作业,包括代码检查、单元测试、编译、打包镜像、发布镜像、触发测试环境或生产环境的部署等,这一切都是自动的。研发成员只需要提交代码到 VCS 就可以了,免去了复杂的人力成本,提高了生产效率。
上次我们讲了如何《通过 Docker 快速部署公司内部 GitLab》,这次来讲讲如何在 GitLab 的基础上进行 CI/CD。GitLab 官方提供了 GitLab Runner 组件来与 GitLab 进行集成,他们可以部署在同一集群内不同机器上(内网进行通信),当 GitLab 检测到代码提交事件,会自动触发 Runner 执行流水线作业。
部署 GitLab Runner
我们以上篇文章作为本篇的上下文,进行安装步骤和原理的讲解。上篇文章中,我们说 GitLab 部署在了集群内 IP 为 192.168.31.43
的这台机器上,那么如果服务器资源充沛的话,最好将 Runner 部署在集群内另外一台机器上,本文以部署在同一机器上为例,若是与 GitLab 部署在不同机器,道理也是相同的,只要相互之间可以通信就行。
我们还是通过 Docker 容器的方式部署 Runner,因此部署 Runner 的这台机器需要安装有 Docker。我们在部署 GitLab 机器的上部署 Runner,之前已经安装了 Docker,若是部署在不同机器,请自行安装。
Docker 安装后,执行以下命令,拉取 Runner 镜像并启动:
$ docker run -d --name gitlab-runner --restart always \
-v /path/to/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
我们给运行 Runner 的这个容器起名为 gitlab-runner
,并将 Runner 的配置目录挂载到了宿主机,Runner 容器中的 docker.sock
与宿主机共享。Runner 在执行每个流水线作业时都会启动一个独立并且隔离的 Docker 容器并在容器中执行用户定义的脚本,通过共享 docker.sock
就可以让 Runner 通过宿主机的 Docker Engine 进行容器的拉取、启动等操作。因 Runner 本身就运行在容器中,通过这种方式可以避免 Docker in Docker 的问题。
这样,Runner 就部署好了,Docker 容器为我们的应用部署带来了极大的便利。下一步就是让部署好的 Runner 与 GitLab 集成在一起。
配置 Runner
对于 GitLab 来讲,Runner 有多种形式:共享 Runner、群组 Runner、项目 Runner。共享 Runner 是 GitLab 上的所有的项目均可以使用的 Runner,群组 Runner 是一个群组内的项目均可以使用的 Runner。我们这里建议使用项目 Runner,也就是说,我们部署的这个 Runner 只为单独一个 GitLab 项目服务。
浏览器打开 GitLab,进入一个项目,项目维护者(maintainer 身份) 可以看到项目的设置选项,进入 设置 - CI/CD
,展开 Runner
选项:
可以看到 手动设置specific Runner
一项,并且展示了设置 Runner 时需要输入的 GitLab URL
和 Token
,通过这两个参数去设置 Runner 便与 GitLab 建立了联系,同时 Token
也指定了要设置的 Runner 只会为该项目服务,对于其他项目,Runner 将会不可见。
接下来我们去设置 Runner。
进入 Runner 容器:
docker exec -it gitlab-runner bash
执行命令 gitlab-runner register
进行交互式配置:
$ gitlab-runner register
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
http://192.168.31.43:9080/
Please enter the gitlab-ci token for this runner
xxx
Please enter the gitlab-ci description for this runner
[hostname] a runner just for the project: demo
Please enter the gitlab-ci tags for this runner (comma separated):
demo-project-runner
Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell:
docker
Please enter the Docker image (eg. ruby:2.6):
alpine:latest
我们逐一讲解:
-
输入 GitLab 的 HTTP 服务地址,进行注册。
- 对于将 GitLab 和 Runner 部署在不同机器的情况来讲,这个 IP 应该是集群内可访问 GitLab 的地址,比如
http://192.168.31.43:9080/
,Runner 容器的网关是docker0
接口,网段为172.17.0.0/24
,属于集群网段192.168.31.0/24
的子网段,Runner 可以向上访问到父级网段的 IP。 - 但对于部署在同一台机器上的情况来讲,通过集群 IP 访问本机上的服务就绕远路了,但 Runner 又不可以通过
localhost:9080
来访问,因为localhost
是对于容器而言的,容器内9080
端口没有服务。我们在 Docker 上启动容器时,若不指定network
,默认网关都是宿主机的docker0
接口,都是在同一网段(bridge network
)下,我们可以通过查看 GitLab 容器在bridge network
下的 IP(命令:docker inspect gitlab
)来获取到该网段的 GitLab 服务 IP。又或者,可以将 GitLab 容器和 Runner 容器加入到同一network
下,可以直接通过容器名+端口号
进行通信,Docker 内部会做域名解析。关于 Docker 的网络原理详见我的这篇文章。
- 对于将 GitLab 和 Runner 部署在不同机器的情况来讲,这个 IP 应该是集群内可访问 GitLab 的地址,比如
- 输入项目 Token 与项目进行绑定。
- 输入这个 Runner 的描述。
- 输入这个 Runner 的标签,多个标签用英文逗号分隔。这个标签会在以后写 CI 配置文件时用到,用来指定一个 Job 让哪个 Runner 来执行。
- 选择执行器,我们这里选择
docker
。也就是说对于每个 Job(你可能还不清楚什么是 Job,我们待会进行讲解,这里可以先理解成 Runner 执行流水线时的每个任务)Runner 都会分配一个独立并隔离的 Docker 容器环境进行 Job 的执行。当然,你也可以选择使用其他的执行器,比如shell
,同样的道理,每个 Job 都会分配一个独立的 shell 环境来执行。 - 输入 Docker 默认镜像。当 CI 配置文件中没有指定一个 Job 所使用的容器镜像时,就会使用这个默认的镜像作为执行环境。
注册完再回到 GitLab 的项目设置页面,查看 Runner,会发现 此项目已激活的Runner
中有了一个刚刚注册的 Runner。
至此,就完成了 GitLab 与 Runner 的集成。以后,每次该项目检测到有代码提交时,都会触发 Runner 执行流水线任务。你可能会问,Runner 怎么才知道我们需要什么样的流水线任务?答案是不知道。我们需要在我们的项目根目录中添加一个 CI 配置文件 .gitlab-ci.yml
来定义流水线任务的每个阶段做什么事情,GitLab 根据这个规定来让 Runner 做事,至于这个配置文件怎么写,放在后面说。
但是需要注意的是,假如我们的项目中不存在 .gitlab-ci.yml
,GitLab 会采用预定义的配置执行流水线,所以我们需要关闭它。
关闭默认流水线
取消勾选 默认为Auto DevOps流水线
,保存修改。
GitLab CI 中的基本概念
-
Runner
- 配合 GitLab 执行 CI 任务,是实际执行 CI 任务的进程,并将执行结果返回给 GitLab。一般一个项目分配一个 Runner,但也可以多个项目共享一个 Runner。
-
Executor
- 执行器,有多种选择,如:
docker
、shell
等,决定 Runner 以什么环境执行作业,一个 Runner 对应一个 Executor。
- 执行器,有多种选择,如:
-
Pipline
- 流水线,流水线代表项目 CI/CD 的整个流程,分为多个阶段(在
.gitlab-ci.yml
中自己规定)。
- 流水线,流水线代表项目 CI/CD 的整个流程,分为多个阶段(在
-
Stage
- 阶段,一个 Pipline 对应多个阶段。
-
Job
- CI/CD 中最小的执行单元,一个 Stage 对应多个 Jobs。
一个项目的流水线:
优化工作
部署并注册 Runner 后,还需要进行一些参数调整和优化工作。
下面是一个 Go 项目的 CI 配置,流水线分为 test
和 build
两个阶段, test_job
使用 golang
镜像作为执行环境,build_job
使用 dind
镜像作为执行环境来进行应用镜像的构建和发布。
我们以这个配置为例,来进行流水线构建优化工作的讲解。
# 流水线分为测试阶段和构建阶段
stages:
- test
- build
# 测试作业
test_job:
stage: test
image: golang:1.14.2
tags:
- demo-project-runner
services:
# 执行测试时依赖 MySQL 服务
- name: mysql:8.0.17
# golang 容器中可与 mysql 容器通信的别名(内部会 DNS 解析)
alias: mysql
command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci']
# 指定作业级变量,会作为环境变量传入 golang 和 mysql 容器
variables:
MYSQL_HOST: mysql
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
# 执行脚本前的工作,设置 goproxy 加快依赖下载速度
before_script:
- go env -w GO111MODULE=on
- go env -w GOPROXY=https://goproxy.cn,direct
# 执行代码检查和单元测试
script:
- go vet ./...
- go test -v -race ./...
# 构建作业
build_job:
stage: build
image: docker:latest
# services:
# - docker:dind
# variables:
# DOCKER_DRIVER: overlay2
# DOCKER_TLS_CERTDIR: ''
script:
# 可将一些变量放在 GitLab 项目设置里,防止密码被包含在代码中
- docker build -t goapp:latest .
- docker login -u $DOCKER_REGISTRY_USERNAME -p $DOCKER_REGISTRY_PASSWORD $DOCKER_REGISTRY_ADDR
- docker tag goapp:latest ${DOCKER_REGISTRY_ADDR}/xvrzhao/goapp:latest
- docker push ${DOCKER_REGISTRY_ADDR}/xvrzhao/goapp:latest
tags:
- demo-project-runner
# 指定 master 分支的提交才会触发该作业
only:
- master
使用 DinD 注意事项
- 若要使用 DinD 镜像执行 Job,则需要 Runner 开启特权模式,否则会报错。进入挂载到宿主机的 Runner 配置目录,编辑
config.toml
文件,修改[runners.docker]
栏中privileged
的值为true
。 -
注意使用 DinD 镜像作为 Job 执行环境时配置文件的写法:(这一项待验证,后来只声明了image
使用docker:latest
,services
使用docker:dind
,并且要声明两个变量DOCKERDRIVER: overlay2
和DOCKERTLS_CERTDIR: ''
,否则会出现问题image: docker:latest
,并没有声明services
和variables
,执行流水线也通过了)。
修改镜像拉取策略
Runner 默认情况下每执行一个 Job 都会重新拉取一次所需镜像,我们可把策略改为:镜像不存在时才拉取。
编辑 Runner 配置文件 config.toml
,在 [runners.docker]
栏加入一行:
pull_policy = "if-not-present"
重启 Runner 容器,使之生效:
docker restart gitlab-runner
挂载 Job 容器的 docker.sock
当 Runner 在每次流水线执行到 build_job
时,都需要启动一个全新的 DinD 镜像,在全新的 DinD 容器内部进行构建操作( docker build
),这就导致构建过程中所产生的中间镜像缓存不能被下次构建使用。
我们需要让 Runner 中的 docker.sock
挂载到每个创建的 Job 容器中。这样,在 DinD Job 容器中执行 docker
命令时,其实是在与外部的 Docker Engine 进行通信,也就是相当于是在使用 Runner 容器中的 Docker Engine,而 Runner 容器又挂载了宿主机的 docker.sock
,故 DinD Job 容器中的 docker
命令是在间接地使用宿主机的 Docker Engine。这样,所有的中间镜像都产生在宿主机,就算不同的 DinD Job 容器也可以共享中间镜像缓存了。
配置方式同样是编辑 config.toml
文件。在 [runners.docker]
栏 volumns
数组中添加一项字符串元素 "/var/run/docker.sock:/var/run/docker.sock"
,并重启 Runner 容器使之生效。