Cut CI Duplication with GitLab Reusable YAML Templates
Learn how a single version‑controlled template can replace dozens of copied .gitlab-ci.yml files, enforce consistent stages, and still let each project override what it needs.
31 Mar 2026, 23:43 UTC

The problem: copy‑paste CI sprawl
Most teams start with a single .gitlab-ci.yml per repository. As the number of services grows, the same build, test, and deploy steps get duplicated across dozens of files. A change to a security scan or a new artifact policy then requires editing every repository — a maintenance burden that also invites drift.
Thesis: a versioned template repository solves the duplication
GitLab 13.9 introduced the include keyword for remote YAML files. By storing a canonical pipeline definition in its own project, you can reference it from any downstream project. The template remains DRY, version‑controlled, and can be extended with project‑specific overrides using extends.
How the template mechanism works
- Base template – a YAML file (e.g.,
templates/ci/base.yml) that defines stages, common jobs, shared variables, and defaultrules. - Include directive – in a consumer project’s
.gitlab-ci.ymlyou addinclude: - project: 'group/template-repo' file: 'templates/ci/base.yml' ref: 'v1.2.0'. Therefpins a tag or branch, giving you explicit version control. - Extends – a consumer job can inherit from a template job and override only what differs:
my-test: extends: .test-template variables: TEST_ENV: "staging".
Worked example: a shared build‑test‑deploy template
Assume a template project infra/ci-templates with the following templates/ci/base.yml:
stages:
- build
- test
- deploy
variables:
DOCKER_DRIVER: overlay2
.build-base: &build-base
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
.test-base: &test-base
stage: test
image: node:20
script:
- npm ci
- npm test
.deploy-base: &deploy-base
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/$APP_NAME $APP_NAME=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
environment:
name: production
only:
- main
build:
<<: *build-base
test:
<<: *test-base
deploy:
<<: *deploy-base
A consumer project team/api-service adds a minimal .gitlab-ci.yml:
include:
- project: 'infra/ci-templates'
file: 'templates/ci/base.yml'
ref: 'v2.1.0'
variables:
APP_NAME: api-service
# Override only the test job to add a lint step
test:
extends: .test-base
script:
- npm ci
- npm run lint
- npm test
When a pipeline runs, GitLab merges the included YAML with the local file. The log shows lines like Using template from infra/ci-templates@templates/ci/base.yml (v2.1.0), confirming the remote source.
Trade‑offs and limitations
- Learning curve – YAML anchors,
extends, and merge semantics can be confusing for newcomers. - Version management – Updating the template without bumping the
refin consumers can silently change behavior. Adopt a semantic‑version tag policy and require consumers to update the pin deliberately. - Secret leakage risk – Variables defined in the base template are inherited by all child pipelines. Keep secrets out of the template; inject them via protected CI/CD variables in each consumer project.
- Cache interaction – Jobs that pull the remote template via
includemay disable the pipeline cache for that job, reducing speed gains. Test cache behavior in your environment.
Verification checklist
- Confirm GitLab version – In the Admin area → Overview, ensure the version is ≥ 13.9. Alternatively, run
gitlab-rails console(requires admin shell access) and evaluateGitlab::VERSION. - Create a test project – Add a
.gitlab-ci.ymlthat includes the template with a known tag. - Run a pipeline – Trigger manually or push a commit. In the pipeline details, look for the
Using templatemessage and verify that stages and job names match the template. - Validate overrides – Check that any
extendsjobs show the combined script (template + local additions). - Lint the YAML – Use
gitlab-ci-lintlocally or the CI Lint page in the UI before committing.
Next steps
Start by extracting the most stable part of your existing pipelines — usually the build and test stages — into a template repository. Tag the first release (e.g., v1.0.0) and update a single low‑risk service to consume it. Once the pipeline runs green, roll the pattern out to the rest of the codebase, enforcing the tag‑pin policy through code‑review guidelines. This incremental approach limits blast radius while delivering the consistency and maintainability gains that reusable templates promise.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.