Perl en Windows Azure con la plantilla FastCGI

Uno de los retos con el que he estado lidiando últimamente es con la posibilidad de ejecutar código Perl en Windows Azure :D Para ello voy a mostrar cómo es posible, haciendo uso de la plantilla pensada para Fast CGI con algunas modificaciones.

Descarga de Strawberry for Perl

El primer paso que debemos dar antes de introducirnos en Visual Studio es descargar el intérprete de Perl. En este caso voy a utilizar Strawberry for Perl, el cual podemos obtener del siguiente enlace. Una vez descargado, ejecutamos el msi para generar el directorio strawberry en nuestro disco local. El mismo contendrá las librerías necesarias para interpretar código Perl.

Creación del proyecto de Windows Azure

Abrimos Visual Studio en modo administrador y creamos un nuevo proyecto del tipo Windows Azure Project. Como rol eligiremos CGI Web Role.
Pulsamos en el botón OK para que genere la solución y conseguir de este modo la siguiente estructura:

Cuando tenemos un proyecto de este tipo, lo más importante es adjuntar y configurar el interprete que vamos a utilizar para nuestro código. En este caso, vamos a añadir el directorio de strawberry para que el mismo sea subido a Azure junto con nuestra aplicación. La forma más fácil de realizar este paso sería ubicar una copia de la carpeta strawberry en el directorio del proyecto WebCgiRole para posteriormente poder incluirlo.

Modificación de los archivos de configuración

En este tipo de proyectos existen dos archivos de configuración: Web.roleconfig y Web.config. En el primero de ellos debemos establecer cuál es el intérprete que vamos a utilizar.
<?xml version="1.0"?>

<configuration>
  <system.webServer>
    <fastCgi>
      <!-- Set the "Enable Full Trust" property to true on the corresponding
           Web Role in the Cloud Service project.
      -->

      <!-- Define the fastCgi application here. The application must be located
           under the role root folder or subfolder. The handler is defined in
           the Web.config file.

           Ensure that all of the handler binaries have their "Build Action" Solution
           Explorer file property set to "Content".

      <application fullPath="%RoleRoot%approotcgi-handler.exe" arguments="arg1 arg2 ..." />
      -->
      <application fullPath="E:approotstrawberryperlbinperl.exe"/>
    </fastCgi>

  </system.webServer>
</configuration>
Por otro lado, en el archivo Web.config debemos especificar el handler o handlers que vamos a utilizar cuando nuestra aplicación reciba peticiones con extensiones .cgi y .pl
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
    <handlers>
      <!-- Define your fastCgi handler here. The scriptProcessor value should map to the
             application's fullpath and arguments values in the Web.roleconfig file.

        <add name="FastGGI Handler"
             verb="*"
             path="{file or extension, ex: *.php}"
             scriptProcessor="%RoleRoot%approotcgi-handler.exe|arg1 arg2 ..."
             modules="FastCgiModule"
             resourceType="Unspecified" />
           -->
      <add name="CGI Handler" verb="*" path="*.cgi"
           scriptProcessor="E:approotstrawberryperlbinperl.exe % s %s"
           modules="CgiModule"
           resourceType="Unspecified" />
      <add name="PL Handler" verb="*" path="*.pl"
           scriptProcessor="E:approotstrawberryperlbinperl.exe % s %s"
           modules="CgiModule"
           resourceType="Unspecified" />
    </handlers>
    <defaultDocument>
      <files>
        <add value="pruebaperl.cgi" />
      </files>
    </defaultDocument>
  </system.webServer>
</configuration>
Lo que conseguimos con ello es crear handlers de la misma forma que si lo hiciéramos desde el apartado Mapping Handlers de la interfaz de Internet Information Services.

ISAPI and CGI Restrictions

Para que todo el proceso tenga éxito, aún queda un asunto pendiente. Para poder ejecutar el proceso perl.exe es necesario añadir el mismo a la lista de ISAPI and CGI Restrictions.
Esta operación podemos realizarla a través de la consola de comandos, haciendo uso de la sección Startup del archivo de Definición de Windows Azure. En el vamos a definir un ejecutable llamado EnableIsapi.bat donde vamos a registrar lo necesario para poder hacer uso de nuestro interprete sin que el mismo sea bloqueado por IIS.
<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="PerlFastCGI" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="WebCgiRole" enableNativeCodeExecution="true">
    <Startup>
      <Task commandLine="EnableIsapi.bat" executionContext="elevated" taskType="simple"></Task>
    </Startup>
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Endpoint1" endpointName="Endpoint1" />
        </Bindings>
      </Site>
    </Sites>
    <Endpoints>
      <InputEndpoint name="Endpoint1" protocol="http" port="80" />
    </Endpoints>
    <Imports>
      <Import moduleName="Diagnostics" />
      <Import moduleName="RemoteAccess" />
      <Import moduleName="RemoteForwarder" />
    </Imports>
  </WebRole>
</ServiceDefinition>
El contenido del .bat será el que sigue:
setlocal enableextensions enabledelayedexpansion

set appcmd="%windir%system32inetsrvappcmd"

%appcmd% set config -section:isapiCgiRestriction /-[path='"e:approotstrawberryperlbinperl.exe %% s %%s"'] /commit:apphost

%appcmd% set config -section:isapiCgiRestriction /+[path='"e:approotstrawberryperlbinperl.exe %% s %%s"'] /commit:apphost

%appcmd% set config -section:isapiCgiRestriction /[path='"e:approotstrawberryperlbinperl.exe %% s %%s"'].allowed:true /commit:apphost

exit /b 0

Subiendo nuestro proyecto a Windows Azure

Para comprobar que la configuración del proyecto de Windows Azure está definida correctamente, vamos a crear dos nuevos archivos: El primero de ellos se llamará pruebaperl.cgi el cual será ejecutado por defecto cuando entremos en nuestro sitio raíz.

#!/perl

print "Content-type: text/htmlrnrn";
for ($i=1; $i<=10; $i++)
{
  print "
	<li> - Prueba de ejecucion Iteracion: $i n";
}

El segundo se llamará prueba.pl donde añadiremos el siguiente contenido:

#!/perl

print "Content-type: text/htmlrnrn";
for ($i=1; $i<=10; $i++)
{
  print "
	<li> - Prueba de ejecucion en pl Iteracion: $i n";
}

Subimos la aplicación al portal de Windows Azure de la forma habitual y esperamos hasta que el estado de la misma se torne a Ready.

Si accedemos al DNS seleccionado deberíamos obtener el siguiente resultado:

Por otro lado, si añadimos a la ruta http://myDNS.cloudapp.net/prueba.pl el resultado esperado sería:

Espero que haya sido de utilidad :D

¡Saludos!

Segunda edición del máster RIAtec

Después del éxito de la primera edición, este año vuelve el máster RIAtec cargado de todas las nuevas tecnologías y el apoyo de grandes profesionales dentro del sector :)
El objetivo de dicho máster es proporcionar al alumno una amplia visión práctica, completando la misma con 9 meses de prácticas en una de las grandes empresas que colaboran con RIAtec.

Para más información sobre el mismo podéis acceder al sitio web del máster, así como a su cuenta de Facebook y Twitter.

¡Feliz semana!

Variables de entorno en Windows Azure

Una de las necesidades que puede tener nuestro servicio en la nube es registrar variables de entorno dentro de nuestras máquinas virtuales. En este post vamos a ver las distintas opciones que tenemos para llevar a cabo esta tarea.

Sección Runtime en ServiceDefinition.csdef

La primera posibilidad a la hora de declarar variables de entorno sería a través de la sección Runtime en el archivo de definición del role.

<?xml version="1.0" encoding="utf-8"?>
<ServiceDefinition name="WindowsAzureEnvironmentVariables" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition">
  <WebRole name="WebRole">
    <Runtime executionContext="elevated">
      <Environment>
        <Variable name="EnviromentVariable" value="Hello from environment variable!"/>
      </Environment>
    </Runtime>
    <Sites>
      <Site name="Web">
        <Bindings>
          <Binding name="Endpoint1" endpointName="Endpoint1" />
        </Bindings>
      </Site>
    </Sites>
    <Endpoints>
      <InputEndpoint name="Endpoint1" protocol="http" port="80" />
    </Endpoints>
    <Imports>
      <Import moduleName="Diagnostics" />
    </Imports>
    <ConfigurationSettings>
    </ConfigurationSettings>
  </WebRole>
  <WorkerRole name="WorkerRole">
    <Imports>
      <Import moduleName="Diagnostics" />
    </Imports>
  </WorkerRole>
</ServiceDefinition>

Sin embargo desde la aparición de Full IIS donde nuestro servicio tiene total disponibilidad sobre IIS, e Internet Information Service Hosteable Web Core pasa a un segundo plano, esta opción ya no funciona con propiedad en aquellos roles de tipo Web. El motivo es que esta sección solamente nos permite declarar variables de entorno a nivel de proceso. Para comprobarlo podemos comentar la sección Sites, la cual habilita Full IIS, en el archivo de definición y comprobar de esta forma que podemos recuperar dicha variable a través de la clase Environment sin problemas.

Environment.GetEnvironmentVariable("EnviromentVariable");

De lo contrario, al intentar realizar la misma acción mostrada en el código anterior, el resultado de la misma será null.

En el caso de los worker role no es necesario tomar ninguna medida adicional, a no ser que utilicemos múltiples procesos para llevar a cabo las tareas.

Nota: Es importante especificar el contexto de ejecución como elevated ya que, de no ser así, el servicio no arrancará (suele aparecer el mensaje de Aborted…).

Clase Environment

La opción más efectiva a la hora de declarar las variables de entorno sigue siendo a través de la clase Environment. Dentro de la misma tenemos varios métodos que nos permitirán definir las variables necesarias con distintos ámbitos. Para ello podemos definir una variable cualquiera de la siguiente forma:

Environment.SetEnvironmentVariable("MyNewVariable","New variable for Windows Azure!");

De esta manera lo que estamos haciendo es declarar la variable llamada MyNewVariable con el valor que le sigue en la declaración pero a nivel de proceso, es decir que si cualquier otro proceso que intentara recuperar dicha variable obtendría como resultado null. Por ello, a través de una sobrecarga en este mismo método podemos especificar cuál es el ámbito que queremos que tenga nuestra variable (funcionalidad todavía no disponible en la sección Runtime de ServiceDefinition.csdef :( ). Generalmente podemos necesitar tres tipos de ámbitos distintos:

using System;
using Microsoft.WindowsAzure.ServiceRuntime;

namespace WebRole
{
    public class WebRole : RoleEntryPoint
    {
        public override bool OnStart()
        {
            Environment.SetEnvironmentVariable("MyNewVariable", "New variable for Windows Azure!"); //Current Process
            Environment.SetEnvironmentVariable("MachineVariable", "Hello", EnvironmentVariableTarget.Machine); //All processes
            Environment.SetEnvironmentVariable("UserVariable", "Hello", EnvironmentVariableTarget.User); //All user processes

            return base.OnStart();
        }
    }
}

Como podemos ver en el código anterior, dependiendo del target asignado a la hora de la definición de la variable la misma tendrá menor o mayor acceso por parte de los procesos que intenten recuperarlas.

Sección Startup en ServiceDefinition.csdef

Por último una de las necesidades más comunes es la modificación de variables ya existentes como es el caso de Path. Para ello la manera más eficaz sería utilizando el comando REG ADD desde la sección Startup donde podemos lanzar ejecutables. Lo primero que debemos saber es dónde están los valores de las variables de entorno en el registro. Para evitaros la búsqueda, las mismas se encuentran en HKEY_LOCAL_MACHINESystemCurrentControlSetControlSession ManagerEnvironment :) .

En el caso de Path vamos a crear un archivo txt e incluimos el siguiente comando:

REG ADD "HKEY_LOCAL_MACHINESystemCurrentControlSetControlSession ManagerEnvironment" /v "Path" /t REG_EXPAND_SZ /d "%PATH%;%ProgramFiles%Microsoft SQL Server100ToolsBinn" /f

Con él vamos a conseguir agregar al contenido que ya tenía Path la ruta de Microsoft SQL Server.

Espero que sea de utilidad :D

¡Saludos!