The modularization of Continuous Integration and Continuous Deployment (CI/CD) pipelines is a critical requirement for any organization scaling its software delivery. Within the GitLab ecosystem, the include keyword serves as the primary mechanism for decomposing monolithic .gitlab-ci.yml files into manageable, reusable, and maintainable components. This capability allows teams to transition from a state where every project maintains a unique, sprawling configuration file to a centralized model where gold-standard pipeline templates are defined once and consumed across hundreds of disparate projects. By decoupling the pipeline logic from the application code, organizations can enforce compliance, standardize deployment patterns, and reduce the operational overhead associated with updating CI configurations across a vast portfolio of microservices.
Fundamental Syntax and Inclusion Methods
The include keyword provides several distinct methods for importing external YAML configurations. This versatility allows developers to source their pipeline logic from local files, remote URLs, predefined GitLab templates, or other projects within the same GitLab instance.
The most basic form of inclusion is the single configuration file. This can be achieved using two different syntax styles. A developer can specify the file directly on the same line as the keyword:
include: 'my-config.yml'
Alternatively, the file can be defined as a single item within an array:
include: - 'my-config.yml'
For more complex requirements, GitLab supports the inclusion of an array of configuration files. This is particularly useful when a pipeline requires a combination of different setup scripts and environment configurations. An array can contain a mixture of shorthand strings and explicit type definitions:
include: - 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml' - 'templates/.after-script-template.yml'
When explicit type definitions are required, the syntax shifts to a key-value pair format. This allows the pipeline to clearly distinguish between different source types:
include: - remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml' - local: 'templates/.after-script-template.yml' - template: Auto-DevOps.gitlab-ci.yml
Furthermore, the system supports project-level inclusions, which are essential for shared organizational libraries. This requires specifying the project path, the branch or tag reference, and the specific file name:
include: - project: 'my-group/my-project' ref: main file: 'templates/.gitlab-ci-template.yml'
The flexibility of these methods ensures that the include functionality is available across all tiers—Free, Premium, and Ultimate—and is supported regardless of the offering, whether it be GitLab.com, GitLab Self-Managed, or GitLab Dedicated.
Deep Dive into Include Types and their Impacts
The choice of inclusion type has significant implications for how the pipeline is managed and updated.
Local Includes
Local includes reference files within the same repository as the .gitlab-ci.yml file. This is the primary method for organizing large pipelines into smaller, logically grouped files (e.g., separating build, test, and deploy stages).
Impact: Local includes ensure that the pipeline configuration is versioned alongside the application code. This means that if a feature branch requires a change to the pipeline logic, that change is captured in the same commit as the code change, ensuring atomic updates.
Remote Includes
Remote includes allow the pipeline to fetch YAML content from any HTTP/HTTPS endpoint.
include: - remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
Impact: This enables the sharing of CI configurations across different GitLab instances or even from external sources. However, it introduces a dependency on the availability of the external server; if the remote host is down, the pipeline may fail to initialize.
Template Includes
Template includes leverage a library of ready-made templates provided by GitLab.
include: - template: Auto-DevOps.gitlab-ci.yml
Impact: Templates provide a "fast track" to industry-standard CI/CD patterns. Using templates like Auto-DevOps allows teams to implement complex build-test-deploy cycles without writing the YAML from scratch.
Project Includes
Project includes allow a pipeline to pull a file from another project on the same instance.
include: - project: 'my-group/my-project' ref: main file: 'templates/.gitlab-ci-template.yml'
Impact: This is the gold standard for enterprise-scale CI. A central "DevOps" project can host all approved pipeline templates. Application teams simply "subscribe" to these templates. When the DevOps team updates the template in the central project, all subscribing pipelines automatically inherit the improvement without requiring a manual commit to every single application repository.
Advanced Configuration Management and Overrides
The interaction between included files and the main configuration file is governed by specific rules regarding defaults and overrides.
The Default Section and Inheritance
A configuration file can define a default section. When an included file contains a default block, its properties are applied to the jobs in the pipeline. This is particularly useful for defining global before_script or after_script actions.
Example of a nested include structure:
Content of .gitlab-ci.yml:
include: - local: /.gitlab-ci/another-config.yml
Content of /.gitlab-ci/another-config.yml:
include: - local: /.gitlab-ci/config-defaults.yml
Content of /.gitlab-ci/config-defaults.yml:
default: after_script: - echo "Job complete."
In this scenario, the after_script is passed up through two levels of nesting to the primary pipeline.
Handling Duplicate Includes and Precedence
GitLab allows the same configuration file to be included multiple times, both in the main file and within nested includes. However, the order of these inclusions is critical. If a file is included and then subsequently overridden, the last inclusion takes precedence.
Consider three files: defaults.gitlab-ci.yml, unit-tests.gitlab-ci.yml, and smoke-tests.gitlab-ci.yml.
defaults.gitlab-ci.yml defines:
default: before_script: echo "Default before script"
unit-tests.gitlab-ci.yml includes defaults.gitlab-ci.yml but overrides it:
include: - template: defaults.gitlab-ci.yml default: before_script: echo "Unit test default override"
smoke-tests.gitlab-ci.yml also includes defaults.gitlab-ci.yml and overrides it:
include: - template: defaults.gitlab-ci.yml default: before_script: echo "Smoke test default override"
If both unit-tests.gitlab-ci.yml and smoke-tests.gitlab-ci.yml are included in the main configuration, the final value of the before_script will depend entirely on which file was included last.
Conditional Inclusions and Rule-Based Logic
The include keyword can be paired with rules to create dynamic pipelines that adapt based on the state of the repository.
The rules:exists Logic
The rules:exists keyword allows GitLab to conditionally include a file based on whether a specific file exists in the repository.
include: - local: builds.yml rules: - exists: - exception-file.md when: never
In the example above, builds.yml will not be included if exception-file.md is present.
A critical nuance exists when using rules:exists within a project-level include. GitLab evaluates the existence of the file relative to the project where the included file is hosted, not the project where the pipeline is running.
For example, if my-group/my-project includes other-file.yml from my-group/other-project, and other-file.yml contains:
include: - project: my-group/my-project ref: main file: my-file.yml rules: - exists: - file.md
GitLab will search for file.md inside my-group/other-project on the specified ref, not within my-group/my-project. To fix this and force the check to happen in the calling project, the project and ref keys must be explicitly defined within the exists rule:
rules: - exists: paths: - file.md project: my-group/my-project ref: main
The rules:changes Logic
The rules:changes directive allows for conditional inclusion based on whether specific files have been modified in a commit. This prevents the pipeline from loading unnecessary configurations (e.g., not loading the Android build YAML if only iOS files were changed).
Real-World Implementation: The Runway Case Study
The practical application of these concepts is evident in the "Runway" infrastructure, used for delivering multi-region AI features. In this architecture, a component called the Reconciler uses Golang and Terraform to align the actual state of services with the desired state.
Application developers configure their pipelines by including a standardized set of tasks from a central infrastructure project.
Example .gitlab-ci.yml in Runway:
yaml
stages:
- validate
- runway_staging
- runway_production
include:
- project: 'gitlab-com/gl-infra/platform/runway/runwayctl'
file: 'ci-tasks/service-project/runway.yml'
inputs:
runway_service_id: example-service
image: "$CI_REGISTRY_IMAGE/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA}"
runway_version: v3.22.0
This implementation demonstrates the use of inputs to parameterize the included YAML, allowing the same base template (runway.yml) to be used across different services by simply passing different IDs and versions.
Furthermore, Runway utilizes a service manifest (e.g., .runway/runway-production.yml) validated via JSON Schema. This manifest defines the regional deployment of the service:
yaml
apiVersion: runway/v1
kind: RunwayService
spec:
container_port: 8181
regions:
- us-east1
- us-west1
- europe-west1
For these multi-region services, the system injects the RUNWAY_REGION environment variable into the container runtime. This provides the necessary context for application developers to ensure downstream dependencies are regionally aware, preventing cross-region latency or data residency violations.
Performance Limits and Technical Constraints
As pipelines grow in complexity, the number of included files can lead to performance degradation during the configuration validation phase.
The Include Limit Issue
There have been documented cases where pipelines fail or time out after fetching a large number of includes. Specifically, issues have been raised when the system attempts to process around 435 includes. To mitigate this, the GitLab backend implements a validation check.
The proposed fix in the lib/gitlab/ci/config/external/mapper.rb file involves introducing a verify_max_includes! method. This method is called before validating each object in the include chain to ensure the system does not exceed a maximum threshold of included files, which would otherwise lead to catastrophic timeouts.
The Relative Path Challenge
A significant pain point for developers has been the lack of relative path support for include:local. Currently, all paths are treated as absolute to the repository root. This makes "forking" a CI configuration difficult, as every URL and path must be updated using tools like sed.
There is a proposal to support relative paths using ./ and ../ syntax. This would require the GitLab backend to track the location of the current file being processed and expand the requested path relative to that file.
Example of proposed relative include syntax:
include: - local: ./template.yml (File in same directory)
include: - local: ./../common.yml (File in upper directory)
This change would allow for a more portable directory structure, where a subdirectory can contain its own set of templates without needing to know the absolute path from the repository root.
Operational Comparison of Include Methods
The following table provides a detailed comparison of the different include methods and their ideal use cases.
| Method | Source Location | Scope | Update Mechanism | Best Use Case |
|---|---|---|---|---|
| Local | Same Repository | Project-specific | Commit-based | Organizing large pipelines |
| Remote | HTTP/HTTPS URL | Global | External Server | Sharing across different instances |
| Template | GitLab Predefined | Global | GitLab Update | Standardized DevOps patterns |
| Project | Another GitLab Project | Organization-wide | Central Project Commit | Centralized compliance and standards |
Strategic Analysis of Modular CI/CD
The transition to a modular include-based architecture is not merely a technical convenience but a strategic imperative for organizations operating at scale. By utilizing include:project, a company can create a "Versioned Pipeline Library." In this model, the central DevOps team releases versions of the pipeline (e.g., v1.0.0, v1.1.0). Application teams can pin their pipelines to a specific version:
include: - project: 'devops/templates' ref: 'v1.1.0' file: 'standard-pipeline.yml'
This prevents the "breaking change" scenario where a global update to a template accidentally breaks hundreds of active pipelines. Application teams can migrate to the new version at their own pace, testing the new pipeline logic in a feature branch before merging.
Moreover, the integration of rules:exists and rules:changes allows for the creation of "Intelligent Pipelines." Instead of a monolithic file that contains every possible job for every possible scenario, the pipeline only loads the logic it needs. This reduces the size of the YAML configuration that GitLab must parse, which directly addresses the performance issues identified in the 435-include limit scenario.
The use of inputs in project includes further elevates the pipeline to a "Function-as-a-Service" model. The template becomes a function, and the inputs are the arguments. This allows the central team to maintain the complex logic (like how to deploy to a multi-region Kubernetes cluster via Runway) while the application developer only needs to provide simple metadata like the runway_service_id.