patterns
Component Design Patterns
This guide covers common patterns for designing effective, reusable CI/CD components.
Pattern Categories
- Job Templates - Reusable single jobs
- Workflow Templates - Complete pipelines
- Composition Patterns - Building blocks
- Extension Patterns - Customization mechanisms
- Multi-Environment Patterns - Deploy across environments
- 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
- Single Responsibility: Each component does one thing well
- Sensible Defaults: Provide defaults for all optional inputs
- Input Validation: Use
optionsandregexto validate inputs - Clear Naming: Descriptive component and input names
- Documentation: Comprehensive README with examples
- Composition: Build complex pipelines from simple components
- Extension Points: Allow customization via hooks
- Error Handling: Fail fast with clear error messages
- Performance: Use parallel execution and caching
- Testing: Test all input combinations
Next Steps
- Testing: Learn component testing strategies
- Creating Components: Build your own components
- Multi-Project Strategy: Roll out to 70+ projects