引言
在CI/CD的世界里,GitLab Runner是连接代码仓库与自动化任务的桥梁。而选择何种执行器(Executor),则从根本上决定了这座桥梁的材质、结构与通行规则。shell
和docker
是其中最基础也最重要的两种选择。它们分别代表了两种截然不同的自动化哲学:一种是与宿主机环境深度融合,追求极致的简便与速度;另一种则是拥抱隔离与标准化,追求环境的纯净与一致。理解它们的内在逻辑,是设计高效、可靠流水线的基石。
Shell 执行器:简单直接的"本地专家"
shell
执行器是最简单、最直接的一种模式。当一个Job被分配给shell
执行器时,它所有的操作都直接在安装了GitLab Runner的宿主机上,以gitlab-runner
这个系统用户的身份来执行。
我们可以把它想象成一位"本地专家"。这位专家就住在工厂(宿主机)里,对工厂里的一草一木、所有工具(已安装的软件、环境变量)都了如指掌。当接到任务(Job)时,他会直接使用工厂里现成的工具来完成工作。
核心特点:
- 环境共享:所有Job共享同一个宿主机环境。这意味着上一个Job安装的依赖、修改的文件,可能会影响到下一个Job。
- 直接访问:Job脚本可以无缝访问宿主机的硬件、文件系统、网络以及所有已安装的软件。
- 高 效 性:没有创建和销毁容器的开销,对于简单的脚本任务,启动和执行速度非常快。
适用场景:
shell
执行器最适合执行那些需要与宿主机环境进行深度交互的任务,最典型的就是部署(Deployment) 。例如,将编译好的二进制文件复制到系统的服务目录、重启一个系统服务、执行数据库迁移脚本等。在这些场景下,利用shell
执行器的直接访问能力,远比在容器里想办法"穿透"到宿主机要方便得多。
潜在风险:
- 环境污染与冲突:不同项目可能需要不同版本的依赖(如Node.js、Python),在共享的宿主机上管理这些依赖会成为一场噩梦。
- 安全性 :Job脚本的权限等同于
gitlab-runner
用户。如果脚本存在漏洞或被恶意篡改,可能会对宿主机造成严重破坏。 - 可复现性差:构建过程严重依赖宿主机的"状态"。如果宿主机环境发生变化,或者需要迁移到新机器,很难保证构建结果的一致性。
Docker 执行器:洁净隔离的"环境魔术师"
docker
执行器则完全不同。它为每一个Job都创造一个全新的、临时的、完全隔离的Docker容器环境。Job的所有指令都在这个容器内部执行,执行完毕后,容器随即被销毁。
它就像一位"环境魔术师"。接到任务后,他不会使用工厂里现成的工具,而是根据任务清单(.gitlab-ci.yml
中的image
指令)瞬间变出一个一模一样、一尘不染的独立实验室(Docker容器)。实验(Job)完成后,整个实验室连同里面的所有东西都会消失得无影无踪。
核心特点:
- 环境隔离:每个Job都在自己的沙盒中运行,互不干扰。项目A的依赖绝对不会影响到项目B。
- 环境即代码 :构建环境由
image
关键字(如image: node:18-alpine
)精确定义,是CI/CD配置的一部分,保证了极高的一致性和可复现性。 - 安全性高:Job脚本被限制在容器内部,对宿主机的访问受到了严格的限制,大大降低了安全风险。
澄清关键概念:Runner在哪 vs Job在哪
这里必须澄清大家提出的那个关键点。使用docker
执行器,我们有两种部署GitLab Runner本身的方式:
- Runner on Host(推荐模式) :将GitLab Runner程序直接安装在宿主机操作系统上。当它执行一个Docker Job时,它会通过
docker.sock
文件与宿主机上的Docker守护进程通信,请求Docker守护进程来创建、运行和销毁Job容器。这是最常见、最直接、也是大家认为"更通用"的模式。 - Runner in Container(进阶模式) :将GitLab Runner程序本身也运行在一个Docker容器里。此时,这个"Runner容器"如果想创建"Job容器",就需要一种与Docker守护进程通信的方式。这就引出了两种复杂的技术:
- Docker-in-Docker (DinD):在"Runner容器"内部再启动一个独立的Docker守护进程。层层嵌套,配置复杂,且可能有效率问题。
- Socket Mounting :将宿主机的
/var/run/docker.sock
文件挂载进"Runner容器"。这让"Runner容器"内的Runner程序可以直接与宿主机上的Docker守护进程通信,从而创建兄弟容器(Sibling Containers)来执行Job。这种方式更高效,但存在安全隐患,因为它给予了容器控制宿主机Docker的巨大权限。
所以,大家的直觉是完全正确的:对于绝大多数应用场景,模式1(Runner on Host)是最佳选择。它兼顾了Docker执行器带来的隔离性好处,同时避免了管理容器化Runner的额外复杂性。
UML 建模:可视化执行流程
让我们用序列图来直观对比这两种执行器的工作流程。
这个图清晰地展示了:
Shell
执行器是Runner进程与宿主机Shell之间的直接对话。Docker
执行器则引入了Docker Daemon作为中间层,所有操作都被封装在短暂的Job容器内。
结论:因地制宜,人尽其才
shell
和docker
执行器并非孰优孰劣,而是各有其专长的舞台。
- 选择
docker
执行器 :作为构建、测试、打包等大多数CI任务的默认和首选。它提供的环境一致性和安全性是现代软件开发流程的基石。 - 选择
shell
执行器 :专门用于那些必须与宿主机直接交互 的特定任务,主要是部署和系统管理。运行shell
执行器的服务器应被视为生产环境的一部分,需要严格的安全管控。
最佳实践通常是将两者结合起来:在开发和测试服务器上部署带有docker
标签的Runner,用于处理绝大多数CI任务;在生产或预发布服务器上,部署带有shell
和特定环境标签(如production
, deploy
)的Runner,专门用于执行部署脚本。通过这种方式,我们就能充分利用每种执行器的优势,构建一个既灵活又稳健的自动化流水线。