CI/CD moderno para Umbraco con Azure DevOps e IIS on-premises


12 dic., 2025 10:27

Comparte esta noticia:

CI/CD Moderno para Umbraco Usando Azure DevOps & IIS On-Premises (Actualización 2025)

Hace algunos años compartí una guía paso a paso sobre cómo desplegar sitios Umbraco en servidores on-premises utilizando los clásicos Release Pipelines de Azure DevOps. Desde entonces, muchas cosas han cambiado.
Hoy, Umbraco se ejecuta sobre .NET moderno, la mayoría de los proyectos utiliza Vite o Tailwind para el tooling frontend, y Azure DevOps impulsa claramente el uso de pipelines en YAML en lugar de los antiguos releases configurados mediante interfaz gráfica.

Esta guía actualizada refleja cómo construimos y entregamos sitios web Umbraco en 2025: utilizando automatización total, despliegues sensibles al entorno y un único pipeline YAML que realiza el deploy a UAT y Producción en servidores IIS dentro de tu propio datacenter.

Si estás modernizando un proceso antiguo o empezando desde cero, este artículo te ofrece una base sólida. Pero recuerda: esto es un boilerplate. Cada organización tiene sus propias necesidades — balanceo de carga, replicación, estrategias de media compartida, gestión de secretos, refuerzo de seguridad y gobernanza de despliegues. Casi siempre será necesario adaptar este flujo a tu infraestructura específica.


Por qué CI/CD para Umbraco On-Premises sigue siendo importante

Muchos equipos continúan alojando Umbraco en su propia infraestructura — a veces por motivos de compliance, otras por integración con sistemas locales y, en otros casos, porque la migración a la nube simplemente no es viable.
Sin embargo, los desarrolladores esperan automatización al estilo cloud, independientemente de dónde se encuentren los servidores.

Los pipelines de CI/CD ofrecen:

  • Despliegues predecibles
  • Rollouts seguros
  • Eliminación de la copia manual de archivos
  • Configuración consistente del servidor
  • Automatización amigable para el developer


Arquitectura de Alto Nivel

Azure DevOps (Cloud)

  • Repositorio Git de tu solución Umbraco
  • Pipeline YAML multi-stage
  • Azure DevOps Environments (UAT & PROD)

Servidores On-Premises

  • Windows Server con IIS
  • Agentes self-hosted de Azure DevOps
  • Carpetas wwwroot dedicadas para UAT & PROD

Flujo de Deployment

  1. El developer hace push a dev o release/uat
  2. El pipeline compila la aplicación (.NET + npm)
  3. El output se despliega en UAT
  4. El mismo artefacto se promociona a Producción


Instalar o Agente Self-Hosted do Azure DevOps

1. Crear Agent Pool

Organization Settings → Agent Pools → New → Self-hosted

2. Descargar el Agente

New Agent → Windows → Download ZIP

3. Configurar el Agente

mkdir C:\azdo-agent
cd C:\azdo-agent

Extraer el ZIP y ejecutar:

.\config.cmd

Después:

  • URL del servidor: https://dev.azure.com/YOURORG/
  • PAT: debe tener permisos Agent Pools (read, manage)
  • Pool: OnPrem-IIS
  • Nombre: UAT-IIS o PROD-IIS
  • Ejecutar como servicio:

Iniciar el agente:

.\run.cmd

Configurar Azure DevOps Environments

  1. Pipelines → Environments
  2. Crear UAT-IIS y registrar el servidor UAT
  3. Crear PROD-IIS y registrar el servidor PROD

Pipeline YAML Completo de CI/CD

Pipeline totalmente funcional — puedes copiar y pegar tal cual.

trigger:
  - dev
  - release/uat

variables:
  buildConfiguration: 'Release'
  projectFolder: 'www'
  publishFolder: '$(Build.ArtifactStagingDirectory)/publish'
  nodeVersion: '20.x'
  offlineTemplate: 'App_Offline.htm.rename'
  offlineFile: 'App_Offline.htm'

stages:
- stage: Build
  displayName: 'Build & Publish Umbraco'
  jobs:
  - job: BuildJob
    displayName: 'Build .NET + Tailwind + Vite'
    pool:
      vmImage: 'windows-latest'
    steps:
    - checkout: self

    - task: NodeTool@0
      displayName: 'Install Node.js'
      inputs:
        versionSpec: '$(nodeVersion)'

    - task: Cache@2
      displayName: 'Cache npm (frontend)'
      inputs:
        key: 'npm-frontend | "$(Build.SourcesDirectory)/$(projectFolder)/package-lock.json"'
        path: '$(Build.SourcesDirectory)/$(projectFolder)/node_modules'

    - task: Cache@2
      displayName: 'Cache npm (backoffice)'
      inputs:
        key: 'npm-backend | "$(Build.SourcesDirectory)/dbl.MadeirasLeiria.backoffice/package-lock.json"'
        path: '$(Build.SourcesDirectory)/dbl.MadeirasLeiria.backoffice/node_modules'

    - task: Npm@1
      displayName: 'npm install (backoffice)'
      inputs:
        command: 'install'
        workingDir: 'dbl.MadeirasLeiria.backoffice'

    - task: UseDotNet@2
      displayName: 'Install .NET 9 SDK'
      inputs:
        packageType: 'sdk'
        version: '9.0.x'

    - task: DotNetCoreCLI@2
      displayName: 'Restore NuGet'
      inputs:
        command: 'restore'
        projects: '$(projectFolder)/www.csproj'

    - task: DotNetCoreCLI@2
      displayName: 'Publish'
      inputs:
        command: 'publish'
        projects: '$(projectFolder)/www.csproj'
        arguments: '--configuration $(buildConfiguration) --output $(publishFolder)'
        zipAfterPublish: false

    - publish: $(publishFolder)
      artifact: drop

- stage: Deploy_UAT
  displayName: 'Deploy to UAT'
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: UATDeploy
    displayName: 'Deploy to UAT`
    environment:
      name: 'UAT-DBL01'
      resourceType: VirtualMachine
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: none
          - download: current
            artifact: drop

          - task: PowerShell@2
            displayName: 'Activate App_Offline'
            inputs:
              targetType: 'inline'
              script: |
                $root="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
                $template=Join-Path $root "$(offlineTemplate)"
                $offline=Join-Path $root "$(offlineFile)"
                if(Test-Path $template){
                  if(Test-Path $offline){ Remove-Item $offline -Force }
                  Rename-Item $template $offline -Force
                }

          - task: PowerShell@2
            displayName: 'Clean UAT (preserve media + logs)'
            inputs:
              targetType: 'inline'
              script: |
                $path="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
                $preserve=@("wwwroot\media","umbraco\Logs","App_Offline.htm")
                $temp=Join-Path $env:TEMP "backup_$(Get-Date -Format yyyyMMddHHmmss)"
                New-Item $temp -ItemType Directory | Out-Null
                foreach($item in $preserve){
                  $src=Join-Path $path $item
                  if(Test-Path $src){
                    $dest=Join-Path $temp $item
                    New-Item (Split-Path $dest) -ItemType Directory -Force | Out-Null
                    robocopy $src $dest /E | Out-Null
                  }
                }
                Get-ChildItem $path | ForEach-Object {
                  if($_.Name -ne "App_Offline.htm"){
                    Remove-Item $_.FullName -Recurse -Force
                  }
                }
                foreach($item in $preserve){
                  $src=Join-Path $temp $item
                  if(Test-Path $src){
                    $dest=Join-Path $path $item
                    New-Item (Split-Path $dest) -ItemType Directory -Force | Out-Null
                    robocopy $src $dest /E | Out-Null
                  }
                }
                Remove-Item $temp -Recurse -Force

          - task: CopyFiles@2
            displayName: 'Copy Files'
            inputs:
              SourceFolder: '$(Pipeline.Workspace)/drop/www'
              Contents: |
                **
                !wwwroot/media/**
                !umbraco/Logs/**
              TargetFolder: 'D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot'
              OverWrite: true

          - task: PowerShell@2
            displayName: 'Set ASPNETCORE_ENVIRONMENT'
            inputs:
              targetType: 'inline'
              script: |
                $config="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot\web.config"
                [xml]$xml = Get-Content $config
                $node=$xml.SelectSingleNode("//environmentVariable[@name='ASPNETCORE_ENVIRONMENT']")
                if($node -eq $null){
                  $envNode=$xml.SelectSingleNode("//environmentVariables")
                  $newNode=$xml.CreateElement("environmentVariable")
                  $newNode.SetAttribute("name","ASPNETCORE_ENVIRONMENT")
                  $newNode.SetAttribute("value","Staging")
                  $envNode.AppendChild($newNode)
                } else {
                  $node.value="Staging"
                }
                $xml.Save($config)

          - task: PowerShell@2
            displayName: 'Set ACLs'
            inputs:
              targetType: 'inline'
              script: |
                $root="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
                $appPool="IIS AppPool\uat.madeirasleiria.pt"
                $dirs=@("umbraco","App_Plugins","Views","wwwroot\media","wwwroot\css","wwwroot\scripts","wwwroot\umbraco")
                foreach($d in $dirs){
                  $full=Join-Path $root $d
                  if(Test-Path $full){
                    icacls $full /grant "$appPool:(OI)(CI)(M)" /T
                  }
                }

          - task: PowerShell@2
            displayName: 'Deactivate App_Offline'
            inputs:
              targetType: 'inline'
              script: |
                $root="D:\WEB\MadeiriasLeiria\uat.madeirasleiria.pt\wwwroot"
                $template=Join-Path $root "$(offlineTemplate)"
                $offline=Join-Path $root "$(offlineFile)"
                if(Test-Path $offline){
                  if(Test-Path $template){ Remove-Item $template -Force }
                  Rename-Item $offline $template -Force
                }

- stage: Deploy_PROD
  displayName: 'Deploy to Production'
  dependsOn: Deploy_UAT
  condition: succeeded()

  jobs:
  - deployment: ProdDeploy
    environment:
      name: 'PROD-DBL01'
      resourceType: VirtualMachine
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: none
          - download: current
            artifact: drop

          # SAME LOGIC AS UAT, WITH PRODUCTION PATHS

¿Necesitas ayuda con tus despliegues de Umbraco?

Si estás trabajando en una nueva configuración de CI/CD o intentando mejorar la existente, y todo empieza a complicarse cuando añades múltiples servidores, sincronización de media, reglas de seguridad o flujos más avanzados… no estás solo. Ayudamos a equipos con esto todos los días.

Nuestros developers han construido y desplegado proyectos Umbraco de todos los tamaños: sitios para pequeñas empresas, grandes plataformas empresariales, proyectos del sector público y todo lo que hay entre estos extremos.
Si deseas que alguien con experiencia revise tu configuración o simplemente necesitas orientación sobre el mejor enfoque, estaremos encantados de ayudarte.

Habla con nuestro equipo

Si quieres revisar tu proceso de deployment o planificar los próximos pasos, ponte en contacto con nosotros.
Sin presión ni compromisos. Solo una conversación cercana sobre lo que deseas conseguir.

Contacte-nos


Si te enfrentas a desafíos relacionados con balanceo de carga, escalabilidad, automatización de despliegues o cualquier tema vinculado a Umbraco y DevOps, habla con nosotros. Podemos ser el equipo adecuado para apoyarte (y nuestros clientes suelen considerar nuestros precios muy competitivos).

Escrito por Marco Teodoro

Fundador & CEO, Double

Conecta-te à minha rede no LinkedIn | Segue a Double no LinkedIn

#Umbraco #AgenciaNearshore #MigracaoUmbraco


comments powered by Disqus