服务
Tier: 基础版,专业版,旗舰版
Offering: JihuLab.com,私有化部署
配置 CI/CD 时,您需要指定一个用于创建作业运行容器的镜像。要指定此镜像,请使用 image 关键字。
您还可以使用 services 关键字指定额外的镜像。此额外镜像用于创建另一个容器,该容器可用于第一个容器。这两个容器可以相互访问,并在运行作业时进行通信。
服务镜像可以运行任何应用程序,但最常见的用例是运行数据库容器,例如:
- MySQL
- PostgreSQL
- Redis
- 极狐GitLab 作为提供 JSON API 的微服务示例
假设您正在开发一个使用数据库进行存储的内容管理系统。您需要一个数据库来测试应用程序中的所有功能。在此场景中,将数据库容器作为服务运行是一个很好的用例。
使用现有镜像并将其作为附加容器运行,而不是每次构建项目时都安装 mysql。
您不仅限于数据库服务。您可以向 .gitlab-ci.yml 添加所需任意数量的服务,或手动修改 config.toml。在 Docker Hub 或您的私有容器镜像仓库中找到的任何镜像都可以用作服务。
有关使用私有镜像的信息,请参见从私有容器镜像仓库访问镜像。
服务继承与 CI 容器本身相同的 DNS 服务器、搜索域和额外主机。
服务如何与作业关联
为了更好地理解容器链接的工作原理,请阅读连接容器。
如果向应用程序添加 mysql 作为服务,则使用该镜像创建一个链接到作业容器的容器。
MySQL 服务容器的主机名为 mysql。要访问数据库服务,请连接到名为 mysql 的主机,而不是套接字或 localhost。更多信息请参见访问服务。
服务健康检查的工作原理
服务旨在提供网络可访问的附加功能。它们可以是像 MySQL 或 Redis 这样的数据库,甚至可以是允许使用 DinD(Docker-in-Docker)的 docker:dind。它几乎可以是 CI/CD 作业继续执行所需的任何东西,并且通过网络访问。
为确保此功能有效,Runner 会:
- 检查容器默认暴露哪些端口。
- 启动一个等待这些端口可访问的特殊容器。
如果检查的第二阶段失败,则会输出警告:*** WARNING:Service XYZ probably didn't start properly。此问题可能由以下原因引起:
- 服务中没有开放端口。
- 服务在超时前未正确启动,且端口无响应。
多数情况下,这会影响作业,但也可能作业仍会成功,即使打印了该警告。例如:
- 服务在警告发出后不久启动,而作业并非从一开始就使用链接的服务。这种情况下,当作业需要访问服务时,它可能已经在等待连接了。
- 服务容器不提供任何网络服务,但它在处理作业的目录(所有服务都将作业目录作为卷挂载在 /builds 下)。在这种情况下,服务完成了它的工作,而作业没有尝试连接它,因此不会失败。
如果服务成功启动,它们会在 before_script 运行之前启动。这意味着您可以编写一个查询服务的 before_script。
服务在作业结束时停止,即使作业失败也是如此。
使用服务镜像提供的软件
当您指定 service 时,它提供网络可访问的服务。数据库是此类服务最简单的例子。
服务功能不会将已定义的 services 镜像中的任何软件添加到作业的容器中。
例如,如果您在作业中定义了以下 services,那么 php、node 或 go 命令不能用于您的脚本,作业会失败:
yaml1job: 2 services: 3 - php:8.4 4 - node:latest 5 - golang:1.25 6 image: alpine:3.23 7 script: 8 - php -v 9 - node -v 10 - go version
如果您需要在脚本中使用 php、node 和 go,您应该:
- 选择包含所有必需工具的现有 Docker 镜像。
- 创建您自己的 Docker 镜像,包含所有必需工具,并在作业中使用它。
在 .gitlab-ci.yml 文件中定义 services
也可以按作业定义不同的镜像和服务:
yaml1default: 2 before_script: 3 - bundle install 4 5test:4.0: 6 image: ruby:4.0 7 services: 8 - postgres:18 9 script: 10 - bundle exec rake spec 11 12test:3.4: 13 image: ruby:3.4 14 services: 15 - postgres:17 16 script: 17 - bundle exec rake spec
或者您可以为 image 和 services 传递一些扩展配置选项:
yaml1default: 2 image: 3 name: ruby:4.0 4 entrypoint: ["/bin/bash"] 5 services: 6 - name: my-postgres:18 7 alias: db,postgres,pg 8 entrypoint: ["/usr/local/bin/db-postgres"] 9 command: ["start"] 10 before_script: 11 - bundle install 12 13test: 14 script: 15 - bundle exec rake spec
访问服务
如果您未指定服务别名,则可以通过构建容器中的两个主机名来访问服务:
- namespace-projectname
- namespace__projectname
带下划线的主机名不符合 RFC 规范,可能会导致第三方应用程序出现问题。
服务主机名的默认别名基于其镜像名称按以下规则创建:
- 冒号 (:) 之后的所有内容都将被去掉。
- 斜杠 (/) 替换为双下划线 (__),并创建主别名。
- 斜杠 (/) 替换为单破折号 (-),并创建辅助别名。
要覆盖默认行为,您可以指定一个或多个服务别名。
连接服务
您可以将相互依赖的服务用于复杂的作业,如端到端测试,其中外部 API 需要与其自己的数据库通信。
例如,对于使用 API 且 API 需要数据库的前端应用端到端测试:
yaml1end-to-end-tests: 2 image: node:latest 3 services: 4 - name: selenium/standalone-firefox:${FIREFOX_VERSION} 5 alias: firefox 6 - name: registry.gitlab.com/organization/private-api:latest 7 alias: backend-api 8 - name: postgres:18 9 alias: db postgres db 10 variables: 11 FF_NETWORK_PER_BUILD: 1 # 激活容器间网络 12 POSTGRES_PASSWORD: supersecretpassword 13 BACKEND_POSTGRES_HOST: postgres 14 script: 15 - npm install 16 - npm test
要使此方案有效,您必须使用为每个作业创建新网络的网络模式。
将 CI/CD 变量传递给服务
您还可以直接在 .gitlab-ci.yml 文件中传递自定义 CI/CD 变量 以微调您的 Docker images 和 services。更多信息,请阅读 .gitlab-ci.yml 定义的变量。
yaml1# 以下变量将自动传递给 Postgres 容器和 Ruby 容器,并在每个容器中可用。 2variables: 3 HTTPS_PROXY: "https://10.1.1.1:8090" 4 HTTP_PROXY: "https://10.1.1.1:8090" 5 POSTGRES_DB: "my_custom_db" 6 POSTGRES_USER: "postgres" 7 POSTGRES_PASSWORD: "example" 8 PGDATA: "/var/lib/postgresql/data" 9 POSTGRES_INITDB_ARGS: "--encoding=UTF8 --data-checksums" 10 11default: 12 services: 13 - name: postgres:18 14 alias: db 15 entrypoint: ["docker-entrypoint.sh"] 16 command: ["postgres"] 17 image: 18 name: ruby:4.0 19 entrypoint: ["/bin/bash"] 20 before_script: 21 - bundle install 22 23test: 24 script: 25 - bundle exec rake spec
services 的可用设置
有关 services: 子键的详细信息,请参见 CI/CD YAML 参考。
从同一镜像启动多个服务
在新的扩展 Docker 配置选项之前,以下配置无法正常工作:
yamlservices: - mysql:latest - mysql:latest
Runner 会启动两个容器,每个都使用 mysql:latest 镜像。然而,根据默认主机名命名,两者都将以 mysql 别名添加到作业容器中。这将导致其中一个服务无法访问。
在新的扩展 Docker 配置选项之后,前面的示例如下所示:
yamlservices: - name: mysql:latest alias: mysql-1 - name: mysql:latest alias: mysql-2
Runner 仍然使用 mysql:latest 镜像启动两个容器,但是现在每个容器都可以通过 .gitlab-ci.yml 文件中配置的别名访问。
为服务设置命令
假设您有一个 super/sql:latest 镜像,其中包含一些 SQL 数据库。您想将其用作作业的服务。再假设此镜像在启动容器时不会启动数据库进程。用户需要手动使用 /usr/bin/super-sql run 作为命令来启动数据库。
在新的扩展 Docker 配置选项之前,您需要:
-
基于 super/sql:latest 镜像创建您自己的镜像。
-
添加默认命令。
-
在作业配置中使用该镜像。
-
my-super-sql:latest 镜像的 Dockerfile:
dockerfileFROM super/sql:latest CMD ["/usr/bin/super-sql", "run"] -
在 .gitlab-ci.yml 的作业中:
yamlservices: - my-super-sql:latest
-
在新的扩展 Docker 配置选项之后,您可以改为在 .gitlab-ci.yml 文件中设置 command:
yamlservices: - name: super/sql:latest command: ["/usr/bin/super-sql", "run"]
command 的语法类似于 Dockerfile CMD。
使用别名作为 Kubernetes 执行器的服务容器名称
版本历史
- 在 极狐GitLab 与 极狐GitLab Runner 17.9 中引入。
您可以使用服务别名作为 Kubernetes 执行器的服务容器名称。极狐GitLab Runner 基于以下条件命名容器:
- 当为一个服务设置多个别名时,服务容器将以第一个别名命名,该别名:
- 尚未被其他服务容器使用。
- 遵循 Kubernetes 标签名称约束。
- 当无法使用别名命名服务容器时,极狐GitLab Runner 回退到 svc-i 模式。
以下示例说明了如何将别名用于 Kubernetes 执行器的服务容器命名。
每个服务一个别名
在以下 .gitlab-ci.yml 文件中:
yaml1job: 2 image: alpine:latest 3 script: 4 - sleep 10 5 services: 6 - name: alpine:latest 7 alias: alpine 8 - name: mysql:latest 9 alias: mysql
系统会创建包含名为 alpine 和 mysql 的容器的作业 Pod,除了标准的 build 和 helper 容器。这些别名被使用是因为它们:
- 未被其他服务容器使用。
- 遵循 Kubernetes 标签名称约束。
但是,在以下 .gitlab-ci.yml 中:
yaml1job: 2 image: alpine:latest 3 script: 4 - sleep 10 5 services: 6 - name: mysql:lts 7 alias: mysql 8 - name: mysql:latest 9 alias: mysql
除了 build 和 helper 容器之外,系统还会创建另外两个名为 mysql 和 svc-0 的容器。mysql 容器对应 mysql:lts 镜像,而 svc-0 容器对应 mysql:latest 镜像。
每个服务多个别名
在以下 .gitlab-ci.yml 文件中:
yaml1job: 2 image: alpine:latest 3 script: 4 - sleep 10 5 services: 6 - name: alpine:latest 7 alias: alpine,alpine-latest 8 - name: alpine:edge 9 alias: alpine,alpine-edge,alpine-latest
除了 build 和 helper 容器之外,系统还会创建另外四个容器:
- alpine 应对应使用 alpine:latest 镜像的容器。
- alpine-edge 应对应使用 alpine:edge 镜像的容器(alpine 别名已被前一个容器使用)。
在此示例中,未使用别名 alpine-latest。
但是,在以下 .gitlab-ci.yml 中:
yaml1job: 2 image: alpine:latest 3 script: 4 - sleep 10 5 services: 6 - name: alpine:latest 7 alias: alpine,alpine-edge 8 - name: alpine:edge 9 alias: alpine,alpine-edge 10 - name: alpine:3.21 11 alias: alpine,alpine-edge
除了 build 和 helper 容器之外,还会创建另外六个容器。
-
alpine 应指向使用 alpine:latest 镜像的容器。
-
alpine-edge 应指向使用 alpine:edge 镜像的容器(alpine 别名已被前一个容器使用)。
-
svc-0 应指向使用 alpine:3.21 镜像的容器(alpine 和 alpine-edge 别名已被前面的容器使用)。
-
svc-i 模式中的 i 并不表示服务在提供列表中的位置。相反,它表示未找到可用别名时服务的位置。
-
当提供了无效别名(不符合 Kubernetes 约束)时,作业会失败并显示以下错误(使用别名 alpine_edge 的示例)。此失败发生,因为别名也用于在作业 Pod 上创建本地 DNS 条目。
plaintextERROR: Job failed (system failure): prepare environment: setting up build pod: provided host alias alpine_edge for service alpine:edge is invalid DNS. a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')。 有关更多信息,请查看 https://gitlab.cn/docs/runner/shells/index/#shell-profile-loading。
-
与 docker run(DinD)并行使用 services
使用 docker run 启动的容器也可以连接到极狐GitLab 提供的服务。
如果启动服务的开销很大或耗时很长,您可以从不同的客户端环境运行测试,而只启动一次被测试的服务。
yaml1access-service: 2 stage: build 3 image: docker:20.10.16 4 services: 5 - docker:dind # docker run 所需 6 - traefik/whoami:latest 7 variables: 8 FF_NETWORK_PER_BUILD: "true" # 激活容器间网络 9 script: | 10 docker run --rm --name curl \ 11 --volume "$(pwd)":"$(pwd)" \ 12 --workdir "$(pwd)" \ 13 --network=host \ 14 curlimages/curl:latest curl "http://traefik-whoami"
要使此方案有效,您必须:
- 使用为每个作业创建新网络的网络模式。
- 不使用 Docker 执行器与 Docker 套接字绑定。如果您必须这样做,那么在前面的示例中,不要使用 host,而是使用为此作业创建的动态网络名称。
Docker 集成的工作原理
以下是 Docker 在作业期间执行步骤的概要。
- 创建任何服务容器:mysql、postgresql、mongodb、redis。
- 创建一个缓存容器以存储 config.toml 和构建镜像(如前述示例中的 ruby:4.0)的 Dockerfile 中定义的所有卷。
- 创建一个构建容器并将所有服务容器链接到构建容器。
- 启动构建容器,并将作业脚本发送到容器。
- 运行作业脚本。
- 将代码检出到:/builds/group-name/project-name/。
- 运行 .gitlab-ci.yml 中定义的任何步骤。
- 检查构建脚本的退出状态。
- 移除构建容器和所有已创建的服务容器。
捕获服务容器日志
服务容器中运行的应用程序生成的日志可以被捕获,以供后续检查和调试。当服务容器成功启动,但由于非预期行为导致作业失败时,可以查看服务容器日志。这些日志可以指示容器中服务的缺失或不正确的配置。
CI_DEBUG_SERVICES 应仅在主动调试服务容器时启用,因为捕获服务容器日志会带来存储和性能影响。
要启用服务日志记录,请将 CI_DEBUG_SERVICES 变量添加到项目的 .gitlab-ci.yml 文件中:
yamlvariables: CI_DEBUG_SERVICES: "true"
允许的值是:
- 启用:TRUE,true,True
- 禁用:FALSE,false,False
任何其他值都会导致错误消息,并有效地禁用该功能。
启用后,所有服务容器的日志都会被捕获,并与其他日志同时流式传输到作业的跟踪日志中。每个容器的日志都会以其容器别名为前缀,并以不同的颜色显示。
请参阅 屏蔽 CI/CD 变量
本地调试作业
以下命令以非 root 权限运行。请验证您可以使用用户账户运行 Docker 命令。
首先创建一个名为 build_script 的文件:
shellcat <<EOF > build_script git clone https://jihulab.com/gitlab-cn/gitlab-runner.git /builds/gitlab-cn/gitlab-runner cd /builds/gitlab-cn/gitlab-runner make runner-bin-host EOF
此示例使用极狐GitLab Runner 仓库,其中包含一个 Makefile,因此运行 make 会执行 Makefile 中定义的目标。您可以运行特定于您项目的命令,而不是 make runner-bin-host。
然后创建一个服务容器:
shelldocker run -d --name service-redis redis:latest
前面的命令使用最新的 Redis 镜像创建了一个名为 service-redis 的服务容器。该服务容器在后台运行 (-d)。
最后,通过执行您之前创建的 build_script 文件来创建一个构建容器:
shelldocker run --name build -i --link=service-redis:redis golang:latest /bin/bash < build_script
前面的命令创建了一个名为 build 的容器,该容器基于 golang:latest 镜像生成,并链接了一个服务。build_script 通过 stdin 管道传递给 bash 解释器,后者随即在 build 容器中执行 build_script。
测试完成后,使用以下命令移除容器:
shelldocker rm -f -v build service-redis
这会强制 (-f) 移除 build 容器、服务容器以及随容器创建而创建的所有卷 (-v)。
使用服务容器时的安全
Docker 特权模式适用于服务。这意味着服务镜像容器可以访问主机系统。您应仅使用来自可信源的容器镜像。
共享 /builds 目录
构建目录作为卷挂载在 /builds 下,并在作业和服务之间共享。作业会在服务运行后将项目检出到 /builds/$CI_PROJECT_PATH 中。您的服务可能需要访问项目文件或存储产物。如果是这样,请等待该目录存在并检出 $CI_COMMIT_SHA。在作业完成检出过程之前所做的任何更改都会被检出过程移除。
服务必须检测作业目录何时填充完毕并准备好进行处理。例如,等待某个特定文件可用。
启动后立即开始工作的服务可能会失败,因为作业数据可能还不可用。例如,容器使用 docker build 命令建立到 DinD 服务的网络连接。该服务指示其 API 开始构建容器镜像。Docker Engine 必须能够访问您在 Dockerfile 中引用的文件。因此,您需要访问服务中的 CI_PROJECT_DIR。但是,Docker Engine 直到作业中调用 docker build 命令时才会尝试访问它。此时,/builds 目录已经填充了数据。试图在启动后立即写入 CI_PROJECT_DIR 的服务可能会失败,并显示 No such file or directory 错误。
在与作业数据交互的服务不受作业本身控制的场景下,请考虑使用 Docker 执行器工作流。