2005 2006 2007 2008 2009 2010 2011 2015 2016 2017 aspnet azure csharp debugging elasticsearch exceptions firefox javascriptajax linux llblgen mongodb powershell projects python security services silverlight training videos wcf wpf xag xhtmlcss

PowerShelling Azure DNS Management

DNS is one of those topics that every developer has to deal with. It's also one of those topics some of us developers get to deal with. I love DNS. It allows all manner of flexible addressing, failover, and load-balancing techniques. Understanding DNS takes you a long way in a whole lot of scenarios.

However, the difference between the fun architecture and possibilities of DNS and the implementation has often been the difference between a great sounding class syllabus and the horrible reality of a boatload of homework. Windows DNS, for example, was historically cryptic GUI gibberish. BIND was OK and has a very simple config format, but it scared most people off (you simply cannot win with people who hate GUIs and command lines).

Azure DNS lets you config via the GUI (noooooob; jk, the GUI is pretty nice), ARM templates, Azure CLI (=Linux), and PowerShell.

But, HOW do you manage the config? What format? How do you deploy?

One way is to do everything in ARM templates. Check out this example:


  "resources": [
    {
      "type": "Microsoft.Network/dnszones",
      "name": "example.org",
      "apiVersion": "2016-04-01",
      "location": "global",
      "properties": { }
    },
    {
      "type": "Microsoft.Network/dnszones/a",
      "name": "example.org/mysubdomain01",
      "apiVersion": "2016-04-01",
      "location": "global",
      "dependsOn": [
        "example.org"
      ],
      "properties": {
        "TTL": 3600,
        "ARecords": [{
            "ipv4Address": "127.0.0.1"
          }
        ]
      }
    }
  ],


There's nothing wrong with that. If you want to have a billion lines of JSON for the sake of a single DNS record, you have fun with that. Ugh. Fine. Yes, it will be idempotent, but are severe overkill for quick updates.

Because PowerShell is effectively the .NET REPL, you can write a simple PowerShell (=.NET) tool to handle any custom format you want.

The following is one way of formatting your DNS updates:


    @(
        @{
            Name="example1"
            Config=@(
            @{
                Type="CNAME"
                Value="hosting01gyfir-alpha.centralus.cloudapp.azure.com"
            })
        },
        @{
            Name= "example2"
            Config=@(
            @{
                Type="A"
                Value="127.0.0.1"
            })
        },
        @{
            Name= "mail"
            Config=@(
            @{
                Type="MX"
                Preference=10
                Exchange="mail.example.com"
            },
            @{
                Type="MX"
                Preference=20
                Exchange="mail2.example.org"
            })
        }
    )


I donno. I find that pretty simple. I'll use that when I setup something new.

Here's my function (with call) to make that work:


    function deployDns { param([Parameter(Mandatory=$true)]$rg, [Parameter(Mandatory=$true)]$zonename, [Parameter(Mandatory=$true)] $ttl, [Parameter(Mandatory=$true)]$records, [bool]$testOnly)
        ($records).foreach({
            $name = $_.Name
            Write-Host "Name: $name"
            $dnsrecords = @()
    
            ($_.Config).foreach({
                $config = $_
                $type = $config.Type
                switch($type) {
                    "CNAME" {
                        Write-Host ("`tCNAME: {0}" -f $config.Value)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Cname $config.Value
                    }
                    "MX" {
                        Write-Host ("`tPreference: {0}" -f $config.Preference)
                        Write-Host ("`tExchange: {0}" -f $config.Exchange)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Preference $config.Preference -Exchange $config.Exchange
                    }
                    "A" {
                        Write-Host ("`tPreference: {0}" -f $config)
                        Write-Host ("`tExchange: {0}" -f $config.Ipv4Address)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Ipv4Address $config.Value
                    }
                    "AAAA" {
                        Write-Host ("`tIpv6Address: {0}" -f $config.Ipv6Address)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Ipv6Address $config.Value
                    }
                    "NS" {
                        Write-Host ("`tNS: {0}" -f $config.Value)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Nsdname $config.Value
                    }
                    "PTR" {
                        Write-Host ("`tPtrdname: {0}" -f $config.Value)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Ptrdname $config.Value
                    }
                    "TXT" {
                        Write-Host ("`tPtrdname: {0}" -f $config.Value)
                        $dnsrecords += New-AzureRmDnsRecordConfig -Value $config.Value
                    }
                }
            })
            Write-Host
            Write-Host "Records:"
            Write-Host $dnsrecords
            if(!$testOnly) {
                New-AzureRmDnsRecordSet -ResourceGroupName $rg -ZoneName $zonename -RecordType $type -Name $name -Ttl $ttl -DnsRecords @dnsrecords
            }
            Write-Host
            Write-Host
        })
    }
    
    deployDns -testOnly $true -rg 'davidbetz01' -zonename "davidbetz.net" -ttl 3600 -records @(
        @{
            Name="example1"
            Config=@(
            @{
                Type="CNAME"
                Value="hosting01gyfir-alpha.centralus.cloudapp.azure.com"
            })
        },
        @{
            Name= "example2"
            Config=@(
            @{
                Type="A"
                Value="127.0.0.1"
            })
        },
        @{
            Name= "mail"
            Config=@(
            @{
                Type="MX"
                Preference=10
                Exchange="mail.example.com"
            },
            @{
                Type="MX"
                Preference=20
                Exchange="mail2.example.org"
            })
        }
    )


Is this insane? Probably. That's what I'm known for. ARM templates might be smarter given their idempotent nature, and I've found myself using the GUI now and again.

For now, just keep in mind that PowerShell lets you me ultra flexible with your Azure configuration, not just Azure DNS.

Allowing access to your Azure VM

You created your VM, you installed and configured all services, and your firewalld/iptables is set correctly. Your nmap tests are even working between systems.

But, you can't access your services external to Azure?

You probably didn't enable access in Azure. You need to allow specific ports in your Azure Network Security Group.

In terms of your Azure objects, your VM uses a NIC, your NIC uses an NSG.

Using PowerShell

Using PowerShell, you can do something like this:

    
    $rg = 'hosting01'
    
    $nsg = Get-AzureRmNetworkSecurityGroup -ResourceGroupName $rg -Name "$rg-nsg-alpha"
    
    $maximum = ($nsg.SecurityRules | measure -Property priority -Maximum).Maximum + 100
    $httpRule = New-AzureRmNetworkSecurityRuleConfig -Name "http" -Protocol Tcp -SourceAddressPrefix * -DestinationAddressPrefix * -SourcePortRange * -DestinationPortRange 80 -Priority $maximum -Description "HTTP" -Direction Inbound -Access Allow
    $nsg.SecurityRules.Add($httpRule)
    
    $maximum = ($nsg.SecurityRules | measure -Property priority -Maximum).Maximum + 100
    $httpsRule = New-AzureRmNetworkSecurityRuleConfig -Name "https" -Protocol Tcp -SourceAddressPrefix * -DestinationAddressPrefix * -SourcePortRange * -DestinationPortRange 443 -Priority $maximum -Description "SSL" -Direction Inbound -Access Allow
    $nsg.SecurityRules.Add($httpsRule)
    
    Set-AzureRmNetworkSecurityGroup -NetworkSecurityGroup $nsg


Use an ARM Template

Or you can just fix your initial ARM template by adding the resource:

See the https://linux.azure.david.betz.space/_/python-uwsgi-nginx on https://linux.azure.david.betz.space for a fuller example.


    {
        "comments": "",
        "type": "Microsoft.Network/networkSecurityGroups",
        "name": "nsg-alpha",
        "apiVersion": "2017-03-01",
        "location": "[resourceGroup().location]",
        "properties": {
            "securityRules": [
                {
                    "name": "default-allow-ssh",
                    "properties": {
                        "protocol": "Tcp",
                        "sourcePortRange": "*",
                        "destinationPortRange": "22",
                        "sourceAddressPrefix": "*",
                        "destinationAddressPrefix": "*",
                        "access": "Allow",
                        "priority": 1000,
                        "direction": "Inbound"
                    }
                },
                {
                    "name": "http",
                    "properties": {
                        "protocol": "Tcp",
                        "sourcePortRange": "*",
                        "destinationPortRange": "80",
                        "sourceAddressPrefix": "*",
                        "destinationAddressPrefix": "*",
                        "access": "Allow",
                        "priority": 1100,
                        "direction": "Inbound"
                    }
                },
                {
                    "name": "https",
                    "properties": {
                        "protocol": "Tcp",
                        "sourcePortRange": "*",
                        "destinationPortRange": "443",
                        "sourceAddressPrefix": "*",
                        "destinationAddressPrefix": "*",
                        "access": "Allow",
                        "priority": 1200,
                        "direction": "Inbound"
                    }
                }
            ]
        },
        "resources": [],
        "dependsOn": []
    }


Add this to your NICs (Microsoft.Network/networkInterfaces) properties:

    
    "type": "Microsoft.Network/networkInterfaces",
    "properties": {
        "networkSecurityGroup": {
            "id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat(variables('nsg-prefix'), variables('names')[0]))]"
        }
    }


...and dependsOn section:


    
    "dependsOn": [
        "[resourceId('Microsoft.Network/networkSecurityGroups', concat(variables('nsg-prefix'), variables('names')[0]))]"
    ]


Using Azure DNS

Though you might be used to doing DNS via bind, there's nothing specific about bind that defines DNS. You can do DNS anywhere; all DNS details are blackboxed.

As such, Azure DNS is perfectly compatible with being a super Linux geek.

You can review the docs for Azure DNS on your own, but I'd like to provide a few "recipes".

Everything that follows assumes that you've done the prerequisite work of getting an active Azure account, creating your resource group (New-AzureRmResourceGroup) and creating a new DNS zone (New-AzureRmDnsZone).


New-AzureRmResourceGroup -Name spartan01
New-AzureRmDnsZone -ResourceGroupName spartan01 -Name "example.com"

In moving to Azure DNS, you have a few things that you need to do. These have NOTHING to do with Azure -- they're just facts of DNS.

PowerShell will be used for the most part, but the Azure CLI syntax will be provided where it's particularly interesting.

E-Mail (MX Records)

First, you need to realize that DNS affects your e-mail. This should be obvious since your e-mail has a domain name right in it. So, you need to make sure your MX records are legit.

Here's a block of PowerShell (UpdateAzureDNSMX.ps1) you should keep around:


    New-AzureRmDnsRecordSet -ResourceGroupName spartan01 -ZoneName "example.com" -RecordType A -Name "mail" -Ttl 3600 -DnsRecords (New-AzureRmDnsRecordConfig -Ipv4Address '203.0.113.1')
    
    New-AzureRmDnsRecordSet -ResourceGroupName spartan01 -ZoneName "example.com" -RecordType MX -Name "mail" -Ttl 3600 -DnsRecords @(
        (New-AzureRmDnsRecordConfig -Preference 10 -Exchange smtp.example.com),
        (New-AzureRmDnsRecordConfig -Preference 20 -Exchange mailstore1.example.com)
    )

This creates a record set, then adds the specific config.

Single IP Address

Let's say you have a bunch of sites and want a single server to handle them all. Let's also assume that you only have one IP address.

You should look into using WebApps, but let's pretend you absolutely need a VM for some wild reason.

What to do?

Simple: CNAME it.

After creating your shiny new VM (perhaps running ./create simple hosting01 from https://linux.azure.david.betz.space), you need to get the systems IP address:

Azure CLI

[dbetz@callisto ~]$ az network public-ip list --query "[?dnsSettings.domainNameLabel!=null]|[?starts_with(dnsSettings.domainNameLabel,'hosting01')].{ dns: dnsSettings.fqdn  }"           
[
  {
    "dns": "hosting01figrg-alpha.centralus.cloudapp.azure.com"
  }
]

PowerShell


    (Get-AzureRmPublicIpAddress -ResourceGroupName hosting01 -Name hosting01-ip-alpha).DnsSettings.Fqdn

Let's use that Azure domain name to create our CNAME records for our domains:


    function create { param($name)
        New-AzureRmDnsRecordSet -ResourceGroupName spartan01 -ZoneName "example.com" -RecordType CNAME -Name $name -Ttl 3600 -DnsRecords (New-AzureRmDnsRecordConfig -Cname "hosting01figrg-alpha.centralus.cloudapp.azure.com")
    }
    create "subdomain01" 
    create "subdomain02" 
    create "subdomain03" 
    create "subdomain04" 

For the sake of this example, all domains will be as subdomains. Doing full, separate domains is the same, but the examples would be longer... and less fun. It's the same idea, though.

If you already have the records (e.g. you move to another VM later), you update:

    
    function update { param($name)
        $rs = Get-AzureRmDnsRecordSet -ResourceGroupName spartan01 -ZoneName "example.com"  -RecordType CNAME -Name $name
        $rs.Records[0] = (New-AzureRmDnsRecordConfig -Cname "hosting01figrg-alpha.centralus.cloudapp.azure.com")
        Set-AzureRmDnsRecordSet -RecordSet $rs
    }
    update "subdomain01" 
    update "subdomain02" 
    update "subdomain03" 
    update "subdomain04" 

Now you have your domains all pointing to the same place.

Stopping here doesn't help you. You still have to figure out how to get your server to handle the multiple sites! Though it's not directly Azure related, I find partial examples to be distasteful and a sign of extreme ignorance on the part of an author.

So, what do you do on the server?

Let's assume you're reading this after 2016 (I'm writing this in 2017, sooooo...), therefore you're not using Apache. You're using Nginx:

server {
    listen 80;

    server_name mysubdomain01.example.com;
    
    return 301 https://mysubdomain01.example.com$request_uri;
}

server {
    listen 443 ssl http2;

    server_name mysubdomain01.example.com;

    ssl_certificate /etc/letsencrypt/live/mysubdomain01.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mysubdomain01.example.com/privkey.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    ssl_prefer_server_ciphers on;

    ssl_dhparam /srv/_cert/dhparam.pem;

    location / {
        add_header Strict-Transport-Security max-age=15552000;
        proxy_pass http://127.0.0.1:8081;        
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header Host mysubdomain01.example.com;
    }
}

I'm not into trivial examples, so the full SSL example is provided, with the letsencrypt paths.

The point is this:

  • listen on the port without the IP
  • server the server_name to your full domain name

Done.

Well, no, we're not done to my satisfaction. You still need some ability to test this. So, here's a quick Node.js application you can use to listen on port 80. Just curl from somewhere else and see if it works.

curl --silent --location https://rpm.nodesource.com/setup_8.x | bash -
yum -y install nodejs

cd
cat > server.js << EOF
http = require('http');
port = parseInt(process.argv[2]);
server = http.createServer( function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end(req.method + ' server ' + port);
});
host = '$PUBLIC_IP';
server.listen(port, host);
EOF

node server.js 8081 &

Azure CDN

When using assets (e.g. png, css) on your website, you'll want to be sure to serve them via an Azure CDN.

I tend to avoid the term "blob" as that's a binary entity -- the moment you start talking about "SVG blobs" or "CSS blobs", you lose any connection to reality. They're assets.

If your CDNS provider (Azure uses both Akamai and Verizon) supports SSL for custom domains, then great. If not, you're stuck with the mycdnendpoint.azurewebsites.net address. You can consider HTTP obsolete. Use SSL or do not host anything.

Assuming your CDN provider supports SSL for custom domains, you have to tell it about your custom domain.

It requires that you prove ownership by creating a cdnverify CNAME entry. Most of the docs are deeply cryptic on this (honestly, RTFM means FU; it's rarely helpful), so I'll make it simple:

if you want to use cdn01.mydomain.net instead of http://mycdnendpoint.azurewebsites.net, you can do this in two ways (choose one):

  • create a cdn01 CNAME pointing to mycdnendpoint.azurewebsites.net

New-AzureRmDnsRecordSet -ResourceGroupName spartan01 -ZoneName "example.com" -RecordType CNAME -Name 'cd01' -Ttl 3600 -DnsRecords (New-AzureRmDnsRecordConfig -Cname "mycdnendpoint.azureedge.net")


OR

  • create a cdnverify.cdn01 CNAME pointing to cdnverify.mycdnendpoint.azurewebsites.net.

New-AzureRmDnsRecordSet -ResourceGroupName spartan01 -ZoneName "example.com" -RecordType CNAME -Name 'cdnverify.cd01' -Ttl 3600 -DnsRecords (New-AzureRmDnsRecordConfig -Cname "cdnverify.mycdnendpoint.azureedge.net")


NS Records

Once your DNS is setup and ready for production, you need to tell your registrar about it.

You can't guess what these records are. You just ask Azure about them:


    (Get-AzureRmDnsZone -ResourceGroupName spartan01 -Name "example.com").NameServers

Here's a preview of something from namecheap.com (use whatever you want... as long as it's not GoDaddy)

ARM Components

Generating MongoDB Sample Data

Last year I wrote about generating better random strings.

Lorem Ipsum is the devil. It messes with us who are students of Latin; Cicero is hard enough without people throwing randomized Cicero in our faces. It's better to use something that isn't part of a linguistic insurgency. Use my Hamlet generator instead.

Anyway...

Because MongoDB is a standard component of any modern architecture these days, we need the ability to generate, not simply strings, but full objects for our test databases.

The following MongoDB script will do just that. Change the value of the run function-call to set the number of objects to throw at MongoDB.

You run this with the MongoDB shell:

./mongo < hamlet.js

Note: The third-party tool Robomongo, while awesome for day-to-day usage, will not work for this. It doens't play nicely with initializeUnorderedBulkOp, which you need for bulk data import. It's like the BULK INSERT command in SQL.

You can use the following with abridged data or this with the full hamlet lexicon.

var raw = "o my offence is rank it smells to heaven hath the primal eldest curse upont a brothers murder pray can i not though inclination be as sharp will stronger guilt defeats strong intent and like man double business bound stand in pause where shall first begin both neglect what if this cursed hand were thicker than itself with blood there rain enough sweet heavens wash white snow whereto serves mercy but confront visage of whats prayer two-fold force forestalled ere we come fall or pardond being down then ill look up fault past form serve turn forgive me foul that cannot since am still possessd those effects for which did crown mine own ambition queen may one retain corrupted currents world offences gilded shove by justice oft tis seen wicked prize buys out law so above no shuffling action lies his true nature ourselves compelld even teeth forehead our faults give evidence rests try repentance can yet when repent wretched state bosom black death limed soul struggling free art more engaged help angels make assay bow stubborn knees heart strings steel soft sinews newborn babe all well";
var data = raw.split(" ");

function hamlet(count) {
    return data[parseInt(Math.random() * data.length)] + (count == 1 ? "" : " " + hamlet(count - 1));
}

function randrange(min, max) {
    if(!max) { max = min; min = 1;}
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function createArray(count, generator) {
    var list = [];
    for(var n=0; n<count; n++) {
        list.push(generator());
    }
    return list;
}

function pad(number){
    return ("0" + number).substr(-2);
}

function createItem() {
    item = {
        "_id": "9780" + randrange(100000000, 999999999),
        "title": hamlet(randrange(4, 8)),
        "authors": createArray(randrange(4), function() { return hamlet(2) }),
        "metadata": {
            "pages": NumberInt(randrange(1, 400)),
            "genre": createArray(randrange(2), function() { return hamlet(1) }),
            "summary": hamlet(randrange(100, 400)),
        },
        "published": new Date(randrange(1960, 2016) + "-" + pad(randrange(12)) + "-" + pad(randrange(28)))
    };

    if (randrange(4) == 1) {
        item.editor = hamlet(1);
    }

    return item;
}

function run(count) {
    var bulk = db.book.initializeUnorderedBulkOp();
    for (var n = 0; n < count; n++) {
        bulk.insert(createItem());
    }
    bulk.execute();
}

run(100000)

Using Azure App Services with Node.js

Often we hear about how Azure is "Microsoft" whereas other cloud providers aren't. In the most obvious sense, they're right. However, when you look closer what they actually mean is that Azure is Microsoft-only and Google/AWS are open to other programming models.

This is, of course, ridiculous and only said by naive, non-Microsoft developers.

People who used Microsoft technologies for over a decade understand that you have to make a distinction between Ballmer and non-Ballmer Microsoft. To give an extreme contrast: the former is VB6, the latter is .NET Core. You can see the transition from Ballmer-nonsense since around .NET 4 (especially in the adoption of ASP.NET MVC as an open-source tool). Ballmer is Silverlight, non-Ballmer is HTML5. You can go on down the line.

In the end: Ballmer-Microsoft is Microsoft-as-evil-empire. Today's Microsoft is very Linux friendly and relies on Github for many SDKs. It's a different beast.

You need to examine Azure through the same Ballmer/non-Ballmer lenses. Put simply: Windows Azure (and Azure ASM) was Ballmer whereas Microsoft Azure (and Azure ARM) is non-Ballmer.

However, we shouldn't look at Azure in an ad hoc manner, but in the context of it's very closely related technologies. Specifically we need to look at the Ballmer/non-Ballmer nature of IIS.

Working with IIS

My life with IIS started around the IIS3 era. I still remember taking the IIS3 exam as an elective (with the TCP/IP exam) for my NT4 MCSE. Thus, I've seen the various large upgrades and incremental updates over a good stretch of time.

The upgrade from IIS6 to IIS7 was easily the largest IIS upgrade. Till then, the biggest upgrade was a silly configuration system update (=IIS4 metabase). The IIS7 upgrade consisted of a systematic, paradigmatic shift. It was the "classical" to the "integrated" pipeline upgrade. You literally have to update your applications to add IIS7 support. After a while, everything was IIS7-first with IIS6 backwards compatibility.

In practice, this classic -> integrated upgrade meant three things: First, instead of relying on the external ASP.NET ISAPI IIS plugin, ASP.NET processing was integrated into IIS. No more interop. This made ASP.NET development more natural. It also gave .NET access to core extensible functionality in IIS. You didn't need to whip out C++ for server extensibility. Second, if you had existing C++ functionality, you had easier access to IIS functionality with the new native IIS API. This second point is critical, because we see that the IIS7 upgrade wasn't just about .NET. Third, web.config was no longer about ASP.NET, but about IIS itself. This point is huge and points to the fact that the web.config format controls all over IIS7+ as seen in the global applicationHost.config file.

IIS6 used the rediculous ISAPI nonsense to do just about everything, including call ASP.NET. The .aspx extension was simply mapped to aspnet_isapi.dll. This wasn't removed from IIS7; it was just separated and called "classic" mode.

In this post-IIS7 world, this meant that you literally had to add handler/module support for both IIS6 and IIS7 (more accurately, classical and integrated models).

Furthermore, the low-level ASP.NET pipeline APIs were also affected. For my very low-level Themelia framework, I had to make checks between completely different pipelines. See the following snippet from my CoreModule (a typical module implementing the System.Web.IHttpModule interface):

View Themelia at Themelia Pro. View Themelia source at Themelia on Github

CoreModule.cs:


            if (HttpRuntime.UsingIntegratedPipeline)
            {
                httpApplication.PostResolveRequestCache += OnProcessRoute;
                httpApplication.PostMapRequestHandler += OnSetHandler;
            }
            else
            {
                httpApplication.PostMapRequestHandler += OnProcessRoute;
                httpApplication.PostMapRequestHandler += OnSetHandler;
            }

Reference: CoreModule.cs

The installation was also different between IIS6 ("classical") and IIS7 ("integrated":

For IIS6, I would add the module to system.web:


    <system.web>
        <httpModules>
            <add name="Themelia" type="Themelia.Web.CoreModule, Themelia.Web"/>
        </httpModules>
    </system.web>

For IIS7, I would add the module to system.webServer:


    <system.webServer>
        <modules>
            <remove name="Session"/>
            <add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition=""/>
            <add name="Themelia" type="Themelia.Web.CoreModule, Themelia.Web"/>
        </modules>
    </system.webServer>

There was also the much more popular concept of a handler. My framework was meant to be a full IIS6-era platform takeover, so I used a more greedy module, but if you're only doing specific framework development, handlers are your choice (ASP.NET MVC used handlers).


    <system.webServer>
        <handlers>
            <add verb="*" path=".png" name="WatermarkHandler" type="WatermarkHandler"/>
        </handlers>
    </system.webServer>

This is still very common. Handler and modules are still the standard was of tapping into the stream of raw power.

.NET is powerful. C# even has an unmanaged mode where you can crap open the covers (via unsafe mode) to do direct *pointer &manipulation. That said, the upgrade to IIS7 wasn't just about .NET; the upgrade provided a native IIS API as well.

This we enter the realm of C/C++ modules: Develop a Native C\C++ Module for IIS 7.0

By removing the ISAPI barrier and providing a clean, native IIS API, C++ developers could connect existing C++ functionality to IIS. It also makes C++ code more readable; familiar web terms like HttpContext, IHttpResponse, and BeginRequest (and other events) are all over IIS C++ code. No more DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) nonsense.

Seriously. Review the C++ ISAPI docs. It's insane. 1990s Microsoft C++ was the worst code ever written. It's just plain satanic.

Consider the following C++ method:


    HRESULT        
    __stdcall        
    RegisterModule(        
        DWORD                           dwServerVersion,    
        IHttpModuleRegistrationInfo *   pModuleInfo,
        IHttpServer *                   pHttpServer            
    )
    {
    }

This is exactly how you register your native module in IIS7.

This is also exactly how IIS handles Node hosting in Azure; it uses the iisnode module. You'll see this example RegisterModule in main.cpp in iisnode:

https://github.com/tjanczuk/iisnode/blob/master/src/iisnode/main.cpp

If you review the following code from CProtocolBridge.cpp in iisnode, you'll see familiar things like IHttpContext and IHttpResponse:

https://github.com/tjanczuk/iisnode/blob/master/src/iisnode/cprotocolbridge.cpp

It's clean interface programming.

Using iisnode

IIS handles most of it's config with your applications web.config. While there are a few global config files, you get tremendous control with your own config.

Hosting a Node application in Azure is as simple as deploying an Azure Web App with a properly configured web.config.

Per the previous explanation of IIS modules, you can see from the following web.config that iisnode is installed just as we would install our own handlers and modules.

    
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <system.webServer>
        <!-- leave false, you enable support in Azure -->
        <webSocket enabled="false" />
        <handlers>
          <add name="iisnode" path="server.js" verb="*" modules="iisnode"/>
        </handlers>
        <rewrite>
          <rules>
            <rule name="StaticContent">
              <action type="Rewrite" url="content{REQUEST_URI}"/>
            </rule>
            <rule name="DynamicContent">
              <conditions>
                <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
              </conditions>
              <action type="Rewrite" url="server.js"/>
            </rule>
            <rule name="Redirect to https" stopProcessing="true">
              <match url="(.*)" />
              <conditions>
                <add input="{HTTPS}" pattern="off" ignoreCase="true" />
              </conditions>
              <action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" redirectType="Permanent" appendQueryString="false" />
            </rule>
          </rules>
        </rewrite>
        <security>
          <requestFiltering>
            <hiddenSegments>
              <remove segment="bin"/>
            </hiddenSegments>
          </requestFiltering>
        </security>
        <httpErrors existingResponse="PassThrough" />
      </system.webServer>
    </configuration>

The following section listens for any requests for all verbs accessing server.js and has iisnode process them:


    <handlers>
        <add name="iisnode" path="server.js" verb="*" modules="iisnode"/>
    </handlers>

The following rewrite rule sends all traffic to server.js:


    <rule name="DynamicContent">
        <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
        </conditions>
        <action type="Rewrite" url="server.js"/>
    </rule>

The following doesn't have anything directly to with iisnode; it excludes the content folder from iisnode processing:


    <rule name="StaticContent">
        <action type="Rewrite" url="content{REQUEST_URI}"/>
    </rule>

I find putting static files on your web server to be rather naive and tasteless, but if you really don't want to use the Azure CDN, this is how you host static content.

The following merely redirects HTTP to HTTPS:


    <rule name="HTTP to HTTPS redirect" stopProcessing="true">
        <match url="(.*)" />
        <conditions>
            <add input="{HTTPS}" pattern="off" ignoreCase="true" />
        </conditions>
        <action type="Redirect" url="https://{HTTP_HOST}/{REQUEST_URI}" redirectType="Permanent" />
    </rule>

Breaking iisnode

To prove that Node hosting is actually this basic, let's break it and fix it.

First, let's see this work:

https://node38eb089b-app-alpha.azurewebsites.net/api/samples

it works

That's the expected output from the application.

Now, let's go to web.config and break it:


  <rule name="DynamicContent">
    <conditions>
      <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
    </conditions>
    <action type="Rewrite" url="server.js"/>
  </rule>

Change that Rewrite from server.js to server2.js:

Save. Refresh browser.

404

Nope.

Go to Kudu. This is the .scm. URL. In this case it's the following:

https://node3638972b-app-alpha.scm.azurewebsites.net/

kudu

Rename server.js to server2.js:

rename server.js to server2.js

Refresh again.

rawcodeoutput

mmmk. Raw output.

The rewrite is telling everything to go to server.js, but nothing is processing it, so it just sends the file back.

This is exactly like accessing an old .aspx page and getting the raw ASP.NET webform code, because you forgot to install ASP.NET (and somehow managed to allow access to .aspx).

Now, let's fix this by telling our IIS module to process server2.js:


    <handlers>
      <add name="iisnode" path="server2.js" verb="*" modules="iisnode"/>
    </handlers>

Refresh and it's all well again:

it works

App Services and App Service Plans

An explanation of Azure web apps using any web platform isn't complete without an understanding of how sites work. We've already seen that it's just IIS, but in what way?

To begin, let's clarify a few Azure terms:

Azure App Service Plans are effectively managed VMs. You can scale these up and out. That is, you can turn an S1 into and S2 to double the RAM or you can turn a single instance into four. Because of this later ability, App Service Plans are also known as server farms. In fact, when developing ARM templates, the type you use to deploy an App Service Plan is Microsoft.Web/serverfarms.

You do not deploy a series of plans to create a farm. Plans are farms. Plans with a size of 1 is just a farm with 1 instance. You are always dealing with herds, you are never dealing with pets. You scale your farm out, you scale all those instances up.

Azure Web Apps are also known as Web Sites and App Services. You deploy these, you back these up, and you add SSL to these. These are similar to IIS virtual applications. When developing ARM templates, the type is Microsoft.Web/sites.

You have to remember the various synonyms for each; you will see them all.

Given this distinction and given the fact that a VM can have multiple IIS applications, you can imagine that you can host multiple Azure Web Apps on a single App Service Plan. This is true. You do NOT deploy a plan every time you deploy a site. You plan your CPU/RAM usage capacity ahead of time and deploy a full solution at once.

To visualize the App Service / App Service Plan distinction, review the following image.

Here I've provide information for three services over two service plans. The first two services share a service plan, the third service is on a different plan.

Notice that the services with the same service plan have the same machine ID and instance ID, but their IIS specifics are different. The third service plan has a different machine ID altogether.

What's so special about the types of Web Apps?

If this is all just the same IIS, what's with the various Node-specific web app types?

web app types

The answer is simple: they exist solely to confuse you.

You can literally deploy a Node.js web app, then deploy and ASP.NET site. It's just IIS. The website deployment will overwrite the web.config with its own.

Given the previous explanations of IIS handler/modules, iisnode-as-module, and the service/plan distinction, you can see that there's no magic. There's nothing Microsoft-only about any of this.

You can always use the normal "Web App" one and be done with it.

Single App Solutions

My websites are generally in ASP.NET or Python/Django, but my APIs are always Node. There was a time when this requires separate... just about everything. Now adays I use nginx to use one source to handle the website as a whole (either http://127.0.0.1:XXXX or a Linux socket) and tell /api to go to Node. This lets me use a single domain (therefore a single SSL cert) for my solution.

Think back through all the mechanics covered so far: IIS can handle .NET and supports modules. iisnode is a module. IIS uses rewriting to send everything to server.js. iisnode handles all traffic sent to server.js.

Let's mix this up: instead of rewriting everything to server.js, let's only rewrite the /api branch of our URL.

To make this example a bit spicier, let's deploy an ASP.NET MVC application to our App Service, then send /api to Node.

To do this, go to the App Service (not the App Service Plan!), then select Deployment Options on the left.

external

In Choose source, select External Repository and put in the following:

https://github.com/Azure-Samples/app-service-web-dotnet-get-started

external

A few minutes later, load the application normally. You'll see the "ASP.NET is a free web framework for..." propaganda.

Now go back into Kudu and the PowerShell Debug Console (explained earlier).

We need to do three things:

  • add our server.js
  • install express
  • tell web.config about server.js

To add server.js, go to site/wwwroot and type the following:

touch server.js

This will create the file. Edit the file and paste in server from the following:

https://github.com/davidbetz/template-azure-node-api/blob/master/server.js

Next we need to install express to handle the API processing. H

For the sake of a demo, type the following:

npm install express

Done.

For the sake of your long-term sanity, create package.json (same way you created server.js), edit in contents, save, then run:

npm install

See sample package.json at:

https://raw.githubusercontent.com/davidbetz/template-azure-node-api/master/package.json

Finally, edit web.config.

You need the splice in the following config in the system:


  <system.webServer>
    <rewrite>
      <rules>
        <rule name="DynamicContent">
          <match url="^api/(.*)" />
          <action type="Rewrite" url="server.js"/>
        </rule>
      </rules>
    </rewrite>
    <handlers>
      <add name="iisnode" path="server.js" verb="*" modules="iisnode"/>
    </handlers>
  </system.webServer>

Upon saving, access the web app and /api/samples. Click around the web app to prove to yourself that it's not just a static page.

asp.net and node together

You have ASP.NET and Node.js in the same Azure web app.

You never have to play with CORS nonsense again.

Powered by
Python / Django / Elasticsearch / Azure / Nginx / CentOS 7

Mini-icons are part of the Silk Icons set of icons at famfamfam.com