Skip to main content

patterns

Component Design Patterns

This guide covers common patterns for designing effective, reusable CI/CD components.

Pattern Categories

  1. Job Templates - Reusable single jobs
  2. Workflow Templates - Complete pipelines
  3. Composition Patterns - Building blocks
  4. Extension Patterns - Customization mechanisms
  5. Multi-Environment Patterns - Deploy across environments
  6. Security Patterns - Security scanning and compliance

Job Template Patterns

Pattern 1: Simple Job Template

Use case: Single, parameterized job

# templates/docker-build.yml spec: inputs: image_name: type: string dockerfile: type: string default: 'Dockerfile' --- build: stage: build image: docker:latest services: - docker:dind script: - docker build -t $[[ inputs.image_name ]] -f $[[ inputs.dockerfile ]] . - docker push $[[ inputs.image_name ]]

Usage:

include: - component: gitlab.com/org/comp/docker-build@1.0.0 inputs: image_name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Pattern 2: Configurable Job with Rules

Use case: Job with conditional execution

# templates/security-scan.yml spec: inputs: scan_type: type: string options: ['sast', 'dast', 'dependency', 'all'] default: 'all' enable_on_merge_request: type: boolean default: true enable_on_main: type: boolean default: true --- .scan-rules: rules: - if: $CI_MERGE_REQUEST_IID && $[[ inputs.enable_on_merge_request ]] - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $[[ inputs.enable_on_main ]] sast-scan: extends: .scan-rules stage: test script: - run-sast-scan rules: - if: $[[ inputs.scan_type == "sast" || inputs.scan_type == "all" ]] when: on_success dast-scan: extends: .scan-rules stage: test script: - run-dast-scan rules: - if: $[[ inputs.scan_type == "dast" || inputs.scan_type == "all" ]] when: on_success dependency-scan: extends: .scan-rules stage: test script: - run-dependency-scan rules: - if: $[[ inputs.scan_type == "dependency" || inputs.scan_type == "all" ]] when: on_success

Pattern 3: Job with Artifacts

Use case: Job producing artifacts for later stages

# templates/build-artifacts.yml spec: inputs: artifact_path: type: string artifact_name: type: string default: 'build' expire_in: type: string default: '1 day' --- build: stage: build script: - make build artifacts: name: $[[ inputs.artifact_name ]] paths: - $[[ inputs.artifact_path ]] expire_in: $[[ inputs.expire_in ]]

Workflow Template Patterns

Pattern 4: Complete Pipeline Template

Use case: Full CI/CD pipeline for specific tech stack

# templates/node-pipeline.yml spec: inputs: node_version: type: string default: '20' enable_lint: type: boolean default: true enable_tests: type: boolean default: true enable_build: type: boolean default: true --- stages: - install - lint - test - build install-dependencies: stage: install image: node:$[[ inputs.node_version ]] script: - npm ci cache: key: $CI_COMMIT_REF_SLUG paths: - node_modules/ artifacts: paths: - node_modules/ expire_in: 1 hour lint: stage: lint image: node:$[[ inputs.node_version ]] script: - npm run lint needs: - install-dependencies rules: - if: $[[ inputs.enable_lint ]] test: stage: test image: node:$[[ inputs.node_version ]] script: - npm test coverage: '/Coverage: \d+\.\d+%/' needs: - install-dependencies rules: - if: $[[ inputs.enable_tests ]] artifacts: reports: junit: junit.xml coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml build: stage: build image: node:$[[ inputs.node_version ]] script: - npm run build needs: - install-dependencies rules: - if: $[[ inputs.enable_build ]] artifacts: paths: - dist/ expire_in: 1 week

Pattern 5: Multi-Stage Deployment Pipeline

Use case: Deploy to multiple environments

# templates/multi-env-deploy.yml spec: inputs: image_name: type: string deploy_to_dev: type: boolean default: true deploy_to_staging: type: boolean default: false deploy_to_production: type: boolean default: false --- stages: - deploy .deploy-template: stage: deploy image: kubectl:latest script: - kubectl set image deployment/app app=$[[ inputs.image_name ]] -n ${NAMESPACE} - kubectl rollout status deployment/app -n ${NAMESPACE} deploy-dev: extends: .deploy-template variables: NAMESPACE: development environment: name: development url: https://dev.example.com rules: - if: $[[ inputs.deploy_to_dev ]] deploy-staging: extends: .deploy-template variables: NAMESPACE: staging environment: name: staging url: https://staging.example.com rules: - if: $[[ inputs.deploy_to_staging ]] needs: - deploy-dev deploy-production: extends: .deploy-template variables: NAMESPACE: production environment: name: production url: https://example.com rules: - if: $[[ inputs.deploy_to_production ]] needs: - deploy-staging when: manual

Composition Patterns

Pattern 6: Base + Extension Components

Use case: Base component with optional enhancements

# templates/base-pipeline.yml spec: inputs: language: type: string options: ['node', 'go', 'python'] --- stages: - build - test build-job: stage: build script: - build-${[[ inputs.language ]]} test-job: stage: test script: - test-${[[ inputs.language ]]}
# templates/enhanced-pipeline.yml spec: inputs: language: type: string enable_security: type: boolean default: false --- include: - component: gitlab.com/org/comp/base-pipeline@1.0.0 inputs: language: $[[ inputs.language ]] # Add security scanning security-scan: stage: test script: - run-security-scan rules: - if: $[[ inputs.enable_security ]]

Usage:

# Use enhanced version with security include: - component: gitlab.com/org/comp/enhanced-pipeline@1.0.0 inputs: language: node enable_security: true

Pattern 7: Modular Components

Use case: Compose multiple independent components

# Project .gitlab-ci.yml include: # Build component - component: gitlab.com/org/comp/docker-build@1.0.0 inputs: image_name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA # Security component - component: gitlab.com/org/comp/security-scan@2.0.0 inputs: scan_type: all # Deployment component - component: gitlab.com/org/comp/k8s-deploy@3.0.0 inputs: image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA environment: staging

Pattern 8: Shared Configuration Component

Use case: Reusable configuration fragments

# templates/cache-config.yml spec: inputs: cache_key: type: string default: $CI_COMMIT_REF_SLUG cache_paths: type: array default: ['node_modules/', '.cache/'] --- .cache-config: cache: key: $[[ inputs.cache_key ]] paths: $[[ inputs.cache_paths ]] policy: pull-push

Usage:

include: - component: gitlab.com/org/comp/cache-config@1.0.0 inputs: cache_paths: ['vendor/', '.bundle/'] build: extends: .cache-config script: - bundle install - bundle exec rails assets:precompile

Extension Patterns

Pattern 9: Hook Points

Use case: Allow consumers to inject custom steps

# templates/build-with-hooks.yml spec: inputs: image_name: type: string --- .pre-build-hook: {} .post-build-hook: {} build: stage: build extends: - .pre-build-hook script: - docker build -t $[[ inputs.image_name ]] . - docker push $[[ inputs.image_name ]] after_script: - !reference [.post-build-hook, after_script]

Usage:

include: - component: gitlab.com/org/comp/build-with-hooks@1.0.0 inputs: image_name: myapp:latest # Override hooks .pre-build-hook: before_script: - echo "Custom pre-build step" - setup-buildx.sh .post-build-hook: after_script: - echo "Custom post-build step" - notify-slack.sh

Pattern 10: Strategy Pattern

Use case: Different algorithms/strategies selectable at runtime

# templates/deployment-strategies.yml spec: inputs: strategy: type: string options: ['rolling', 'blue-green', 'canary'] default: 'rolling' image: type: string environment: type: string --- .deploy-rolling: script: - kubectl set image deployment/app app=$[[ inputs.image ]] - kubectl rollout status deployment/app .deploy-blue-green: script: - deploy-blue-green.sh $[[ inputs.image ]] .deploy-canary: script: - deploy-canary.sh $[[ inputs.image ]] --percentage 10 deploy: stage: deploy extends: .deploy-$[[ inputs.strategy ]] environment: name: $[[ inputs.environment ]]

Multi-Environment Patterns

Pattern 11: Environment Matrix

Use case: Deploy to multiple environments in parallel

# templates/multi-region-deploy.yml spec: inputs: regions: type: array default: ['us-east-1', 'eu-west-1'] image: type: string --- deploy-multi-region: stage: deploy parallel: matrix: - REGION: $[[ inputs.regions ]] script: - deploy.sh --region ${REGION} --image $[[ inputs.image ]] environment: name: production/${REGION}

Usage:

include: - component: gitlab.com/org/comp/multi-region-deploy@1.0.0 inputs: regions: ['us-east-1', 'us-west-2', 'eu-west-1', 'ap-south-1'] image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Pattern 12: Progressive Delivery

Use case: Gradual rollout with gates

# templates/progressive-deploy.yml spec: inputs: image: type: string enable_canary: type: boolean default: true enable_full_rollout: type: boolean default: true --- stages: - deploy-canary - verify-canary - deploy-full canary-deployment: stage: deploy-canary script: - deploy-canary.sh $[[ inputs.image ]] --percentage 10 environment: name: production-canary rules: - if: $[[ inputs.enable_canary ]] verify-canary: stage: verify-canary script: - run-smoke-tests.sh - check-error-rate.sh rules: - if: $[[ inputs.enable_canary ]] full-deployment: stage: deploy-full script: - deploy.sh $[[ inputs.image ]] --percentage 100 environment: name: production rules: - if: $[[ inputs.enable_full_rollout ]] when: manual # Require approval after canary verification

Security Patterns

Pattern 13: Comprehensive Security Scanning

Use case: Run all security scans

# templates/security-comprehensive.yml spec: inputs: enable_sast: type: boolean default: true enable_dast: type: boolean default: false enable_dependency_scan: type: boolean default: true enable_secret_detection: type: boolean default: true fail_on_high: type: boolean default: true --- stages: - security .security-template: stage: security allow_failure: false rules: - if: $[[ inputs.fail_on_high ]] allow_failure: false - when: on_success allow_failure: true sast: extends: .security-template script: - run-sast-scan rules: - if: $[[ inputs.enable_sast ]] artifacts: reports: sast: gl-sast-report.json dast: extends: .security-template script: - run-dast-scan rules: - if: $[[ inputs.enable_dast ]] artifacts: reports: dast: gl-dast-report.json dependency-scanning: extends: .security-template script: - run-dependency-scan rules: - if: $[[ inputs.enable_dependency_scan ]] artifacts: reports: dependency_scanning: gl-dependency-scanning-report.json secret-detection: extends: .security-template script: - run-secret-detection rules: - if: $[[ inputs.enable_secret_detection ]] artifacts: reports: secret_detection: gl-secret-detection-report.json

Pattern 14: Compliance Gates

Use case: Enforce compliance before deployment

# templates/compliance-gate.yml spec: inputs: require_tests: type: boolean default: true require_security_scan: type: boolean default: true min_coverage: type: number default: 80 --- stages: - compliance compliance-check: stage: compliance script: - | # Check test coverage if [ "$[[ inputs.require_tests ]]" = "true" ]; then COVERAGE=$(get-coverage) if [ $COVERAGE -lt $[[ inputs.min_coverage ]] ]; then echo "Coverage $COVERAGE% < $[[ inputs.min_coverage ]]%" exit 1 fi fi # Check security scan passed if [ "$[[ inputs.require_security_scan ]]" = "true" ]; then if ! check-security-scan-passed; then echo "Security scan failed or not run" exit 1 fi fi echo " All compliance checks passed" allow_failure: false

Performance Patterns

Pattern 15: Parallel Testing

Use case: Run tests in parallel for faster feedback

# templates/parallel-test.yml spec: inputs: test_command: type: string default: 'npm test' parallel_count: type: number default: 4 --- test: stage: test parallel: $[[ inputs.parallel_count ]] script: - $[[ inputs.test_command ]] --shard ${CI_NODE_INDEX}/${CI_NODE_TOTAL} artifacts: reports: junit: junit-${CI_NODE_INDEX}.xml when: always

Pattern 16: Conditional Stages

Use case: Skip stages based on changes

# templates/conditional-stages.yml spec: inputs: build_on_changes: type: array default: ['src/**/*', 'package.json'] test_on_changes: type: array default: ['src/**/*', 'test/**/*'] --- build: stage: build script: - npm run build rules: - changes: $[[ inputs.build_on_changes ]] test: stage: test script: - npm test rules: - changes: $[[ inputs.test_on_changes ]]

Notification Patterns

Pattern 17: Multi-Channel Notifications

Use case: Notify on success/failure

# templates/notifications.yml spec: inputs: slack_webhook: type: string email: type: string notify_on_success: type: boolean default: false notify_on_failure: type: boolean default: true --- .notify-slack: script: - | curl -X POST $[[ inputs.slack_webhook ]] \ -H 'Content-Type: application/json' \ -d "{\"text\": \"${MESSAGE}\"}" notify-success: stage: .post extends: .notify-slack variables: MESSAGE: " Pipeline succeeded: $CI_PROJECT_NAME" rules: - if: $[[ inputs.notify_on_success ]] when: on_success notify-failure: stage: .post extends: .notify-slack variables: MESSAGE: " Pipeline failed: $CI_PROJECT_NAME" rules: - if: $[[ inputs.notify_on_failure ]] when: on_failure

Anti-Patterns to Avoid

Anti-Pattern 1: Too Many Inputs

Problem:

spec: inputs: # 20+ inputs - too complex! build_command: type: string test_command: type: string lint_command: type: string # ... 17 more inputs

Solution: Break into multiple focused components.

Anti-Pattern 2: Hardcoded Values

Problem:

deploy: script: - kubectl apply -f k8s/ --namespace production # Hardcoded!

Solution: Use inputs for all configurable values.

Anti-Pattern 3: No Default Values

Problem:

spec: inputs: timeout: type: number # No default - always required

Solution: Provide sensible defaults for optional inputs.

Anti-Pattern 4: Overly Generic Components

Problem:

# Component tries to handle all languages spec: inputs: language: type: string options: ['node', 'go', 'python', 'java', 'rust', ...] # Too many!

Solution: Create language-specific components.

Anti-Pattern 5: Tight Coupling

Problem:

# Component assumes specific project structure deploy: script: - kubectl apply -f deployment/k8s/production/ # Assumes path exists

Solution: Use inputs to specify paths, don't assume structure.

Best Practices Summary

  1. Single Responsibility: Each component does one thing well
  2. Sensible Defaults: Provide defaults for all optional inputs
  3. Input Validation: Use options and regex to validate inputs
  4. Clear Naming: Descriptive component and input names
  5. Documentation: Comprehensive README with examples
  6. Composition: Build complex pipelines from simple components
  7. Extension Points: Allow customization via hooks
  8. Error Handling: Fail fast with clear error messages
  9. Performance: Use parallel execution and caching
  10. Testing: Test all input combinations

Next Steps

References