Ejemplos GitLab CI

Ejemplos de configuración GitLab CI/CD incluyendo pipelines multi-etapa, caching, artifacts y estrategias de despliegue

💻 GitLab CI Pipeline Básico yaml

🟢 simple ⭐⭐

Pipeline GitLab CI simple con etapas de construcción, prueba y despliegue para aplicación Node.js

⏱️ 20 min 🏷️ gitlab-ci, pipeline, nodejs, testing
Prerequisites: GitLab basics, YAML syntax, CI/CD concepts
# .gitlab-ci.yml - Basic GitLab CI Pipeline

# Define pipeline stages
stages:
  - prepare
  - build
  - test
  - security
  - deploy

# Global variables
variables:
  NODE_VERSION: "18"
  APP_NAME: "my-app"
  ARTIFACT_EXPIRATION: "1 week"

# Cache configuration
cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - node_modules/
    - .npm/

# Default configuration for all jobs
default:
  image: node:$NODE_VERSION-alpine
  before_script:
    - echo "Starting job: $CI_JOB_NAME"
    - echo "Branch: $CI_COMMIT_REF_NAME"
    - echo "Commit: $CI_COMMIT_SHORT_SHA"
    - npm ci --cache .npm --prefer-offline
  after_script:
    - echo "Completed job: $CI_JOB_NAME"

# Job to prepare the environment
prepare:
  stage: prepare
  script:
    - echo "Preparing build environment..."
    - node --version
    - npm --version
    - echo "Environment preparation completed"
  artifacts:
    reports:
      dotenv: prepare.env
    expire_in: 1 hour
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Build job
build:
  stage: build
  script:
    - echo "Building application..."
    - npm run build
    - echo "Build completed successfully"
  artifacts:
    name: "$APP_NAME-$CI_COMMIT_SHORT_SHA"
    paths:
      - dist/
      - build/
      - package.json
      - package-lock.json
    expire_in: $ARTIFACT_EXPIRATION
    reports:
      junit: test-results.xml
  coverage: '/Liness*:s*(d+.d+)%/'
  dependencies:
    - prepare

# Unit tests
test:unit:
  stage: test
  script:
    - echo "Running unit tests..."
    - npm run test:unit
    - echo "Unit tests completed"
  artifacts:
    reports:
      junit: coverage/junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    when: always
    expire_in: 1 week
  dependencies:
    - build

# Integration tests
test:integration:
  stage: test
  services:
    - name: postgres:15-alpine
      alias: postgres
      variables:
        POSTGRES_DB: test_db
        POSTGRES_USER: test_user
        POSTGRES_PASSWORD: test_password
    variables:
      DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db"
  script:
    - echo "Running integration tests..."
    - npm run test:integration
    - echo "Integration tests completed"
  artifacts:
    reports:
      junit: integration-test-results.xml
    when: always
    expire_in: 1 week
  dependencies:
    - build
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# E2E tests
test:e2e:
  stage: test
  image: cypress/browsers:node-18.16.1-chrome114-ff114-edge
  script:
    - echo "Running E2E tests..."
    - npm run build
    - npm run test:e2e
    - echo "E2E tests completed"
  artifacts:
    reports:
      junit: cypress/results/junit.xml
    paths:
      - cypress/videos/
      - cypress/screenshots/
    when: always
    expire_in: 1 week
  dependencies:
    - build
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Code quality analysis
code_quality:
  stage: test
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  allow_failure: true
  script:
    - echo "Running code quality analysis..."
    - docker run
        --env SOURCE_CODE="$PWD"
        --volume "$PWD":/code
        --volume /var/run/docker.sock:/var/run/docker.sock
        "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.29" /code
  artifacts:
    reports:
      codequality: gl-code-quality-report.json
    expire_in: 1 week
  dependencies: []
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Security scanning
security:dependency_scan:
  stage: security
  image: node:$NODE_VERSION-alpine
  script:
    - echo "Running dependency security scan..."
    - npm audit --audit-level=high --json > npm-audit-report.json || true
    - npm audit --audit-level=high
    - echo "Dependency scan completed"
  artifacts:
    reports:
      sast: npm-audit-report.json
    paths:
      - npm-audit-report.json
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# SAST scanning
security:sast:
  stage: security
  image: node:$NODE_VERSION-alpine
  script:
    - echo "Running SAST scan..."
    - npm install -g semgrep
    - semgrep --config=auto --json --output=semgrep-report.json .
    - echo "SAST scan completed"
  artifacts:
    reports:
      sast: semgrep-report.json
    expire_in: 1 week
  allow_failure: true
  dependencies: []
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Container scanning (if using Docker)
security:container_scan:
  stage: security
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
  script:
    - echo "Building Docker image for scanning..."
    - docker build -t $IMAGE_TAG .
    - echo "Running container security scan..."
    - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
        -v $PWD:/tmp/.cache/ aquasec/trivy:latest image
        --format json
        --output /tmp/.cache/container-scan-report.json
        $IMAGE_TAG
    - echo "Container scan completed"
  artifacts:
    reports:
      container_scanning: /tmp/.cache/container-scan-report.json
    expire_in: 1 week
  allow_failure: true
  dependencies: []
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - exists:
        - Dockerfile

# Deploy to staging
deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - echo "Deploying to staging environment..."
    - echo "Deploying version: $CI_COMMIT_SHORT_SHA"
    - apk add --no-cache curl
    # Example deployment script
    - |
      curl -X POST "$STAGING_DEPLOY_WEBHOOK" \
        -H "Authorization: Bearer $STAGING_DEPLOY_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{
          "ref": "'$CI_COMMIT_REF_NAME'",
          "sha": "'$CI_COMMIT_SHA'",
          "environment": "staging"
        }'
    # Health check
    - sleep 30
    - curl -f "https://staging.example.com/health" || exit 1
    - echo "Staging deployment completed"
  dependencies:
    - build
    - test:unit
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Deploy to production
deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - echo "Deploying to production environment..."
    - echo "Deploying version: $CI_COMMIT_SHORT_SHA"
    - apk add --no-cache curl
    # Production deployment with manual approval handled by environment protection
    - |
      curl -X POST "$PRODUCTION_DEPLOY_WEBHOOK" \
        -H "Authorization: Bearer $PRODUCTION_DEPLOY_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{
          "ref": "'$CI_COMMIT_REF_NAME'",
          "sha": "'$CI_COMMIT_SHA'",
          "environment": "production"
        }'
    # Health check
    - sleep 60
    - curl -f "https://example.com/health" || exit 1
    - echo "Production deployment completed"
  dependencies:
    - deploy:staging
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  needs:
    - deploy:staging

# Cleanup old deployments
cleanup:
  stage: deploy
  image: alpine:3.18
  script:
    - echo "Cleaning up old deployments..."
    - echo "This job removes old containers and temporary files"
    - |
      # Example cleanup commands
      echo "Cleaning Docker images older than 30 days"
      echo "Removing temporary build files"
    - echo "Cleanup completed"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: on_success
  dependencies: []

💻 GitLab CI Monorepo Pipeline yaml

🟡 intermediate ⭐⭐⭐⭐

Pipeline monorepo avanzado con filtrado de rutas, trabajos paralelos y estrategia matricial

⏱️ 35 min 🏷️ gitlab-ci, monorepo, matrix, helm, blue-green
Prerequisites: GitLab CI advanced, Docker, Kubernetes, Helm, Monorepo concepts
# .gitlab-ci.yml - Monorepo Pipeline with Matrix Strategy

# Global configuration
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  KUBERNETES_NAMESPACE_OVERWRITE: "gitlab-ci"
  FF_USE_FASTZIP: "true"
  TRANSFER_METER_FREQUENCY: "5s"

# Include common configuration
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Code-Quality.gitlab-ci.yml

# Global cache configuration
.cache_base: &cache_base
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .npm/
    - .cache/

# Global default
default:
  image: node:18-alpine
  cache: *cache_base

# Pipeline stages
stages:
  - validate
  - plan
  - build
  - test
  - security
  - release
  - deploy

# Workflow rules
workflow:
  rules:
    - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_BRANCH =~ /^release\/.*/
    - if: $CI_COMMIT_TAG
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# Job to validate configuration
validate:config:
  stage: validate
  image: alpine:3.18
  script:
    - echo "Validating pipeline configuration..."
    - echo "Validating YAML syntax..."
    - apk add --no-cache yq
    - yq eval '.' .gitlab-ci.yml > /dev/null
    - echo "Configuration validation completed"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  dependencies: []

# Generate build matrix
generate:matrix:
  stage: plan
  image: node:18-alpine
  script:
    - |
      cat > matrix.json << EOF
      {
        "packages": [
          {"name": "frontend", "path": "packages/frontend", "framework": "react"},
          {"name": "backend", "path": "packages/backend", "framework": "nestjs"},
          {"name": "api-gateway", "path": "packages/api-gateway", "framework": "express"},
          {"name": "admin", "path": "packages/admin", "framework": "vue"},
          {"name": "shared", "path": "packages/shared", "framework": "typescript-lib"}
        ],
        "environments": ["dev", "staging"],
        "node_versions": ["16", "18", "20"]
      }
      EOF
    - cat matrix.json
  artifacts:
    reports:
      dotenv: matrix.env
    paths:
      - matrix.json
    expire_in: 1 hour
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# Build all packages using matrix strategy
build:packages:
  stage: build
  image: node:18-alpine
  parallel:
    matrix:
      - PACKAGE: ["frontend", "backend", "api-gateway", "admin", "shared"]
        NODE_VERSION: ["16", "18", "20"]
  script:
    - echo "Building package: $PACKAGE with Node.js $NODE_VERSION"
    - cd packages/$PACKAGE
    - npm ci --cache ../../.npm --prefer-offline
    - npm run build
    - echo "Build completed for $PACKAGE"
  artifacts:
    name: "$PACKAGE-build-$CI_COMMIT_SHORT_SHA"
    paths:
      - packages/$PACKAGE/dist/
      - packages/$PACKAGE/build/
      - packages/$PACKAGE/lib/
    expire_in: 1 week
    when: always
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "packages/$PACKAGE/**/*"
    - if: $CI_PIPELINE_SOURCE == "push"
      changes:
        - "packages/$PACKAGE/**/*"

# Docker build matrix
build:docker:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  parallel:
    matrix:
      - PACKAGE: ["frontend", "backend", "api-gateway", "admin"]
        REGISTRY: ["registry.gitlab.com", "docker.io"]
  variables:
    IMAGE_TAG: "$CI_REGISTRY_IMAGE/$PACKAGE:$CI_COMMIT_SHA"
    DOCKERFILE_PATH: "packages/$PACKAGE/Dockerfile"
  script:
    - echo "Building Docker image for $PACKAGE"
    - echo "Registry: $REGISTRY"
    - echo "Image tag: $IMAGE_TAG"
    - |
      if [ -f "$DOCKERFILE_PATH" ]; then
        docker build
          --build-arg NODE_VERSION=18
          --build-arg PACKAGE_NAME=$PACKAGE
          -t $IMAGE_TAG
          -f $DOCKERFILE_PATH
          packages/$PACKAGE/

        echo "Image built successfully: $IMAGE_TAG"

        # Tag with commit SHA and branch
        docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE/$PACKAGE:$CI_COMMIT_REF_SLUG

        # Tag with latest for main branch
        if [ "$CI_COMMIT_REF_NAME" = "$CI_DEFAULT_BRANCH" ]; then
          docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE/$PACKAGE:latest
        fi

        # Push to registry
        echo "Pushing images..."
        docker push $IMAGE_TAG
        docker push $CI_REGISTRY_IMAGE/$PACKAGE:$CI_COMMIT_REF_SLUG

        if [ "$CI_COMMIT_REF_NAME" = "$CI_DEFAULT_BRANCH" ]; then
          docker push $CI_REGISTRY_IMAGE/$PACKAGE:latest
        fi

        echo "Images pushed successfully"
      else
        echo "No Dockerfile found for $PACKAGE, skipping"
      fi
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_BRANCH =~ /^release\/.*/
    - if: $CI_COMMIT_TAG
  dependencies:
    - build:packages

# Test matrix
test:unit:
  stage: test
  parallel:
    matrix:
      - PACKAGE: ["frontend", "backend", "api-gateway", "admin", "shared"]
        COVERAGE: ["true", "false"]
  script:
    - echo "Running unit tests for $PACKAGE"
    - cd packages/$PACKAGE
    - npm ci --cache ../../.npm --prefer-offline
    - |
      if [ "$COVERAGE" = "true" ]; then
        npm run test:coverage
      else
        npm run test
      fi
  artifacts:
    reports:
      junit: "packages/$PACKAGE/test-results.xml"
      coverage_report:
        coverage_format: cobertura
        path: "packages/$PACKAGE/coverage/cobertura-coverage.xml"
    when: always
    expire_in: 1 week
    paths:
      - "packages/$PACKAGE/coverage/"
    expire_in: 1 week
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "packages/$PACKAGE/**/*"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_BRANCH =~ /^release\/.*/

# Integration tests with matrix
test:integration:
  stage: test
  services:
    - name: postgres:15-alpine
      alias: postgres
      variables:
        POSTGRES_DB: integration_test_db
        POSTGRES_USER: test_user
        POSTGRES_PASSWORD: test_password
    - name: redis:7-alpine
      alias: redis
  parallel:
    matrix:
      - PACKAGE: ["backend", "api-gateway"]
        ENVIRONMENT: ["dev", "staging"]
  variables:
    DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/integration_test_db"
    REDIS_URL: "redis://redis:6379"
  script:
    - echo "Running integration tests for $PACKAGE in $ENVIRONMENT"
    - cd packages/$PACKAGE
    - npm ci --cache ../../.npm --prefer-offline
    - npm run test:integration -- --environment=$ENVIRONMENT
  artifacts:
    reports:
      junit: "packages/$PACKAGE/integration-test-results.xml"
    when: always
    expire_in: 1 week
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "packages/$PACKAGE/**/*"

# E2E tests
test:e2e:
  stage: test
  image: cypress/browsers:node-18-chrome114
  services:
    - name: postgres:15-alpine
      alias: postgres
      variables:
        POSTGRES_DB: e2e_test_db
        POSTGRES_USER: test_user
        POSTGRES_PASSWORD: test_password
  parallel:
    matrix:
      - PACKAGE: ["frontend", "admin"]
        BROWSER: ["chrome", "firefox"]
  script:
    - echo "Running E2E tests for $PACKAGE with $BROWSER"
    - cd packages/$PACKAGE
    - npm ci --cache ../../.npm --prefer-offline
    - npm run build
    - npm run test:e2e -- --browser=$BROWSER
  artifacts:
    reports:
      junit: "packages/$PACKAGE/cypress/results/junit.xml"
    paths:
      - "packages/$PACKAGE/cypress/videos/"
      - "packages/$PACKAGE/cypress/screenshots/"
    when: always
    expire_in: 1 week
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "packages/$PACKAGE/**/*"
        - "cypress/**/*"

# Performance tests
test:performance:
  stage: test
  image: node:18-alpine
  script:
    - echo "Running performance tests..."
    - npm install -g artillery
    - artillery run tests/performance/load-test.yml
  artifacts:
    reports:
      performance: performance-report.json
    paths:
      - performance-report.json
    expire_in: 1 week
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Security scanning with matrix
security:sast:
  stage: security
  extends: .sast-analyzer
  parallel:
    matrix:
      - SCAN_TYPE: ["semgrep", "eslint", "typescript"]
        PACKAGE: ["frontend", "backend", "api-gateway", "admin", "shared"]
  variables:
    SEARCH_MAX_DEPTH: "10"
    ANALYZER_TARGET_DIR: "packages/$PACKAGE"
  script:
    - echo "Running $SCAN_TYPE scan for $PACKAGE"
    - cd packages/$PACKAGE
    - |
      case "$SCAN_TYPE" in
        "semgrep")
          semgrep --config=auto --json --output=../../semgrep-$PACKAGE-report.json . || true
          ;;
        "eslint")
          npm run lint -- --format=json --output-file=../../eslint-$PACKAGE-report.json || true
          ;;
        "typescript")
          npm run type-check || true
          ;;
      esac
  artifacts:
    reports:
      sast: "semgrep-$PACKAGE-report.json"
    paths:
      - "semgrep-$PACKAGE-report.json"
      - "eslint-$PACKAGE-report.json"
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Create releases
release:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  script:
    - echo "Creating release for $CI_COMMIT_TAG"
    - |
      release-cli create         --name "Release $CI_COMMIT_TAG"         --description "Release created using the release-cli"         --tag-name "$CI_COMMIT_TAG"         --ref "$CI_COMMIT_SHA"         --assets-link "{"name":"Frontend Build","url":"$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/artifacts/$CI_COMMIT_TAG/raw/packages/frontend/dist/?job=build:packages"}"         --assets-link "{"name":"Backend Build","url":"$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/artifacts/$CI_COMMIT_TAG/raw/packages/backend/dist/?job=build:packages"}"
  rules:
    - if: $CI_COMMIT_TAG
  dependencies:
    - build:packages

# Deploy to environments
deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
    on_stop: stop:staging
  script:
    - echo "Deploying to staging environment..."
    - |
      # Deploy using Helm
      helm upgrade --install app-staging ./helm/app \
        --namespace staging \
        --set image.tag=$CI_COMMIT_SHA \
        --set environment=staging \
        --set ingress.hosts[0].host=staging.example.com \
        --wait --timeout=10m
    - echo "Staging deployment completed"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  dependencies:
    - build:docker
    - test:unit
    - test:integration

# Stop staging environment
stop:staging:
  stage: deploy
  environment:
    name: staging
    action: stop
  script:
    - echo "Stopping staging environment..."
    - helm uninstall app-staging --namespace staging || true
    - echo "Staging environment stopped"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual

# Deploy to production
deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - echo "Deploying to production environment..."
    - echo "Deploying version: $CI_COMMIT_SHA"
    - |
      # Blue-green deployment
      ./scripts/blue-green-deploy.sh \
        --image-tag $CI_COMMIT_SHA \
        --namespace production \
        --release-name app-prod
    - echo "Production deployment completed"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  dependencies:
    - deploy:staging

# Performance monitoring after deployment
monitor:production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - echo "Monitoring production deployment..."
    - sleep 300 # Wait 5 minutes
    - |
      # Check application health
      response=$(curl -s -o /dev/null -w "%{http_code}" https://example.com/health)
      if [ "$response" != "200" ]; then
        echo "Health check failed with status: $response"
        exit 1
      fi
    - echo "Production monitoring completed successfully"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: on_success
  dependencies:
    - deploy:production

# Cleanup old artifacts and images
cleanup:
  stage: deploy
  image: alpine:3.18
  script:
    - echo "Cleaning up old resources..."
    - |
      # Clean old Docker images (requires proper authentication)
      echo "Cleaning Docker images older than 30 days..."
      # Add actual cleanup commands here

      # Clean old job artifacts
      echo "Pipeline cleanup completed"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: on_success
  dependencies: []

💻 GitLab CI Pipeline Multi-Ambiente yaml

🔴 complex ⭐⭐⭐⭐⭐

Pipeline multi-ambiente integral con feature flags, despliegue canary y auto-escalado

⏱️ 50 min 🏷️ gitlab-ci, multi-environment, canary, blue-green, monitoring
Prerequisites: Advanced GitLab CI, Kubernetes, Helm, DevOps, Monitoring
# .gitlab-ci.yml - Multi-Environment Pipeline with Advanced Features

# Global variables and configuration
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  KUBERNETES_MEMORY_REQUEST: "1Gi"
  KUBERNETES_MEMORY_LIMIT: "4Gi"
  KUBERNETES_CPU_REQUEST: "500m"
  KUBERNETES_CPU_LIMIT: "2000m"

# Pipeline stages
stages:
  - setup
  - validate
  - build
  - test
  - security
  - performance
  - release
  - deploy
  - post-deploy
  - cleanup

# Global workflow rules
workflow:
  rules:
    - if: $CI_COMMIT_MESSAGE =~ /^\[skip-ci\]/
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      variables:
        DEPLOY_ENVIRONMENT: "production"
    - if: $CI_COMMIT_BRANCH =~ /^release\/.*/
      variables:
        DEPLOY_ENVIRONMENT: "staging"
    - if: $CI_COMMIT_BRANCH =~ /^feature\/.*/
      variables:
        DEPLOY_ENVIRONMENT: "development"
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      variables:
        DEPLOY_ENVIRONMENT: "review"
    - if: $CI_COMMIT_TAG
      variables:
        DEPLOY_ENVIRONMENT: "production"

# Include templates
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Code-Quality.gitlab-ci.yml
  - local: '/ci/templates/common.yml'
  - local: '/ci/templates/deployments.yml'

# Environment-specific variables
variables:
  # Development
  DEV_NAMESPACE: "development"
  DEV_INGRESS_HOST: "dev.example.com"
  DEV_REPLICAS: "1"

  # Staging
  STAGING_NAMESPACE: "staging"
  STAGING_INGRESS_HOST: "staging.example.com"
  STAGING_REPLICAS: "2"

  # Production
  PROD_NAMESPACE: "production"
  PROD_INGRESS_HOST: "example.com"
  PROD_REPLICAS: "5"

# Setup stage jobs
setup:environment:
  stage: setup
  image: alpine:3.18
  script:
    - echo "Setting up environment for $DEPLOY_ENVIRONMENT"
    - |
      cat > environment.env << EOF
      DEPLOY_ENVIRONMENT=$DEPLOY_ENVIRONMENT
      NAMESPACE=$(eval "echo ${${DEPLOY_ENVIRONMENT^^}_NAMESPACE}")
      INGRESS_HOST=$(eval "echo ${${DEPLOY_ENVIRONMENT^^}_INGRESS_HOST}")
      REPLICAS=$(eval "echo ${${DEPLOY_ENVIRONMENT^^}_REPLICAS}")
      CI_COMMIT_REF_SLUG=$CI_COMMIT_REF_SLUG
      CI_COMMIT_SHORT_SHA=$CI_COMMIT_SHORT_SHA
      CI_COMMIT_TIMESTAMP=$CI_COMMIT_TIMESTAMP
      EOF
    - cat environment.env
  artifacts:
    reports:
      dotenv: environment.env
    expire_in: 1 hour
  rules:
    - if: $DEPLOY_ENVIRONMENT

setup:infrastructure:
  stage: setup
  image: hashicorp/terraform:1.5
  script:
    - echo "Setting up infrastructure for $DEPLOY_ENVIRONMENT"
    - cd infrastructure/$DEPLOY_ENVIRONMENT
    - terraform init
    - terraform fmt -check
    - terraform validate
    - terraform plan -out=terraform.plan
    - terraform apply -auto-approve terraform.plan
  environment:
    name: $DEPLOY_ENVIRONMENT
    url: https://$INGRESS_HOST
  artifacts:
    paths:
      - "infrastructure/$DEPLOY_ENVIRONMENT/terraform.plan"
      - "infrastructure/$DEPLOY_ENVIRONMENT/terraform.tfstate"
    expire_in: 1 week
  rules:
    - if: $DEPLOY_ENVIRONMENT
      changes:
        - "infrastructure/**/*"
  cache:
    key: "terraform-$DEPLOY_ENVIRONMENT"
    paths:
      - "infrastructure/$DEPLOY_ENVIRONMENT/.terraform/"

# Validate configuration
validate:config:
  stage: validate
  image: node:18-alpine
  script:
    - echo "Validating configuration files..."
    - npm ci
    - npm run lint
    - npm run type-check
    - echo "Configuration validation completed"
  artifacts:
    reports:
      codequality: gl-code-quality-report.json
    paths:
      - coverage/
    expire_in: 1 week
  rules:
    - if: $DEPLOY_ENVIRONMENT
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'

# Build applications
build:frontend:
  stage: build
  image: node:18-alpine
  variables:
    NODE_ENV: "production"
  script:
    - echo "Building frontend application..."
    - cd frontend
    - npm ci --cache ../.npm --prefer-offline
    - npm run build
    - npm run analyze
  artifacts:
    name: "frontend-$CI_COMMIT_SHORT_SHA"
    paths:
      - frontend/dist/
      - frontend/build-stats.json
      - frontend/bundle-analysis.html
    expire_in: 2 weeks
    reports:
      dotenv: frontend/version.env
  dependencies:
    - setup:environment
  rules:
    - if: $DEPLOY_ENVIRONMENT

build:backend:
  stage: build
  image: node:18-alpine
  script:
    - echo "Building backend application..."
    - cd backend
    - npm ci --cache ../.npm --prefer-offline
    - npm run build
    - npm run package
  artifacts:
    name: "backend-$CI_COMMIT_SHORT_SHA"
    paths:
      - backend/dist/
      - backend/package/
    expire_in: 2 weeks
    reports:
      dotenv: backend/version.env
  dependencies:
    - setup:environment
  rules:
    - if: $DEPLOY_ENVIRONMENT

# Build Docker images
build:docker:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  parallel:
    matrix:
      - SERVICE: ["frontend", "backend", "api-gateway"]
        REGISTRY: ["$CI_REGISTRY"]
  variables:
    IMAGE_NAME: "$CI_REGISTRY_IMAGE/$SERVICE"
    IMAGE_TAG: "$CI_COMMIT_SHA"
  script:
    - echo "Building Docker image for $SERVICE"
    - echo "Image: $IMAGE_NAME:$IMAGE_TAG"
    - |
      docker buildx create --use
      docker buildx build
        --platform linux/amd64,linux/arm64
        --build-arg BUILD_DATE="$CI_COMMIT_TIMESTAMP"
        --build-arg VCS_REF="$CI_COMMIT_SHA"
        --build-arg VERSION="$IMAGE_TAG"
        --tag "$IMAGE_NAME:$IMAGE_TAG"
        --tag "$IMAGE_NAME:latest"
        --push
        ./$SERVICE/
    - echo "Docker build completed for $SERVICE"
  rules:
    - if: $DEPLOY_ENVIRONMENT
      exists:
        - "$SERVICE/Dockerfile"
  dependencies:
    - build:frontend
    - build:backend

# Testing stage
test:unit:
  stage: test
  image: node:18-alpine
  parallel:
    matrix:
      - SERVICE: ["frontend", "backend", "api-gateway"]
        COVERAGE: ["true"]
  script:
    - echo "Running unit tests for $SERVICE"
    - cd $SERVICE
    - npm ci --cache ../.npm --prefer-offline
    - |
      if [ "$COVERAGE" = "true" ]; then
        npm run test:coverage
      else
        npm run test
      fi
  artifacts:
    reports:
      junit: "$SERVICE/test-results.xml"
      coverage_report:
        coverage_format: cobertura
        path: "$SERVICE/coverage/cobertura-coverage.xml"
    when: always
    expire_in: 1 week
    paths:
      - "$SERVICE/coverage/"
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  rules:
    - if: $DEPLOY_ENVIRONMENT

test:integration:
  stage: test
  image: node:18-alpine
  services:
    - name: postgres:15-alpine
      alias: postgres
      variables:
        POSTGRES_DB: test_db
        POSTGRES_USER: test_user
        POSTGRES_PASSWORD: test_password
    - name: redis:7-alpine
      alias: redis
  variables:
    DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db"
    REDIS_URL: "redis://redis:6379"
  script:
    - echo "Running integration tests..."
    - cd backend
    - npm ci --cache ../.npm --prefer-offline
    - npm run test:integration
    - echo "Integration tests completed"
  artifacts:
    reports:
      junit: "backend/integration-test-results.xml"
    when: always
    expire_in: 1 week
  rules:
    - if: $DEPLOY_ENVIRONMENT

test:e2e:
  stage: test
  image: cypress/browsers:node-18-chrome114
  services:
    - name: postgres:15-alpine
      alias: postgres
      variables:
        POSTGRES_DB: e2e_db
        POSTGRES_USER: e2e_user
        POSTGRES_PASSWORD: e2e_password
  parallel:
    matrix:
      - SPEC: ["login", "dashboard", "profile", "admin"]
        BROWSER: ["chrome", "firefox"]
  variables:
    CYPRESS_baseUrl: "http://backend:3000"
    DATABASE_URL: "postgresql://e2e_user:e2e_password@postgres:5432/e2e_db"
  script:
    - echo "Running E2E tests for $SPEC with $BROWSER"
    - cd frontend
    - npm ci --cache ../.npm --prefer-offline
    - npm run build
    - npm run test:e2e -- --spec "cypress/e2e/$SPEC/**/*" --browser $BROWSER
  artifacts:
    reports:
      junit: "frontend/cypress/results/junit.xml"
    paths:
      - "frontend/cypress/videos/"
      - "frontend/cypress/screenshots/"
    when: always
    expire_in: 1 week
  rules:
    - if: $DEPLOY_ENVIRONMENT

# Performance testing
test:performance:
  stage: performance
  image: node:18-alpine
  services:
    - name: k6:latest
      alias: k6
  parallel:
    matrix:
      - TEST_TYPE: ["load", "stress", "spike"]
        VUS: [10, 50, 100]
  script:
    - echo "Running $TEST_TYPE performance test with $VUS virtual users"
    - npm install -g k6
    - |
      cat > test-config.js << EOF
      import http from 'k6/http';
      import { check, sleep } from 'k6';

      export let options = {
        stages: [
          { duration: '2m', target: $VUS },
          { duration: '5m', target: $VUS },
          { duration: '2m', target: 0 },
        ],
        thresholds: {
          http_req_duration: ['p(99)<1500'],
          http_req_failed: ['rate<0.1'],
        },
      };

      export default function () {
        let response = http.get('https://$INGRESS_HOST/api/health');
        check(response, {
          'status was 200': (r) => r.status == 200,
          'response time < 500ms': (r) => r.timings.duration < 500,
        });
        sleep(1);
      }
      EOF
    - k6 run test-config.js --out json=performance-$TEST_TYPE.json
  artifacts:
    reports:
      performance: "performance-$TEST_TYPE.json"
    paths:
      - "performance-$TEST_TYPE.json"
    expire_in: 1 week
  rules:
    - if: $DEPLOY_ENVIRONMENT == "staging" || $DEPLOY_ENVIRONMENT == "production"

# Security scanning
security:container:
  stage: security
  image: aquasec/trivy:latest
  parallel:
    matrix:
      - IMAGE: ["frontend", "backend", "api-gateway"]
  script:
    - echo "Scanning container image for $IMAGE"
    - trivy image --format json --output trivy-$IMAGE-report.json $CI_REGISTRY_IMAGE/$IMAGE:$CI_COMMIT_SHA
    - trivy image --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE/$IMAGE:$CI_COMMIT_SHA
  artifacts:
    reports:
      container_scanning: "trivy-$IMAGE-report.json"
    paths:
      - "trivy-$IMAGE-report.json"
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $DEPLOY_ENVIRONMENT

# Create release
release:
  stage: release
  image: node:18-alpine
  script:
    - echo "Creating release for $DEPLOY_ENVIRONMENT"
    - npm ci
    - |
      # Generate release notes
      cat > release-notes.md << EOF
      # Release $CI_COMMIT_SHORT_SHA

      ## Environment: $DEPLOY_ENVIRONMENT
      ## Branch: $CI_COMMIT_REF_NAME
      ## Commit: $CI_COMMIT_SHA
      ## Timestamp: $CI_COMMIT_TIMESTAMP

      ### Changes
      $(git log --oneline $(git describe --tags --abbrev=0)..HEAD)

      ### Image Tags
      - Frontend: $CI_REGISTRY_IMAGE/frontend:$CI_COMMIT_SHA
      - Backend: $CI_REGISTRY_IMAGE/backend:$CI_COMMIT_SHA
      - API Gateway: $CI_REGISTRY_IMAGE/api-gateway:$CI_COMMIT_SHA

      ### Deployment
      - Namespace: $NAMESPACE
      - Ingress Host: $INGRESS_HOST
      EOF
    - cat release-notes.md
  artifacts:
    paths:
      - "release-notes.md"
    expire_in: 1 month
  rules:
    - if: $DEPLOY_ENVIRONMENT
  dependencies:
    - build:docker

# Deployment strategies
deploy:development:
  stage: deploy
  environment:
    name: $DEPLOY_ENVIRONMENT
    url: https://$INGRESS_HOST
    on_stop: stop:development
  script:
    - echo "Deploying to $DEPLOY_ENVIRONMENT environment..."
    - |
      # Deploy using Helm
      helm upgrade --install app-$NAMESPACE ./helm/app \
        --namespace $NAMESPACE \
        --create-namespace \
        --set image.tag=$CI_COMMIT_SHA \
        --set environment=$DEPLOY_ENVIRONMENT \
        --set ingress.hosts[0].host=$INGRESS_HOST \
        --set replicas=$REPLICAS \
        --set resources.requests.memory="256Mi" \
        --set resources.limits.memory="512Mi" \
        --wait --timeout=10m
    - |
      # Configure feature flags
      ./scripts/configure-feature-flags.sh \
        --environment $DEPLOY_ENVIRONMENT \
        --config-file feature-flags/$DEPLOY_ENVIRONMENT.json
    - echo "Deployment to $DEPLOY_ENVIRONMENT completed"
  rules:
    - if: $DEPLOY_ENVIRONMENT == "development" || $DEPLOY_ENVIRONMENT == "review"
  dependencies:
    - release

deploy:canary:
  stage: deploy
  environment:
    name: canary
    url: https://canary.example.com
  script:
    - echo "Starting canary deployment..."
    - |
      # Deploy 10% traffic to canary
      helm upgrade --install app-canary ./helm/app \
        --namespace canary \
        --create-namespace \
        --set image.tag=$CI_COMMIT_SHA \
        --set environment=canary \
        --set ingress.hosts[0].host=canary.example.com \
        --set replicas=1 \
        --set canary.enabled=true \
        --set canary.traffic=10 \
        --wait --timeout=10m
    - |
      # Monitor canary for 10 minutes
      echo "Monitoring canary deployment..."
      for i in {1..20}; do
        response=$(curl -s -o /dev/null -w "%{http_code}" https://canary.example.com/health)
        if [ "$response" != "200" ]; then
          echo "Canary health check failed: $response"
          helm uninstall app-canary --namespace canary
          exit 1
        fi
        echo "Canary health check passed ($i/20)"
        sleep 30
      done
    - echo "Canary deployment successful"
  rules:
    - if: $DEPLOY_ENVIRONMENT == "production"
      when: manual
  dependencies:
    - release

deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
    on_stop: stop:staging
  script:
    - echo "Deploying to staging environment..."
    - |
      # Blue-green deployment
      ./scripts/blue-green-deploy.sh \
        --namespace $STAGING_NAMESPACE \
        --image-tag $CI_COMMIT_SHA \
        --ingress-host $STAGING_INGRESS_HOST \
        --replicas $STAGING_REPLICAS
    - echo "Staging deployment completed"
  rules:
    - if: $DEPLOY_ENVIRONMENT == "staging"
  dependencies:
    - release

deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - echo "Deploying to production environment..."
    - |
      # Manual approval for production
      echo "⚠️  DEPLOYING TO PRODUCTION ⚠️"
      echo "Image tag: $CI_COMMIT_SHA"
      echo "Environment: $PROD_NAMESPACE"
      echo "Ingress: $PROD_INGRESS_HOST"
    - |
      # Progressive deployment with auto-scaling
      ./scripts/progressive-deploy.sh \
        --namespace $PROD_NAMESPACE \
        --image-tag $CI_COMMIT_SHA \
        --ingress-host $PROD_INGRESS_HOST \
        --initial-replicas $PROD_REPLICAS \
        --max-replicas 20 \
        --cpu-threshold 70 \
        --memory-threshold 80
    - echo "Production deployment completed"
  rules:
    - if: $DEPLOY_ENVIRONMENT == "production"
      when: manual
  dependencies:
    - deploy:canary

# Post-deployment validation
validate:deployment:
  stage: post-deploy
  image: alpine:3.18
  script:
    - echo "Validating deployment in $DEPLOY_ENVIRONMENT..."
    - apk add --no-cache curl jq
    - |
      # Health check
      response=$(curl -s -o /dev/null -w "%{http_code}" https://$INGRESS_HOST/health)
      if [ "$response" != "200" ]; then
        echo "❌ Health check failed: $response"
        exit 1
      fi
      echo "✅ Health check passed"
    - |
      # API functionality check
      api_response=$(curl -s https://$INGRESS_HOST/api/version)
      version_check=$(echo $api_response | jq -r '.version // empty')
      if [ -z "$version_check" ]; then
        echo "❌ API version check failed"
        exit 1
      fi
      echo "✅ API functionality check passed: $version_check"
    - echo "Deployment validation completed successfully"
  rules:
    - if: $DEPLOY_ENVIRONMENT
      when: on_success

monitor:performance:
  stage: post-deploy
  image: node:18-alpine
  script:
    - echo "Monitoring performance after deployment..."
    - npm install -g artillery
    - |
      # Performance monitoring
      artillery run tests/performance/monitor.yml --output monitor-results.json
      cat monitor-results.json | jq '.aggregate.latency.p99' > p99-latency.txt
      P99_LATENCY=$(cat p99-latency.txt)
      echo "P99 Latency: ${P99_LATENCY}ms"

      # Alert if latency is too high
      if (( $(echo "$P99_LATENCY > 1000" | bc -l) )); then
        echo "⚠️  High P99 latency detected: ${P99_LATENCY}ms"
        # Send alert notification
        curl -X POST "$SLACK_WEBHOOK_URL" \
          -H 'Content-type: application/json' \
          --data '{
            "text": "⚠️ High latency detected in production: '${P99_LATENCY}'ms"
          }'
      fi
  artifacts:
    paths:
      - "monitor-results.json"
      - "p99-latency.txt"
    expire_in: 1 week
  rules:
    - if: $DEPLOY_ENVIRONMENT == "production"
      when: on_success

# Environment stop jobs
stop:development:
  stage: cleanup
  environment:
    name: development
    action: stop
  script:
    - echo "Stopping development environment..."
    - helm uninstall app-development --namespace $DEV_NAMESPACE || true
    - kubectl delete namespace $DEV_NAMESPACE || true
    - echo "Development environment stopped"
  rules:
    - if: $DEPLOY_ENVIRONMENT == "development"
      when: manual
  dependencies: []

stop:staging:
  stage: cleanup
  environment:
    name: staging
    action: stop
  script:
    - echo "Stopping staging environment..."
    - helm uninstall app-staging --namespace $STAGING_NAMESPACE || true
    - kubectl delete namespace $STAGING_NAMESPACE || true
    - echo "Staging environment stopped"
  rules:
    - if: $DEPLOY_ENVIRONMENT == "staging"
      when: manual
  dependencies: []

# Cleanup jobs
cleanup:old-images:
  stage: cleanup
  image: docker:24.0.5
  script:
    - echo "Cleaning up old Docker images..."
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - |
      # Keep only last 10 images
      docker images $CI_REGISTRY_IMAGE --format "table {{.Repository}}:{{.Tag}}" |       grep -v latest |       tail -n +11 |       awk '{print $1}' |       xargs -r docker rmi || true
    - echo "Old images cleanup completed"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: on_success
  dependencies: []

cleanup:old-artifacts:
  stage: cleanup
  image: alpine:3.18
  script:
    - echo "Cleaning up old artifacts..."
    - |
      # Clean old GitLab artifacts (requires API token)
      echo "Cleaning artifacts older than 30 days..."
      # Add actual cleanup commands using GitLab API
    - echo "Artifacts cleanup completed"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: on_success
  dependencies: []