Back to Blog

From Legacy Includes to the Component Catalog: Scaling Your GitLab CI/CD Architecture

Most of us started by copy-pasting .gitlab-ci.yml snippets or building a "monster repo" of YAML files that everyone remotely included via a fragile git ref. It worked for three projects, but once you hit thirty, every change feels like pulling a loose thread on a cheap sweater.

GitLab finally addressed this scaling nightmare with the CI/CD Component Catalog. It’s essentially a package manager for your pipeline logic, moving us away from "hope-and-pray" includes toward versioned, validated, and discoverable building blocks.

The Fragility of the "Remote Include"

In the old days (read: last year), if you wanted to share a Docker build script across your organization, you probably did something like this:

include:
  - remote: 'https://gitlab.com/my-org/templates/-/raw/main/docker-build.yml'

This is dangerous for two reasons. First, if someone pushes a breaking change to main in that template repo, every pipeline in your company starts failing simultaneously. Second, there’s no formal way to pass arguments. You had to rely on global environment variables. If your template expected DOCKER_IMAGE_NAME and the user forgot to set it, the job would simply crash—or worse, deploy to a default production bucket by mistake.

Enter the Component: Formalizing the Contract

A CI/CD Component is just a repository (or a sub-directory in one) that contains a spec section. This spec is the most important part—it defines the "input variables" your pipeline requires. It’s a hard contract. If a user doesn’t provide a required input, the pipeline won't even start.

Here is what a basic templates/my-component.yml looks like:

spec:
  inputs:
    stage:
      default: test
    node_version:
      type: string
---
my-job:
  stage: $[[ inputs.stage ]]
  image: node:$[[ inputs.node_version ]]
  script:
    - npm install
    - npm test

Notice the --- separator. Everything above it is the metadata (the inputs); everything below it is the actual YAML that gets injected into the project. The $[[ inputs.variable ]] interpolation happens before the YAML is parsed, which is much more robust than standard environment variable substitution.

Why "Inputs" Beat "Variables"

When you use standard GitLab variables ($MY_VAR), the pipeline has to actually run the job to see if the variable exists. With spec: inputs, GitLab validates the configuration at the moment the pipeline is created.

I’ve spent too many hours debugging pipelines where a variable was named DEPLOY_ENV in one script and ENVIRONMENT in another. Components solve this by making the requirements explicit:

# In the consuming project
include:
  - component: $CI_SERVER_FQDN/my-org/nodejs-component/[email protected]
    inputs:
      node_version: "20.10.0"

If I omit node_version, GitLab throws a linting error immediately. That feedback loop is seconds instead of minutes.

Scaling with the Catalog

The "Catalog" part of this feature is what makes it professional. You can designate a project as a Catalog Resource. This gives you a UI where developers can browse available components, read READMEs, and see version history.

To make this work, you follow a specific directory structure:

my-component-repo/
├── README.md
├── LICENSE
├── templates/
│   ├── build.yml
│   └── deploy.yml
└── .gitlab-ci.yml (for testing the component itself)

You release new versions by simply creating a Git Tag. This is the "Aha!" moment for platform teams. Instead of telling everyone to "use the latest," you can tell them to stay on @1.2.0 while you test @2.0.0 in a sandbox.

Some "Gotchas" to Keep in Mind

Transitioning isn't perfectly seamless. Here are a few things that tripped me up:

1. Global Keywords: Components cannot contain global keywords like default or stages. If your component needs a specific stage, you should pass the stage name as an input so the consuming project can map it to their own stages list.
2. The Interpolation Syntax: It uses $[[ inputs.name ]], not ${{ inputs.name }} or $inputs.name. The double brackets and the dollar sign are mandatory.
3. Local Testing: Testing a component while you're writing it can be clunky. I usually create a test-pipeline.yml in the same repo that includes the component using a local path to verify changes before tagging a release.

Is it worth the migration?

If you are managing more than five repositories, yes.

The transition allows you to treat your CI/CD infrastructure like actual software. You get versioning, documentation, and input validation. Stop treating your pipelines like a collection of scripts and start treating them like a platform. Your 3 AM self will thank you the next time you need to update a core security scanning step across 500 microservices.