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

proyesipsacrnicztcfpruoreiratatalcctpbdepeseriidetue-pstrmn//msipp/trr-ep/llisuatsldp/cfe/ayetomt/lurpeirmlsnea/etse.sy/ml

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:

  1. Planificación inicial cuidadosa de la arquitectura de pipelines
  2. Implementación gradual con validación en cada etapa
  3. Monitoreo continuo y optimización basada en métricas
  4. Automatización completa desde el desarrollo hasta producción
  5. Security-first approach con controles integrados
  6. 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.