GitLab CI/CD

CI/CDGitLabYAMLAutomationDevOpsPipelineContainerSecurity

CI/CD Tool

GitLab CI/CD

Overview

GitLab CI/CD is a CI/CD system integrated with GitLab. It features powerful pipeline capabilities, Auto DevOps, Kubernetes integration, and security scanning, providing a comprehensive DevOps platform.

Details

GitLab CI/CD is an integrated CI/CD (Continuous Integration/Continuous Delivery) platform provided by GitLab Inc. Since GitLab's inception in 2011, it has evolved continuously and now provides a comprehensive DevOps solution beyond simple code management. Pipelines are defined in the .gitlab-ci.yml file within repositories, automating build, test, and deployment processes. Through GitLab Runner's support for diverse execution environments (Docker, Kubernetes, VM), matrix builds, and parallel execution, it enables fast and flexible CI/CD. In 2024, the CI/CD Catalog was officially released, enabling efficient pipeline construction through reusable components. The Auto DevOps feature automatically generates CI/CD pipelines without configuration, integrating comprehensive security scanning including SAST, DAST, and dependency scanning. Tight integration with GitLab's unique Kanban boards, Issue management, Merge Requests, Container Registry, and Package Registry provides a one-stop solution from development to deployment.

Pros and Cons

Pros

  • All-in-One Platform: Integrates Git, CI/CD, security, and monitoring
  • Auto DevOps: Automatically generates CI/CD pipelines with zero configuration
  • Powerful Security Features: Built-in SAST, DAST, and dependency scanning
  • Flexible Execution Environments: Support for Docker, Kubernetes, VM, and self-hosted runners
  • CI/CD Catalog: Efficient pipeline construction with reusable components
  • Comprehensive Integration: Tight integration with Issues, MRs, and Container Registry
  • GitOps Support: GitOps workflow support in Kubernetes environments
  • Free Tier: 400 minutes of free CI/CD execution time monthly on GitLab.com

Cons

  • Learning Curve: Need to understand YAML syntax and complex feature set
  • Execution Time Limits: Maximum 8-hour limit per job on GitLab.com
  • Cost: Premium features required for large-scale use even with self-hosting
  • Resource Consumption: High system resource usage due to feature richness
  • Complex Debugging: Difficult to identify causes of pipeline failures
  • Vendor Lock-in: Risk of dependency on GitLab-specific features
  • Performance: UI response degradation in large-scale projects

Key Links

Code Examples

Basic Pipeline Configuration

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

default:
  image: ubuntu:latest

variables:
  DOCKER_DRIVER: overlay2

build_job:
  stage: build
  script:
    - echo "Building the application..."
    - npm install
    - npm run build
  artifacts:
    paths:
      - build/
    expire_in: 1 hour

test_job:
  stage: test
  script:
    - echo "Running tests..."
    - npm run test
  dependencies:
    - build_job

deploy_job:
  stage: deploy
  script:
    - echo "Deploying to production..."
    - ./deploy.sh
  environment:
    name: production
    url: https://example.com
  only:
    - main

Node.js Project CI/CD

# .gitlab-ci.yml
image: node:18

stages:
  - install
  - test
  - build
  - deploy

cache:
  paths:
    - node_modules/
    - .npm/

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

install_dependencies:
  stage: install
  script:
    - npm ci --cache .npm --prefer-offline
  artifacts:
    paths:
      - node_modules/
    expire_in: 30 minutes

lint:
  stage: test
  script:
    - npm run lint
  dependencies:
    - install_dependencies

unit_tests:
  stage: test
  script:
    - npm run test:unit
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/
  dependencies:
    - install_dependencies

build_app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  dependencies:
    - install_dependencies
  only:
    - main
    - develop

deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging..."
    - npm run deploy:staging
  environment:
    name: staging
    url: https://staging.example.com
  dependencies:
    - build_app
  only:
    - develop

deploy_production:
  stage: deploy
  script:
    - echo "Deploying to production..."
    - npm run deploy:production
  environment:
    name: production
    url: https://example.com
  dependencies:
    - build_app
  only:
    - main
  when: manual

Docker Image Build and Registry Push

# .gitlab-ci.yml
stages:
  - build
  - test
  - push

variables:
  DOCKER_HOST: tcp://docker:2376
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_TLS_VERIFY: 1
  DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"

services:
  - docker:dind

before_script:
  - docker info

build_image:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker save $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA > image.tar
  artifacts:
    paths:
      - image.tar
    expire_in: 1 hour

test_image:
  stage: test
  script:
    - docker load < image.tar
    - docker run --rm $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA npm test
  dependencies:
    - build_image

push_image:
  stage: push
  script:
    - docker load < image.tar
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
  dependencies:
    - build_image
  only:
    - main

Matrix Builds and Multi-Platform Support

# .gitlab-ci.yml
stages:
  - test
  - build

test_matrix:
  stage: test
  image: node:${NODE_VERSION}
  script:
    - npm install
    - npm test
  parallel:
    matrix:
      - NODE_VERSION: ["16", "18", "20", "22"]
        PLATFORM: ["linux", "darwin"]
  except:
    variables:
      - $NODE_VERSION == "16" && $PLATFORM == "darwin"

build_multiarch:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
    - docker buildx create --use --name multiarch-builder
  script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
    - docker buildx build 
      --platform linux/amd64,linux/arm64,linux/arm/v7 
      --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG 
      --tag $CI_REGISTRY_IMAGE:latest 
      --push .
  only:
    - tags

Conditional Pipelines and Manual Deployment

# .gitlab-ci.yml
workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_TAG
    - if: $CI_PIPELINE_SOURCE == "schedule"

stages:
  - test
  - security
  - deploy

variables:
  ENVIRONMENT: "development"

.base_job: &base_job
  before_script:
    - echo "Starting job..."
  after_script:
    - echo "Job completed."

unit_tests:
  <<: *base_job
  stage: test
  script:
    - npm run test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'

security_scan:
  stage: security
  script:
    - echo "Running security scans..."
  include:
    - template: Security/SAST.gitlab-ci.yml
    - template: Security/Dependency-Scanning.gitlab-ci.yml
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_PIPELINE_SOURCE == "schedule"

deploy_staging:
  <<: *base_job
  stage: deploy
  script:
    - echo "Deploying to staging environment..."
    - ./deploy.sh staging
  environment:
    name: staging/$CI_COMMIT_REF_SLUG
    url: https://staging-$CI_COMMIT_REF_SLUG.example.com
    on_stop: stop_staging
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual

stop_staging:
  <<: *base_job
  stage: deploy
  script:
    - echo "Stopping staging environment..."
    - ./cleanup.sh staging
  environment:
    name: staging/$CI_COMMIT_REF_SLUG
    action: stop
  when: manual
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual

deploy_production:
  <<: *base_job
  stage: deploy
  script:
    - echo "Deploying to production..."
    - ./deploy.sh production
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
    - if: $CI_COMMIT_TAG
      when: manual

Security Scanning and Compliance

# .gitlab-ci.yml
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/DAST.gitlab-ci.yml

stages:
  - build
  - test
  - security
  - deploy

variables:
  SAST_EXCLUDED_PATHS: "tests/, spec/, vendor/"
  SECURE_LOG_LEVEL: "debug"

build_app:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

container_scanning:
  variables:
    CS_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  dependencies:
    - build_app

dast_scan:
  stage: security
  variables:
    DAST_WEBSITE: https://staging.example.com
    DAST_AUTH_URL: https://staging.example.com/login
  script:
    - echo "Running DAST scan..."
  artifacts:
    reports:
      dast: gl-dast-report.json
  only:
    - schedules

security_report:
  stage: security
  script:
    - echo "Generating security compliance report..."
    - ./generate-security-report.sh
  artifacts:
    reports:
      junit: security-report.xml
    paths:
      - security-report.html
  dependencies:
    - sast
    - dependency_scanning
    - container_scanning

Kubernetes Deployment

# .gitlab-ci.yml
stages:
  - build
  - deploy

variables:
  KUBE_NAMESPACE: $CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG
  ROLLOUT_RESOURCE_TYPE: deployment

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

.deploy_template: &deploy_template
  image: bitnami/kubectl:latest
  before_script:
    - kubectl config use-context $KUBE_CONTEXT
    - kubectl create namespace $KUBE_NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
    - envsubst < k8s/deployment.yaml | kubectl apply -f -
    - envsubst < k8s/service.yaml | kubectl apply -f -
  script:
    - kubectl rollout status $ROLLOUT_RESOURCE_TYPE/$CI_PROJECT_NAME -n $KUBE_NAMESPACE
    - kubectl get all -n $KUBE_NAMESPACE

deploy_staging:
  <<: *deploy_template
  stage: deploy
  variables:
    KUBE_CONTEXT: staging-cluster
    REPLICAS: "2"
  environment:
    name: staging
    url: https://staging.example.com
    kubernetes:
      namespace: $KUBE_NAMESPACE
  only:
    - develop

deploy_production:
  <<: *deploy_template
  stage: deploy
  variables:
    KUBE_CONTEXT: production-cluster
    REPLICAS: "5"
  environment:
    name: production
    url: https://example.com
    kubernetes:
      namespace: $KUBE_NAMESPACE
  only:
    - main
  when: manual