Network as code: Building a site-to-site VPN to the cloud with Azure Building Blocks and PowerShell DSC

Configuring infrastructure in the cloud era

How many of us that regularly manage IT infrastructure know, in the most minute detail, exactly how everything hangs together in our datacenters? Every configuration setting, every dependency, every protocol in play. Anyone? Or put another way, if a bomb went off in your datacenter tomorrow, would you know the exact steps to configure your new kit, such that it was identical to what you had pre-bombing?

I can tell you that the number of operators that could claim the above is small, this is because the majority of infrastructure in the world is managed by multiple people applying untraceable tweak on top of untraceable tweak. The truth is that you cannot easily reproduce or scale on-demand the output of a human directly interacting with infrastructure, which is why this modus operandi has no place in the cloud era. At least, not if you want the gains offered by the cloud.

Okay, so direct interaction with infrastructure is bad, does that mean scripting is the answer? It’s a step in the right direction, in that at least they can be stored securely in source control and replayed if necessary. The problem with imperative scripts, though, is that they tend to assume a starting state and define strict steps to get you to a specific end state. What do scripts do if a machine is already in a partially configured state? Fail with a message saying the first dependency is already installed, most likely. It is possible to write idempotent scripts, but it’s not easy. This is the reason for the rise in desired-state configuration management systems like Ansible, Puppet and Chef. They allow you to define a declarative (not imperative) configuration that will be applied consistently no matter what state a particular endpoint is in.

Azure VPN as code

Infrastructure-as-code is not just for installing and configuring software packages on VMs, it can also drive something as fundamental as your network connectivity to the cloud. In this post, I’m going to demonstrate the deployment and configuration of a VPN between an Azure Virtual Network and my on-premises lab using some of my favorite configuration management tools. Azure Building Blocks for the cloud-side configuration and PowerShell DSC on a local Windows 2012 R2 server for the lab-side.

Requirements

Tool Version used in this post Link Notes
Azure CLI 2.0.21 Microsoft Dependency of Azure Building Blocks
Azure Building Blocks 2.0.4 GitHub
PowerShell 4+ For the local Windows Server 2012 R2. PowerShell DSC requires at least PowerShell 4.
xRemoteAccess N/A GitHub DSC resource for configuring RemoteAccess service.

Stage 1: Deploying the VirtualNetwork

Fire up your shell of choice. Because Azure CLI and Azure Building Blocks are cross-platform, I’m using Bash on Ubuntu on Windows (with the Windows Subsystem for Linux) just to take the cross-platformness to the extreme. Anything that runs az and azbb on your system will do though.

Virtual Network JSON

Azure Building Blocks takes a JSON parameters file containing the desired state of one or more Azure resources, then using a combination of compiled AzureRM JSON templates and the Azure CLI, deploys the required infrastructure to Azure.

Why not just go straight to the AzureRM templates? The Azure Building Blocks parameters files are much less verbose and easier to hand-edit, not a pleasant task with raw ARM JSON. The compiler also has a lot of Azure infrastructure best practices built-in, i.e. if you don’t specify an optional property it will make a sensible choice for you. This keeps the verbosity of the JSON down also. If you’ve never seen Building Blocks JSON before you will be surprised how much easier they are to work with versus normal ARM templates.

To get a feel for Azure Building Blocks, let’s first create a JSON file that defines a simple Virtual Network with no VPN. Create a new file and enter the following:

{
  "$schema": "https://raw.githubusercontent.com/mspnp/template-building-blocks/master/schemas/buildingBlocks.json",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "buildingBlocks": {
      "value": [
        {
          "type": "VirtualNetwork",
          "settings": [
            {
              "name": "Azure-VNet",
              "addressPrefixes": [
                "172.16.0.0/16"
              ],
              "subnets": [
                {
                  "name": "GatewaySubnet",
                  "addressPrefix": "172.16.0.0/24"
                },
                {
                  "name": "Subnet-001",
                  "addressPrefix": "172.16.1.0/24"
                }
              ]
            }
          ]
        }
      ]
    }
  }
}

This JSON defines a single Virtual Network, named Azure-VNet, with an address range and two subnets, named GatewaySubnet and Subnet-001, each with their own address ranges also. Pretty simple, right? GatewaySubnet is going to be used exclusively by our VPN infrastructure later on and Subnet-001 is a normal subnet for any VMs we want to connect to over the VPN.

Interesting parts of this JSON definition are:

  • “$schema”: a schema definition for a building blocks parameters file.
  • “value”: An array containing a series of object definitions representing Azure resources.
  • “type”: the type of an Azure resource required.
  • “settings”: type-specific settings for the resource. Can be required or optional.

Save the above as vnet.json. Let’s try running this through the Azure Building Blocks CLI tool, azbb:

azbb --deploy --parameters-file vnet.json --resource-group Azure-VNet --subscription-id e8ef4b09-2eaf-40be-8639-752e7d4af30d --location uksouth
Note: The above subscription ID is just a random GUID. To figure out what your subscription ID is, run: az account list then copy the id property of the subscription you want to use.

We’ve told Azure Building Blocks to deploy the virtual network into a resource group named Azure-VNet (the same name as the network object itself) in a specific subscription in the uksouth region (feel free to substitute for a region more suitable to your location). You should see output similar to the below:

  parameters written to /home/Kirk/azure-building-blocks/vnet-output.json

{
  "id": "/subscriptions/e8ef4b09-2eaf-40be-8639-752e7d4af30d/resourceGroups/Azure-VNet",
  "location": "uksouth",
  "managedBy": null,
  "name": "Azure-VNet",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null
}
{
  "id": "/subscriptions/e8ef4b09-2eaf-40be-8639-752e7d4af30d/resourceGroups/Azure-VNet/providers/Microsoft.Resources/deployments/bb-01-vnet",
  "name": "bb-01-vnet",
  "properties": {
    "correlationId": "REMOVED",
    "debugSetting": null,
    "dependencies": [
      {
        "dependsOn": [
          {
            "id": "/subscriptions/e8ef4b09-2eaf-40be-8639-752e7d4af30d/resourceGroups/Azure-VNet/providers/Microsoft.Resources/deployments/bb-01-vnet-vnet-0-k7q3ykincijwc",
            "resourceGroup": "Azure-VNet",
            "resourceName": "bb-01-vnet-vnet-0-k7q3ykincijwc",
            "resourceType": "Microsoft.Resources/deployments"
          }
        ],
        "id": "/subscriptions/e8ef4b09-2eaf-40be-8639-752e7d4af30d/resourceGroups/workaround/providers/Microsoft.Resources/deployments/bb-01-vnet-vnetPeering-0-k7q3ykincijwc",
        "resourceGroup": "workaround",
        "resourceName": "bb-01-vnet-vnetPeering-0-k7q3ykincijwc",
        "resourceType": "Microsoft.Resources/deployments"
      }
    ],
    "mode": "Incremental",
    "outputs": {},
    "parameters": {
      "deploymentContext": {
        "type": "Object",
        "value": {
          "parentTemplateUniqueString": "bb-01-vnet",
          "sasToken": ""
        }
      },
      "virtualNetworkPeerings": {
        "type": "Array",
        "value": []
      },
      "virtualNetworks": {
        "type": "Array",
        "value": [
          {
            "location": "UKSouth",
            "name": "Azure-VNet",
            "properties": {
              "addressSpace": {
                "addressPrefixes": [
                  "172.16.0.0/16"
                ]
              },
              "dhcpOptions": {
                "dnsServers": []
              },
              "subnets": [
                {
                  "name": "GatewaySubnet",
                  "properties": {
                    "addressPrefix": "172.16.0.0/24"
                  }
                },
                {
                  "name": "Subnet-001",
                  "properties": {
                    "addressPrefix": "172.16.1.0/24"
                  }
                }
              ]
            },
            "resourceGroupName": "Azure-VNet",
            "subscriptionId": "e8ef4b09-2eaf-40be-8639-752e7d4af30d",
            "tags": {}
          }
        ]
      }
    },
    "parametersLink": null,
    "providers": [
      {
        "id": null,
        "namespace": "Microsoft.Resources",
        "registrationState": null,
        "resourceTypes": [
          {
            "aliases": null,
            "apiVersions": null,
            "locations": [
              null
            ],
            "properties": null,
            "resourceType": "deployments"
          }
        ]
      }
    ],
    "provisioningState": "Succeeded",
    "template": null,
    "templateLink": {
      "contentVersion": "1.0.0.0",
      "uri": "https://raw.githubusercontent.com/mspnp/template-building-blocks/v2.0.0/templates/buildingBlocks/virtualNetworks/virtualNetworks.json"
    },
    "timestamp": "2017-12-05T09:10:28.595858+00:00"
  },
  "resourceGroup": "Azure-VNet"
}

You can hopefully see “provisioningState”: “Succeeded”, which indicates success. Go and take a look at the Azure portal and see that the resource group and the virtual network and the specified subnets exist.

Stage 2:  Deploying a Virtual Network Gateway

Now that we know how to use a Azure Building Blocks parameters file, let’s extend it to define a VPN gateway that we can use to connect our on-premises lab to Azure. First, let’s create a copy of vnet.json and work on that copy.

cp vnet.json vnet-and-vpn-gateway.json

Open vnet-and-vpn-gateway.json in your editor of choice and extend the value array of the buildingBlocks object as follows:

{
  "$schema": "https://raw.githubusercontent.com/mspnp/template-building-blocks/master/schemas/buildingBlocks.json",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "buildingBlocks": {
      "value": [
        {
          "type": "VirtualNetwork",
          "settings": [
            {
              "name": "Azure-VNet",
              "addressPrefixes": [
                "172.16.0.0/16"
              ],
              "subnets": [
                {
                  "name": "GatewaySubnet",
                  "addressPrefix": "172.16.0.0/24"
                },
                {
                  "name": "Subnet-001",
                  "addressPrefix": "172.16.1.0/24"
                }
              ]
            }
          ]
        },
        {
          "type": "VirtualNetworkGateway",
          "settings": [
            {
              "name": "Azure-VNet-Gateway",
              "gatewayType": "Vpn",
              "vpnType": "RouteBased",
              "sku": "Basic",
              "isPublic": true,
              "virtualNetwork": {
                "name": "Azure-VNet"
              }
            }
          ]
        }
      ]
    }
  }
}

That was easy, wasn’t it? Only 15 lines of simple JSON added to define a VPN gateway in the cloud.

Run the following in the shell to deploy the resources into the same resource group as previously:

azbb --deploy --parameters-file vnet-and-vpn-gateway.json --resource-group Azure-VNet --subscription-id e8ef4b09-2eaf-40be-8639-752e7d4af30d --location uksouth

Because our parameters file gets compiled to an AzureRM template and the virtual network portion of this template already exists (from our previous run) azbb will leave our previous virtual network untouched, as it is already in the desired state, only adding the new gateway to the resource group. This makes everything idempotent, a very important characteristic of infrastructure-as-code systems.

This deployment operation could take a while, as provisioning a virtual network gateway can take up to about 45 minutes. Go make yourself a coffee and come back, you should see we have successfully provisioned a VPN gateway on completion.

Note: You might be wondering why we only had to specify the virtual network in the gateway’s settings and not a specific subnet within that network. This is because Virtual Network Gateways always operate on a subnet with the name GatewaySubnet (this cannot be overridden), which is why the first subnet in our Virtual Network definition has that name.

Stage 3: Configuring a Local Network Gateway

We’re mostly done with the cloud-side of our VPN, now to turn our attention to the on-premises component.

In the enterprise, you could have any number of appliances handling VPN connectivity for you (e.g. a Cisco ASA), hopefully configured in an infrastructure-as-code style. Choose whatever is appropriate for your environment, but to make this guide as accessible as possible I’m going to use something most of us have access to – a Windows Server 2012 R2 virtual machine.

We’re going to configure the VM using PowerShell DSC. The configuration uses only one DSC resource, the xRemoteAccess resource from GitHub. Unfortunately, despite being written by a Microsoft employee, the resource does not seem to be available on PowerShell Gallery at the moment. It requires a manual download and installation.

Note: PowerShell DSC is a declarative server configuration language based on PowerShell. It is similar to the DSLs of other popular configuration management tools, such as Ansible, Puppet and Chef. Check out our introduction to PowerShell DSC if you are unfamilar with the language.

The following is my DSC configuration for a local site-to-site VPN gateway:

Configuration LocalS2SGateway {

    Import-DscResource -ModuleName PSDesiredStateConfiguration, xRemoteAccess

    Node $AllNodes.Where{$_.Role -contains "LocalS2SGateway"}.NodeName {

        LocalConfigurationManager
        {
            RebootNodeIfNeeded = $true
        }

        WindowsFeature Routing
        {
            Name = 'Routing'
            Ensure = 'Present'
        }

        WindowsFeature RemoteAccessPowerShell
        {
            Name = 'RSAT-RemoteAccess-PowerShell'
            Ensure = 'Present'
            DependsOn = '[WindowsFeature]Routing'
        }

        Service RemoteAccess
        {
            Name = 'RemoteAccess'
            StartupType = 'Automatic'
            State = 'Running'
            DependsOn = '[WindowsFeature]Routing'
        }

        RemoteAccess VpnS2S
        {
            VpnType = 'VpnS2S'
            Ensure = 'Present'
            DependsOn = '[Service]RemoteAccess'
        }

        VpnS2SInterface IKEv2
        {
            Name = $ConfigurationData.AzureVNet.IP
            Destination = $ConfigurationData.AzureVNet.IP
            IPv4Subnet = $ConfigurationData.AzureVNet.Subnet
            SharedSecret = $ConfigurationData.AzureVNet.SharedSecret
            Protocol = 'IKEv2'
            AuthenticationMethod = 'PSKOnly'
            ResponderAuthenticationMethod = 'PSKOnly'
            NumberOfTries = 3
            InitiateConfigPayload = $false
            Ensure = 'Present'
            DependsOn = "[RemoteAccess]VpnS2S"
        }

        VpnServerIPsecConfiguration Encryption
        {
            EncryptionType = 'MaximumEncryption'
            DependsOn = '[VpnS2SInterface]IKEv2'
        }
    }
}

This configuration ensures:

  • The Routing feature of Windows Server is installed.
  • The Remote Server Administration Tools for PowerShell is installed.
  • The RemoteAccess service is running and set to start automatically.
  • Site-to-site VPN is installed using IPsec, IKEv2 and pre-shared key authentication.

The core of the VPN configuration is defined in a ConfigurationData data structure and consumed here as variables to keep the configuration reasonably generic, with minimal instance-specific data. Here is the setup script used to apply the configuration to the VM:

$ErrorActionPreference = 'Stop'

# Download and unzip xRemoteAccess from GitHub, as it is not currently available
# in PowerShell Gallery.
$modulePath = "$env:ProgramFiles\WindowsPowerShell\Modules"
Invoke-WebRequest -UseBasicParsing -Uri https://github.com/mgreenegit/xRemoteAccess/archive/master.zip `
    -OutFile "$modulePath\xRemoteAccess.zip"
Add-Type -Assembly "System.IO.Compression.FileSystem"
[IO.Compression.ZipFile]::ExtractToDirectory("$modulePath\xRemoteAccess.zip", "$modulePath\xRemoteAccess")
Remove-Item -Path "$modulePath\xRemoteAccess.zip"

# WORKAROUND: PowerShell 4 does not like version folders in the module directory of DSC resources.
# So we need to remove everything out of the version directory and up a level for the xRemoteAccess
# resources to work.
if ($PSVersionTable.PSVersion -like "4*") {
    Copy-Item -Path "$modulePath\xRemoteAccess\xRemoteAccess-master\*" -Destination "$modulePath\xRemoteAccess" -Force -Recurse
    Remove-Item -Path "$modulePath\xRemoteAccess\xRemoteAccess-master" -Force -Recurse
}

# Source configuration
. C:\LocalS2SGateway.ps1

# Configuration data, separated from the DSC configuration itself to make it more reusable.
$configData = @{
    AllNodes = @(
        @{
            NodeName = $env:ComputerName
            Role = "LocalS2SGateway"
        }
    );

    AzureVNet = @{
        IP = "51.132.183.53"
        Subnet = "172.16.1.0/24:100"
        SharedSecret = "UseARealKey123!"
    }
}

# Compile our DSC configuration into a MOF file. Apply our config data.
LocalS2SGateway -OutputPath ".\MOF" -ConfigurationData $configData

# Apply the configuration to the machine.
Start-DscConfiguration -Path ".\MOF" -Wait

# Restart RemoteAccess service to finalize any configuration changes
Restart-Service -Name RemoteAccess

The AzureVNet part of $configData specifies the public IP of our Azure virtual network gateway (go find this in the portal if you don’t know it yet), the IP range of the Subnet-001 subnet and a shared secret, which we will share with the Azure gateway next.

Note: The above DSC is written for WMF 4, the version that comes with Windows Server 2012 R2 by default. If you’re running WMF 5 your DSC is likely to look different as various parts of the language have changed. I wouldn’t expect the above to work on WMF 5 without changing a few things.

Stage 4: Establishing the VPN connection

Azure-side

Finally, we need to create the VPN connection between our gateways. This you can do with Azure Building Blocks, so let’s create a copy of our latest parameters file and extend it again.

cp vnet-and-vpn-gateway.json vnet-and-vpn-connection.json

Open vnet-and-vpn-connection.json and extend it to match the below:

{
  "$schema": "https://raw.githubusercontent.com/mspnp/template-building-blocks/master/schemas/buildingBlocks.json",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "buildingBlocks": {
      "value": [
        {
          "type": "VirtualNetwork",
          "settings": [
            {
              "name": "Azure-VNet",
              "addressPrefixes": [
                "172.16.0.0/16"
              ],
              "subnets": [
                {
                  "name": "GatewaySubnet",
                  "addressPrefix": "172.16.0.0/24"
                },
                {
                  "name": "Subnet-001",
                  "addressPrefix": "172.16.1.0/24"
                }
              ]
            }
          ]
        },
        {
          "type": "VirtualNetworkGateway",
          "settings": [
            {
              "name": "Azure-VNet-Gateway",
              "gatewayType": "Vpn",
              "vpnType": "RouteBased",
              "sku": "Basic",
              "isPublic": true,
              "virtualNetwork": {
                "name": "Azure-VNet"
              }
            }
          ]
        },
        {
          "type": "Connection",
          "settings": [
            {
              "name": "Azure-VPN-Connection",
              "connectionType": "IPsec",
              "routingWeight": 10,
              "vpnType": "RouteBased",
              "sharedKey": "UseARealKey123!",
              "virtualNetworkGateway": {
                "name": "Azure-VNet-Gateway"
              },
              "localNetworkGateway": {
                "name": "Azure-Local-Gateway",
                "ipAddress": "2.219.71.91",
                "addressPrefixes": [
                  "192.168.0.0/24"
                ]
              }
            }
          ]
        }
      ]
    }
  }
}

Then redeploy with:

azbb --deploy --parameters-file vnet-and-vpn-connection.json --resource-group Azure-VNet --subscription-id e8ef4b09-2eaf-40be-8639-752e7d4af30d --location uksouth

The above JSON blob with type Connection defines the cloud-side of the site-to-site VPN connection, i.e. configures IPsec and the pre-shared authentication key. It specifies the virtual network gateway and local network gateway that will form the connection. Understandably, Azure needs more information about your localNetworkGateway than the cloud-side gateway. It’s properties are:

  • name: specifies a name for your local gateway configuration in Azure.
  • ipAddress: the public IP address of your on-premises local gateway.
  • addressPrefixes: specifies the IP ranges whose traffic should be sent over the private connection to your local gateway.

Once the redeployment has succeeded, we can establish the connection on our local side.

Local-side

To establish the local-side of our VPN connection we can simply extend our PowerShell DSC configuration. Add the following DSC resource definition to the end of the previous configuration:

VpnS2SInterfaceConnection Connect
{
    Ensure = 'Present'
    Name = $ConfigurationData.AzureVNet.IP
    DependsOn = '[VpnServerIPsecConfiguration]Encryption'
}

Recompile the MOF and apply the configuration with Start-DscConfiguration (repeat some of the steps from the above setup script). Due to all the other resources being in the correct state already, the only change on the machine should be the establishment of the VPN connection.

Use the Get-VpnS2SInterface cmdlet to check that status of the connection on completion.

connection_state

Stage 5: Testing the VPN

The moment of truth, can we connect to a machine in Azure over our new VPN?

If you have VMs connected to the Azure virtual network already, try to connect to one over Remote Desktop Connection (assuming you allow RDP traffic) using its private IP address.

If you don’t, quickly run through the new VM wizard in Azure to create a test machine, I chose a Server 2016 machine with a small disk. Be sure to attach it to the Azure-VNet virtual network and the Subnet-001 subnet, setting the Public IP option to None. Once provisioned, find the private IP address in the Networking blade of the VM in the portal. Open Remote Desktop Connection on your local gateway VM and attempt to connect to that private IP address. You should be successful.

rdp_over_private_ip

Closing thoughts

Hopefully, you can see that if we lost our cloud network or on-premises connectivity in a disaster, we now have the exact specification required (as code) to re-establish that core infrastructure with automation. There is virtually no scope for misconfiguring this VPN or suffering downtime as a result. I would be committing the above files to source control so that any future edits to the configuration can be vetted and reviewed by the appropriate people/process.

It’s also important to remember that infrastructure-as-code technologies are not just for configuring software packages on VMs. We need to be building these deployable, verifiable specifications for all infrastructure we require in running our critical services.

Block Heads: Mastering block storage with the Azure Blob Service

The importance of practice

In software development, I find it to be true that you never know how to use a tool properly until you have already used it at least once before. That’s why, when you take a wrong turn and need to start over with something, you can usually get back to where you were in half the time it took you originally.

It’s important to realize

Automating Office 365 with Microsoft Graph API

An API gateway to business productivity

What would you build if you could process the data generated by your business operations in real time? You could, for example:

  • See trending/abandoned documents and usage patterns.
  • Scan calendars to suggest optimum meeting times.
  • Map collaboration points between departments.
  • Automate a change management/approval process.
  • Manage a backlog of work.

And that’s just for starters! I would be willing to bet that a very large slice of business performed in the world today is driven by the Microsoft Office apps, so imagine the potential gains around automating some of that? It’s got to be huge.

Authenticating with Azure AD and OpenID Connect

Identity in the cloud

Identity management in the cloud is a totally different ball game to when everything was installed and accessed on the corporate network. Users in the enterprise authenticated with an on-premises directory service (e.g. Active Directory Domain Services) and this determined the apps and data they had access to. Occasionally, cross-forest federations were established to allow users belonging to one corporate domain to access resources in another.

Nowadays, with the proliferation of apps and services available in the cloud and the speed and ease with which we consume them

Acting School: Azure’s use of a classic computational model

Recently, I have been fortunate enough to undertake some Azure training under one of the Solution Architects at Microsoft. We spent a lot of time focusing on Azure Service Fabric, Microsoft’s platform for developing microservice-based solutions. Much of what I heard reminded me of my experiences of Docker Swarm and Kubernetes, two very popular container orchestration platforms, and many of my questions in the training sessions were centered around why I might want to use Service Fabric over one of these other platforms that I knew a little better.

One point the trainer made that has stuck with me since is: containers often contain microservices, but microservices are not containers. It was clear to me then

A software development lifecycle for the Cloud Era

Back in February, I was given the chance to deliver a presentation for the BCS, the chartering body for IT and computing in the UK, on the evolution of the software development lifecycle as we race into the Cloud era.

Well, I say that. I was originally approached to do a talk about test automation, but as I was thinking about what I might be able to add to that arena it occurred to me that the testing phase of the classical SDLC gets far more coverage from an automation sense than any other. Much of the modern thinking on how to deliver software efficiently automates much more of the process than just the testing. I began researching how the most progressive teams used automation to drive some of the lesser covered phases and a talk on how automation technologies are taking these over became much more compelling to me. Hopefully, the audience agreed!