Azure DevOps Integration Guide¶
Integrate CloudSlash scanning into Azure Pipelines with automated quality gates and tabular scan summaries.
Quick Start (5-Minute Setup)¶
Prerequisites¶
- Azure DevOps Organization (formerly Visual Studio Team Services)
- Project with Pipeline permissions
- CloudSlash API key (from dashboard)
- (Optional) Azure Service Connection
Step 1: Install CloudSlash Task¶
Add CloudStencil to your azure-pipelines.yml:
# azure-pipelines.yml
trigger:
- main
stages:
- stage: Infrastructure_Scan
displayName: Infrastructure Security & Cost Scan
jobs:
- job: CloudSlash_Scan
displayName: Run Security Scan
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'CloudSlash Infrastructure Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
mode: pr
- task: PublishBuildArtifacts@1
displayName: 'Publish Scan Report'
inputs:
PathtoPublish: 'cloudslash-output'
ArtifactName: 'cloudslash-report'
publishLocation: 'Container'
- task: PublishPipelineSummary@1
displayName: 'Add Pipeline Summary'
condition: always()
inputs:
summaryText: |
## Infrastructure Scan Results
**Status**: ${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}
**Findings**: ${{ steps.CloudSlash_Scan.outputs['findings-count'] }}
selector: tail
Step 2: Add Variables¶
Go to your pipeline → Edit → Variables:
- Add
CLOUDSLASH_API_KEYwith your CloudSlash API key
Note: Variables are per-project only; they aren't available at the org level.
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: "$(SLACK_WEBHOOK)"
Done! Your first scan will run on the next pipeline run.
Full Configuration Reference¶
Common Workflow Patterns¶
Pattern 1: PR/MR Cost Scanning¶
Prevent costly changes before merging:
# azure-pipelines.yml
trigger:
- main
variables:
agentImage: ubuntu-latest
stages:
- stage: Cost_Analysis
displayName: Cost Analysis for PR
jobs:
- deployment:
environment: Staging
name: Cost_Analysis_Job
pool:
vmImage: $(agentImage)
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'Cost Analysis Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
mode: pr
- task: PublishPipelineSummary@1
displayName: 'Add Cost Analysis Summary'
condition: always()
inputs:
summaryText: |
## Cost Analysis Report
**Status**: ✅ PASSED
**Repository**: $(Build.Repository.Name)
**Branch**: $(Build.SourceBranchName)
**Changed Files**: $(ChangedFiles)
selector: tail
Pattern 2: Scheduled Drift Detection¶
Monitor production infrastructure changes continuously:
# azure-pipelines.yml
trigger:
- none # Don't trigger on PR/commit
schedules:
- cron: '0 2 * * *' # Daily at 2 AM
displayName: 'Daily Drift Detection'
branches:
include:
- main
- staging
always: true
stages:
- stage: Drift_Detection
displayName: Infrastructure Drift Detection
jobs:
- deployment:
environment: Production
name: Drift_Detection_Job
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'Drift Detection Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
mode: scheduled
scan-id: $(Build.BuildId)
- task: PublishPipelineSummary@1
displayName: 'Add Drift Detection Summary'
condition: always()
inputs:
summaryText: |
## Infrastructure Drift Detection
**Schedule**: Daily at 2 AM UTC
**Status**: ${{ steps.Clamp.outputs['nodrift'] }}
**Repository**: $(Build.Repository.Name)
selector: tail
Pattern 3: Deployment Gates¶
Block deployments that don't meet security requirements:
# azure-pipelines.yml
trigger:
- main
stages:
- stage: Security_Check
displayName: Security Gate Check
jobs:
- deployment:
environment: Production
name: Security_Gate_Job
pool:
vmImage: ubuntu-latest
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'Security Gate Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
mode: deployment-gate
fail-on-warnings: true
fail-on-errors: true
- task: Bash@3
displayName: 'Verify Security Check'
inputs:
targetType: inline
script: |
if [ "${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}" != "true" ]; then
echo "❌ Security Gate Failed"
exit 1
fi
echo "✅ Security Gate Passed"
- task: PublishPipelineSummary@1
displayName: 'Add Security Gate Summary'
condition: always()
inputs:
summaryText: |
## Deployment Security Gate
**Environment**: Production
**Status**: ${{ steps.CloudSlash_Scan.outputs['scan-status'] }}
selector: tail
Pattern 4: Monorepo Setup¶
Centralized scanning for multiple repositories:
# azure-pipelines.yml (root monorepo)
trigger:
- main
stages:
- stage: Infrastructure
displayName: Infrastructure Scan for All Subrepos
jobs:
- job: Monorepo_Scan
displayName: Scan All Subrepos
pool:
vmImage: 'windows-latest' # Works cross-platform often, but safer on windows for multi-repo
strategy:
matrix:
frontend:
subrepo: 'frontend'
backend:
subrepo: 'backend'
infra:
subrepo: 'infrastructure'
data:
subrepo: 'data-pipeline'
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'CloudSlash Scan - $(subrepo)'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
- task: PublishBuildArtifacts@1
displayName: 'Publish $(subrepo) Results'
inputs:
PathtoPublish: 'cloudslash-output'
ArtifactName: 'cloudslash-$(subrepo)-report'
publishLocation: 'Container'
- stage: Aggregate_Results
displayName: Aggregate All Subrepo Results
dependsOn: Infrastructure
jobs:
- job: Aggregate
displayName: Aggregate Reports
pool:
vmImage: 'ubuntu-latest'
steps:
- download: current
artifact: cloudslash-frontend-report
- download: current
artifact: cloudslash-backend-report
- download: current
artifact: cloudslash-infra-report
- download: current
artifact: cloudslash-data-report
- task: PowerShell@2
displayName: 'Aggregate Reports'
inputs:
targetType: inline
script: |
$frontend = Get-Content "$(Pipeline.Workspace)/cloudslash-frontend-report/*.json" | Out-String
$backend = Get-Content "$(Pipeline.Workspace)/cloudslash-backend-report/*.json" | Out-String
Write-Host "Frontend Findings: $frontend"
Write-Host "Backend Findings: $backend"
Pattern 5: Multi-Cloud Setup¶
Unified scanning across AWS, Azure, and GCP:
# azure-pipelines.yml
trigger:
- main
stages:
- stage: AWS_Scan
displayName: AWS Infrastructure Scan
jobs:
- deployment:
environment: Staging
name: AWS_Scan_Job
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'AWS Infrastructure Scan'
inputs:
api-key: $(AWS_CLOUDSLASH_API_KEY)
config: aws-cloudslash.yml
- stage: Azure_Scan
displayName: Azure Infrastructure Scan
dependsOn: AWS_Scan
jobs:
- deployment:
environment: Staging
name: Azure_Scan_Job
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'Azure Infrastructure Scan'
inputs:
api-key: $(AZURE_CLOUDSLASH_API_KEY)
config: azure-cloudslash.yml
- stage: GCP_Scan
displayName: GCP Infrastructure Scan
dependsOn: Azure_Scan
jobs:
- deployment:
environment: Staging
name: GCP_Scan_Job
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
- task: CloudSlash@1
displayName: 'GCP Infrastructure Scan'
inputs:
api-key: $(GCP_CLOUDSLASH_API_KEY)
config: gcp-cloudslash.yml
- stage: MultiCloud_Aggregation
displayName: Aggregate Multi-Cloud Results
dependsOn: [AWS_Scan, Azure_Scan, GCP_Scan]
jobs:
- deployment:
environment: Production
name: Aggregate_Job
pool:
vmImage: 'ubuntu-latest'
steps:
- download: current
artifact: AWS_Artifact
- download: current
artifact: Azure_Artifact
- download: current
artifact: GCP_Artifact
- task: PowerShell@2
displayName: 'Aggregate Multi-Cloud Reports'
inputs:
targetType: inline
script: |
Write-Host "✅ Multi-cloud scan completed successfully for $(Build.Repository.Name)"
# aws-cloudslash.yml
version: 1.0
accounts:
- provider: aws
name: production
accounts:
- id: "123456789012"
# azure-cloudslash.yml
version: 1.0
accounts:
- provider: azure
name: production
subscriptions:
- id: "11111111-1111-1111-1111-111111111111"
# gcp-cloudslash.yml
version: 1.0
accounts:
- provider: gcp
name: production
projects:
- id: "my-project-123"
Security Best Practices¶
1. Use Service Connections¶
For service-to-service authentication:
- task: AzureCLI@2
displayName: 'Connect to Azure'
inputs:
azureSubscription: 'My-Service-Connection'
2. Restrict Pipeline Permissions¶
Limit who can run pipelines:
# In Azure DevOps project settings
- Lock pipelines or set up restriction (available in resource lock or otherwise)
- Use approvers for certain environments
3. Rate Limit Protection¶
Prevent API rate limit issues:
- task: AzurePowerShell@5
displayName: 'Check Rate Limits'
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
Inline: |
# Check API rate limits
$consumerName = 'cloudslash-scan'
Get-AzRateLimitInformation -MaxCapacity -ConsumerName $consumerName | Out-String
4. Validate Inputs¶
Prevent injection attacks:
- task: PowerShell@2
displayName: 'Validate Environment'
inputs:
targetType: inline
script: |
$allowedEnvironments = @('dev', 'staging', 'prod')
if ($env:ENVIRONMENT -notin $allowedEnvironments) {
Write-Error "Invalid environment: $env:ENVIRONMENT"
exit 1
}
5. Use Conditional Steps¶
Only run scans on specific triggers:
- task: CloudSlash@1
displayName: 'Run Scan'
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/heads/main'))
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
Multi-Account Setup¶
Using Assumed Roles for AWS¶
- task: CloudSlash@1
displayName: 'AWS Cross-Account Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
assume-role-arn: "arn:aws:sts::123456789012:assumed-role/CloudSlashRole/scan-session"
Using RBAC for Azure¶
- task: AzurePowerShell@5
displayName: 'Azure RBAC Scan'
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
Inline: |
# Configure RBAC permissions
Set-AzRoleAssignment -ObjectId $(CloudSlashPrincipalObjectID) -RoleDefinitionName 'Contributor' -Scope /subscriptions/$(SubscriptionID)
Using Organization-wide Scanning¶
# Offload or sync scanning jobs to a scan runner (optional)
trigger:
- main
stages:
- stage: SSA_SYNC
displayName: Sync Scan to Scan Runner
jobs:
- deployment:
environment: ScanRunner
name: Deploy_Scan_Job
pool:
vmImage: windows-latest
steps:
- checkout: self
- task: AzurePowerShell@5
displayName: 'Deploy to Scan Runner'
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
Inline: |
... # Deploy scanning job to runner
- stage: SSA_OFFLOAD
displayName: Offload Scanning to Runner
dependsOn: SSA_SYNC
jobs:
- deployment:
environment: ScanRunner
name: Send_Scan_Request
pool:
vmImage: windows-latest
steps:
- task: AzurePowerShell@5
displayName: 'Queue Scan Job'
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
Inline: |
... # Send scan request to runner
Notification Integration¶
Slack Integration¶
- task: PowerShell@2
displayName: 'Send Slack Notification'
condition: always()
inputs:
targetType: inline
script: |
$webhook = "$(SLACK_WEBHOOK)"
$status = "${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}"
$body = @{
blocks = @(
@{
type = "section"
text = @{
type = "mrkdwn"
text = "### Infrastructure Scan"
}
},
@{
type = "section"
fields = @(
@{
type = "mrkdwn"
text = "**Status**:`$status`"
}
)
},
@{
type = "section"
fields = @(
@{
type = "mrkdwn"
text = "**Status**:`$status`"
}
)
}
)
}
Invoke-RestMethod -Uri $webhook -Method Post -Body ($body | ConvertTo-Json) -ContentType "application/json"
Email Notifications¶
- task: PowerShell@2
displayName: 'Send Email Report'
condition: always()
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
Inline: |
$reportPath = "$(Pipeline.Workspace)/cloudslash-report/cost-analysis.html"
$subject = "Infrastructure Scan Report - $(Build.BuildId)"
$body = Get-Content $reportPath | Out-String
Send-AzNotification -ServicePrincipalId "$(EmailServicePrincipalId)" -CertificateThumbprint "$(CertificateThumbprint)" -TenantId "$(TenantId)" -Subject $subject -Body $body
Webhook Integration¶
- task: PowerShell@2
displayName: 'Send Webhook Notification'
condition: always()
inputs:
targetType: inline
script: |
$webhookUrl = "$(CUSTOM_WEBHOOK_URL)"
$status = "${{ steps.CloudSlash_Scan.outputs['scan-passed'] }}"
$body = @{
pipelineId = "$(Build.BuildId)"
status = $status
timestamp = (Get-Date).ToUniversalTime()
repository = "$(Build.Repository.Name)"
} | ConvertTo-Json
Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $body -ContentType "application/json"
Troubleshooting¶
Common Issues¶
Issue 1: "api-key is missing"¶
Symptom: CloudSlash task fails with authentication error
Solution:
- task: CloudSlash@1
displayName: 'CloudSlash Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY) # Variable must exist in project
Check: Verify variable exists in Project → Process → Variables Check: Variable was saved Check: Variable is not empty Check: Variable has unclosed double quotes causing yaml to fail
Issue 2: Scan tasks fail with "not found"¶
Symptom: "The task 'CloudSlash' was not found"
Solution:
- task: CloudSlash@1 # Ensure correct version format and presence
displayName: 'CloudSlash Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
# If missing, add library search path
# Deploy cloudslash-task to group/library via nuget or deploy azure devops tasks library
Issue 3: Pipeline permissions¶
Symptom: "Unauthorized - Access denied"
Solution:
# Verify user has access in azure devops project
# Grant Contributor role for pipeline operation
# Verify VSTS_ACCESS_TOKEN exists or use personal access token or service connection
Issue 4: Auth token issues¶
Symptom: "Invalid credentials" when using assumed-role-arn
Solution:
- task: AzurePowerShell@5
displayName: 'Configure Assumed Role'
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
Inline: |
$roleArn = "arn:aws:sts::123456789012:assumed-role/CloudSlashRole/$(New-GUID)"
$stsResult = Invoke-AzRoleAssignment -RoleArn $roleArn -Duration Hours 24
$assumedRoleArn = $stsResult.RoleArn
Write-Host "##vso[task.setvariable variable=ASSUMED_ROLE_ARN]$assumedRoleArn"
Issue 5: Repository references¶
Symptom: Error referencing repository or branch
Solution:
# use $(Build.Repository.Name), $(Build.SourceBranchName), $(Build.BuildId)
# Use default trigger variables instead of local attempts
Feature Comparison Matrix¶
Azure DevOps 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: N/A (Tool-dependent; use based on existing Azure presence; for additional cross-platform options see GitHub Actions, GitLab CI, CircleCI, Jenkins)
Click for detailed feature descriptions
### Azure DevOps Strengths - ✅ **Enterprise-focused**: Rich enterprise features - ✅ **Azure-native**: Seamless integration with Azure services - ✅ **Service Connections**: Built-in authentication for Azure resources - ✅ **Approval Gates**: Built-in deployment approvals for production ### Azure DevOps Weaknesses - ❌ Markdown limitations: Markdown parsing is limited, formatting up to 2048 literals cannot be split - ❌ Matrix-type multiplexing not ideal: Matrix jobs can be verbose but complete (used for usage in monorepos) - ❌ Self-hosted runners: Use of self-hosted runners (e.g., Windows for Docker) not free - ❌ Privacy controls: Restrict pipeline loading in external storage not improved - ❌ Join-like Artifact download not available: Cross-jobs artifact download supported only within same stage (no cross-stage)Common Patterns¶
Pattern 1: Conditional Scanning¶
Run scans based on changes:
- deployment:
environment: Staging
name: Conditional_Scan
pool:
vmImage: ubuntu-latest
steps:
- task: PowerShell@2
displayName: 'Check for Changes'
inputs:
targetType: inline
script: |
$changedFiles = git diff --name-only main
if ($changedFiles.Count -gt 0) {
Write-Host "##vso[task.setvariable variable=SCAN_REQUIRED]true"
}
- task: CloudSlash@1
displayName: 'CloudSlash Scan'
condition: eq(variables.SCAN_REQUIRED, 'true')
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
Pattern 2: Tag-based Scanning¶
Run scans only on tags:
trigger:
- none # Don't trigger on other branches
schedules:
# None, use only tags for optional schedule
# Tag-based triggers can integrate with git tags and Azure DevOps tag handlers/runners
stages:
- stage: Tag_Scan
displayName: Tag-Based Scan
condition: startsWith(dependencies.Build.resources.buildSourceVersion, 'refs/tags/v')
jobs:
- deployment:
environment: Production
name: Tag_Scan_Job
pool:
vmImage: ubuntu-latest
steps:
- task: CloudSlash@1
displayName: 'Run Tag Scan'
inputs:
api-key: $(CLOUDSLASH_API_KEY)
config: cloudslash.yml
Pattern 3: Environment-specific Rules¶
Different rules based on environment:
# Self-hosted runner for custom scan tool locations
stages:
- stage: Infrastructure
displayName: Infrastructure
jobs:
- deployment:
environment: Production
name: Scan
pool:
vmImage: ubuntu-latest
strategy:
deploy:
steps:
# Proxy or offload scanning via self-hosted runner
- task: AzurePowerShell@5
displayName: 'Offload to Self-Hosted'
inputs:
azureSubscription: 'My-Service-Connection'
ScriptType: InlineScript
script: |
... # script sends scan request to self-hosted runner
FAQ¶
Are variables available in all stages?
No: Variables are per-project only and not thread-safe; they are cleared between stages (job/trigger overrides can cause invalid derivations). Use pool.vmImage='windows-latest' and rebuild artifacts accordingly.Can I use self-hosted runners?
Yes, use $(agentpool): azure-self-hosted-pool.How do I set up environment-specific variables?
Use Variable Groups in Azure DevOps: Project → Pipelines → Library → Variable Groups: - Add `dev`, `staging`, `prod` to a group with prefixed variables (e.g., `dev.CLOUDSLASH_API_KEY`, `prod.CLOUDSLASH_API_KEY`) - Set with group: $(dev) or $(prod) in pipeline prior to deployment. This avoids the previous limitation (two deploys, ensures selectivity).Can I deploy pipelines to other orgs?
No: Pipelines are not deployable to other orgs or CSAs.Documentation Version: 2.0.0 Last Updated: September 10, 2026 Related: CI/CD Integration Guide | Best Practices