Making Network Changes During an Azure IaaS ARM Template Deployment

In my last post, I discussed the deployment of a Domain Controller to Azure IaaS using ARM templates and DSC. The example I used was as simple as possible but this led me to exclude a couple of things that warrant further discussion -

  • Configuring a static IP on the DC NIC
  • Changing the DNS server configured against the Virtual Network after DC deployment so that it uses the DC static IP

During this post, I'll continue the example used last time. If you want to follow along at home, catch up by reading this.

Configuring a Static IP on the DC NIC

I open my solution in Visual Studio and start by selecting the NetworkInterface of the DC in the JSON Outline -

ARMVNET01

The JSON I had up until now reads as

     {
      "apiVersion": "2015-06-15",
      "type": "Microsoft.Network/networkInterfaces",
      "name": "[variables('nicName')]",
      "location": "[resourceGroup().location]",
      "tags": {
        "displayName": "NetworkInterface"
      },
      "dependsOn": [
        "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
        "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
      ],
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig1",
            "properties": {
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
              },
              "subnet": {
                "id": "[variables('subnetRef')]"
              }
            }
          }
        ]
      }
    }

To assign a static IP address, I need to modify the privateIPAllocationMethod property and add a privateIPAddress property -

     {
      "apiVersion": "2015-06-15",
      "type": "Microsoft.Network/networkInterfaces",
      "name": "[variables('nicName')]",
      "location": "[resourceGroup().location]",
      "tags": {
        "displayName": "NetworkInterface"
      },
      "dependsOn": [
        "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
        "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
      ],
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig1",
            "properties": {
              "privateIPAddress": "10.0.0.4",
              "privateIPAllocationMethod": "Static",
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
              },
              "subnet": {
                "id": "[variables('subnetRef')]"
              }
            }
          }
        ]
      }
    }

I need to make sure that the IP address I choose is within the subnet upon which the VM resides (in this case 10.0.0.0/24) and I need to skip the first three IPs as these are reserved by Azure. If I wanted to, I could define the DC static IP address using a variable or parameter but for this post, I'll keep it simple.

Deploying the DC now will result in the static IP assignment we're looking for.

Changing VNet DNS Server During Deployment

In the example I'm working with, everything I've demonstrated up to this point is enough to deploy the Domain Controller. If however, I want to provision other VMs in the same deployment and have them join the new forest/domain, I need to modify the VNet DNS server.

Initially, the DNS server for the VNet will be unspecified, allowing the DC to use Azure's default DNS server so that it can fetch the DSC script from my storage account. Once the forest/domain is deployed, I want the VNet updated so that any new VMs use the DC as the preferred DNS server.

Steps to achieve this aren't entirely obvious. I can't just deploy the VNet twice in the same template. The trick is to use a separate template for the VNet deployment and to cascade this VNet template off the main deployment as two separate deployments.

The first thing to do is to add a new JSON template to the solution using -

ARMVNET02

and then -

ARMVNET03

Now I add the following to my new VirtualNetwork.json file -

 {
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "addressPrefix": {
      "type": "string"
    },
    "dnsServers": {
      "type": "array",
      "defaultValue": [ ]
    },
    "subnets": {
      "type": "array"
    },
    "virtualNetworkName": {
      "type": "string"
    }
  },
  "variables": {
  },
  "resources": [
    {
      "comments": "Internal vNet used by deployment.",
      "type": "Microsoft.Network/virtualNetworks",
      "name": "[parameters('virtualNetworkName')]",
      "apiVersion": "2015-06-15",
      "location": "[resourcegroup().location]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "[parameters('addressPrefix')]"
          ]
        },
        "dhcpOptions": {
          "dnsServers": "[parameters('dnsServers')]"
        },
        "subnets": "[parameters('subnets')]"
      }
    }
  ],
  "outputs": {
  }
}

Here, my parameters allow me to pass in the virtualNetworkName, addressPrefix, an array of subnets and an array of dnsServers. It's important to note that the default value for dnsServers is an empty array [] which will be used when we do not specify a value.

Next I go back to my WindowsVirtualMachine.json file and locate the VirtualNetwork in the JSON Outline -

ARMVNET04

I remove the JSON that is highlighted in WindowsVirtualMachine.json and replace it with -

     {
      "name": "createVNet",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2015-01-01",
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "uri": "[concat(parameters('_artifactsLocation'), '/', 'VirtualNetwork.json')]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "virtualNetworkName": {
            "value": "[variables('virtualNetworkName')]"
          },
          "addressPrefix": {
            "value": "[variables('addressPrefix')]"
          },
          "subnets": {
            "value": [
              {
                "name": "[variables('subnetName')]",
                "properties": {
                  "addressPrefix": "[variables('subnetPrefix')]"
                }
              }
            ]
          }
        }
      }
    }

Now that I've changed how the VNet is deployed, I need to make a change to the NIC used by the DC. To do this, I select NetworkInterface in the JSON Outline -

ARMVNET01

Now I edit the dependsOn property to reflect the change in VNet deployment by changing it from -

       "dependsOn": [
        "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
        "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
      ],

to -

       "dependsOn": [
        "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
        "Microsoft.Resources/deployments/createVNet"
      ],

The createVNet deployment uses the VirtualNetwork.json file but expects to find it in the _artifactsLocation used by the deployment script -

         "templateLink": {
          "uri": "[concat(parameters('_artifactsLocation'), '/', 'VirtualNetwork.json')]",
          "contentVersion": "1.0.0.0"
        },

The _artifactsLocation is the storage account where the deployment script Deploy-AzureResourceGroup.ps1 uploads the DSC resources (see my last post). To have it upload the JSON templates as well, I open Deploy-AzureResourceGroup.ps1, locate -

     # Copy Configuration Data files into staging directory
    Get-ChildItem $DSCSourceFolder -File -Filter '*.psd1' | Copy-Item -Destination $ArtifactStagingDirectory -Force

    # Create DSC configuration archive
 if (Test-Path -Path $DSCSourceFolder)
   {
       Get-ChildItem -Path $DSCSourceFolder -Filter *.ps1 | ForEach-Object {

           $archiveName = $_.BaseName + '.ps1.zip'
         $archivePath = Join-Path -Path $ArtifactStagingDirectory -ChildPath $archiveName
            
            # Create the .ps1.zip file DSC Archive
          Publish-AzureRmVMDscConfiguration -ConfigurationPath $_.FullName `
              -OutputArchivePath $archivePath `
               -Force `
                -Verbose
        }
   }

and edit to give -

     # Copy Configuration Data files into staging directory
    Get-ChildItem $DSCSourceFolder -File -Filter '*.psd1' | Copy-Item -Destination $ArtifactStagingDirectory -Force

 # Copy templates into the staging directory
    $TemplatePath = (Get-Item $TemplateFile).Directory.Fullname
  Get-ChildItem $TemplatePath -File | Copy-Item -Destination $ArtifactStagingDirectory -Force
 
    # Create DSC configuration archive
  if (Test-Path -Path $DSCSourceFolder)
   {
       Get-ChildItem -Path $DSCSourceFolder -Filter *.ps1 | ForEach-Object {

           $archiveName = $_.BaseName + '.ps1.zip'
         $archivePath = Join-Path -Path $ArtifactStagingDirectory -ChildPath $archiveName
            
            # Create the .ps1.zip file DSC Archive
          Publish-AzureRmVMDscConfiguration -ConfigurationPath $_.FullName `
              -OutputArchivePath $archivePath `
               -Force `
                -Verbose
        }
   }

The last thing I do is copy the JSON for createVNet and add it again to the end of the resources section of WindowsVirtualMachine.json. I then change the name of the resource from createVNet to updateVNetDNS, add dnsServers and add a dependsOn property that causes the re-deployment of the VNet to wait for the DSC extension to complete -

     {
      "name": "updateVNetDNSAddress",
      "type": "Microsoft.Resources/deployments",
      "apiVersion": "2015-01-01",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/',variables('vmName'),'/extensions/dscDC')]"
      ],
      "properties": {
        "mode": "Incremental",
        "templateLink": {
          "uri": "[concat(parameters('_artifactsLocation'), '/', 'VirtualNetwork.json')]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "virtualNetworkName": {
            "value": "[variables('virtualNetworkName')]"
          },
          "addressPrefix": {
            "value": "[variables('addressPrefix')]"
          },
          "subnets": {
            "value": [
              {
                "name": "[variables('subnetName')]",
                "properties": {
                  "addressPrefix": "[variables('subnetPrefix')]"
                }
              }
            ]
          },
          "dnsServers": {
            "value": [
              "10.0.0.4"
            ]
          }
        }
      }
    }

Conclusion

Running this deployment will provision the VNet without a defined DNS server, wait for the VM to be fully provisioned along with promotion to the role of a Domain Controller and then update the VNet to use the IP address of the DC as the preferred DNS Server.

For convenience, I've included the complete Visual Studio solution here.