CI/CD con Azure DevOps: La Guía Completa para Automatizar tus Despliegues
Azure DevOps se ha consolidado como una de las plataformas más robustas para implementar CI/CD (Continuous Integration/Continuous Deployment) en entornos empresariales. Esta guía completa te permitirá dominar desde los conceptos básicos hasta las implementaciones más avanzadas, con ejemplos prácticos y configuraciones reales de producción.
Fundamentos de CI/CD en Azure DevOps
¿Qué es Azure DevOps?
Azure DevOps es una suite completa de servicios de desarrollo que incluye:
- Azure Repos: Control de versiones con Git y TFVC
- Azure Pipelines: Automatización de CI/CD multiplataforma
- Azure Boards: Gestión de proyectos y trabajo
- Azure Test Plans: Herramientas de testing manual y exploratorio
- Azure Artifacts: Gestión de paquetes y feeds
Componentes Clave de Azure Pipelines
Build Pipelines (Integración Continua)
- Compilación automática del código fuente
- Ejecución de pruebas automatizadas
- Análisis de calidad de código
- Generación de artefactos
Release Pipelines (Entrega Continua)
- Despliegue automatizado a múltiples entornos
- Estrategias de deployment avanzadas
- Gestión de aprobaciones y gates
- Rollback automático
Configuración Inicial del Proyecto
Creación de la Organización y Proyecto
# Crear proyecto desde Azure CLI
az extension add --name azure-devops
az devops configure --defaults organization=https://dev.azure.com/tu-organizacion
az devops project create --name "MiProyectoCI" --description "Proyecto CI/CD" --visibility private
Configuración del Repositorio
# Conectar repositorio local con Azure Repos
git remote add azure https://tu-organizacion@dev.azure.com/tu-organizacion/MiProyectoCI/_git/MiProyectoCI
git push -u azure main
Estructura de Proyecto Recomendada
Build Pipelines: Configuración Avanzada
Pipeline YAML Completo para Aplicación .NET
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
- release/*
paths:
exclude:
- docs/*
- README.md
variables:
buildConfiguration: 'Release'
vmImageName: 'windows-latest'
solution: '**/*.sln'
buildPlatform: 'Any CPU'
pool:
vmImage: $(vmImageName)
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: Build
displayName: 'Build job'
steps:
- task: NuGetToolInstaller@1
displayName: 'Install NuGet'
- task: NuGetCommand@2
displayName: 'Restore NuGet packages'
inputs:
restoreSolution: '$(solution)'
- task: SonarCloudPrepare@1
displayName: 'Prepare SonarCloud analysis'
inputs:
SonarCloud: 'SonarCloud'
organization: 'tu-organizacion'
scannerMode: 'MSBuild'
projectKey: 'tu-proyecto-key'
- task: VSBuild@1
displayName: 'Build solution'
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployDefaultTarget=WebPublish /p:PublishUrl="$(build.artifactStagingDirectory)\\"'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
- task: VSTest@2
displayName: 'Run Unit Tests'
inputs:
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'
testSelector: 'testAssemblies'
testAssemblyVer2: |
**\*test*.dll
**\*tests*.dll
!**\*testadapter.dll
!**\obj\**
codeCoverageEnabled: true
testRunTitle: 'Unit Tests'
- task: SonarCloudAnalyze@1
displayName: 'Run SonarCloud analysis'
- task: SonarCloudPublish@1
displayName: 'Publish SonarCloud results'
inputs:
pollingTimeoutSec: '300'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
failTaskOnFailedTests: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- task: PublishBuildArtifacts@1
displayName: 'Publish build artifacts'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
publishLocation: 'Container'
Pipeline para Aplicaciones Node.js
# azure-pipelines-node.yml
trigger:
branches:
include:
- main
- develop
variables:
nodeVersion: '18.x'
npmCache: $(Pipeline.Workspace)/.npm
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: Build
displayName: 'Build Node.js app'
steps:
- task: NodeTool@0
displayName: 'Install Node.js $(nodeVersion)'
inputs:
versionSpec: '$(nodeVersion)'
- task: Cache@2
displayName: 'Cache npm dependencies'
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: |
npm | "$(Agent.OS)"
path: $(npmCache)
- script: |
npm config set cache $(npmCache)
npm ci
displayName: 'Install dependencies'
- script: |
npm run lint
displayName: 'Run ESLint'
- script: |
npm run test:coverage
displayName: 'Run tests with coverage'
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results.xml'
failTaskOnFailedTests: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: 'coverage/cobertura-coverage.xml'
- script: |
npm run build
displayName: 'Build application'
- task: ArchiveFiles@2
displayName: 'Archive build artifacts'
inputs:
rootFolderOrFile: 'dist'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/app.zip'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifacts'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'node-app'
Pipeline para Contenedores Docker
# azure-pipelines-docker.yml
trigger:
branches:
include:
- main
variables:
dockerRegistryServiceConnection: 'myACRConnection'
imageRepository: 'myapp'
containerRegistry: 'myregistry.azurecr.io'
dockerfilePath: '**/Dockerfile'
tag: '$(Build.BuildId)'
vmImageName: 'ubuntu-latest'
pool:
vmImage: $(vmImageName)
stages:
- stage: Build
displayName: 'Build and Push'
jobs:
- job: Build
displayName: 'Build Docker Image'
steps:
- task: Docker@2
displayName: 'Build and push image'
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
latest
- task: CmdLine@2
displayName: 'Run security scan'
inputs:
script: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd):/root/.cache/ \
aquasec/trivy image $(containerRegistry)/$(imageRepository):$(tag)
- task: PublishBuildArtifacts@1
displayName: 'Publish Kubernetes manifests'
inputs:
PathtoPublish: 'k8s'
ArtifactName: 'manifests'
Release Pipelines: Estrategias de Despliegue
Release Pipeline Clásico
Configuración de Entornos Múltiples
# Configuración de Release Pipeline (via UI o REST API)
{
"environments": [
{
"name": "Development",
"deployPhases": [
{
"deploymentInput": {
"skipArtifactsDownload": false,
"artifactsDownloadInput": {},
"queueId": 1,
"demands": [],
"enableAccessToken": false,
"timeoutInMinutes": 0
},
"workflowTasks": [
{
"taskId": "497d490f-eea7-4f2b-ab94-48d9c1acdcb1",
"version": "2.*",
"name": "Azure App Service Deploy",
"inputs": {
"azureSubscription": "$(azureSubscription)",
"appType": "webApp",
"WebAppName": "myapp-dev"
}
}
]
}
],
"preDeploymentGates": {
"id": 0,
"gatesOptions": null,
"gates": []
},
"postDeploymentGates": {
"id": 0,
"gatesOptions": null,
"gates": []
}
}
]
}
YAML-Based Release Pipeline
# azure-pipelines-release.yml
trigger: none # Solo manual o por completion de build
resources:
pipelines:
- pipeline: buildPipeline
source: 'CI-Pipeline'
trigger:
branches:
include:
- main
variables:
- group: 'Production-Variables'
- name: azureSubscription
value: 'MyAzureSubscription'
stages:
- stage: Development
displayName: 'Deploy to Development'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs:
- deployment: DeployToDev
displayName: 'Deploy to Dev Environment'
environment: 'Development'
strategy:
runOnce:
deploy:
steps:
- download: buildPipeline
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to Azure Web App'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webApp'
appName: 'myapp-dev'
package: '$(Pipeline.Workspace)/buildPipeline/drop/WebApp.zip'
- stage: Staging
displayName: 'Deploy to Staging'
dependsOn: Development
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToStaging
displayName: 'Deploy to Staging Environment'
environment: 'Staging'
strategy:
runOnce:
deploy:
steps:
- download: buildPipeline
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to Staging'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webApp'
appName: 'myapp-staging'
package: '$(Pipeline.Workspace)/buildPipeline/drop/WebApp.zip'
- task: CmdLine@2
displayName: 'Run smoke tests'
inputs:
script: |
curl -f https://myapp-staging.azurewebsites.net/health || exit 1
- stage: Production
displayName: 'Deploy to Production'
dependsOn: Staging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProduction
displayName: 'Deploy to Production'
environment: 'Production'
strategy:
canary:
increments: [10, 25, 50, 100]
deploy:
steps:
- download: buildPipeline
artifact: drop
- task: AzureWebApp@1
displayName: 'Deploy to Production Slot'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webApp'
appName: 'myapp-prod'
deployToSlotOrASE: true
resourceGroupName: 'myapp-rg'
slotName: 'canary'
package: '$(Pipeline.Workspace)/buildPipeline/drop/WebApp.zip'
- task: AzureCLI@2
displayName: 'Route traffic to canary'
inputs:
azureSubscription: '$(azureSubscription)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az webapp traffic-routing set \
--name myapp-prod \
--resource-group myapp-rg \
--distribution canary=$(strategy.increment)
on:
success:
steps:
- task: AzureCLI@2
displayName: 'Complete deployment'
inputs:
azureSubscription: '$(azureSubscription)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
if [ "$(strategy.increment)" == "100" ]; then
az webapp deployment slot swap \
--name myapp-prod \
--resource-group myapp-rg \
--slot canary \
--target-slot production
fi
failure:
steps:
- task: AzureCLI@2
displayName: 'Rollback deployment'
inputs:
azureSubscription: '$(azureSubscription)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az webapp traffic-routing clear \
--name myapp-prod \
--resource-group myapp-rg
Estrategias de Despliegue Avanzadas
Blue-Green Deployment
# blue-green-deployment.yml
stages:
- stage: BlueGreenDeploy
displayName: 'Blue-Green Deployment'
jobs:
- deployment: BlueGreen
displayName: 'Blue-Green Deployment'
environment: 'Production'
strategy:
runOnce:
deploy:
steps:
- task: PowerShell@2
displayName: 'Determine current slot'
inputs:
targetType: 'inline'
script: |
$currentSlot = az webapp show --name myapp-prod --resource-group myapp-rg --query "tags.currentSlot" -o tsv
if ($currentSlot -eq "blue") {
$targetSlot = "green"
} else {
$targetSlot = "blue"
}
Write-Host "##vso[task.setvariable variable=targetSlot]$targetSlot"
Write-Host "Deploying to slot: $targetSlot"
- task: AzureWebApp@1
displayName: 'Deploy to target slot'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webApp'
appName: 'myapp-prod'
deployToSlotOrASE: true
resourceGroupName: 'myapp-rg'
slotName: '$(targetSlot)'
package: '$(Pipeline.Workspace)/drop/WebApp.zip'
- task: PowerShell@2
displayName: 'Health check and swap'
inputs:
targetType: 'inline'
script: |
$slotUrl = "https://myapp-prod-$(targetSlot).azurewebsites.net/health"
$maxRetries = 10
$retryCount = 0
do {
try {
$response = Invoke-WebRequest -Uri $slotUrl -TimeoutSec 30
if ($response.StatusCode -eq 200) {
Write-Host "Health check passed"
# Swap slots
az webapp deployment slot swap --name myapp-prod --resource-group myapp-rg --slot $(targetSlot) --target-slot production
# Update tag
az webapp update --name myapp-prod --resource-group myapp-rg --set tags.currentSlot=$(targetSlot)
break
}
} catch {
Write-Host "Health check failed, retry $($retryCount + 1)/$maxRetries"
Start-Sleep -Seconds 30
}
$retryCount++
} while ($retryCount -lt $maxRetries)
if ($retryCount -eq $maxRetries) {
Write-Error "Deployment failed health checks"
exit 1
}
Rolling Updates para Kubernetes
# k8s-rolling-update.yml
stages:
- stage: KubernetesDeployment
displayName: 'Deploy to Kubernetes'
jobs:
- deployment: K8sDeploy
displayName: 'Kubernetes Rolling Update'
environment: 'kubernetes-prod.default'
strategy:
rolling:
maxSurge: '25%'
maxUnavailable: '25%'
deploy:
steps:
- task: KubernetesManifest@0
displayName: 'Deploy to Kubernetes cluster'
inputs:
action: 'deploy'
kubernetesServiceConnection: 'k8s-connection'
namespace: 'default'
manifests: |
$(Pipeline.Workspace)/manifests/deployment.yml
$(Pipeline.Workspace)/manifests/service.yml
containers: '$(containerRegistry)/$(imageRepository):$(Build.BuildId)'
imagePullSecrets: 'acr-secret'
- task: KubernetesManifest@0
displayName: 'Check rollout status'
inputs:
action: 'rolloutStatus'
kubernetesServiceConnection: 'k8s-connection'
namespace: 'default'
resourceType: 'deployment'
resourceName: 'myapp-deployment'
on:
failure:
steps:
- task: KubernetesManifest@0
displayName: 'Rollback deployment'
inputs:
action: 'rollback'
kubernetesServiceConnection: 'k8s-connection'
namespace: 'default'
resourceType: 'deployment'
resourceName: 'myapp-deployment'
Gestión de Variables y Secretos
Library Groups
# Crear variable group desde CLI
az pipelines variable-group create \
--name "Production-Config" \
--variables \
DatabaseConnection="Server=prod-sql.database.windows.net" \
ApiBaseUrl="https://api.myapp.com" \
--authorize true
Azure Key Vault Integration
# Integración con Key Vault
variables:
- group: Production-Config
- group: KeyVault-Secrets # Linked to Azure Key Vault
steps:
- task: AzureKeyVault@2
displayName: 'Get secrets from Key Vault'
inputs:
azureSubscription: '$(azureSubscription)'
KeyVaultName: 'myapp-keyvault'
SecretsFilter: 'DatabasePassword,ApiKey,JwtSecret'
RunAsPreJob: true
- task: PowerShell@2
displayName: 'Use secrets'
inputs:
targetType: 'inline'
script: |
# Los secretos están disponibles como variables de entorno
Write-Host "Database connection configured"
# No imprimir secretos en logs
env:
DB_PASSWORD: $(DatabasePassword)
API_KEY: $(ApiKey)
Variable Templates
# variables-template.yml
variables:
- name: buildConfiguration
value: 'Release'
- name: vmImageName
value: 'ubuntu-latest'
- ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
- name: environment
value: 'production'
- ${{ if eq(variables['Build.SourceBranchName'], 'develop') }}:
- name: environment
value: 'development'
# Pipeline principal
trigger:
- main
- develop
variables:
- template: variables-template.yml
stages:
- stage: Build
variables:
buildPlatform: 'Any CPU'
jobs:
- job: Build
pool:
vmImage: $(vmImageName)
steps:
- script: echo "Building for $(environment)"
Monitoring y Observabilidad
Application Insights Integration
# Configuración de monitoring
steps:
- task: DotNetCoreCLI@2
displayName: 'Install Application Insights'
inputs:
command: 'custom'
custom: 'tool'
arguments: 'install --global Microsoft.ApplicationInsights.SnapshotCollector'
- task: AzureCLI@2
displayName: 'Configure Application Insights'
inputs:
azureSubscription: '$(azureSubscription)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Crear Application Insights si no existe
az monitor app-insights component create \
--app myapp-insights \
--location eastus \
--resource-group myapp-rg \
--application-type web
# Obtener instrumentation key
INSTRUMENTATION_KEY=$(az monitor app-insights component show \
--app myapp-insights \
--resource-group myapp-rg \
--query "instrumentationKey" -o tsv)
echo "##vso[task.setvariable variable=APPINSIGHTS_INSTRUMENTATIONKEY]$INSTRUMENTATION_KEY"
- task: AzureWebApp@1
inputs:
appSettings: '-APPINSIGHTS_INSTRUMENTATIONKEY $(APPINSIGHTS_INSTRUMENTATIONKEY) -ASPNETCORE_ENVIRONMENT $(environment)'
Custom Metrics y Alertas
# deployment-metrics.yml
steps:
- task: PowerShell@2
displayName: 'Send deployment metrics'
inputs:
targetType: 'inline'
script: |
$deploymentStart = Get-Date
# Simular proceso de deployment
Start-Sleep -Seconds 30
$deploymentEnd = Get-Date
$duration = ($deploymentEnd - $deploymentStart).TotalSeconds
# Enviar métricas a Application Insights
$instrumentationKey = "$(APPINSIGHTS_INSTRUMENTATIONKEY)"
$telemetry = @{
"name" = "Microsoft.ApplicationInsights.Event"
"time" = $deploymentStart.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
"iKey" = $instrumentationKey
"data" = @{
"baseType" = "EventData"
"baseData" = @{
"name" = "DeploymentCompleted"
"properties" = @{
"Environment" = "$(environment)"
"BuildId" = "$(Build.BuildId)"
"Duration" = $duration.ToString()
"Status" = "Success"
}
}
}
}
$json = $telemetry | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri "https://dc.applicationinsights.azure.com/v2/track" -Method POST -Body $json -ContentType "application/json"
Testing Automatizado
Unit Testing Integration
# comprehensive-testing.yml
stages:
- stage: Test
displayName: 'Comprehensive Testing'
jobs:
- job: UnitTests
displayName: 'Unit Tests'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: '**/*UnitTests.csproj'
arguments: '--configuration $(buildConfiguration) --logger trx --collect:"XPlat Code Coverage" --results-directory $(Agent.TempDirectory)'
publishTestResults: false
- task: PublishTestResults@2
displayName: 'Publish unit test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '$(Agent.TempDirectory)/**/*.trx'
failTaskOnFailedTests: true
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- job: IntegrationTests
displayName: 'Integration Tests'
dependsOn: UnitTests
pool:
vmImage: 'ubuntu-latest'
services:
postgres: postgres:13
redis: redis:6
steps:
- task: DockerCompose@0
displayName: 'Start test services'
inputs:
action: 'Run services'
dockerComposeFile: 'docker-compose.test.yml'
buildImages: false
- task: DotNetCoreCLI@2
displayName: 'Run integration tests'
inputs:
command: 'test'
projects: '**/*IntegrationTests.csproj'
arguments: '--configuration $(buildConfiguration) --logger trx'
env:
ConnectionStrings__Database: 'Server=localhost;Database=testdb;User Id=postgres;Password=postgres;'
ConnectionStrings__Redis: 'localhost:6379'
- task: DockerCompose@0
displayName: 'Stop test services'
inputs:
action: 'Run services'
dockerComposeFile: 'docker-compose.test.yml'
dockerComposeCommand: 'down'
condition: always()
- job: EndToEndTests
displayName: 'E2E Tests'
dependsOn: IntegrationTests
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
- script: |
npm install -g @playwright/test
npx playwright install
displayName: 'Install Playwright'
- script: |
npm run test:e2e
displayName: 'Run E2E tests'
env:
BASE_URL: 'https://myapp-staging.azurewebsites.net'
- task: PublishTestResults@2
displayName: 'Publish E2E test results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: 'test-results/junit.xml'
failTaskOnFailedTests: true
condition: always()
Performance Testing
# performance-testing.yml
- job: PerformanceTests
displayName: 'Performance Tests'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
- script: |
npm install -g artillery
displayName: 'Install Artillery'
- task: CmdLine@2
displayName: 'Run load tests'
inputs:
script: |
artillery run performance-tests/load-test.yml \
--output $(Agent.TempDirectory)/load-test-results.json
env:
TARGET_URL: 'https://myapp-staging.azurewebsites.net'
- task: CmdLine@2
displayName: 'Generate performance report'
inputs:
script: |
artillery report $(Agent.TempDirectory)/load-test-results.json \
--output $(Agent.TempDirectory)/performance-report.html
- task: PublishHtmlReport@1
displayName: 'Publish performance report'
inputs:
reportDir: '$(Agent.TempDirectory)'
tabName: 'Performance Report'
Seguridad y Compliance
Security Scanning
# security-pipeline.yml
stages:
- stage: SecurityScans
displayName: 'Security Scanning'
jobs:
- job: StaticAnalysis
displayName: 'Static Analysis Security Testing'
steps:
- task: SonarCloudPrepare@1
inputs:
SonarCloud: 'SonarCloud'
organization: 'myorg'
scannerMode: 'MSBuild'
projectKey: 'myproject'
extraProperties: |
sonar.exclusions=**/node_modules/**,**/wwwroot/lib/**
sonar.security.hotspots.maxHotspots=0
- task: VSBuild@1
inputs:
solution: '**/*.sln'
- task: SonarCloudAnalyze@1
- task: SonarCloudPublish@1
inputs:
pollingTimeoutSec: '300'
- job: DependencyCheck
displayName: 'Dependency Vulnerability Scan'
steps:
- task: PowerShell@2
displayName: 'Install OWASP Dependency Check'
inputs:
targetType: 'inline'
script: |
Invoke-WebRequest -Uri "https://github.com/jeremylong/DependencyCheck/releases/download/v7.4.4/dependency-check-7.4.4-release.zip" -OutFile "dependency-check.zip"
Expand-Archive -Path "dependency-check.zip" -DestinationPath "$(Agent.TempDirectory)/dependency-check"
- task: CmdLine@2
displayName: 'Run dependency check'
inputs:
script: |
$(Agent.TempDirectory)/dependency-check/bin/dependency-check.sh \
--project "MyProject" \
--scan "." \
--format "ALL" \
--out "$(Agent.TempDirectory)/dependency-reports" \
--suppression "dependency-check-suppressions.xml"
- task: PublishBuildArtifacts@1
displayName: 'Publish dependency reports'
inputs:
pathToPublish: '$(Agent.TempDirectory)/dependency-reports'
artifactName: 'dependency-scan-results'
- job: ContainerScan
displayName: 'Container Security Scan'
condition: succeeded()
steps:
- task: CmdLine@2
displayName: 'Scan Docker image with Trivy'
inputs:
script: |
# Instalar Trivy
sudo apt-get update
sudo apt-get install wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
# Escanear imagen
trivy image --format json --output $(Agent.TempDirectory)/trivy-report.json $(containerRegistry)/$(imageRepository):$(Build.BuildId)
# Verificar severidad crítica
CRITICAL_COUNT=$(cat $(Agent.TempDirectory)/trivy-report.json | jq '.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL") | .VulnerabilityID' | wc -l)
if [ $CRITICAL_COUNT -gt 0 ]; then
echo "Found $CRITICAL_COUNT critical vulnerabilities"
exit 1
fi
Compliance Checks
# compliance-pipeline.yml
- stage: Compliance
displayName: 'Compliance Validation'
jobs:
- job: PolicyValidation
displayName: 'Azure Policy Validation'
steps:
- task: AzureCLI@2
displayName: 'Check resource compliance'
inputs:
azureSubscription: '$(azureSubscription)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Verificar compliance de recursos
az policy state list --resource-group myapp-rg \
--query "[?complianceState=='NonCompliant'].{Resource:resourceId, Policy:policyDefinitionName}" \
--output table
# Fallar si hay recursos no conformes críticos
NON_COMPLIANT=$(az policy state list --resource-group myapp-rg \
--query "[?complianceState=='NonCompliant' && contains(policyDefinitionName, 'security')] | length(@)")
if [ $NON_COMPLIANT -gt 0 ]; then
echo "Critical compliance violations found"
exit 1
fi
- job: DataProtectionAudit
displayName: 'Data Protection Audit'
steps:
- task: PowerShell@2
displayName: 'Audit data protection settings'
inputs:
targetType: 'inline'
script: |
# Verificar configuraciones de seguridad
$webAppName = "myapp-prod"
$resourceGroup = "myapp-rg"
# Verificar HTTPS only
$httpsOnly = az webapp show --name $webAppName --resource-group $resourceGroup --query "httpsOnly" -o tsv
if ($httpsOnly -ne "true") {
Write-Error "HTTPS Only not enabled"
exit 1
}
# Verificar TLS version
$tlsVersion = az webapp config show --name $webAppName --resource-group $resourceGroup --query "minTlsVersion" -o tsv
if ($tlsVersion -lt "1.2") {
Write-Error "TLS version below 1.2"
exit 1
}
Write-Host "Data protection audit passed"
Optimization y Best Practices
Pipeline Performance
# optimized-pipeline.yml
trigger:
batch: true # Batch changes
branches:
include:
- main
paths:
exclude:
- docs/**
- '*.md'
variables:
MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository
npm_config_cache: $(Pipeline.Workspace)/.npm
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
- job: ParallelBuild
displayName: 'Parallel Build Tasks'
strategy:
parallel: 3 # Run 3 jobs in parallel
steps:
- task: Cache@2
displayName: 'Cache Maven dependencies'
inputs:
key: 'maven | "$(Agent.OS)" | **/pom.xml'
restoreKeys: |
maven | "$(Agent.OS)"
maven
path: $(MAVEN_CACHE_FOLDER)
- task: Cache@2
displayName: 'Cache npm dependencies'
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: |
npm | "$(Agent.OS)"
path: $(npm_config_cache)
- script: |
# Usar cache para acelerar builds
export MAVEN_OPTS="-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)"
mvn clean compile -DskipTests
displayName: 'Fast compile'
- script: |
# Tests en paralelo
mvn test -DforkCount=2C -DreuseForks=true
displayName: 'Parallel tests'
condition: eq(variables['Agent.JobName'], 'Job_1')
- script: |
# Static analysis en paralelo
mvn checkstyle:check spotbugs:check
displayName: 'Static analysis'
condition: eq(variables['Agent.JobName'], 'Job_2')
- script: |
# Security scan en paralelo
mvn dependency-check:check
displayName: 'Security scan'
condition: eq(variables['Agent.JobName'], 'Job_3')
Resource Management
# resource-optimization.yml
resources:
containers:
- container: postgres
image: postgres:13-alpine
env:
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: --health-cmd "pg_isready" --health-interval 10s
jobs:
- job: OptimizedDeployment
displayName: 'Resource Optimized Deployment'
timeoutInMinutes: 30 # Timeout explicito
cancelTimeoutInMinutes: 2
pool:
vmImage: 'ubuntu-latest'
demands:
- npm
- docker
services:
postgres: postgres
variables:
DOCKER_BUILDKIT: 1
COMPOSE_DOCKER_CLI_BUILD: 1
steps:
- checkout: self
fetchDepth: 1 # Shallow clone
lfs: false
- task: DockerCompose@0
displayName: 'Optimized docker build'
inputs:
action: 'Build services'
dockerComposeFile: 'docker-compose.yml'
additionalDockerComposeFiles: 'docker-compose.override.yml'
dockerComposeFileArgs: |
BUILDKIT_INLINE_CACHE=1
DOCKER_BUILDKIT=1
- script: |
# Limpiar recursos después del build
docker system prune -af --volumes
docker builder prune -af
displayName: 'Cleanup Docker resources'
condition: always()
Multi-Environment Templates
# templates/deployment-template.yml
parameters:
- name: environment
type: string
- name: azureSubscription
type: string
- name: appName
type: string
- name: resourceGroup
type: string
- name: deploymentSlots
type: object
default: []
jobs:
- deployment: Deploy${{ parameters.environment }}
displayName: 'Deploy to ${{ parameters.environment }}'
environment: '${{ parameters.environment }}'
variables:
- group: '${{ parameters.environment }}-Variables'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
# Deploy to staging slot first if production
- ${{ if eq(parameters.environment, 'Production') }}:
- task: AzureWebApp@1
displayName: 'Deploy to staging slot'
inputs:
azureSubscription: '${{ parameters.azureSubscription }}'
appName: '${{ parameters.appName }}'
deployToSlotOrASE: true
resourceGroupName: '${{ parameters.resourceGroup }}'
slotName: 'staging'
package: '$(Pipeline.Workspace)/drop/*.zip'
- task: AzureAppServiceManage@0
displayName: 'Warm up staging slot'
inputs:
azureSubscription: '${{ parameters.azureSubscription }}'
action: 'Start Azure App Service'
WebAppName: '${{ parameters.appName }}'
resourceGroupName: '${{ parameters.resourceGroup }}'
SpecifySlotOrASE: true
Slot: 'staging'
- script: |
# Health check
for i in {1..10}; do
response=$(curl -s -o /dev/null -w "%{http_code}" "https://${{ parameters.appName }}-staging.azurewebsites.net/health")
if [ $response -eq 200 ]; then
echo "Health check passed"
break
fi
echo "Attempt $i failed, retrying..."
sleep 30
done
displayName: 'Health check staging slot'
- task: AzureAppServiceManage@0
displayName: 'Swap to production'
inputs:
azureSubscription: '${{ parameters.azureSubscription }}'
action: 'Swap Slots'
WebAppName: '${{ parameters.appName }}'
resourceGroupName: '${{ parameters.resourceGroup }}'
SourceSlot: 'staging'
SwapWithProduction: true
# Direct deployment for non-production
- ${{ if ne(parameters.environment, 'Production') }}:
- task: AzureWebApp@1
displayName: 'Deploy to ${{ parameters.environment }}'
inputs:
azureSubscription: '${{ parameters.azureSubscription }}'
appName: '${{ parameters.appName }}'
resourceGroupName: '${{ parameters.resourceGroup }}'
package: '$(Pipeline.Workspace)/drop/*.zip'
# Pipeline principal usando template
stages:
- stage: DeployDev
displayName: 'Deploy Development'
jobs:
- template: templates/deployment-template.yml
parameters:
environment: 'Development'
azureSubscription: '$(devSubscription)'
appName: 'myapp-dev'
resourceGroup: 'myapp-dev-rg'
- stage: DeployProd
displayName: 'Deploy Production'
dependsOn: DeployDev
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- template: templates/deployment-template.yml
parameters:
environment: 'Production'
azureSubscription: '$(prodSubscription)'
appName: 'myapp-prod'
resourceGroup: 'myapp-prod-rg'
Troubleshooting y Debugging
Common Issues y Solutions
1. Pipeline Timeouts
# Configurar timeouts apropiados
jobs:
- job: LongRunningJob
timeoutInMinutes: 60 # Por defecto son 60 minutos
cancelTimeoutInMinutes: 5
steps:
- script: |
# Mostrar progreso para evitar timeouts por inactividad
for i in {1..100}; do
echo "Processing item $i of 100"
# Tu proceso aquí
sleep 1
done
2. Variable Resolution Issues
# Debug de variables
steps:
- script: |
echo "Build ID: $(Build.BuildId)"
echo "Source Branch: $(Build.SourceBranch)"
echo "Custom Variable: $(myCustomVar)"
env | grep -i azure | sort
displayName: 'Debug variables'
3. Artifact Download Problems
# Debugging artifact issues
steps:
- download: current
artifact: drop
displayName: 'Download artifacts'
- script: |
echo "Pipeline Workspace: $(Pipeline.Workspace)"
ls -la $(Pipeline.Workspace)
find $(Pipeline.Workspace) -type f -name "*.zip" | head -10
displayName: 'Debug artifacts'
Logging y Monitoring
# Enhanced logging pipeline
variables:
system.debug: true # Verbose logging
steps:
- task: PowerShell@2
displayName: 'Enhanced deployment logging'
inputs:
targetType: 'inline'
script: |
# Función de logging
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] [$Level] $Message"
# Enviar logs a Azure Monitor
$logData = @{
timestamp = $timestamp
level = $Level
message = $Message
buildId = "$(Build.BuildId)"
environment = "$(environment)"
} | ConvertTo-Json
# Aquí integrarías con tu sistema de logging
}
Write-Log "Starting deployment process"
try {
# Tu proceso de deployment
Write-Log "Deployment completed successfully" "SUCCESS"
} catch {
Write-Log "Deployment failed: $($_.Exception.Message)" "ERROR"
throw
}
Integración con Herramientas Externas
Slack Notifications
# slack-integration.yml
steps:
- task: PowerShell@2
displayName: 'Send Slack notification'
condition: always()
inputs:
targetType: 'inline'
script: |
$status = if ("$(Agent.JobStatus)" -eq "Succeeded") { "success" } else { "failure" }
$color = if ($status -eq "success") { "good" } else { "danger" }
$payload = @{
channel = "#devops"
username = "Azure DevOps"
text = "Deployment $status for $(Build.Repository.Name)"
attachments = @(
@{
color = $color
fields = @(
@{ title = "Environment"; value = "$(environment)"; short = $true }
@{ title = "Build ID"; value = "$(Build.BuildId)"; short = $true }
@{ title = "Branch"; value = "$(Build.SourceBranchName)"; short = $true }
@{ title = "Commit"; value = "$(Build.SourceVersion)".Substring(0,7); short = $true }
)
actions = @(
@{
type = "button"
text = "View Build"
url = "$(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri "$(slackWebhook)" -Method POST -Body $payload -ContentType "application/json"
Jira Integration
# jira-integration.yml
steps:
- task: PowerShell@2
displayName: 'Update Jira tickets'
inputs:
targetType: 'inline'
script: |
# Extraer ticket IDs del commit message
$commitMessage = "$(Build.SourceVersionMessage)"
$jiraPattern = "[A-Z]+-\d+"
$tickets = [regex]::Matches($commitMessage, $jiraPattern) | ForEach-Object { $_.Value }
foreach ($ticket in $tickets) {
$updateData = @{
transition = @{
id = "31" # ID de transición a "In Testing"
}
fields = @{
customfield_10100 = "$(Build.BuildId)" # Build ID custom field
}
} | ConvertTo-Json -Depth 3
$auth = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("$(jiraUser):$(jiraToken)"))
try {
Invoke-RestMethod -Uri "https://your-domain.atlassian.net/rest/api/3/issue/$ticket/transitions" `
-Method POST `
-Headers @{ Authorization = "Basic $auth" } `
-ContentType "application/json" `
-Body $updateData
Write-Host "Updated ticket $ticket"
} catch {
Write-Warning "Failed to update ticket $ticket: $($_.Exception.Message)"
}
}
Azure DevOps proporciona una plataforma robusta y escalable para implementar CI/CD empresarial. La clave del éxito está en:
- Planificación inicial cuidadosa de la arquitectura de pipelines
- Implementación gradual con validación en cada etapa
- Monitoreo continuo y optimización basada en métricas
- Automatización completa desde el desarrollo hasta producción
- Security-first approach con controles integrados
- Documentation y knowledge sharing para el equipo
Con esta configuración completa, tendrás un sistema de CI/CD robusto, escalable y mantenible que puede evolucionar con las necesidades de tu organización.