G
GuideDevOps
Lesson 5 of 11

GitLab CI

Part of the CI/CD Pipelines tutorial series.

GitLab CI/CD: DevOps-Native Platform

GitLab CI/CD is part of the GitLab platform (Git repo + CI/CD + monitoring all integrated). It's known for:

  • ✅ Single-file configuration (.gitlab-ci.yml)
  • ✅ Powerful autoscaling runners (efficient resource usage)
  • ✅ Built-in container registry & package management
  • ✅ Advanced features like parent-child pipelines
  • ✅ Native Kubernetes integration
  • ✅ Feature flags, scheduled pipelines, manual gates

Structure: The .gitlab-ci.yml File

Everything defined in one place:

# Global configuration
image: node:18
 
variables:
  NPM_REGISTRY_URL: https://registry.npmjs.org
  DOCKER_DRIVER: overlay2
 
stages:
  - build
  - test
  - deploy
 
# ... job definitions below

Comprehensive .gitlab-ci.yml Example

image: node:18-alpine
 
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  CI_REGISTRY_IMAGE: registry.gitlab.com/mycompany/myapp
  NPM_CACHE_FOLDER: $CI_PROJECT_DIR/.npm
 
cache:
  key:
    files:
      - package-lock.json
    prefix: "${CI_COMMIT_REF_SLUG}"
  paths:
    - .npm
    - node_modules/
 
stages:
  - lint
  - build
  - test
  - coverage
  - deploy-staging
  - deploy-production
 
# ============ LINT JOBS ============
 
lint-code:
  stage: lint
  script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm run lint
  only:
    - merge_requests
    - main
    - develop
  allow_failure: false
 
lint-dependencies:
  stage: lint
  script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm audit --production --verbose
  artifacts:
    reports:
      dependency_scanning: gl-dependency-scanning-report.json
  allow_failure: false
 
# ============ BUILD JOBS ============
 
build-node-app:
  stage: build
  script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm run build
  artifacts:
    paths:
      - dist/
      - node_modules/
    expire_in: 1 day
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm
      - node_modules/
 
build-docker-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker build
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        --tag $CI_REGISTRY_IMAGE:latest
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - main
    - develop
  cache: {}
 
# ============ TEST JOBS ============
 
test-unit:
  stage: test
  script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm test -- --coverage --watchAll=false
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/
    expire_in: 1 day
  coverage: '/All files[^|]*\|[^|]*\ (\d+\.?\d*)/'  # Parse coverage percentage
 
test-e2e:
  stage: test
  script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm run build
    - npm run test:e2e
  artifacts:
    when: always
    paths:
      - cypress/videos/
      - cypress/screenshots/
    expire_in: 1 week
  only:
    - merge_requests
    - main
    - develop
 
test-performance:
  stage: test
  script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm run build
    - npm run test:performance
  artifacts:
    paths:
      - lighthouse-report.html
    expire_in: 1 week
  only:
    - main
    - develop
 
# ============ CODE COVERAGE ============
 
coverage-report:
  stage: coverage
  image: haynes/jacoco2cobertura:1.0.8
  script:
    - python /opt/cover2cover.py coverage/coverage-final.json src --output coverage/cobertura-coverage.xml
  coverage: '/Total.*?([0-9]{1,3})%/'
  dependencies:
    - test-unit
  only:
    - merge_requests
    - main
 
# ============ STAGING DEPLOYMENT ============
 
deploy-staging:
  stage: deploy-staging
  image: alpine:latest
  before_script:
    - apk add --no-cache kubectl curl
    - mkdir -p $HOME/.kube
    - echo "$KUBE_CONFIG_STAGING" | base64 -d > $HOME/.kube/config
  script:
    - kubectl config use-context staging
    - kubectl set image deployment/myapp-staging
        myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        -n staging
    - kubectl rollout status deployment/myapp-staging -n staging --timeout=5m
    
    # Run smoke tests
    - apt-get update && apt-get install -y curl
    - for i in {1..30}; do curl -f https://staging.myapp.com/health && echo "✅ Staging is healthy" && break || (sleep 2 && echo "Waiting for health check ($i/30)..."); done
  environment:
    name: staging
    url: https://staging.myapp.com
    auto_stop_in: 1 day
  only:
    - develop
  when: on_success
  tags:
    - kubernetes
 
# ============ PRODUCTION DEPLOYMENT ============
 
deploy-production:
  stage: deploy-production
  image: alpine:latest
  before_script:
    - apk add --no-cache kubectl curl
    - mkdir -p $HOME/.kube
    - echo "$KUBE_CONFIG_PRODUCTION" | base64 -d > $HOME/.kube/config
  script:
    - kubectl config use-context production
    
    # Blue-green deployment
    - |
      CURRENT_COLOR=$(kubectl get deployment myapp-blue -n production &>/dev/null && echo "blue" || echo "green")
      NEW_COLOR=$([ "$CURRENT_COLOR" = "blue" ] && echo "green" || echo "blue")
      
      echo "Current active: $CURRENT_COLOR → Deploying to: $NEW_COLOR"
      
      kubectl set image deployment/myapp-$NEW_COLOR \
        myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        -n production
      
      kubectl rollout status deployment/myapp-$NEW_COLOR -n production --timeout=5m
      
      # Smoke tests on new deployment
      kubectl run smoke-test-$NEW_COLOR \
        --image=curlimages/curl:latest \
        --rm -i --restart=Never \
        -- curl -f http://myapp-$NEW_COLOR:3000/health || exit 1
      
      # Switch traffic if tests pass
      kubectl patch service myapp -n production \
        -p '{"spec":{"selector":{"version":"'$NEW_COLOR'"}}}'
      
      echo "✅ Production deployment successful"
  environment:
    name: production
    url: https://myapp.com
  only:
    - tags  # Only deploy on git tags (e.g., v1.2.3)
  when: manual  # Manual approval required
  tags:
    - kubernetes
 
deploy-production-rollback:
  stage: deploy-production
  image: alpine:latest
  before_script:
    - apk add --no-cache kubectl
    - mkdir -p $HOME/.kube
    - echo "$KUBE_CONFIG_PRODUCTION" | base64 -d > $HOME/.kube/config
  script:
    - kubectl config use-context production
    - kubectl rollout undo deployment/myapp -n production
    - kubectl rollout status deployment/myapp -n production --timeout=5m
  environment:
    name: production
    action: rollback
  only:
    - main
  when: manual  # Manual trigger for rollback
  tags:
    - kubernetes
 
# ============ BEFORE/AFTER HOOKS ============
 
.build-setup: &build-setup
  before_script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm run lint
 
.test-setup: &test-setup
  before_script:
    - npm ci --cache $NPM_CACHE_FOLDER
    - npm run build

GitLab Runners: The Execution Engine

Running Locally (Development)

# Install GitLab Runner
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | bash
sudo apt-get install gitlab-runner
 
# Register runner with GitLab
sudo gitlab-runner register \
  --url https://gitlab.com/ \
  --registration-token $REGISTRATION_TOKEN \
  --executor docker \
  --docker-image node:18 \
  --description "Docker runner"
 
# Start runner
sudo gitlab-runner start

Runner Types

TypeUse CaseSetup
Shared RunnersPublic builds, quick feedbackPre-configured by GitLab admins
Group RunnersShared by multiple projectsConfigure once for entire group
Project RunnersPrivate/secure buildsPer-project configuration
Container RunnersKubernetes/DockerAuto-scaling on demand

Kubernetes Runner (Auto-Scaling)

# Install GitLab Runner Helm chart
helm repo add gitlab https://charts.gitlab.io
helm repo update
 
helm install gitlab-runner gitlab/gitlab-runner \
  --set gitlabUrl=https://gitlab.company.com/ \
  --set gitlabRegistrationToken=$REGISTRATION_TOKEN \
  --set runners.image=ubuntu:22.04 \
  --set runners.privileged=true \
  --set runners.tags={kubernetes,docker} \
  --set rbac.create=true

Advanced Features

Parent-Child Pipelines

# .gitlab-ci.yml
include:
  - local: '/ci/build.yml'
  - local: '/ci/test.yml'
  - local: '/ci/deploy.yml'
 
# ci/build.yml
stages:
  - build
 
build-trigger:
  stage: build
  trigger:
    include: ci/test.yml
    strategy: depend  # Wait for child pipeline

Feature Flags

test-new-feature:
  stage: test
  script:
    - npm test -- --feature-flag=new-checkout
  only:
    variables:
      - $ENABLE_NEW_CHECKOUT == "true"

Scheduled Pipelines

# Run daily security scan
security-scan:
  stage: test
  script:
    - npm audit
  only:
    - schedules

GitLab CI vs Jenkins vs GitHub Actions

FeatureGitLab CIJenkinsGitHub Actions
Setup ComplexitySimple (1 file)Complex (GUI + Groovy)Medium (YAML)
Cloud RunnersYes (free tier)Self-hostedYes (included)
Kubernetes NativeExcellent ✅GoodGood
Container RegistryBuilt-in ✅PluginsDocker Hub only
Cost$0 (shared runners)Free (self-host)Free/per-minute
Best ForDevOps teamsEnterprise flexibilityGitHub users
Learning CurveGentleSteepMedium