Fri, 05 Mar 2010

Using Other Web Servers on Windows Azure

Ryan Dunn and I recently recorded the third episode of our new Channel 9 show, Cloud Cover, which should go live on Channel 9 tomorrow (Friday). In this week’s episode, I do a demo of a worker role with an input endpoint. That’s a fancy way to say I use a non-IIS web server in Windows Azure.

The technique I demonstrate is exactly the same as what’s used by our Tomcat Solution Accelerator, but it’s also been used to run Apache, Mongrel, and other web servers. In this post, I’ll show how to run Mongoose, a tiny web server, in the cloud.

Background: Endpoints in Windows Azure

In Windows Azure, all inbound communication happens via endpoints declared in ServiceDefinition.csdef. There are two types of endpoints: input and internal. Input endpoints are those that are exposed to the internet, and internal endpoints are used for communication inside your application.

Most of the time, you’ll use input endpoints with Windows Azure web roles, where IIS handles incoming requests and routes them to your web application. To use a web server other than IIS (or another kind of server, like SMTP or FTP), you’ll need to instead use a worker role.

Worker Roles with Input Endpoints

To use an input endpoint with a worker role, you need to do two things:

  1. Define the input endpoint in your service definition.
  2. Listen for inbound traffic on the correct port.

Defining Your Input Endpoint

Defining an input endpoint is easy. If you’re using Visual Studio, you can simply double-click on your worker role and add a new input, like you see below.

adding an endpoint - screenshot

Notice that I’ve chosen TCP as the protocol, not HTTP. If I were to choose HTTP, traffic would come through http.sys, which I don’t want. TCP gives me the same kind of access as if I launched the web server locally and started listening on the port directly. I’m choosing port 80, because that’s the standard HTTP port.

Under the covers, this is the same as adding the following bit of XML to ServiceDefinition.csdef:


    <Endpoints>
      <InputEndpoint name="WorkerIn" protocol="tcp" port="80" />
    </Endpoints>

Listening for Traffic on the Right Port

When you specify a port in your endpoint definition, that’s the port that Windows Azure will expose to the internet via the load balancer. Traffic that comes through the load balancer is then routed to your application on a different port, which is chosen at runtime by Windows Azure.

That means you’ll need to call the Windows Azure runtime API to determine which port your application should listen on. The following line of code queries the API for our endpoint and determines the correct port:

int port = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["WorkerIn"].IPEndpoint.Port;

Launching a Web Server

At this point, we have an endpoint, and we know what port we need to listen on. From our code, we could now start listening for traffic via WCF, TCPListener, or anything else.

In our case, however, we want to run an external process (our web server) to handle the traffic. The following is the full code for our worker role’s Run() method, which launches a lightweight web server called Mongoose.

public override void Run()
{
    string mongooseroot = Path.Combine(Environment.GetEnvironmentVariable("RoleRoot") + @"\", @"approot\mongoose");
    int port = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["WorkerIn"].IPEndpoint.Port;

    Process p = new Process()
    {
        StartInfo = new ProcessStartInfo(Path.Combine(mongooseroot, @"mongoose-2.8.exe"), "-ports " + port)
        {
            UseShellExecute = false,
            WorkingDirectory = mongooseroot
        }
    };

    p.Start();
    p.WaitForExit();
    throw new Exception("Mongoose quit on me!");
}

[UPDATE 3/26/2010: This code is sub-optimal. This initialization should really happen in OnStart(), not Run(). See my new blog post “Do Initialization in OnStart()” for details and updated code.]

The first line is probably the most difficult to get right if you try to write this code yourself. This line is responsible for finding the correct path to our Mongoose binary (and web content). It uses the path %RoleRoot%\approot. This is where the code for your role sits on the virtual machine in the cloud.

There’s a bit of a gotcha here, which is that you really need to add the backslash to %RoleRoot% before using Path.Combine(). If you don’t do this, your code will run fine on your local development machine, but it will fail in the cloud. This is because the local path to your role root is a directory, while in the cloud, it’s a drive. In the cloud, you’ll get a value like E: (note there’s no trailing backslash). When you add approot to that, you’ll end up with E:approot, which is not correct. (It should be E:\approot.) To be safe in both locations, make sure to explicitly add the trailing backslash.

The second line is one we saw above. This captures the correct port in a local variable.

Next we launch a new Mongoose process, passing the correct port on the command-line, setting the correct working directory (so Mongoose can find our web content).

Finally, we wait forever for the Mongoose process to exit. Mongoose, like your Run() method, should never exit unless there’s an error, so when Mongoose exits, we raise an exception and exit ourselves. This will indicate to Windows Azure that something has gone wrong, and our role will get restarted.

Packaging the Web Server with Your Role

VS file properties - screenshotThe above code assumes that Mongoose is part of my deployed worker role. To make sure that Mongoose and my web content are actually packaged as part of the role during my build process, I need to set them in Visual Studio to always copy to the output directory when built. See Jim Nakashima’s blog post “Adding Files to your Windows Azure Service Package” for a full explanation.

Try It Out

I’ve zipped up everything (except the Mongoose binary) so you can try this sample yourself. To get the application working, just download Mongoose and place it in the Mongoose folder under the worker role.

Note that because you don’t have an HTTP input endpoint, Visual Studio doesn’t know to launch the web browser. To view the application once it’s running, open up the Development Fabric UI and check what port you’re on (should be port 80, or often 81 or 82 if you’re running other things). Then just browse to localhost on that port in the browser.

Download the full VS2010 RC solution here.

It’s also running in the cloud (for a week or so until I pull it down) at http://workerinput.cloudapp.net.