WSL2 本地 GitLab CI/CD 实验:Runner、DinD 与 Container Registry
WSL2 本地 GitLab CI/CD 实验:Runner、DinD 与 Container Registry
实验状态:已完成(2026-10-08)
实操范围:GitLab CE、Shell / Docker Executor、MR Pipeline、Protected Branch、Artifact、Cache、Docker Socket Binding、Docker-in-Docker(DinD)、GitLab Container Registry、Runner 并发与resource_group。
Kubernetes Executor 仅做架构梳理,没有部署集群。
本文是分阶段实际操作与排障记录,不是可直接投产的安全配置手册。文中的 HTTP Registry、privilegedDinD、Docker Socket 挂载仅用于隔离的本地 Lab。
实验后期的主要交付链路:
1 | Merge Request |
1. 实验目标
我过去使用 Jenkins 做过 CI/CD、自动化测试、测试环境部署和生产 SRE,但没有长期实际使用过 GitLab CI。
因此这次实验并不是单纯学习 .gitlab-ci.yml 的语法,而是希望通过一个本地可控环境理解:
- GitLab CI Pipeline 如何触发;
- GitLab Runner 在整个系统中的角色;
- Shell Executor 和 Docker Executor 有什么区别;
- Job 之间如何传递文件;
- Cache 和 Artifact 有什么区别;
- Runner 如何和受限测试资源配合;
- Docker Executor 为什么会引入新的网络、镜像仓库和安全问题;
- CI Job 应该如何选择运行镜像;
- 这些机制和过去使用 Jenkins 时的经验如何对应。
实验环境:
1 | Windows |
2. GitLab CE 本地部署
GitLab CE 运行在 Docker 中。
初期部署时简化后的 Compose 配置(Registry 配置后来在第 28 节加入):
1 | services: |
部署初期在 Windows 本机使用过如下 hosts 映射:
1 | 127.0.0.1 gitlab.test |
后来改为多台开发机访问同一个 GitLab,统一使用 E5 Windows 的局域网地址 192.168.5.3。WSL 实际解析结果也曾是 gitlab.test → 192.168.5.3,因此后续必须处理 Windows → WSL2 的端口转发。不要把早期的 127.0.0.1 hosts 示例当作所有设备最终的配置。
最初使用过:
1 | gitlab.local |
后来改为:
1 | gitlab.test |
原因之一是 .local 通常与 mDNS 有特殊关联,本地实验使用 .test 更明确。
GitLab 内部实际使用的 URL 可以通过:
1 | docker exec gitlab \ |
检查。
期望结果:
1 | http://gitlab.test:8929 |
这个配置后来非常重要,因为 GitLab Runner clone repository 时会使用 GitLab 生成的 repository URL。
3. 用户、Group 与 Project
为了避免所有操作都使用 root,我创建了普通用户:
1 | jason |
然后创建 Group:
1 | devops-lab |
以及实验项目:
1 | devops-lab/ci-playground |
root 只作为 GitLab Instance Administrator 使用。
日常代码、MR、Pipeline 等操作都使用普通用户。
这样更接近真实企业里的权限模型。
4. Protected Branch 实验
创建另一个 Developer 用户后,对 main 设置 Protected Branch。
实验结果:
1 | Developer |
也就是说:
1 | feature branch |
而不能直接绕过流程:
1 | Developer |
Protected Environment 在当前 GitLab CE / Free 环境不可用,因此暂时没有继续做该实验。
5. GitLab Runner:Shell Executor
首先安装了 GitLab Runner,并使用:
1 | Shell Executor |
Runner 最终作为 systemd service 运行:
1 | systemctl status gitlab-runner |
服务实际类似:
1 | gitlab-runner run |
5.1 User Mode Runner 的问题
最开始手动运行:
1 | gitlab-runner run |
导致 Runner 工作目录受到启动位置影响。
出现过:
1 | rm: cannot remove ... gitlab_runner_env: |
原因是:
1 | gitlab-runner 用户 |
在操作:
1 | zxy 用户拥有的目录 |
之后改为 systemd system-mode Runner。
这样 Runner:
1 | 运行用户:gitlab-runner |
职责边界更清楚。
5.2 .bash_logout 导致 Prepare Environment 失败
切换到 system-mode 后遇到:
1 | Preparing environment |
最终定位到:
1 | /home/gitlab-runner/.bash_logout |
删除该文件后 Shell Executor 恢复正常。
这个问题让我意识到:
Shell Executor 并不是简单执行几条 shell 命令,它还受到 Runner 用户 shell profile 的影响。
例如:
1 | .bashrc |
都可能影响 CI Job。
6. Runner Tags
最初 Runner 在线,但 Job 一直处于 Pending。
Runner Tag:
1 | shell |
Job 没有:
1 | tags: |
同时 Runner 又没有允许:
1 | Run untagged jobs |
因此无法调度。
关系可以理解为:
1 | Job tags |
例如:
1 | job: |
只能由包含:
1 | shell |
tag 的 Runner 执行。
7. 最小 Pipeline
最初的 Pipeline:
1 | stages: |
Build:
1 | build-job: |
Test:
1 | test-job: |
Deploy:
1 | deploy-job: |
这完成了最基本的:
1 | Build |
8. Artifact
Build Job 中:
1 | artifacts: |
会让 GitLab 保存:
1 | build/app.txt |
后续 Job 可以继续使用。
这个机制后来在 Docker Executor 实验中被进一步验证。
9. Docker Executor
除了 Shell Runner,又注册了一个:
1 | devops-lab-docker-runner |
Executor:
1 | docker |
测试 Job:
1 | docker-env-test: |
实际结果:
1 | whoami |
1 | hostname |
1 | pwd |
OS:
1 | Alpine Linux |
这和 Shell Executor 有非常明显的区别。
10. Shell Executor vs Docker Executor
| 项目 | Shell Executor | Docker Executor |
|---|---|---|
| 执行位置 | Runner Host | 临时 Container |
| OS | Host OS | Image 决定 |
| 用户 | gitlab-runner | Image 决定 |
| 工具链 | Host 安装 | Image 提供 |
| 文件系统隔离 | 较弱 | 较强 |
| Job 结束 | 环境仍存在 | Container 销毁 |
| 依赖环境 | Host | Docker + Registry |
Docker Executor 最大的一个优点是:
1 | image: python:3.13 |
或者:
1 | image: maven:3.9-eclipse-temurin-21 |
Job 可以自己定义工具链。
不需要在 Runner Host 上安装所有版本的:
1 | Java |
11. Docker Executor 网络问题
Docker Executor 第一次 clone repository 时出现:
1 | Failed to connect to gitlab.test:8929 |
Shell Executor 可以访问:
1 | gitlab.test |
但 Container 不能。
原因是:
1 | Host 网络命名空间 |
对于 Container:
1 | 127.0.0.1 |
指向的是 Container 自己,而不是 WSL Host。
Runner 配置中增加:
1 | extra_hosts = [ |
之后 Job Container 可以访问:
1 | gitlab.test |
这也是 Docker Executor 相比 Shell Executor 很重要的区别:
Host 能访问某个服务,不代表 CI Container 也能访问。
后续如果使用:
1 | GitLab |
都必须站在 Job Container 的网络视角重新检查。
12. Docker Registry 网络波动
Docker Executor 还遇到过:
1 | Pulling docker image alpine:latest |
然后:
1 | net/http: timeout awaiting response headers |
Retry 后成功。
Maven Image 也出现过长时间卡在:
1 | Pulling docker image |
这说明 Docker Executor 又增加了一层依赖:
1 | GitLab Runner |
所以:
1 | Pipeline 失败 |
不一定意味着:
1 | script 失败 |
也可能发生在:
1 | Preparing Docker Executor |
阶段。
常见排查方向:
1 | Registry |
13. Docker Job 文件隔离
为了验证不同 Docker Job 是否共享文件系统,做了如下实验。
Job A:
1 | docker-write: |
Job B:
1 | docker-read: |
结果:
1 | cat: can't open '/tmp/docker-test': |
原因:
1 | Job A |
两个 Job 并不是同一个 Container。
14. 使用 Artifact 跨 Job 传递文件
将 Job A 修改为:
1 | docker-write: |
Job B:
1 | docker-read: |
之后成功。
因此:
1 | Job Container A |
而不是:
1 | Container A filesystem |
15. Cache
接下来验证 Cache。
测试配置:
1 | cache-build: |
第一次:
1 | Cache MISS |
后续 Pipeline:
1 | Cache HIT |
Runner 日志:
1 | Creating cache demo-cache-protected... |
16. Local Cache
这里的:
1 | No URL provided |
并不是错误。
当前没有配置:
1 | S3 |
等 Shared Cache Backend。
所以 Cache 保存在当前 Runner 本地。
可以把 cache 看成一种:
1 | Disposable |
即:
1 | Cache 正常 |
因此:
Cache 不应该成为构建正确性的唯一来源。
16.1 并发 Job 与 Cache
如果多个 Job 使用同一个 cache key:
1 | cache: |
同时又都允许:
1 | pull + push |
那么多个 Job 可能同时恢复 cache,再分别写入并上传。
例如:
1 | Job A Job B |
因此共享 cache 本质上属于:
1 | Shared Mutable State |
在大规模并发环境中,需要考虑:
- Cache key 设计;
- 哪些 Job 可以写 cache;
- 哪些 Job 只读取 cache;
- Cache 损坏后的恢复方式;
- Local Cache 与 Shared Cache 的故障域。
当前实验暂时不继续深入这一部分。
17. Artifact vs Cache
目前实验后,我对两者的理解是:
Artifact
目的:
1 | 传递 Pipeline 的构建结果 |
例如:
1 | JAR |
典型流程:
1 | Build |
Artifact 更接近:
1 | Pipeline Output |
Cache
目的:
1 | 减少重复下载或重复计算 |
例如:
1 | .m2/repository |
即使 Cache 被删除:
1 | Build 仍然应该能够完成 |
只是:
1 | 速度变慢 |
因此一个重要原则是:
Cache 应该是可重新生成的。
18. Maven Cache
为了验证真实 Dependency Cache,创建了一个最小 Maven 项目。
CI:
1 | maven-build: |
第一次运行:
1 | Maven 下载 dependencies |
Runner 保存:
1 | .m2/repository |
第二次运行:
1 | Maven 没有重新下载这些 dependencies |
说明 Maven Cache 生效。
完整流程:
1 | Pipeline #1 |
19. MR Pipeline
最开始配置:
1 | workflow: |
这样会产生:
1 | Branch Pipeline |
也就是 feature branch push 一次,创建 MR 后又跑一次。
实际需求并不是每一个开发过程中的 push 都执行完整 CI。
所以最终改成:
1 | workflow: |
最终效果:
1 | Feature Push |
1 | Create / Update MR |
1 | Merge main |
这更符合希望模拟的开发流程。
20. Waterfall CI Model
另一个项目:
1 | waterfall-ci-playground |
用于模拟以前接触过的一种更偏传统产品研发的流程:
1 | Feature |
Merge 后:
1 | 不会立即发布正式版本 |
而是在某个 milestone:
1 | 创建正式 Release |
因此 Workflow:
1 | workflow: |
也就是说:
1 | Feature Push |
21. 有限 Testbed 与 resource_group
实际硬件测试中,一个非常重要的问题是:
1 | Testbed 数量有限 |
两个 Pipeline 如果同时部署同一台机器:
1 | Pipeline A Deploy |
Pipeline A 测到的已经可能不是自己的代码。
GitLab 可以使用:
1 | resource_group: testbed-01 |
限制:
1 | 同一个 resource_group |
因此:
1 | Pipeline A |
这里还有一个重要设计点:
对于本次模拟的独占 Testbed,Deploy 和 Test 最好放在同一个占有
resource_group的 Job 内。
否则:
1 | Deploy Job |
中间可能被其他 Pipeline 抢走 Testbed。
22. Docker Image 与 CI Job Image
在开始 Docker Image Build 之前,需要先明确:
1 | image: docker:cli |
并不等于:
1 | docker exec -it <container> sh |
docker exec 的含义是:
1 | 已经存在的 Container |
而 GitLab CI 中:
1 | image: docker:cli |
表示:
1 | GitLab Runner |
例如:
1 | docker:cli image |
因此:
CI Job 的
image:可以理解为这个 Job 的“临时工作机模板”。
23. 如何选择 CI Job Image
选择 Image 时,不需要背大量 Image 名字。
更合理的思路是:
1 | 这个 Job 要执行什么命令? |
例如:
1 | mvn + JDK |
1 | Python |
1 | npm / node |
1 | Docker CLI |
1 | 简单 shell 工具 |
23.1 Image 可以包含多个工具
Image 并不是:
1 | JDK |
只能四选一。
例如:
1 | maven:3.9-eclipse-temurin-21 |
本身就是一个组合环境:
1 | Maven 3.9 |
因此 Java Maven Build 可以直接:
1 | java-build: |
23.2 Spring Boot 通常不是 CI 环境中的安装工具
对于 Spring Boot 项目:
1 | JDK |
并不是需要在 Job Container 中分别安装三个软件。
通常结构是:
1 | Job Container |
Spring Boot:
1 | 由 pom.xml 定义 |
例如:
1 | <dependency> |
然后:
1 | Maven |
因此最普通的 Spring Boot Job 可能只有:
1 | java-build: |
23.3 一个 Job 同时需要很多工具怎么办
如果只缺少少量附加工具,例如:
1 | curl |
可以临时安装:
1 | image: maven:3.9-eclipse-temurin-21 |
如果团队长期固定需要:
1 | JDK |
则更适合维护自己的 CI Image:
1 | registry.company.com/ci/java-build:2026.09 |
例如:
1 | FROM maven:3.9-eclipse-temurin-21 |
然后:
1 | image: registry.company.com/ci/java-build:2026.09 |
这种方式与过去 Jenkins Agent 预装工具的思路很相似:
1 | Jenkins Agent |
23.4 不要把所有职责都塞进一个 Job
另一种方法是拆 Job。
例如:
1 | Maven Build |
可以拆成:
1 | Job A |
所以可以使用这样的判断:
1 | 少量附加工具 |
23.5 本地检查陌生 Image
如果不确定某个 Image 里面有什么,可以先本地检查。
例如:
1 | docker run --rm docker:cli docker version |
查看 OS:
1 | docker run --rm docker:cli cat /etc/os-release |
进入 shell:
1 | docker run --rm -it \ |
然后:
1 | which docker |
Maven Image:
1 | docker run --rm \ |
因此 Image 选择不需要完全依赖名称猜测。
24. Docker Socket Binding —— 已验证
为了让 Docker Executor 中的 Job 使用 Docker CLI,Runner 配置增加:
1 | [runners.docker] |
然后使用:
1 | docker-socket-test: |
实验成功。
24.1 docker version
实际输出同时存在:
1 | Client: |
以及:
1 | Server: Docker Engine - Community |
其中:
1 | Client |
来自:
1 | docker:cli Job Container |
而:
1 | Server |
来自:
1 | WSL Host Docker daemon |
两者版本不完全相同,但 Docker API 都是:
1 | 1.56 |
因此能够正常通信。
24.2 docker info
更加明确的证据来自:
1 | Operating System: Ubuntu 26.04.1 LTS |
当前 CI Job 使用的是:
1 | docker:cli |
Image。
但 docker info 返回的却是:
1 | WSL2 Ubuntu Host |
的信息。
这证明:
1 | Job Container |
24.3 Job 可以看到 Host Container
docker ps 实际看到:
1 | runner-aww8idcik-project-3-concurrent-0-...-build |
其中:
1 | gitlab |
是宿主机长期运行的 GitLab Container。
另外:
1 | runner-...-build |
正是当前正在运行这个 CI Job 的 Container。
也就是说:
Job Container 通过 Host Docker daemon,甚至可以看到“自己”。
24.4 这不是 Docker-in-Docker
当前结构并不是:
1 | Host Docker |
而更接近:
1 | WSL Host Docker daemon |
Job Container 中只有:
1 | Docker CLI |
它通过:
1 | /var/run/docker.sock |
直接向宿主机 Docker daemon 发命令。
因此这种结构也常被理解为:
1 | Sibling Containers |
而不是:
1 | Nested Containers |
25. Docker Socket 的安全边界
当前 Runner 没有要求:
1 | privileged = true |
但:
1 | /var/run/docker.sock |
本身已经提供了很高权限。
如果 Job 可以控制 Host Docker daemon,那么它理论上可以:
1 | 创建 Container |
因此:
1 | Docker Job Container isolation |
这也是为什么:
1 | privileged = false |
并不意味着当前 Job 就是低权限的。
第 27 节通过 DinD 对比:
1 | Socket Binding |
理解二者不同的安全和资源边界。
26. Docker Socket Binding:实际构建 Docker Image
第 24~25 节已经证明 CI 中的 Docker CLI 能经由挂载的 /var/run/docker.sock 访问 WSL 宿主机 Docker Daemon。这次进一步完成真正的 Image Build。
实验用 Dockerfile:
1 | FROM alpine:3.24 |
build-job 生成 build/app.txt 并保存为 Artifact,镜像构建 Job 通过 needs 获取该文件:
1 | docker-image-build: |
这段脚本只展示实验所需的关键 Job;项目还包含 Maven 等其他 Job,实际提交需要保持 .gitlab-ci.yml 中对应的 Stage 和 Workflow 一致。
第一次 docker build 在访问 Docker Hub Token 服务时失败:
1 | failed to fetch anonymous token |
随后在 WSL 宿主机手动拉取 alpine:3.24,重新运行该 Job 后成功。先前已证明 Job 使用的是宿主机 Daemon,因此它能利用宿主机已有的基础镜像。
通过后,docker image ls waterfall-demo 可以在 WSL 宿主机看到镜像;这是 Socket Binding 与下一节 DinD 镜像存储行为的关键区别。
27. Docker-in-Docker:独立 Docker Daemon
27.1 为 DinD 单独注册 Group Runner
我没有直接在原 Docker Runner 上开启 privileged,而是在 devops-lab Group 下新建 Runner:
1 | Runner name: devops-lab-dind-runner |
注册时容易误把 dind 当成 Executor。实际上 gitlab-runner register 提示 Enter an executor 时仍应输入 docker;dind 是选用的 Runner Tag 和 CI 服务模式,并不是 Executor 类型。
新 Runner 的核心配置(注册产生的 Token 等字段省略):
1 | [[runners]] |
这里没有挂载宿主机 /var/run/docker.sock。传统 DinD 要启动自己的 Docker Daemon,因此本次 Lab 的 Runner 开启了 privileged = true。这属于高风险设置,不能直接作为面向不可信代码的公共 Runner 配置。关闭 Untagged Job 仅避免误调度,Runner Tag 本身也不是权限隔离边界。
27.2 DinD Job
1 | docker-dind-build: |
上面是 DinD 基础构建实验的目标配置;第 28 节的 Registry 实验改用了另一个 Dockerfile,以避开 Docker Hub 访问问题。
DOCKER_HOST=tcp://docker:2375 指向 DinD Service。DOCKER_TLS_CERTDIR="" 关闭该实验网络内 DinD 的 TLS,不可照搬到开放网络。常规做法是限制服务网络并优先采用更安全的构建架构。
27.3 DNS 故障:Maven 与 DinD 同时受影响
最初 Maven 失败:
1 | repo.maven.apache.org: Temporary failure in name resolution |
DinD 构建则失败在:
1 | lookup registry-1.docker.io on 172.19.96.1:53: i/o timeout |
Windows ipconfig 显示 vEthernet (WSL) 为 172.19.96.1/20。这个 IP 是当时 Windows 侧 WSL 虚拟网卡地址,不是人为给 WSL 固定的服务 IP,也不能因为 DNS 出问题就删除它。
我分别在普通 Docker 容器内尝试默认 DNS、223.5.5.5 和路由器 192.168.5.1:默认 DNS 解析失败,后两者成功。因此针对 两个 Docker Executor Runner 增加:
1 | [runners.docker] |
对 DinD 的独立 Daemon 还增加了服务参数:
1 | services: |
调整后 Maven 与相应 DinD Job 均曾构建通过,确认该 DNS 问题获得缓解。但之后又遇到 registry-1.docker.io:443 的连接超时:DNS 能解析,不代表 Docker Hub HTTPS 链路就一定可达。国内网络访问受限时,DinD 的临时镜像存储也不能直接复用 WSL 宿主机已拉取的 Alpine Image。
27.4 Socket Binding 与 DinD 对比
| 维度 | Docker Socket Binding | Docker-in-Docker |
|---|---|---|
| Daemon | WSL 宿主机 Docker Daemon | DinD Service 内独立 Daemon |
| 连接方式 | Unix Socket | 本实验 TCP 2375 |
| 镜像存储 | 宿主机镜像存储 | DinD Service 自己的临时存储 |
| 基础镜像缓存 | 可直接复用宿主机缓存 | 默认无法复用宿主机缓存 |
| 主要风险 | Job 可控制宿主机 Docker Daemon | 传统 privileged DinD 会削弱隔离 |
本实验中两者均能执行 Docker 命令,但它们控制的是不同的 Daemon。后续用 Container Registry 将镜像从 DinD 环境交付出去。
28. GitLab Container Registry:Build → Push → Pull
28.1 启用本地 Registry
在现有 GitLab CE Docker Compose 的 GITLAB_OMNIBUS_CONFIG 中增加:
1 | external_url 'http://gitlab.test:8929' |
端口映射对应增加:
1 | ports: |
执行 docker compose up -d 重新创建容器后,保持原有 config、logs、data 持久化目录不变。通过 docker exec gitlab gitlab-ctl status 可看到 registry 进程运行。
最初尝试的是端口 5050,但它遇到了 Windows/WSL2 网络访问问题,最终改为 5600。
28.2 代理、portproxy 与 Windows 排除端口
首次执行:
1 | curl -i http://gitlab.test:5050/v2/ |
得到的竟是 503 Forwarding failure (Privoxy@localhost)。这不是 Registry 的 503,而是 WSL 的 HTTP 代理尝试通过 SOCKS5 转发本地服务失败。使用下面的命令可以排除代理干扰:
1 | curl -i --noproxy '*' http://gitlab.test:5050/v2/ |
此时转为 Connection refused。进一步检查:
1 | WSL → 127.0.0.1:5050/v2/ HTTP 401(正常) |
说明 Registry 内部正常,问题发生在访问 Windows LAN 地址 192.168.5.3 的路径上。查看 Windows:
1 | netsh interface portproxy show all |
虽然 portproxy 已有 0.0.0.0:5050 → 172.19.106.103:5050,但 Windows 没有创建监听。原因最终定位为 Windows 的排除端口范围:
1 | 5041 5140 |
5050 落在这个范围内。因此没有去删除系统保留区间,而是调整 Registry 对外端口为 5600,同步修改 GitLab 配置、Docker Compose、Windows portproxy 和允许访问的 Windows 防火墙规则。
1 | # 在管理员 PowerShell 执行;WSL IP 应先用 hostname -I 核对 |
最终:
1 | curl -i --noproxy '*' http://gitlab.test:5600/v2/ |
返回:
1 | 401 Unauthorized |
这里的 401 是预期行为:Registry 已经可达,但尚未携带认证信息。WSL IP 可能随网络重建而变化,Windows portproxy 的目标 IP 需要相应核对和更新。
28.3 Docker Hub 不稳定时,用 scratch 进行封闭实验
DinD 访问 Docker Hub 一度出现:
1 | Head https://registry-1.docker.io/v2/library/alpine/manifests/3.24 |
为避免外部 Registry 阻断本次内部 Registry Push/Pull实验,新建 Dockerfile.registry-test:
1 | FROM scratch |
scratch 是 Docker 的特殊空基础镜像,不会为基础镜像层请求 Docker Hub;它也没有 shell 或 cat,所以不能像 Alpine 镜像一样直接 docker run ... cat,验证采用 docker create + docker cp。
28.4 DinD 登录、Push、删除、Pull 与文件校验
下面是本次成功实验的核心 Job 配置;build-job 和 maven-build 的定义仍在其他位置:
1 | docker-dind-build: |
这里使用了 GitLab CI 内置的 CI_REGISTRY、CI_REGISTRY_IMAGE、CI_REGISTRY_USER、CI_REGISTRY_PASSWORD。--password-stdin 避免把认证密码直接写进命令参数;不要在日志中打印密码。
由于 Registry 是 HTTP,本次受控 Lab 在 DinD Daemon 上指定 --insecure-registry=gitlab.test:5600。这不等于给 WSL 宿主机 Docker Daemon 配置了同样的 HTTP Registry 信任;不同 Daemon 需分别管理。生产环境应使用 TLS。
28.5 Layer 上传成功,但 Manifest 提交失败
首次运行时,各 Layer 已显示 Pushed,最后仍报:
1 | error from registry: blob unknown to registry - sha256:21bb52... |
这说明镜像 Push 不只是传输各个 Layer,还要由 Registry 接受最终 Manifest / Image Index。排查方向指向 BuildKit 默认 Attestation 与 Registry 校验之间的兼容性;不能仅凭 Layer 显示 Pushed 判断镜像上传完成。
在 Job 变量中加入:
1 | BUILDX_NO_DEFAULT_ATTESTATIONS: "1" |
重新运行后 Pipeline 通过,完成镜像 Push、从 DinD 本地删除、重新 Pull 和文件校验。这个 Workaround 是为了验证实验链路,不能简单推广为所有 Registry 生产环境的标准做法。
至此确认:DinD 临时环境内构建的镜像,可以通过 GitLab Container Registry 保存并重新获取。本次没有验证从 WSL 宿主机另一个 Docker Daemon 拉取该 HTTP Registry 镜像;也没有通过 scratch 镜像验证应用容器实际启动。
29. Runner 并发与 resource_group 实验
29.1 为什么最初看不出资源锁效果
在当前 Runner 服务的 /etc/gitlab-runner/config.toml 中发现:
1 | concurrent = 1 |
开始使用两个 Pipeline 进行验证时,第一个 testbed-exclusive 正在等待 90 秒,第二个 Pipeline 连 maven-build 等 Build Job 都停留在 Pending。这并不意味着所有 Job 被 resource_group 锁住:全局 Runner 并发为 1,本身就足以使无关 Job 排队。
将全局并发改为:
1 | concurrent = 4 |
重新运行后,另一个 testbed-exclusive 曾在界面显示:
1 | 此作业正在等待资源:testbed-01 |
这确认 GitLab 能识别资源占用,但跨 Pipeline 演示又受到 Maven Central 下载缓慢影响:等第二条 Pipeline 跑到 Testbed 阶段时,第一条早已结束。因此改为一个不依赖 Maven 的最小实验。
29.2 同一 Pipeline 两个 Job 直接竞争 Testbed
保留 testbed Stage,在同一 Pipeline 添加两个临时 Job:
1 | testbed-lock-a: |
这里的 needs: [] 是实验用的显式无依赖声明,让两个 Job 可以在 Pipeline 创建时直接进入调度,而不等待前面 build、image 等 Stage。它不是普通部署 Job 必须采用的配置。
验证结果:两个 Job 没有同时持有 testbed-01,后一个等待前一个释放资源后执行;实验通过。随后删除两个临时 Job,恢复原有 Pipeline。
29.3 最终理解
| 机制 | 层级 | 目的 |
|---|---|---|
concurrent |
GitLab Runner 进程 | 限制总体同时执行的 Job 数 |
limit |
单个 Runner | 限制该 Runner 可同时执行的 Job 数 |
needs |
Pipeline 内 Job DAG | 明确依赖并允许跨 Stage 提前调度 |
resource_group |
同一 GitLab 项目内 | 让同一资源组的 Job 互斥执行 |
resource_group 并不是整个 GitLab Group 中的全局分布式锁。对于真实物理 Testbed,必须结合资源所有权、超时回收和 Deploy/Test 锁范围设计。对本实验而言,已验证的是同一项目中 Job 的资源互斥。
30. Kubernetes Executor:架构学习(未实操)
本次没有安装 Kubernetes Runner,也没有创建 Kubernetes CI Pod。这里只总结 Kubernetes Executor 的典型工作方式,不把理论当作实验结果。
1 | GitLab Server |
与已经体验过的两种 Executor 对比:
| 维度 | Shell Executor | Docker Executor | Kubernetes Executor |
|---|---|---|---|
| 运行位置 | Runner Host | Docker 容器 | Kubernetes Pod |
| 工具来源 | Host 环境 | Job Image | Job Image |
| 隔离边界 | 较弱 | 容器 | Pod / 容器及集群策略 |
| 扩缩容 | 依赖 Host | 依赖 Docker Host | 利用集群资源调度 |
| 典型排障 | Shell Profile / 权限 | 网络 / 镜像 / Daemon | Pod Events / 调度 / ServiceAccount / NetworkPolicy |
Kubernetes Executor 不自动提供 Docker Daemon。 在 Kubernetes Job 里构建容器镜像,仍需选择适当的构建方式和权限模型,例如专门的 BuildKit 方案;不能直接假设 docker build 一定可用。
31. 总结:与 Jenkins 经验的对应关系
从最初的 GitLab CE 部署到最终的 Registry 和资源互斥实验,这次最大的收获不是记住几个 YAML 关键字,而是把 CI/CD 拆成明确的边界:
- GitLab Server 管理项目、MR、Pipeline、Artifact、Registry 和资源组调度;
- GitLab Runner 实际领取和执行 Job,Executor 决定执行环境;
- Artifact 传递准确的构建产物,Cache 只负责加速可重建的数据;
needs表达依赖关系,Stage 表达默认阶段顺序,resource_group处理有限资源互斥;- Socket Binding 和 DinD 看似都运行
docker build,实则有不同的 Daemon、安全边界和镜像存储; - Container Registry 才是脱离单个构建环境、让镜像可交付和复用的关键环节。
实际踩坑也非常集中:Shell Runner 的 .bash_logout、容器访问 gitlab.test、Docker Hub/Maven Central 的 DNS 与网络、WSL2 Windows portproxy 的端口排除、Privoxy 误代理本地 Registry、BuildKit Attestation 与 Registry Manifest 校验、Runner 全局并发掩盖 resource_group 等。
这些问题提醒我:Pipeline Failed 不一定是代码错误;需要先定位失败发生在 Git、Runner 准备环境、工具下载、构建、镜像上传,还是资源调度环节。
此次实验到这里告一段落。后续如果继续扩展,我更希望围绕安全的构建方案、可靠的内部镜像仓库、Runner 可观测性和 Kubernetes Executor 的真实资源限制展开,而不是继续为单纯增加组件而增加组件。