GitLab CI/CD Template Guide

Optimizing Pipeline Implementation Using GitLab CI Templates

Featured image

Image Reference link



Overview

Learn how to optimize your GitLab CI implementation using templates. Templates help reduce code duplication and maintain consistent pipeline configurations across projects.

Using GitLab CI Templates

Built-in Templates

# Reference GitLab's built-in templates
include:
  - template: Terraform/Base.gitlab-ci.yml
  - template: Jobs/SAST-IaC.gitlab-ci.yml

Project Templates

If the template is defined in your gitlab’s project, you can write it as follows.

# Reference templates from your project
include:
  - project: 'somaz94/server'
    ref: master
    file: '/template/my_template_file.yml'



🔧 Creating Custom Templates

Build Template Example



Using Build Template

And it can be used as below.

build_manual_image:
  <<: *common_job_config
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.22.0-debug
  before_script: *common_build_before_script
  script: *common_build_script
  artifacts:
    paths:
      - build_status.txt
      - service_status.txt
  rules:
    - if: '($CI_PIPELINE_SOURCE == "web")'
  tags:
    - build-image

And it can be used for other jobs.

.change_files_game: &change_files_game
  changes:
    - apps/game/src/**/*

build_auto_image_game:
  <<: *common_job_config
  stage: build
  image: 
    name: gcr.io/kaniko-project/executor:v1.22.0-debug
  before_script: *common_build_before_script
  script: *common_build_script
  variables:
    SERVICE: $GAME_SERVICE
    IMAGE_URL: $IMAGE_URL_GAME
    IMAGE_URL_LATEST: $IMAGE_URL_LATEST_GAME
  artifacts:
    paths:
      - build_status.txt
      - service_status.txt
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'
      <<: *change_files_game
  tags:
    - build-image

Slack Notification Template

The template for sending a message to SLACK can also be written as follows. You can receive the result as an artifact for each job and send a message.

  .common_update_after_script: &common_update_after_script
    - >
      if [ "$CI_JOB_STATUS" = "success" ]; then
        echo "✅ success" > /builds/somaz94/server/update_status.txt
      elif [ "$CI_JOB_STATUS" = "failed" ]; then
        echo "❌ failed" > /builds/somaz94/server/update_status.txt
      elif [ "$CI_JOB_STATUS" = "canceled" ]; then
        echo "⚠️ canceled" > /builds/somaz94/server/update_status.txt
      else
        echo "🔍 unknown" > /builds/somaz94/server/update_status.txt
 ...
 
 update_manual_image:
  stage: update
  image: alpine:latest
  interruptible: true
  retry:
    max: 2  # Maximum of 2 retries
    when:
      - runner_system_failure
      - unknown_failure
  before_script: *common_update_before_script
  script: *common_update_script 
  after_script: *common_update_after_script
  artifacts:
    paths:
      - update_status.txt
  rules:
    - if: $CI_PIPELINE_SOURCE == "web"
  tags:
    - deploy-image
  dependencies:
    - build_manual_image

I used the SLACK webhook, and I used the workflow builder to write so that I could only get the result value. HTTPS

Notification Script Template

Create a Slack notify template.



Using Notification Template

And write the yml file as below.

notify_slack:
  stage: notify
  image: curlimages/curl:latest
  script: *common_notify_slack_script
  rules:
    - if: $CI_PIPELINE_SOURCE == "web"
    - if: $CI_PIPELINE_SOURCE == "trigger"
  tags:
    - deploy-image
  when: always

...
notify_slack_auto_game:
  stage: notify
  image: curlimages/curl:latest
  script: *common_notify_slack_script
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push"'
      <<: *change_files_game
  tags:
    - deploy-image
  when: always
  dependencies:
    - build_auto_image_game
    - update_auto_image_game

If used in this way, a template can be created and applied to various jobs as follows. HTTPS



More Template Examples

Docker Build Template with Multiple Stages

This template handles Docker builds with dedicated stages for building, testing, and pushing:

.docker_build_template: &docker_build_template
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_BUILDKIT: 1
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - export IMAGE_TAG=${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}
    - export FULL_IMAGE_NAME=$CI_REGISTRY_IMAGE:$IMAGE_TAG
  script:
    - echo "Building image $FULL_IMAGE_NAME"
    - >
      docker build 
      --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') 
      --build-arg VCS_REF=$CI_COMMIT_SHORT_SHA
      --build-arg VERSION=$CI_COMMIT_REF_NAME
      -t $FULL_IMAGE_NAME
      -t $CI_REGISTRY_IMAGE:latest
      .
    - docker push $FULL_IMAGE_NAME
    - docker push $CI_REGISTRY_IMAGE:latest

Multi-Environment Deployment Template

Template for deploying to different environments based on conditions:

.deploy_template: &deploy_template
  image: 
    name: bitnami/kubectl:latest
    entrypoint: [""]
  variables:
    GIT_STRATEGY: none
  before_script:
    - kubectl config use-context $KUBE_CONTEXT
    - |
      if [[ -n "$NAMESPACE" ]]; then
        # Ensure namespace exists
        kubectl get namespace $NAMESPACE || kubectl create namespace $NAMESPACE
      fi

# Development deployment
.deploy_dev: &deploy_dev
  <<: *deploy_template
  variables:
    NAMESPACE: development
    REPLICAS: 1
    RESOURCES_REQUESTS_CPU: 100m
    RESOURCES_REQUESTS_MEMORY: 128Mi
    RESOURCES_LIMITS_CPU: 200m
    RESOURCES_LIMITS_MEMORY: 256Mi
  script:
    - echo "Deploying to development environment"
    - envsubst < k8s/dev/deployment.yml | kubectl apply -f -
    - kubectl rollout status deployment/$CI_PROJECT_NAME -n $NAMESPACE
  environment:
    name: development
    url: https://dev.example.com
  only:
    - develop

# Production deployment
.deploy_prod: &deploy_prod
  <<: *deploy_template
  variables:
    NAMESPACE: production
    REPLICAS: 3
    RESOURCES_REQUESTS_CPU: 200m
    RESOURCES_REQUESTS_MEMORY: 256Mi
    RESOURCES_LIMITS_CPU: 400m
    RESOURCES_LIMITS_MEMORY: 512Mi
  script:
    - echo "Deploying to production environment"
    - envsubst < k8s/prod/deployment.yml | kubectl apply -f -
    - kubectl rollout status deployment/$CI_PROJECT_NAME -n $NAMESPACE
  environment:
    name: production
    url: https://example.com
  only:
    - main
  when: manual

Test Templates for Different Languages

Templates for running tests in different languages:

# Node.js testing template
.node_test_template: &node_test_template
  image: node:16-alpine
  cache:
    key: ${CI_COMMIT_REF_SLUG}-node
    paths:
      - node_modules/
  before_script:
    - npm ci
  script:
    - npm run lint
    - npm run test:ci
  artifacts:
    reports:
      junit: junit-reports/*.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

# Python testing template
.python_test_template: &python_test_template
  image: python:3.9-slim
  cache:
    key: ${CI_COMMIT_REF_SLUG}-python
    paths:
      - .pip-cache/
  variables:
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
    PYTHONPATH: "$CI_PROJECT_DIR"
  before_script:
    - python -m pip install --upgrade pip
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
  script:
    - python -m pytest --cov=./ --cov-report=xml --junitxml=junit.xml
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml


Advanced Template Organization

Splitting Templates into Files

For larger projects, you can organize templates in separate files:

project-root/
├── .gitlab-ci.yml
└── .gitlab/
    ├── ci/
    │   ├── templates/
    │   │   ├── build.yml
    │   │   ├── test.yml
    │   │   ├── deploy.yml
    │   │   └── notify.yml
    │   └── jobs/
    │       ├── frontend.yml
    │       └── backend.yml

Main .gitlab-ci.yml:

include:
  - local: '.gitlab/ci/templates/build.yml'
  - local: '.gitlab/ci/templates/test.yml'
  - local: '.gitlab/ci/templates/deploy.yml'
  - local: '.gitlab/ci/templates/notify.yml'
  - local: '.gitlab/ci/jobs/frontend.yml'
  - local: '.gitlab/ci/jobs/backend.yml'

stages:
  - build
  - test
  - deploy
  - notify

variables:
  GLOBAL_VAR: "value"

Creating Pipeline Template Library

For shared templates across multiple projects, create a dedicated template repository:

# In your template repository
# templates/docker-build.yml
.docker_build:
  image: docker:latest
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG

Usage in your project:

include:
  - project: 'your-group/gitlab-ci-templates'
    ref: main
    file: '/templates/docker-build.yml'

build_image:
  extends: .docker_build
  # Add custom configuration here


Generating Dynamic Templates

Template with Environment Variables

Create templates with variables that can be substituted:

.deploy_template: &deploy_template
  script:
    - echo "Deploying to ${ENVIRONMENT} environment"
    - |
      cat > kubernetes/${ENVIRONMENT}/kustomization.yaml << EOF
      apiVersion: kustomize.config.k8s.io/v1beta1
      kind: Kustomization
      namespace: ${NAMESPACE}
      resources:
        - deployment.yaml
        - service.yaml
      images:
        - name: app-image
          newName: ${CI_REGISTRY_IMAGE}
          newTag: ${CI_COMMIT_SHORT_SHA}
      EOF
    - kubectl apply -k kubernetes/${ENVIRONMENT}/

Matrix Jobs with Templates

Use GitLab’s matrix feature to generate multiple jobs from a template:

.test_template: &test_template
  stage: test
  script:
    - echo "Running tests for ${PYTHON_VERSION} on ${OS}"
    - pip install -r requirements.txt
    - pytest

python-tests:
  parallel:
    matrix:
      - PYTHON_VERSION: ["3.8", "3.9", "3.10"]
        OS: ["linux", "windows"]
  image: python:${PYTHON_VERSION}-${OS}
  <<: *test_template


Template Best Practices


Template Design Principles:

1. Reusability: - Make templates generic enough to be used in different contexts
- Use variables for customizable parts
- Document required variables and their formats
- Avoid hardcoding project-specific values

2. Maintainability: - Keep templates focused on specific responsibilities
- Add comments to explain complex logic
- Version templates with semantic versioning
- Use descriptive names for templates and anchors

3. Performance: - Keep templates lightweight
- Use caching where appropriate
- Consider pipeline execution time when designing templates
- Avoid unnecessary logic or commands

4. Fault Tolerance: - Include proper error handling
- Add retry mechanisms for network-dependent operations
- Provide informative error messages
- Validate inputs and prerequisites


Troubleshooting Templates


Common Template Issues:

1. Reference Errors: - Verify anchor names match their references
- Check for typos in variable names
- Ensure templates are included before they're referenced
- Look for YAML indentation issues

2. Variable Scope Problems: - Remember that job-level variables override global ones
- Use YAML anchors to share variable definitions
- Consider how variables interact with included templates
- Watch for CI_* variable name collisions

3. Include Issues: - Verify project paths and file names in includes
- Check access permissions for included projects
- Ensure referenced branches or tags exist
- Look for circular includes

# Debug job for CI variables
debug_variables:
  stage: .pre
  image: alpine:latest
  script:
    - echo "== CI Variables =="
    - env | grep "^CI_" | sort
    - echo "== Custom Variables =="
    - env | grep -v "^CI_" | sort
  rules:
    - if: '$CI_PIPELINE_SOURCE == "web"'
      when: manual
  tags:
    - linux



Reference