Classic ASP en Windows Server Containers

Todavía hay aplicaciones Classic ASP dando servicio en muchos entornos y... como es de esperar en tecnología de hace 18 años, no se adaptan especialmente bien a los nuevos paradigmas de computación como la nube o DevOps... Sin embargo, ¡Docker y los Windows Server Containers vienen al rescate!

8 min de lectura
Classic ASP en Windows Server Containers

Classic ASP es -seguramente para bien- uno de los grandes olvidados en la historia del desarrollo web con tecnologías Microsoft que fue rápidamente reemplazado por ASP.NET a partir del año 2002. No por ello no deja de haber un buen número de aplicaciones desarrolladas en ASP que claman por un plan urgente de modernización.

Voy a ser claro desde el inicio del artículo: si tienes una aplicación Classic ASP dando servicio en producción, o hoy una muy buena razón para ello o debería estar planificada la reescritura o reemplazo por alguna con tecnologías más modernas. Paradójicamente, si bien la última versión de Classic ASP -la 3.0- se publicó el 17 de febrero de 2000, Microsoft sigue soportándola en las versiones actuales de sus sistemas operativos: Windows Server 2016 y Windows 10. Pero a pesar de ello, recordermos que es tecnología descatalogada cuya última versión se publicó hace 18 años.

asp0

¿Classic ASP en un Windows Server Container? ¿Iba en serio?

Sí, totalmente en serio. No niego que resulta cuanto menos paradójico como los Windows Server Containers soportan Classic ASP pero no ASP.NET 1.1 (no confundir con ASP.NET Core 1.1). Afortunadamente es muy sencillo de entender.

Los Windows Server Containers y concretamente Docker es una tecnología que aterrizó en Windows por primera vez en Windows Server 2016 y Windows 10 1703, lo cual se traduce en: ¿quiere saber si tu aplicación Windows puede ejecutarse como contenedor? Intenta primero ejecutarla sin contenereizar bajo Windows Server 2016 y si lo consigues vas en el buen camino.

Windows Server 2016 no soporta .NET 1.1, de hecho, las últimas versiones de Windows en soportarlo fueron Windows Server 2008 y Windows Vista. Sin embargo Windows Server 2016 sí que soporta Classic ASP 3.0 y esto nos dá via libre para la contenerización.

Construyendo el contenedor de una aplicación Classic ASP

Como ocurre en muchas ocasiones, no es que servidor haya inventado la pólvora aquí. Patrick Scott ya creó un Windows Server Container para hospedar Classic ASP, publicando el Dockerfile en este hilo de StackOverflow. Aunque debo decir que el Dockerfile no cumple con las best practices recomendadas para minimizar el número de layers utilizados, lo cierto es que constituía una base estupenda para empezar.

Primera prueba de concepto: phpinfo()... aspinfo()

Por otro lado necesitaba alguna aplicación ASP para realizar la prueba de concepto. Lo primero que se me vino a la cabeza es el incombustible phpinfo(), una simple función de PHP que nos genera una página formateada completa con multitud de información de nuestro entorno, muy útil para depuraciones. ASP no tenía un equivalente de Microsoft, pero sí que muchos usuarios se lanzaron a desarrollarla. Efectivamente encontré dos:

  • Una desarrollada por Jereme Hancock en este repo de Github. Muy vistosa y completa pero con algún bug en el parsing de variables de sesión y cookies. La nombré como aspinfo.asp.
  • Otra en Planet Source Code de Dennis Pallett, más escueta pero que no le he visto problemas de funcionamiento. La nombré como aspinfolite.asp.

Una aplicación completa: ASP Chat

ASPChat es una aplicación prueba de concepto desarrolada por Stephen McDonald y publicada en Github. Consiste en un chat en tiempo real con el que podemos intercambiar mensajes de texto todos los usuarios que nos conectemos a dicha web.

Es una prueba estupenda para comprobar si ASP funciona correctamente.

El Dockerfile

Introducidos todos los conceptos el Dockerfile resultante es el siguiente:

# escape=`
FROM microsoft/iis:windowsservercore-1803

LABEL title="Classic ASP Chat Docker" `
    maintainer="Carlos Milán Figueredo" `
    version="latest" `
    contrib1="https://stackoverflow.com/questions/40099339/dockerize-asp-classic-on-iis" `
    contrib2="https://github.com/jeremehancock/aspinfo" `
    contrib3="http://planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=8048&lngWId=4" `
    contrib4="https://github.com/stephenmcd/aspchat" `
    url="https://calnus.com" `
    twitter="@cmilanf" `
    usage="docker run -it --rm -p 80:80 -p 443:443 cmilanf/aspchat" `
    thnaksto1="Beatriz Sebastián Peña"

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'; $VerbosePreference = 'Continue'; "]

RUN Install-WindowsFeature Web-ASP; `
    Install-WindowsFeature Web-CGI; `
    Install-WindowsFeature Web-ISAPI-Ext; `
    Install-WindowsFeature Web-ISAPI-Filter; `
    Install-WindowsFeature Web-Includes; `
    Install-WindowsFeature Web-HTTP-Errors; `
    Install-WindowsFeature Web-Common-HTTP; `
    Install-WindowsFeature Web-Performance; `
    Install-WindowsFeature WAS; `
    Import-Module IISAdministration;

RUN New-Item -Path c:\msi -ItemType Directory; `
    Invoke-WebRequest 'https://download.microsoft.com/download/C/9/E/C9E8180D-4E51-40A6-A9BF-776990D8BCA9/rewrite_amd64.msi' -UseBasicParsing -OutFile c:\msi\urlrewrite2.msi; `
    Start-Process -FilePath 'C:\\Windows\\System32\\msiexec.exe'  -ArgumentList '/i', 'c:\\msi\\urlrewrite2.msi', '/qn', '/norestart', '/L c:\\msi\urlrewrite2.log' -NoNewWindow -Wait; `
    Invoke-WebRequest 'https://download.microsoft.com/download/1/E/7/1E7B1181-3974-4B29-9A47-CC857B271AA2/English/X64/msodbcsql.msi' -UseBasicParsing -OutFile c:\msi\msodbcsql.msi; `
    Start-Process -FilePath 'C:\\Windows\\System32\\msiexec.exe' -ArgumentList '/i', 'c:\\msi\\msodbcsql.msi', 'IACCEPTMSODBCSQLLICENSETERMS=YES', 'ADDLOCAL=ALL', '/qn', '/norestart', '/L c:\\msi\\msodbcsql.log' -NoNewWindow -Wait

RUN Invoke-WebRequest 'https://raw.githubusercontent.com/jeremehancock/aspinfo/master/aspinfo.asp' -OutFile "C:\\inetpub\\wwwroot\\aspinfo.asp" ; `
    $aspchat = @('default.asp', 'functions_js.asp', 'functions_vbs.asp', 'constants_vbs.asp'); `
    foreach($file in $aspchat) { Invoke-WebRequest "https://raw.githubusercontent.com/stephenmcd/aspchat/master/$file" -OutFile "C:\\inetpub\\wwwroot\\$file" }

RUN & c:\windows\system32\inetsrv\appcmd.exe unlock config /section:system.webServer/asp; `
    & c:\windows\system32\inetsrv\appcmd.exe unlock config /section:system.webServer/handlers; `
    & c:\windows\system32\inetsrv\appcmd.exe unlock config /section:system.webServer/modules; `
    & c:\windows\system32\inetsrv\appcmd.exe set config -section:system.webServer/httpErrors -errorMode:Detailed; `
    & c:\windows\system32\inetsrv\appcmd.exe set config -section:asp -scriptErrorSentToBrowser:true

COPY aspinfolite.asp C:\inetpub\wwwroot\

EXPOSE 80

Vayamos viendo el archivo:

  • Línea 1. Cambiamos el carácter de escape del Dockerfile que por defecto es \. Honestamente, no utilicé esto en este otro Windows Server Container porque por aquel entonces no lo conocía. Si hacemos coincidir el caracter de escape de PowerShell con el de Dockerfile, veréis que se nos hace la vida mucho más fácil, en especial manejando rutas de sistemas de archivo Windows.
  • Línea 2. Partiremos de la imagen microsoft/iis:windowsservercore-1803, que nos deja listo un IIS. Microsoft continua en sus esfuerzos de adelgazar Windows Server, y muy notablemente la imagen de Server Core en su versión 1803 pesa unos 1,6 GB aproximados versus los 10 GB de la versión RTM de Windows Server 2016. Lamentablemente NanoServer no incorpora soporte Classic ASP.
  • Línea 15. Si no especificamos SHELL, en Windows se nos asigna cmd.exe, pero PowerShell nos va a dar mucha más flexibilidad.
  • Línea 18. Instalamos las features de Windows Server que vamos a necesitar, entre las que encontramos Web-ASP que es la crucial para nuestro propósito.
  • Línea 29. Instalamos un par de dependencias interesantes: el URL Rewrite y el driver ODBC de SQL Server, un clásico para conexión a base de datos en aplicaciones ASP.
  • Línea 35. Descargamos e instalamos en el Default Web Site las aplicaciones ya comentadas.
  • Línea 39. Nos aseguramos que las secciones de configuración de IIS relativas a ASP están habilitadas. Por otro lado como esto es una aplicación de prueba, habilitamos mostrar los errores por el navegador con independiencia de dónde venga el visitante.
  • Línea 45. No he encontrado un lugar fiable para descargar el ASPinfo de Dennis, por lo que decidí incorporarlo como archivo.
  • Línea 47. Exponemos el puerto 80/tcp; no vamos a entrar con temas de TLS para esta prueba.

Puede llamar la atención que este Dockerfile no tiene ENTRYPOINT ni CMD, ¿cómo va a funcionar? No hay de que preocuparse porque la imagen base microsoft/iis:windowsservercore-1803 es quien nos provee de uno.

Podemos construir la imagen Docker desde una máquina con Windows Server Containers mediante el método estándar:

C:\ASPchat> docker build -t mirepo/aspchat:latest .

Y para ejecutar la imagen desde el repositiorio de servidor:

C:\> docker run -d --rm --name aspchat cmilanf/aspchat
C:\> docker inspect aspchat

docker inspect aspchat nos devolverá entre muchas otras cosas la IP del contendor al que debemos conectarnos, en mi caso:

"IPAddress": "192.168.222.122"

El resultado:

asp1

asp2

asp3

¿Hemos terminado? Me temo que no...

Session State

Lo que acabamos de conseguir es genial... si nuestra aplicación fuera a ser un único contenedor. Si mantenemos esto así, ¿que ocurre con la alta disponibilidad? ¿las capacidades de autosanado? ¿el cambio de versión inteligente? ... Muchas de las capacidades de los contenedores se nos caen y... acabamos tratando nuestro contenedor de aplicación como si fuera una máquina virtual. Servidor siempre piensa que si vas a tratar tu aplicación contenerizada como una máquina virtual, quizás debiste quedarte en una máquina virtual (excepciones aplican).

Tal y como tenemos nuestra aplicación no podemos escalarla a múltiples contenedores, ¿por qué? Porque el estado de la sesión o Session State está en la memoria del proceso de IIS, por lo que si un usuario salta a otro servidor en un balanceo perderá toda la información de su sesión y lo que estuviera haciendo con la aplicación. En otras palabras: tendrá que iniciar la sesión y empezar desde 0 rezando porque no ocurra otra vez.

Recordemos siempre que los contenedores deben vivir y morir dinámicamente sin que esto nos importe, por lo que el estado de la aplicación no debe residir en ese contenedor.

El siguiente esquema de Microsoft muestra los modelos de almacenaje del estado de sesión en ASP.NET:

asp4

¿Cuales de ellos soporta Classic ASP? Sólo InProc... ¿Estamos sin opciones? Afortunadamente no.

COM al rescate con SQL Server

El modelo de objetos de ASP podía extenderse utilizando DLLs con interfaces COM y justamente esto nos proporciona una vía de escape para la situación del Session State.

En este artículo de Microsoft se nos muestra como crear un objeto COM que maneje el estado de la sesión de nuestra aplicación almacenándolo en la base de datos SQL Server. Una vez creado este objeto siguiendo las directrices del artículo sólo tenemos que usarlo en nuestro código ASP en lugar de estándar de manejo de sesión.

El artículo es del año 2003 y se escribió en un contexto de proyectos a largo plazo migración de aplicaciones ASP a ASP.NET donde era necesaria una situación de coexistencia entre los dos lenguajes y... 15 años después nos viene de lujo para hacer una implementación adecuada como contenedor de nuestra aplicación.

¿Y otras alternativas actuales como Redis o Consul?

En realidad utilizando el anterior modelo COM podemos extender ASP con lo que queramos. De hecho el objeto COM puede estar desarrollado perfectamente en C#, tal y como muestra el siguiente ejemplo de cliente Redis como objeto COM pensado justamente para utilizar como caché en aplicaciones ASP clásico.

Conclusiones

Docker nos brinda una oportundiad excelente de modernizar nuestras ancianas aplicaciones web ASP para que puedan aprovechar nuevos paradigmas como la nube o DevOps, siempre que consigamos superar retos de adaptación como el del Session State. A pesar de ello, considero esta acción una solución de contingencia para extender la vida de la aplicación disminuyendo drásticamente sus costes de mantenimiento; pero sigo insistiendo en que el objetivo final debería ser su decomisión, reconstrucción o reemplazo.

Happy Windows Server Containers!