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.
- 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_scriptregistra la función de PowerShell, ya que el script del servidor contiene una definición de función. - El script
Invoke-BuildWebes 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.batmaneja la descarga de archivos de configuración. Luego se limpia la caché HTTP de NuGet. - La compilación con
msbuild.exese realiza en dos fases: restauración y compilación propiamente dicha. - El conmutador
/verbosity:minimalreduce la cantidad de registro. Para diagnóstico, usar/verbosity:diagnostic. - El objetivo
/t:_CopyWebApplicationrequiere que el archivo.csprojdel proyecto web lo importe (detalles en documentación anterior). - Es necesario establecer tanto
WebProjectOutputDir(para archivos de contenido como .aspx, .css) comoOutputPath(para ensamblados .dll).
Descripción de los cuatro parámetros:
$msbuildDir: Ruta al directorio que contienemsbuild.exe.$projectRoot: Directorio de trabajo asignado por GitLab Runner al clonar el repositorio (variable internaCI_PROJECT_DIR).$targetProjectPath: Ruta relativa al archivo.csprojdel 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.
- 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 esD:\apps, este valor representaapps, 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:
- Prepara los parámetros, sanitizando la versión y escapando comillas simples.
- 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.
- 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-WebApplicationde IIS. El parámetro-Forceactualiza la ruta física de la aplicación si esta ya existe, permitiendo despliegues "blue-green" a nivel de directorio.