极狐GitLab

流水线配置到处复制粘贴,用组件与目录把它收回去

极狐GitLab
2026年10月8日
29151
分享:

流水线配置到处复制粘贴,用组件与目录把它收回去

一个中等规模的研发组织,CI 配置长什么样?大概率是这样的:十几个项目里躺着十几个 .gitlab-ci.yml,其中"跑单元测试""构建镜像""扫依赖"三段配置内容高度相似,但不是完全相同——A 项目加了一行缓存,B 项目改了镜像版本,C 项目把 stage 从 test 挪到了 build。

没有人知道哪份是"标准版"。想给全公司的构建作业统一加一个安全扫描步骤,只能挨个项目改,改到第五个的时候发现前两个又被别人改回去了。

这类问题的根源不在于工程师懒,而在于流水线配置缺少一个"可复用单元"。把一段配置变成可版本化、可参数化、可发布的组件,是收敛这类重复的第一步。

什么是 CI/CD 组件与 CI/CD 目录

CI/CD 组件是一个可复用的单一流水线配置单元,用 include: component 关键字引入。它和用 include 引用其他文件的区别在于:组件可以被列入 CI/CD 目录(CI/CD Catalog),可以发布并使用特定版本,也可以在同一个项目里定义多个组件并一起做版本控制。

CI/CD 目录则是已发布组件的可发现列表,使用者可以在其中搜索具备所需功能的组件,而不是去翻同事的仓库。

一个组件项目最多包含 100 个组件,项目内所有组件一起版本控制;如果某个组件需要独立的版本节奏,应把它移到专用的组件项目。

为什么复制粘贴的配置一定会失控

流水线配置的重复和代码重复不一样。代码重复至少还有编译期和单测兜底,配置重复的后果往往在半年后才暴露:

1. 修复无法扩散。 某个构建镜像的 tag 存在安全漏洞,需要全量升级。如果配置是复制的,就得逐个仓库搜索替换;如果是组件,升一个版本号、发一个新版本,引用方下次流水线就拿到了。

2. 版本引用是"移动目标"。 用分支名或 ~latest 引用外部配置,意味着上游任何一次提交都会静默改变下游所有流水线的行为。官方文档对此的建议很直接:依赖其他项目的组件时,应把版本固定到目录中的某个版本,而不是使用 ~latest 或 Git 引用这类移动目标。

3. 局部优化互相污染。 每个团队都在自己的副本上做微调,最终形成十几种互斥的"最佳实践",平台团队想推统一规范时无从下手。

4. 缺少测试环节。 复制粘贴的配置通常没有任何验证。组件项目可以在根目录写自己的 .gitlab-ci.yml,像测试普通项目一样在流水线里测试组件行为。

把一段重复配置改成组件,分五步

第 1 步:建组件项目并按目录结构放文件

组件项目的仓库必须包含一个 README.md 和一个顶层 templates/ 目录。目录里既可以为每个组件放单个 .yml 文件,也可以为每个组件建一个含 template.yml 的子目录(子目录里其他文件不会随组件发布,可用来放测试脚本或 Dockerfile)。

my-components/
├── README.md
├── .gitlab-ci.yml
└── templates/
    ├── build-image.yml
    └── secret-detection/
        └── template.yml

第 2 步:用 spec 声明输入参数

组件模板顶部用 spec 定义描述与输入,--- 之后才是真正的作业配置。$[[ inputs.xxx ]] 用来插值。

# templates/build-image.yml
spec:
  inputs:
    stage:
      default: build
    base_image:
      default: alpine:3.20
---
build-image:
  stage: $[[ inputs.stage ]]
  image: $[[ inputs.base_image ]]
  script:
    - echo "build in stage $[[ inputs.stage ]]"

注意:组件中不能使用 spec:include,组件应当自包含,不依赖外部文件。

第 3 步:在业务项目里引用它

引用格式为 <实例域名>/<项目路径>/<组件名>@<版本>:

# 业务项目的 .gitlab-ci.yml
include:
  - component: $CI_SERVER_FQDN/my-org/ci-components/build-image@1.2.0
    inputs:
      stage: test
      base_image: node:20-alpine

stages:
  - test

组件版本支持三种写法,优先级从高到低:提交 SHA、分支名或标签(如 1.0.0)、以及 ~latest 或部分语义化版本。发布到目录的组件必须使用语义化版本打标签;1.2 会选中最新的 1.2.*,1 会选中最新的 1.*.*,两者都不会命中预发布版本。

第 4 步:给组件项目写测试流水线

组件的测试项目和普通项目没有区别,关键是引用当前提交的 SHA:

# 组件项目根目录的 .gitlab-ci.yml
include:
  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/build-image@$CI_COMMIT_SHA
    inputs:
      stage: build

stages: [build, test, release]

ensure-job-added:
  stage: test
  image: badouralix/curl-jq
  script:
    - |
      route="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs"
      count=`curl --silent --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "$route" | jq 'map(select(.name | contains("build-image"))) | length'`
      if [ "$count" != "1" ]; then exit 1; else echo "Component Job present"; fi

第 5 步:打语义化标签并发布到目录

测试作业全部通过后,在 release 阶段创建版本,组件即进入 CI/CD 目录:

create-release:
  stage: release
  image: registry.gitlab.com/gitlab-org/cli:latest
  script: echo "Creating release $CI_COMMIT_TAG"
  rules:
    - if: $CI_COMMIT_TAG
  release:
    tag_name: $CI_COMMIT_TAG
    description: "Release $CI_COMMIT_TAG of components repository $CI_PROJECT_PATH"

如果组件项目是私有的,引用方需要做身份验证。

落地时的四个坑

坑 1:在组件里用全局关键字。 default、stages 这类全局关键字会作用于合并后的整个流水线,包括主 .gitlab-ci.yml 里定义的作业和其他组件里的作业。替代方案是把配置显式写到每个作业上,或者用 extends 但配一个不容易撞名的名字。

坑 2:硬编码实例域名。 组件里写死 gitlab.com 或某个私有化实例域名,组件就无法跨实例复用。官方建议统一使用 $CI_SERVER_FQDN 和 $CI_API_V4_URL 这两个预定义变量。

坑 3:假设 API 资源一定公开。 公开实例上某些 API 可以匿名访问,但私有化部署实例里组件项目可能是私有或内部的。需要访问令牌时,应通过输入参数或变量有选择地传入。

坑 4:为了 DRY 引入过深依赖。 官方的建议反直觉但很实用——少量重复通常比拥有依赖更好。依赖应保持在最低限度,本地资源优先(例如用 include:local 保证多个文件使用同一个 Git SHA)。

什么情况下别这么做

  • 组件只会用一次。 只有一个项目用的配置,抽象成组件只会增加一次跳转成本。

  • 团队还没有版本纪律。 组件的价值建立在语义化版本之上,如果团队连打标签都不规范,@1.2.0 和 @分支名 混用,收敛反而会引入新的不确定性。

  • 配置里全是实例专属的硬编码。 先做变量化改造,再谈组件化,否则搬过去的只是一堆新的硬编码。

  • 流水线总量很小。 三五个项目、每个十几行的场景,直接维护比建目录体系更省事。

常见问题

Q1:组件和普通 include 文件有什么区别?

普通 include 只是把文件内容合并进来,没有版本概念,也无法被目录发现。组件可以发布并使用特定版本,能进入 CI/CD 目录被搜索,且同一个项目可以定义多个组件一起做版本控制。

Q2:~latest 能不能用在生产流水线?

不建议。~latest 只在你明确希望始终使用绝对最新版本、且能接受破坏性变更时才用。生产流水线应固定到具体版本或语义化部分版本(如 1.2),这样既能自动接收补丁和次要更新,又不会被主要版本的破坏性变更打穿。

Q3:组件可以嵌套引用其他组件吗?

可以,但要谨慎。依赖其他项目的组件时必须把版本固定到目录中的某个版本,不要用 ~latest 或 Git 引用;同时要评估依赖项的权限,优先选择所需权限最少的方案——比如构建镜像可以考虑用不需要特权 Docker 守护进程的方案,而不是直接上特权 Runner。

想了解完整能力清单

组件的输入类型、组件上下文表达式、目录发布与搜索的完整规则,官方文档写得更细:

极狐GitLab 私有化部署:组件与 CI/CD 目录在基础版、专业版、旗舰版均可用,支持 JihuLab.com 与私有化部署两种交付形态。若想在自己环境里试用组件收敛流水线配置,可在 gitlab.cn 申请私有化部署试用。