Skip to content

GitLab CI Integration Guide

Configure continuous FinOps scanning, MR comments, and security gating in .gitlab-ci.yml.

Quick Start (5-Minute Setup)

Prerequisites

  • GitLab account with admin access
  • GitLab CI/CD enabled on project
  • CloudSlash API key (from dashboard)
  • (Optional) GitLab Personal Access Token

Step 1: Install CloudSlash

Add CloudStencil to your .gitlab-ci.yml:

# .gitlab-ci.yml
stages:
  - infrastructure
  - metrics

cloudslash-scan:
  stage: metrics
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config cloudslash.yml
  artifacts:
    reports:
      cloudslash: cloudslash-report.json
  only:
    - branches
    - merge_requests
    - tags

Step 2: Add Variables

Go to your project → Settings → CI/CD → Variables:

  • Add CLOUDSLASH_API_KEY with your CloudSlash API key
  • (Optional) Add GITLAB_TOKEN for API access to public repositories

Step 3: Configure CloudSlash

Create cloudslash.yml in your repository root:

version: 1.0
accounts:
  - provider: aws
    name: production
    accounts:
      - id: "123456789012"
        role_arn: "arn:aws:iam::123456789012:role/CloudSlashAccess"

security:
  compliance:
    enabled: true
    benchmark: cis

notifications:
  slack:
    webhook_url: "${{ GITLAB_SLACK_WEBHOOK }}"

Done! Prepended MRs will automatically run scans.

Full Configuration Reference

Common Workflow Patterns

Pattern 1: PR/MR Cost Scanning

Prevent costly changes before merging:

# .gitlab-ci.yml
stages:
  - infrastructure
  - cost-analysis

cost-scan:
  stage: cost-analysis
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config cloudslash.yml
  # Run on each MR branch
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: always
  # Add MR notes
  artifacts:
    reports:
      cloudslash: cost-analysis.json
    expire_in: 1 week
    paths:
      - cost-analysis.json
      - cost-analysis.html
  allow_failure: true
  # Merge request comment with summary
  after_script:
    - |
      # Generate PR comment
      SCRIPT_DIR="$(dirname "$0")"
      python3 $SCRIPT_DIR/add_pr_comment.py

creates script: .gitlab-ci/scripts/add_pr_comment.py

#!/usr/bin/env python3
import json
import sys

# Read the report
with open('cost-analysis.json', 'r') as f:
    data = json.load(f)

# Generate comment
title = f"📊 Cost Analysis for {data['summary']['repository']}"
comment = f"""
### Cost Analysis
**Pipeline**: {data['summary']['pipeline_id']}
**Author**: {data['summary']['author']}
**Duration**: {data['summary']['duration']}s

**Total Estimated Monthly Cost**: $**{data['summary']['estimated_cost']}**

### Findings:

"""
for finding in data['findings']:
    comment += f"- **{finding['severity']}**: {finding['resource']} - $**{finding['cost']}/month**\n"

comment += f"""
[View full report]({data['summary']['report_url']})
"""

# Send to MR comment
print(comment)

Pattern 2: Scheduled Drift Detection

Monitor production infrastructure changes continuously:

# .gitlab-ci.yml
stages:
  - maintenance
  - drift-detection

drift-detection:
  stage: maintenance
  image: cloudslash/scan:latest
  script:
    - |
      SCAN_ID="drift-$(date +%Y%m%d-%H%M%S)"
      echo "Running drift detection with scan-id: $SCAN_ID"
      cloudslash scan --config cloudslash.yml --mode scheduled --scan-id "$SCAN_ID"
  rules:
    # Run daily at 2 AM UTC (save resources in off-peak hours)
    - if: '$CI_SCHEDULED == "true"'
      when: always
    # Allow manual trigger from UI
    - if: '$GITLAB_USER_LOGIN == "manual-trigger"'
      when: manual
  artifacts:
    reports:
      cloudslash: drift-detection-report.json
    paths:
      - drift-detection-report.json
      - drift-detection-report.html
    expire_in: 30 days
  after_script:
    - |
      # Create issue on MR if drift detected
      SCRIPT_DIR="$(dirname "$0")"
      python3 $SCRIPT_DIR/check-drift.py --scan-id "$SCAN_ID"

creates script: .gitlab-ci/scripts/check-drift.py

#!/usr/bin/env python3
import json
import subprocess
import sys
from datetime import datetime

def check_drift(scan_id):
    # Fetch report from CloudSlash
    result = subprocess.run(
        ['cloudslash', 'report', '--id', scan_id, '--format', 'json'],
        capture_output=True,
        text=True
    )

    if result.returncode != 0:
        print(f"Error fetching report: {result.stderr}")
        return

    data = json.loads(result.stdout)

    # Check for drift
    if data.get('drift', {}).get('change_count', 0) > 0:
        title = f"⚠️ Infrastructure Drift Detected (Scan: {scan_id})"
        body = f"""## 🚨 Infrastructure Drift Detected

**Scan ID**: {scan_id}
**Time**: {datetime.utcnow().isoformat()}

**Drift Changes**: {data['drift']['change_count']}
**Potential Cost Impact**: $**{data['drift']['cost_impact']}**

### Modified Resources:

"""
        for change in data['drift']['changes']:
            body += f"- **{change['resource']}** ({change['provider']}): {change['change_type']}\n"

        body += f"""
[View full report](#report-viewer)
"""
        print("DRIFT_DETECTED")
        print(f"ISSUE_TITLE={title}")
        print(f"ISSUE_BODY={body}")
    else:
        print("NO_DRIFT")
        print("✅ Infrastructure is in sync with your IaC.")

def main():
    scan_id = sys.argv[1] if len(sys.argv) > 1 else None
    if scan_id:
        check_drift(scan_id)
    else:
        print("Usage: python3 check-drift.py --scan-id <scan_id>")

if __name__ == '__main__':
    main()

Pattern 3: Deployment Gates

Block deployments that don't meet security requirements:

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

deployment-security-check:
  stage: security
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config cloudslash.yml --mode deployment-gate
  needs:
    - build
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual  # Require manual approval
  allow_failure: false
  # Send results to GitLab merge request
  artifacts:
    reports:
      cloudslash: deployment-gate-report.json
  after_script:
    - |
      if [ "$CLOUDSLASH_PASSED" != "true" ]; then
        echo "❌ Security checks failed"
        exit 1
      fi
      echo "✅ Security checks passed"

Note: This would require a merged codebase, so it's typically used after merge:

# .gitlab-ci.yml (in protected branch)
stages:
  - deploy

production-deployment-gate:
  stage: deploy
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config cloudslash.yml --mode deployment-gate --fail-on-warnings
  when: manual  # Require approval
  only:
    - main

Pattern 4: Monorepo Setup

Centralized scanning for multiple repositories:

# .gitlab-ci.yml (root monorepo)
stages:
  - docs
  - infra-scan

subrepo-scan:
  stage: infra-scan
  image: cloudslash/scan:latest
  script:
    # Scan frontend-api
    - |
      cd frontend-api
      cloudslash scan --config cloudslash.yml
    # Scan backend-api
    - |
      cd backend-api
      cloudslash scan --config cloudslash.yml
    # Scan infrastructure
    - |
      cd infrastructure
      cloudslash scan --config cloudslash.yml
  artifacts:
    paths:
      - frontend-api/cloudslash-results.json
      - backend-api/cloudslash-results.json
      - infrastructure/cloudslash-results.json
    expire_in: 30 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  after_script:
    - |
      # Aggregate results and create comment
      SCRIPT_DIR="$(dirname "$0")"
      python3 $SCRIPT_DIR/aggregate-results.py

# .gitlab-ci.yml (in each subro repo)
cloudslash-scan:
  stage: infra-scan
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config cloudslash.yml
  artifacts:
    reports:
      cloudslash: cloudslash-results.json

Aggregate script: .gitlab-ci/scripts/aggregate-results.py

#!/usr/bin/env python3
import json
import sys

def aggregate_results():
    results = {
        'frontend': None,
        'backend': None,
        'infrastructure': None
    }

    # Read each subrepo result
    try:
        with open('frontend-api/cloudslash-results.json') as f:
            results['frontend'] = json.load(f)
    except FileNotFoundError:
        pass

    try:
        with open('backend-api/cloudslash-results.json') as f:
            results['backend'] = json.load(f)
    except FileNotFoundError:
        pass

    try:
        with open('infrastructure/cloudslash-results.json') as f:
            results['infrastructure'] = json.load(f)
    except FileNotFoundError:
        pass

    # Generate aggregate comment
    title = "📊 Monorepo Infrastructure Scan"
    comment = f"""### Monorepo Infrastructure Scan

**Backend**: {len(results['backend'].get('findings', [])) if results['backend'] else 0} findings
**Frontend**: {len(results['frontend'].get('findings', [])) if results['frontend'] else 0} findings
**Infrastructure**: {len(results['infrastructure'].get('findings', [])) if results['infrastructure'] else 0} findings

[View individual reports]
"""

    print(title)
    print(comment)

if __name__ == '__main__':
    aggregate_results()

Pattern 5: Multi-Cloud Setup

Unified scanning across AWS, Azure, and GCP:

# .gitlab-ci.yml
stages:
  - infrastructure
  - multi-cloud-check

aws-monitoring:
  stage: infrastructure
  image: cloudslash/scan:latest
  variables:
    CLOUDSLASH_CONFIG: aws-config.yml
  script:
    - cloudslash scan --config "$CLOUDSLASH_CONFIG"
  artifacts:
    reports:
      cloudslash: aws-monitoring.json

azure-monitoring:
  stage: infrastructure
  image: cloudslash/scan:latest
  variables:
    CLOUDSLASH_CONFIG: azure-config.yml
  script:
    - cloudslash scan --config "$CLOUDSLASH_CONFIG"
  artifacts:
    reports:
      cloudslash: azure-monitoring.json

gcp-monitoring:
  stage: infrastructure
  image: cloudslash/scan:latest
  variables:
    CLOUDSLASH_CONFIG: gcp-config.yml
  script:
    - cloudslash scan --config "$CLOUDSLASH_CONFIG"
  artifacts:
    reports:
      cloudslash: gcp-monitoring.json

aggregate-multi-cloud:
  stage: multi-cloud-check
  image: python:3.9
  script:
    - >
      python3 << 'EOF'
      # Read all three cloud reports
      with open('aws-monitoring.json') as f:
          aws_data = json.load(f)
      with open('azure-monitoring.json') as f:
          azure_data = json.load(f)
      with open('gcp-monitoring.json') as f:
          gcp_data = json.load(f)

      # Aggregate findings
      all_findings = (aws_data.get('findings', []) +
                     azure_data.get('findings', []) +
                     gcp_data.get('findings', []))

      total_cost = (aws_data.get('summary', {}).get('estimated_cost', 0) +
                   azure_data.get('summary', {}).get('estimated_cost', 0) +
                   gcp_data.get('summary', {}).get('estimated_cost', 0))

      print(f"Total Multi-Cloud Findings: {len(all_findings)}")
      print(f"Estimated Monthly Cost: $**{total_cost}**")
      print(f"AWS Findings: {len(aws_data['findings'])}")
      print(f"Azure Findings: {len(azure_data['findings'])}")
      print(f"GCP Findings: {len(gcp_data['findings'])}")
      EOF
  dependencies:
    - aws-monitoring
    - azure-monitoring
    - gcp-monitoring

# Example configs
# aws-config.yml
version: 1.0
accounts:
  - provider: aws
    name: production
    accounts:
      - id: "111111111111"
        role_arn: "arn:aws:iam::111111111111:role/CloudSlashRole"

# azure-config.yml
version: 1.0
accounts:
  - provider: azure
    name: production
    subscriptions:
      - id: "11111111-1111-1111-1111-111111111111"

# gcp-config.yml
version: 1.0
accounts:
  - provider: gcp
    name: production
    projects:
      - id: "my-project-123"

Security Best Practices

1. Use Protected Variables

Store credentials in project-level variables:

# ✅ DO THIS: Use protected variables
variables:
  CLOUDSLASH_API_KEY: "sk-api-key"
  AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY
  AWS_SECRET_ACCESS_KEY: $AWS_SECRET

2. Mask Sensitive Variables

In .gitlab-ci.yml, set roles to masked:

drift-detection:
  mask:
    - CLOUDSLASH_API_KEY
    - AWS_ACCESS_KEY_ID
    - AWS_SECRET_ACCESS_KEY
  script:
    - cloudslash scan --config cloudslash.yml

3. Run in Stages

Use proper CI/CD pipeline stages:

stages:
  - build
  - test
  - infrastructure-scan
  - deploy

security-scan:
  stage: infrastructure-scan
  script:
    - cloudslash scan --config security-config.yml
  dependencies: [test-results]

deploy:
  stage: deploy
  script:
    - echo "Deploy..."
  dependencies: [security-scan]
  only:
    - main

4. Use Rules Instead of If

For conditional execution in pipelines:

tag-based-scan:
  stage: infrastructure-scan
  script:
    - cloudslash scan --config config.yml
  rules:
    - if: $CI_COMMIT_TAG
      when: always
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
    - when: never  # Don't run on other branches

5. Limit Runner Permissions

Use generic runners with limited permissions:

# .gitlab-ci.yml
security-scan:
  image: cloudslash/scan:latest
  services: []  # Disable service containers
  tags: ["generic", "security-scan"]
  script:
    - cloudslash scan --config cloudslash.yml

Multi-Account Setup

Using OAuth 2.0

For enterprise GitLab setups:

# .gitlab-ci.yml
oauth-scan:
  image: cloudslash/scan:latest
  variables:
    CLOUDSLASH_OAUTH_TOKEN: $CLOUDSLASH_OAUTH_TOKEN
  script:
    - cloudslash scan --config cloudslash.yml --auth-type oauth

Using SSH Keys

For SSH-based authentication:

ssh-scan:
  image: cloudslash/scan:latest
  before_script:
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  script:
    - cloudslash scan --config cloudslash.yml --auth-type ssh

Using CI Job Variables

Environment-specific configurations:

stages:
  - test
  - production-scan

dev-scan:
  stage: test
  variables:
    CLOUDSLASH_MODE: pr
    CLOUDSLASH_ACCOUNTS: development-accounts
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config .gitlab-ci/config.yml

prod-scan:
  stage: production-scan
  variables:
    CLOUDSLASH_MODE: deployment-gate
    CLOUDSLASH_ACCOUNTS: production-accounts
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config .gitlab-ci/config.yml
  when: manual

Notification Integration

Slack Integration

slack-notification:
  image: python:3.9
  before_script:
    - pip install requests
  script:
    - >
      python3 << 'EOF'
      import requests
      import json
      import os

      with open('cloudslash-report.json') as f:
          report = json.load(f)

      webhook_url = os.getenv('SLACK_WEBHOOK_URL')
      if not webhook_url:
          print("No webhook URL configured")
          exit(0)

      message = {
          "blocks": [
              {
                  "type": "header",
                  "text": {
                      "type": "plain_text",
                      "text": f"📊 CloudSlash Scan: {report['summary']['status']}"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": f"*Result:*\n{report['summary']['status']}"
                      },
                      {
                          "type": "mrkdwn",
                          "text": f"*Findings:*\n{report['summary']['findings']}"
                      },
                      {
                          "type": "mrkdwn",
                          "text": f"*Duration:*\n{report['summary']['duration']}s"
                      }
                  ]
              }
          ]
      }

      response = requests.post(webhook_url, json=message)
      print(f"Slack notification sent: {response.status_code}")
      EOF
  dependencies:
    - cloudslash-scan

Email Notifications

email-report:
  image: composer:latest
  script:
    - composer install
    - php send_email.php --report cloudslash-report.html
  after_script:
    - cat send_email.log
  allow_failure: true

send_email.php:

<?php
require 'vendor/autoload.php';

$email = $_SERVER['CI_REGISTRY_USER'] . '@yourcompany.com';
$subject = 'CloudSlash Report';
$body = file_get_contents('cloudslash-report.html');

// Send email (using SwiftMailer or PHPMailer)

GitLab Merge Request Comments

merge-request-comment:
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config cloudslash.yml --mode pr
  artifacts:
    reports:
      cloudslash: pr-report.json
  after_script:
    - >
      if [ -f "pr-report.json" ]; then
        TOKEN="${CI_JOB_TOKEN}"
        PROJECT_ID="${CI_PROJECT_ID}"
        MR_IID="${CI_MERGE_REQUEST_IID}"
        URL="https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests/${MR_IID}/notes"

        # Read the comment file
        COMMENT_FILE="pr-comment.md"
        COMMENT=$(cat "$COMMENT_FILE")

        # POST to MR
        RESPONSE=$(curl --header "PRIVATE-TOKEN: $TOKEN" \
          --header "Content-Type: application/json" \
          --data '{"body": "'"${COMMENT}"'"}' \
          "$URL")

        echo "MR comment posted: $RESPONSE"
      fi

Troubleshooting

Common Issues

Issue 1: Permissions Denied

Symptom: "Permission denied: /path/to/report"

Solution:

# Fix permission issues
infra-scan:
  before_script:
    - chmod +x cloudslash
    - chmod -R 755 .
  script:
    - cloudslash scan --config cloudslash.yml

Issue 2: SSH Auth Errors

Symptom: "SSH authentication failed"

Solution:

before_script:
  - eval $(ssh-agent -s)
  - ssh-add -l
  - git config --global user.name "GitLab Runner"
  - git config --global user.email "runner@gitlab.com"

Issue 3: Image Not Pulling

Symptom: "Error: image cloudslash/scan:latest not found"

Solution:

image: cloudslash/scan:latest
# OR use dockerfile.yml for custom image

Issue 4: caching fails

Symptom: "Cannot compile/play with this language version"

Solution:

service-docker:
  image: docker:dind
  services:
    - docker:dind
  script:
    - docker pull cloudslash/scan:latest

NOTE: Linux image caching may have different behavior than macOS. Verify results are hash-consistent and stable across providers.

Issue 5: Swiftpackage Runner Missing

Symptom: No swift's CI integration or Swift installed, preventing swift-specific sample code from running. Use macOS runners or configure s.version for consumers, or skip or remove sample Swift workflows.

Workaround in docs: Document that macOS runners required, pass s.version if using swift Example from CI/CD: https://gitlab.com/gl_versions/samples/-/blob/main/.swiftlint_omitted.yml

Feature Comparison Matrix

GitLab CI vs Other Platforms

Feature GitHub Actions GitLab CI Azure DevOps CircleCI Jenkins
Setup Time (min) ⚡ 2-3 ⚡ 2-3 ⭐ 3-5 ⚡ 2-4 ⭐ 5-10
PR/MR Comments ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
Free Plan ⭐⭐⭐⭐⭐ (unlimited) ⭐⭐⭐⭐⭐ (unlimited) ⭐⭐⭐⭐ (limited) ⭐⭐⭐ (limited) ⭐⭐ (limited)
Scheduled Jobs ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Monorepo Support ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Multi-Cloud Support ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Docker Execution ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
Custom Actions ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
Cost Build Minutes ⚡ Free (public) ⚡ Free (public) ⭐ Free (small) ⭐ Limited ⚡ Free (self-hosted)
Integrations ⭐⭐⭐⭐⭐ (AWS, Azure, GCP) ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

Best Overall: GitLab CI (First-class PR/MR support, unlimited free pipelines)


Click for detailed feature descriptions ### GitLab CI Strengths - ✅ **Native PR/MR integration**: Seamless integration with GitLab UI - ✅ **Variable management**: Built-in variable masking and group-level secrets - ✅ **MR comments**: Automatic merge request comments on scan failure - ✅ **Protected branches**: Built-in protection and approval workflows - ✅ **Free for public and small projects**: No build minute limits ### GitLab CI Weaknesses - ❌ ❌ Limited Docker-in-Docker (DIN/DinD) support compared to GitHub Actions - ❌ Matrix jobs configuration can be verbose - ❌ No self-hosted runners for free (requires premium) - ❌ Stage dependency ordering must be careful (fail-silence risk)

Common Patterns

Pattern 1: Conditional Scan Execution

Run scans based on changes:

dev-scan:
  stage: test
  script:
    - cloudslash scan --config cloudslash.yml
  rules:
    # Only run when config files change
    - changes:
        - **/*config*.yaml
        - **/*config*.yml
        - infra/*
      when: always

# OR shallow check for changed resources
check-changed-resources:
  stage: test
  script:
    - git diff --name-only main > changes.txt
    - cloudslash scan --config cloudslash.yml --changed-file changes.txt
  when: manual

Pattern 2: Scan with Different Rules Per Branch

Different rules for main vs dev:

# For main/production branches
prod-scan:
  stage: security
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config config.yml --mode deployment-gate
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always
  only:
    - main

# For dev branches
dev-scan:
  stage: test
  image: cloudslash/scan:latest
  script:
    - cloudslash scan --config config.yml --mode pr
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop" || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "develop"'
      when: always
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: never

Pattern 3: Containerized Scanner

Build custom Docker image with tools:

build-scanner:
  stage: build
  image: docker:19
  services:
    - docker:dind
  script:
    - docker build -t cloudslash-scanner:latest .
  artifacts:
    paths:
      - cloudslash-scanner:latest

infra-scan:
  stage: test
  image: cloudslash-scanner:latest
  script:
    - cloudslash scan --config cloudslash.yml

Best Practices

1. Use Artifacts for Reports

Store scan results for downstream jobs:

infra-scan:
  script:
    - cloudslash scan --config cloudslash.yml
  artifacts:
    reports:
      cloudslash: report.json
    paths:
      - report.json
      - report.html
    expire_in: 30 days

2. Cache Dependencies

Cache CloudSlash and dependencies:

infra-scan:
  image: cloudslash/scan:latest
  before_script:
    - export CLOUDSLASH_CONFIG=$PWD/cloudslash.yml
    - export CACHED_ACCESS_TOKEN_PARTS=cloudslash-shared/lib/dep-cache
    - |
      # Build cache keys from config and deps
      CONFIG_HASH=$(md5sum cloudslash.yml | cut -d' ' -f1)
      DEP_HASH=$(md5sum -r cloudslash-shared/lib/dep-cache | md5sum | cut -d' ' -f1)
      CACHE_KEY="cloudslash:$CONFIG_HASH:$DEP_HASH"
    - |
      docker ps -q
      docker pull ci:cloudslash:$CONFIG_HASH:$DEP_HASH || docker build -t ci:cloudslash:$CONFIG_HASH:$DEP_HASH .
    - CI_RUNTIME=$CI_RUNTIME
    - CLOUDSLASH_SDK_RUNTIME=$CLOUDSLASH_SDK_RUNTIME

Note: Ensure hashes are stable and reproduceable. Test manually in .gitlab-ci.yml to confirm.

3. Use Tags for Runner Selection

Run scans on appropriate runners:

infra-scan:
  tags:
    - docker
    - large
  image: cloudslash/scan:latest

FAQ

Can I use CloudSlash on forks? Yes, fork branches can use the variable environment passed by downstream CI, but the variable is not the same as the CI environment variable; the downstream CI is already set up for fork usage. If set, it is the same var as the runner env. When running on a forker-provided runner, the path to used cloudslash-runner (US/Linux) cross-platform images is /; fork uses that target; use the default path.
How do I set up scheduled scans? Use the CI scheduler:
scheduled-job:
  stage: maintenance
  script:
    - cloudslash scan --config cloudslash.yml
  timezone: UTC
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: always
Go to: CI/CD → Schedules → New schedule.
Can I cache the scanner container? Yes, use the cached image for consistent scans:
infra-scan:
  image: cloudslash/scan:latest
  script:
    - |
      if docker inspect cloudslash-scanner:latest >/dev/null 2>&1; then
        docker pull cloudslash-scanner:latest
      else
        docker build -t cloudslash-scanner:latest .
      fi
    - cloudslash scan --config cloudslash.yml

Documentation Version: 2.0.0 Last Updated: September 10, 2026 Related: CI/CD Integration Guide | Best Practices