Freigeben über


Scripts-to-Tools: Auto-provisioning Azure Virtual Networks with PowerShell and XML

A few weeks ago, I wrote about Auto-Provisioning a New Active Directory Domain Controller in the Azure Cloud using the new VM Agent custom script extension via the Azure Management Portal.  Since then, several people in the community have asked about also automating the steps for provisioning Virtual Networks on Azure.

Click to enlarge ...

In this article, we’ll walk through an approach for auto-provisioning Azure VNets using PowerShell, and along the way, you’ll also pick up some useful tips to expand your PowerShell skills from simple scripting to creating reusable tools.

Using “Set-AzureVNetConfig” …

As my friend and colleague, Kevin Remde, wrote in one of his articles – we can use the Set-AzureVNetConfig cmdlet from the Azure PowerShell Module to provision Virtual Network configurations on Azure. 

But … this cmdlet uses an “all or nothing” approach, meaning that it is scoped around provisioning all VNets in an Azure subscription as a single unit using an XML configuration file.  This may be fine when setting up the initial VNets in new Azure subscriptions, but it may not be useful when needing to incrementally provision additional VNets inside an existing subscription. 

What we really need is “New-AzureVNet”

In these cases, what we may really need is a “New-AzureVNet” cmdlet that can incrementally add new VNets, DNS servers and subnets to an existing Azure subscription. This approach is particularly useful in larger organizations that are managing multiple Azure subscriptions across dev, test, staging and production workloads, because it allows them to maintain consistent configurations for individual VNets in each subscription.

Let’s build our own “New-AzureVNet” function!

Hey! PowerShell provides us with the ability to easily define custom functions that can be used much like built-in cmdlets.  We can use this capability to build our own New-AzureVNet tool! After our function is defined, we’ll be able to easily add new Azure VNets incrementally by invoking the function as a single PowerShell command line, such as:

New-AzureVNet `
-newDnsServerName labdns01 `
-newDnsServerIP 10.2.1.4 `
-newVNetName labnet01 `
-newVNetAddressRange 10.2.0.0/16 `
-newSubnetAddressRange 10.2.1.0/24 `
-newVNetLocation "West US"

Along the way, we’ll also learn some useful scripting skills that you can use to elevate your PowerShell abilities from creating simple scripts to making reusable tools that others can use in your organization.

Getting Started …

To follow along in this step-by-step article on building a New-AzureVNet function, you’ll need an active Azure subscription.  If you don’t yet have an active Azure subscription, you can sign-up for a FREE subscription at the following link location …

Once you have an active Azure subscription, you'll also need to download and install the latest Azure PowerShell module …

Task 1: What’s your Function?

First, we’ll need to create a new PowerShell script file named myAzureLibrary.ps1 and define our new function inside this file with the code block below.

function New-AzureVNet {

}

Note that I’m using a naming convention for this function that matches the typical PowerShell verb-noun naming convention used with cmdlets. By following this naming convention, it makes it easy for others to quickly understand the general purpose of this function and leverage it in ways that is consistent with other cmdlets.

Although we are defining only a single function in myAzureLibrary.ps1 in this article, you could certainly include a number of function definitions in a single script file that you leverage in other scripts as a function "library".

Task 2: Empowering Self-Discovery

When defining functions in PowerShell, I like to make them as self-documenting as possible.  This empowers others to self-discover and quickly understand how a particular function is intended to work and what parameters, if any, are expected. Including comment-based help within a function is a great way to provide this information for others. 

function New-AzureVNet {

<#
.SYNOPSIS
New-AzureVNet provisions new Azure Virtual Networks in an existing Azure Subscription
.DESCRIPTION
New-AzureVNet defines new Azure Virtual Network and DNS Server information,
merges with an existing Azure Virtual Network configuration,
and then provisions the resulting final configuration in an existing Azure subscription.
For demonstration purposes only.
No support or warranty is supplied or inferred.
Use at your own risk.
.PARAMETER newDnsServerName
The name of a new DNS Server to provision
.PARAMETER newDnsServerIP
The IPv4 address for the new DNS Server
.PARAMETER newVNetName
The name of a new Azure Virtual Network to provision
.PARAMETER newVNetLocation
The name of the Azure datacenter region in which to provision the new Azure Virtual Network
.PARAMETER newVNetAddressRange
The IPv4 address range for the new Azure Virtual Network in CIDR format. Ex) 10.1.0.0/16
.PARAMETER newSubnetName
The name of a new subnet within the Azure Virtual Network
.PARAMETER newSubnetAddressRange
The IPv4 address range for the subnet in the new Azure Virtual Network in CIDR format. Ex) 10.1.0.0/24
.PARAMETER configFile
Specify file location for writing finalized Azure Virtual Network configuration in XML format.
.INPUTS
Parameters above.
.OUTPUTS
Final Azure Virtual Network XML configuration that was successfully provisioned.
.NOTES
Version: 1.0
Creation Date: Aug 1, 2014
Author: Keith Mayer (
https://KeithMayer.com )
Change: Initial function development
.EXAMPLE
New-AzureVNet -Verbose
Provision a new Azure Virtual Network using default values.
.EXAMPLE
New-AzureVNet -newDnsServerName labdns01 -newDnsServerIP 10.1.0.5 -newVNetName labnet01 -newVNetLocation "West US"
Provision a new Azure Virtual Network using specific values.
#>

}

After your comment-based help is defined, you’ll be able to dot-source myAzureLibrary.ps1 from another PowerShell session or script, and then use the standard Get-Help cmdlet to display help for your function.

. .\myAzureLibrary.ps1

Get-Help New-AzureVNet –Full

I find that including comment-based help as an early step in the process of defining a new function is also a great way to help me scope the expected purpose, parameters, input and output of a new function before getting too far down in the “weeds” of coding the function.

Task 3: Well, I Declare! (Parameters, that is)

Our next step in defining the function is declaring the input parameters for a Virtual Network that our function will provision.  Rather than hard-coding values in the body of the function, using input parameters makes our function more general-purpose by allowing it to be easily invoked with unique values for each scenario in which it is used. 

As part of the parameter declaration, we can also define default values for each parameter that should be used in the event that those parameters are not specified when invoking the function. These default values can make it really easy for other users to get started with using the function by reducing the number of parameters that they must specify for simple scenarios.

function New-AzureVNet {

… prior code lines from above …

[CmdletBinding()]
param
(
[string]$newDnsServerName = 'dns01',
[string]$newDnsServerIP = '10.1.0.4',
[string]$newVNetName = 'vnet01',
[string]$newVNetLocation = 'East US',
[string]$newVNetAddressRange = '10.1.0.0/16',
[string]$newSubnetName = 'Subnet-1',
[string]$newSubnetAddressRange = '10.1.0.0/24',
[string]$configFile = "$env:Temp\AzureVNetConfig.XML"
)

}

In the example above, we’re defining 8 parameters with a default value for each parameter.  This will allow the function to be invoked without specifying any parameters to build a simple proof-of-concept VNet with generic values. Alternatively, the function can be invoked with specific parameter values to override the defaults.

I’ve also included the [CmdletBinding()] attribute in the code block above.  This allows the function to work more like a real cmdlet.  In particular, I generally include this attribute in functions so that the –Verbose parameter can be used when calling the function.  Write-Verbose cmdlets can be included in the body of the function that display output only when using this parameter.  This can be very useful for including comments in the function that also serve as functional output for tracking and debugging purposes.

Task 4: Shall we BEGIN?

In many cases, when a function begins executing, there’s some initial steps that may need to be performed as either initial setup or cleanup from prior runs of the function.  We can accommodate these initial steps that help prepare the environment in which our function will execute by including a BEGIN block in our function.

function New-AzureVNet {

… prior code lines from above …

begin {

    Write-Verbose "Deleting $configFile if it exists"
Del $configFile -ErrorAction:SilentlyContinue

}

}

In the example above, we’ve added a simple BEGIN block that cleans up any prior occurrence of the temporary configuration file that this function uses.  This helps to prepare a “clean” starting environment so that our function doesn’t accidentally “inherit” any old values from prior runs of this function.

Task 5: Now, get to work – the PROCESS block …

Now that we have a “stub” defined for our new function, we can “get to work” on writing the code that will perform the actual work of provisioning a new Azure VNet in our subscription.  Typically, this code is included inside the PROCESS block within a function as follows:

function New-AzureVNet {

… prior code lines from above …

begin {

    Write-Verbose "Deleting $configFile if it exists"
Del $configFile -ErrorAction:SilentlyContinue

}

process {

# INSERT CODE HERE

}

}

In terms of our specific New-AzureVNet function, this PROCESS block is where we’ll insert the code to perform the following steps in provisioning our new Azure VNet:

  1. Build a generic XML configuration template for the new Azure VNet
     
  2. Add new DNS attribute values to the generic XML configuration template for the new Azure VNet
     
  3. Add new VNet attribute values to the generic XML configuration template for the VNet name, subnets and IP address ranges we’re adding
     
  4. Get the existing VNet configuration for other VNets that may already existing in our Azure subscription
     
  5. Merge the new DNS and VNet  attribute values we’re adding with any DNS, VNet and Local Network configuration that may already existing in our Azure subscription
     
  6. Save the new merged VNet configuration to a temporary file
     
  7. Call Set-AzureVNetConfig cmdlet to Provision the new VNet configuration from the temporary file

When beginning to write the code in a PROCESS block, I often start out defining each of the general tasks that need to be performed as functional comments with Write-Verbose, such as …

function New-AzureVNet {

… prior code lines from above …

begin {

    Write-Verbose "Deleting $configFile if it exists"
Del $configFile -ErrorAction:SilentlyContinue

}

process {

    Write-Verbose "Build generic XML template for new Virtual Network"

    Write-Verbose "Add DNS attribute values to XML template"

    Write-Verbose "Add VNet attribute values to XML template"

    Write-Verbose "Get existing VNet configuration from Azure subscription"

    Write-Verbose "Merge existing DNS servers into new VNet XML configuration"

    Write-Verbose "Merge existing VNets into new VNet XML configuration"

    Write-Verbose "Merge existing Local Networks into new VNet XML configuration"

    Write-Verbose "Saving new VNet XML configuration to $configFile"

    Write-Verbose "Provisioning new VNet configuration from $configFile"

}

}

… this approach provides a good starting point for coding and testing the PROCESS block one section at a time.  Of course, along the way, I may decide that some of these code sections would really be better served by defining them as functions themselves.  However, in terms of simplicity for the example in this article, I’ll code this PROCESS block as a single code block without nested functions.

Now, it’s pretty straightforward to start coding and testing each section, leveraging the –Verbose switch when I test the function to determine in which code section the function is executing. 

For our example function that provisions new Azure Virtual Networks, here’s the completed PROCESS block that does all the work …

function New-AzureVNet {

    … prior code lines from above …

    process {

        Write-Verbose "Build generic XML template for new Virtual Network"
$newVNetConfig = [xml] '
<NetworkConfiguration xmlns:xsd="
https://www.w3.org/2001/XMLSchema" xmlns:xsi=" https://www.w3.org/2001/XMLSchema-instance" xmlns=" https://schemas.microsoft.com/ServiceHosting/2011/07/NetworkConfiguration" >
<VirtualNetworkConfiguration>
<Dns>
<DnsServers>
<DnsServer name="" IPAddress="" />
</DnsServers>
</Dns>
<VirtualNetworkSites>
<VirtualNetworkSite name="" Location="">
<AddressSpace>
<AddressPrefix></AddressPrefix>
</AddressSpace>
<Subnets>
<Subnet name="">
<AddressPrefix></AddressPrefix>
</Subnet>
</Subnets>
<DnsServersRef>
<DnsServerRef name="" />
</DnsServersRef>
</VirtualNetworkSite>
</VirtualNetworkSites>
</VirtualNetworkConfiguration>
</NetworkConfiguration>
'

        Write-Verbose "Add DNS attribute values to XML template"
$newDnsElements = $newVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.Dns.DnsServers.DnsServer
$newDnsElements.SetAttribute('name', $newDnsServerName)
$newDnsElements.SetAttribute('IPAddress', $newDnsServerIP)

        Write-Verbose "Add VNet attribute values to XML template"
$newVNetElements = $newVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.VirtualNetworkSites.VirtualNetworkSite
$newVNetElements.SetAttribute('name', $newVNetName)
$newVNetElements.SetAttribute('Location', $newVNetLocation)
$newVNetElements.AddressSpace.AddressPrefix = $newVNetAddressRange
$newVNetElements.Subnets.Subnet.SetAttribute('name', $NewSubNetName)
$newVNetElements.Subnets.Subnet.AddressPrefix = $newSubnetAddressRange
$newVNetElements.DnsServersRef.DnsServerRef.SetAttribute('name', $newDnsServerName)

        Write-Verbose "Get existing VNet configuration from Azure subscription"
$existingVNetConfig = [xml] (Get-AzureVnetConfig).XMLConfiguration

        Write-Verbose "Merge existing DNS servers into new VNet XML configuration"
$existingDnsServers = $existingVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.Dns.DnsServers
if ($existingDnsServers.HasChildNodes) {
ForEach ($existingDnsServer in $existingDnsServers.ChildNodes) {
if ($existingDnsServer.name -ne $newDnsServerName) {
$importedDnsServer = $newVNetConfig.ImportNode($existingDnsServer,$True)
$newVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.Dns.DnsServers.AppendChild($importedDnsServer) | Out-Null
}
}
}

        Write-Verbose "Merge existing VNets into new VNet XML configuration"
$existingVNets = $existingVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.VirtualNetworkSites
if ($existingVNets.HasChildNodes) {
ForEach ($existingVNet in $existingVNets.ChildNodes) {
if ($existingVNet.name -ne $newVNetName) {
$importedVNet = $newVNetConfig.ImportNode($existingVNet,$True)
$newVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.VirtualNetworkSites.AppendChild($importedVNet) | Out-Null
}
}
}

        Write-Verbose "Merge existing Local Networks into new VNet XML configuration"
$existingLocalNets = $existingVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.LocalNetworkSites
if ($existingLocalNets.HasChildNodes) {
$dnsNode = $newVNetConfig.NetworkConfiguration.VirtualNetworkConfiguration.Dns
$importedLocalNets = $newVNetConfig.ImportNode($existingLocalNets,$True)
$newVnetConfig.NetworkConfiguration.VirtualNetworkConfiguration.InsertAfter($importedLocalNets,$dnsNode) | Out-Null
}

        Write-Verbose "Saving new VNet XML configuration to $configFile"
$newVNetConfig.Save($configFile)

        Write-Verbose "Provisioning new VNet configuration from $configFile"
Set-AzureVNetConfig -ConfigurationPath $configFile | Out-Null

    }

   }

Tip! You’ll note that a few lines in the code block above end with the Out-Null cmdlet.  This cmdlet is used to discard output from the preceding command pipeline that would otherwise end up being included in the function’s returned output.  We want our New-AzureVNet function to return only the final XML output for the VNet configuration that was successfully applied, so we’re using Out-Null to discard any other output that may occur along the way. If you’re familiar with functions in other programming or scripting languages, this may seem a bit bizarre, but this is a common consideration when building functions in PowerShell.

Task 6: All’s well that ENDs well!

When a function has completed the PROCESS block, we will likely have some code that we’d like to have executed to clean-up our environment or return certain values before the function ends.  We can perform this with an END block within our function.

function New-AzureVNet {

    … prior code lines from above …

    end {

        Write-Verbose "Deleting $configFile if it exists"
Del $configFile -ErrorAction:SilentlyContinue

        Write-Verbose "Returning the final VNet XML Configuration"
(Get-AzureVnetConfig).XMLConfiguration

    }

}

In the example in this article, we’re using the END block in our function to clean up temporary files and also return the final XML output that reflects the  VNet configuration that was successfully processed for our Azure subscription.

Task 7: Gotta Run!

After defining your new function, be sure to save your code in myAzureLibrary.ps1, and then you’re ready for a test run …

# Import Azure PowerShell Module
Import-Module 'C:\Program Files (x86)\Microsoft SDKs\Windows Azure\PowerShell\ServiceManagement\Azure\Azure.psd1'

# Dot-source the function script for New-AzureVNet
. .\myAzureLibrary.ps1

# Authenticate to Azure subscription
Add-AzureAccount

# Select Azure subscription, if more than one
$subscriptionName = 'Free Trial'
Select-AzureSubscription `
–SubscriptionName $subscriptionName

# Provision a new VNet using our new function[xml]$newAzureVNet = New-AzureVNet -Verbose

# Show resulting XML$newAzureVNet.InnerXml

# Show new provisioned VNet
Get-AzureVNetSite

After this simple test run, execute the New-AzureVNet function with various combinations of parameters to make sure those work successfully for you, too.

Continue your Hybrid Cloud learning!

In this article, we’ve defined a custom PowerShell function to quickly provision a new Virtual Network in an existing Azure subscription.  Along the way, we’ve explored an approach that you can leverage to elevate your PowerShell skills from simple scripts to reusable tools.

To continue your learning on Microsoft Azure and Hybrid Cloud, be sure to join our FREE Hybrid Cloud study track in our online Early Experts study group!

To continue learning more about PowerShell, be sure to check out these great courses on Microsoft Virtual Academy!