Implementación automática de aplicaciones ASP.NET clásicas con .NET Framework 4.8 usando GitLab Runner en Windows

Introducción

La automatización del despliegue permite a los desarrolladores liberarse de tareas manuales tediosas. Para aplicaciones web tradicionales alojadas en IIS de Windows, Docker no siempre es la solución más práctica. Este artículo detalla cómo utilizar el servidor OpenSSH de Windows para transferir archivos del sitio web y luego emplear PowerShell para crear y configurar directorios virtuales y aplicaciones en IIS.

  1. Compilación automatizada de la aplicación web ASP.NET

1.1. Configuración en .gitlab-ci.yml del repositorio GitLab

compilar-orden-miembros:
  stage: compile-web
  tags:
    - runner-win
  before_script:
    - . $WEB_BUILD_SCRIPT_PATH
  script:
    - Invoke-BuildWeb -MsBuildDir $MSBUILD_DIRECTORY -ProjectRoot $CI_PROJECT_DIR -TargetProjectPath "/projects/specific-app/SampleApp.Web/SampleApp.Web.csproj" -OutputPath $WEB_DEPLOYMENT_DIR

Explicación:

  • El comando en before_script registra la función de PowerShell, ya que el script del servidor contiene una definición de función.
  • El script Invoke-BuildWeb es la función de PowerShell que acepta cuatro parámetros.

1.2. Script de PowerShell en el Windows GitLab Runner

function Invoke-BuildWeb {
    param (
        [string]$msbuildDir,
        [string]$projectRoot,
        [string]$targetProjectPath,
        [string]$deploymentOutput
    )

    $csprojFile = "$projectRoot\$targetProjectPath"
    if ($targetProjectPath -match "^[/\\]") {
        $csprojFile = "$projectRoot$targetProjectPath"
    }
    
    Write-Host "Archivo del proyecto: $csprojFile"
    Write-Host "Directorio de salida: $deploymentOutput"

    Set-Location -Path $projectRoot
    Copy-Item -Path "x:/automation-scripts/dependencies-fetch.bat" -Destination . -Force
    .\dependencies-fetch.bat
    
    if ($LASTEXITCODE -ne 0) {
        throw "Error al descargar el archivo Directory.Build.props y dependencias."
    }
    
    dotnet nuget locals http-cache --clear

    $msbuildExe = Join-Path -Path $msbuildDir -ChildPath "msbuild.exe"
    if (-not (Test-Path -Path $msbuildExe)) {
        throw "No se encontró msbuild.exe en: $msbuildDir"
    }
    
    Push-Location -Path $msbuildDir
    .\msbuild.exe -restore $csprojFile /verbosity:minimal
    if ($LASTEXITCODE -ne 0) {
        throw "Error al restaurar paquetes NuGet para: $csprojFile"
    }

    & "X:\tools\binding-redirect-fixer\BindingRedirect.exe" check --project:$csprojFile --save:true
    if ($LASTEXITCODE -ne 0) {
        throw "Error al actualizar los binding redirects en web.config: $csprojFile"
    }

    .\msbuild.exe $csprojFile /t:ResolveReferences /t:Compile /p:Configuration=Release /t:_CopyWebApplication /p:WebProjectOutputDir=$deploymentOutput /p:OutputPath="$deploymentOutput\bin" /verbosity:minimal
    Pop-Location
    
    exit $LASTEXITCODE
}

Notas clave:

  • El script dependencies-fetch.bat maneja la descarga de archivos de configuración. Luego se limpia la caché HTTP de NuGet.
  • La compilación con msbuild.exe se realiza en dos fases: restauración y compilación propiamente dicha.
  • El conmutador /verbosity:minimal reduce la cantidad de registro. Para diagnóstico, usar /verbosity:diagnostic.
  • El objetivo /t:_CopyWebApplication requiere que el archivo .csproj del proyecto web lo importe (detalles en documentación anterior).
  • Es necesario establecer tanto WebProjectOutputDir (para archivos de contenido como .aspx, .css) como OutputPath (para ensamblados .dll).

Descripción de los cuatro parámetros:

  • $msbuildDir: Ruta al directorio que contiene msbuild.exe.
  • $projectRoot: Directorio de trabajo asignado por GitLab Runner al clonar el repositorio (variable interna CI_PROJECT_DIR).
  • $targetProjectPath: Ruta relativa al archivo .csproj del proyecto dentro del repositorio.
  • $deploymentOutput: Directorio absoluto donde se copiarán los artefactos. Debe estar fuera del directorio de trabajo del stage actual, ya que stages posteriores no pueden acceder a sus archivos.
  1. Transferencia automatizada del sitio web al servidor de producción

Esta etapa requiere preparación previa en el servidor web destino:

  • Instalar el servidor OpenSSH.
  • Configurar el acceso mediante clave pública SSH para la cuenta administrativa, deshabilitando la autenticación por contraseña.

2.1. Definición del stage en .gitlab-ci.yml

subir-orden-miembros-a-dev:
  stage: deploy-web
  tags:
    - runner-win
  script:
    - Set-Location -Path $RUNNER_SCRIPTS_DIR
    - |
      powershell.exe -File .\deploy-web.ps1 `
        -SshKeyFile $SSH_PRIVATE_KEY_PATH `
        -TargetUser $DEV_SERVER_USER `
        -TargetHost $DEV_SERVER_ADDRESS `
        -WebRootParentFolder $WEB_ROOT_PARENT `
        -IisSiteName $IIS_SITE_NAME `
        -AppPoolName $IIS_APP_POOL_NAME `
        -VirtualAppName $VIRTUAL_APP_NAME `
        -LocalBuildPath $WEB_DEPLOYMENT_DIR `
        -VersionTag $APP_VERSION_ID

Consideraciones de formato:

  • Los parámetros se deben pasar directamente con $VARIABLE, sin usar ${VARIABLE} en esta estructura.
  • No se deben incluir comillas dobles dentro de los valores en cada línea.

Parámetros detallados:

  • $SSH_PRIVATE_KEY_PATH: Ruta completa a la clave privada SSH almacenada en el servidor del Runner. Los permisos de la carpeta deben restringirse exclusivamente a la cuenta del servicio del Runner.
  • $DEV_SERVER_USER: Nombre de usuario (ej. Administrator) para autenticarse vía SSH en el servidor web.
  • $DEV_SERVER_ADDRESS: Nombre DNS o dirección IP del servidor web.
  • $WEB_ROOT_PARENT: Nombre del directorio padre en el servidor web (ej. apps). Si la ruta física es D:\apps, este valor representa apps, añadiendo seguridad.
  • $IIS_SITE_NAME: Nombre del sitio en IIS (ej. DefaultWebSite). Por consistencia en el paso de parámetros, no debe contener espacios.
  • $IIS_APP_POOL_NAME: Nombre del grupo de aplicaciones de IIS. También debe omitir espacios.
  • $VIRTUAL_APP_NAME: Nombre de la aplicación virtual dentro del sitio de IIS. Aparecerá en la URL, por lo que solo debe contener caracteres alfanuméricos y guiones.
  • $LOCAL_BUILD_PATH: Ruta absoluta en el serviodr del Runner donde se encuentran los artefactos compilados.
  • $APP_VERSION_ID: Identificador de versión (ej. 1.2.3). Se reemplazarán los puntos por guiones bajos para usarlo como nombre de directorio.

2.2. Script de PowerShell en el servidor Runner (deploy-web.ps1)

param (
    [Parameter(Mandatory=$true)]
    [string]$SshKeyFile,
    [Parameter(Mandatory=$true)]
    [string]$LocalBuildPath,
    [Parameter(Mandatory=$true)]
    [string]$TargetUser,
    [Parameter(Mandatory=$true)]
    [string]$TargetHost,
    [Parameter(Mandatory=$true)]
    [string]$WebRootParentFolder,
    [parameter(Mandatory=$true)]
    [string]$IisSiteName,
    [Parameter(Mandatory=$true)]
    [string]$AppPoolName,
    [Parameter(Mandatory=$true)]
    [string]$VirtualAppName,
    [Parameter(Mandatory=$true)]
    [string]$VersionTag
)

# Configurar codificación UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$VersionTag = $VersionTag -replace '\.', '_'

# Escapar caracteres problemáticos en cadenas
$SafeSiteName = $IisSiteName -replace "'", "''"
$SafePoolName = $AppPoolName -replace "'", "''"
$SafeAppName = $VirtualAppName -replace "'", "''"
$SafeVersion = $VersionTag -replace "'", "''"

$remoteScript = "C:\automation\Initialize-WebApp.ps1"

# Construir el comando de inicialización remota
$remoteArguments = "-SiteName '$SafeSiteName' -AppPoolName '$SafePoolName' -AppName '$SafeAppName' -AppVersion '$SafeVersion'"

# Construir y ejecutar el comando SSH
$sshCommand = "ssh -i `"$SshKeyFile`" -o StrictHostKeyChecking=no `"$TargetUser@$TargetHost`" 'powershell.exe -File `"$remoteScript`" $remoteArguments'"
Write-Host "Ejecutando comando SSH: $sshCommand"
Invoke-Expression -Command $sshCommand

if ($LASTEXITCODE -ne 0) {
    Write-Error "Fallo en la inicialización remota. Código de salida: $LASTEXITCODE"
    exit $LASTEXITCODE
}

# Transferir archivos usando SCP
$destinationPath = "C:\$WebRootParentFolder\$VirtualAppName\$VersionTag"
scp -i $SshKeyFile -o StrictHostKeyChecking=no -r "$LocalBuildPath\*" "$TargetUser@$TargetHost`:$destinationPath"

exit $LASTEXITCODE

Lógica principal:

  1. Prepara los parámetros, sanitizando la versión y escapando comillas simples.
  2. Ejecuta un script PowerShell en el servidor web remoto mediante SSH. Este script crea el directorio de versión y configura la aplicación en IIS.
  3. Transfiere los archivos compilados al nuevo directorio de versión en el servidor remoto usando scp.

2.3. Script en el servidor web (Initialize-WebApp.ps1)

Este script se ejecuta dentro del srevidor web objetivo. Su función es preparar el entorno en IIS.

param(
    [Parameter(Mandatory=$true)]
    [string]$SiteName,
    [Parameter(Mandatory=$true)]
    [string]$AppPoolName,
    [Parameter(Mandatory=$true)]
    [string]$AppName,
    [Parameter(Mandatory=$true)]
    [string]$AppVersion
)

# Asegurar codificación correcta
chcp 65001 | Out-Null
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

$AppVersion = $AppVersion -replace '\.', '_'

# Definir rutas físicas
$BasePhysicalPath = "C:\webapps\$AppName"
$VersionedPath = Join-Path -Path $BasePhysicalPath -ChildPath $AppVersion

# Crear directorio para la nueva versión
New-Item -Path $VersionedPath -ItemType Directory -Force | Out-Null
Write-Host "Directorio creado: $VersionedPath"

# Cargar el módulo de administración de IIS
$IISAdminModule = "C:\Windows\System32\WindowsPowerShell\v1.0\Modules\WebAdministration\WebAdministration.psd1"
if (-not (Test-Path $IISAdminModule)) {
    Write-Error "Módulo WebAdministration no encontrado."
    exit 1
}
Import-Module $IISAdminModule

# Validar existencia del sitio y grupo de aplicaciones
$sitePath = "IIS:\Sites\$SiteName"
if (-not (Test-Path $sitePath)) {
    Write-Error "Sitio '$SiteName' no existe en IIS."
    exit 1
}
if (-not (Get-ChildItem IIS:\AppPools | Where-Object {$_.Name -eq $AppPoolName})) {
    Write-Error "Grupo de aplicaciones '$AppPoolName' no existe."
    exit 1
}

# Crear o actualizar la aplicación web virtual
Write-Host "Configurando aplicación '$AppName' bajo el sitio '$SiteName'."
New-WebApplication -Name $AppName -Site $SiteName -ApplicationPool $AppPoolName -PhysicalPath $VersionedPath -Force | Out-Null

# Verificar éxito
$applicationPath = Join-Path -Path $sitePath -ChildPath $AppName
if (Test-Path $applicationPath) {
    Write-Host "La aplicación '$AppName' está configurada en '$VersionedPath'."
} else {
    Write-Error "No se pudo crear la aplicación web."
    exit 1
}

exit 0

Propósito y mecanismo:

  • Crea un directorio físico único por versión. Esto es necesario porque los archivos .dll en uso por IIS no pueden ser sobrescritos.
  • Utiliza el cmdlet New-WebApplication de IIS. El parámetro -Force actualiza la ruta física de la aplicación si esta ya existe, permitiendo despliegues "blue-green" a nivel de directorio.

Etiquetas: GitLab Runner .NET Framework 4.8 ASP.NET Web Forms IIS PowerShell

Publicado el 6-20 20:08