More Fun with ARM Templates

All this time, all I’ve wanted to do with an Azure Resource Manager (ARM) template was create a bunch of identical computers.   The biggest roadblock I’ve faced is figuring out how to make the ARM template not one gigantic document with similarly-named resources in it.

Each VM requires a public IP address, a NIC, and a VM resource.   When I’ve put those into a template, if I named everything to make sense to me (like “Computer1” would have “Computer1-IP” and “Computer1-NIC” attached to it), the document gets really hard to follow.    I can copy and paste the definitions for these three objects, but eventually it gets ugly and confusing, and when something doesn’t work, it’s a pain to work around.

Enter the “count” parameter!   There’s a neat way that you can tell the ARM engine to iterate through a list of resources and create however many you want it to.   The engine breaks down the JSON file and duplicates the guts of it “n” times, assigning a suffix with the number on it at the end of the item names.

Below is my ARM template that creates as many VMs as you want, each with a public IP address and a NIC.

———————

{
“$schema”: “https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#”,
“contentVersion”: “1.0.0.0”,
“parameters”: {
“userImageStorageAccountName”: {
“type”: “string”,
“metadata”: {
“description”: “This is the name of the your storage account”
}
},
“userImageStorageContainerName”: {
“type”: “string”,
“metadata”: {
“description”: “This is the name of the container in your storage account”
}
},
“userImageVhdName”: {
“type”: “string”,
“metadata”: {
“description”: “This is the name of the your customized VHD”
}
},
“adminUserName”: {
“type”: “string”,
“metadata”: {
“description”: “UserName for the Virtual Machine”
}
},
“adminPassword”: {
“type”: “securestring”,
“metadata”: {
“description”: “Password for the Virtual Machine”
}
},
“osType”: {
“type”: “string”,
“allowedValues”: [
“windows”,
“linux”
],
“metadata”: {
“description”: “This is the OS that your VM will be running”
}
},
“vmSize”: {
“type”: “string”,
“metadata”: {
“description”: “This is the size of your VM”
}
},
“vmNameBase”: {
“type”: “string”,
“metadata”: {
“description”: “This is the size of your VM”
}
},
“vmCount”:{
“type”: “int”,
“defaultValue”: 1,
“metadata”: {
“description”: “The number of VMs to build.”
}
}
},
“variables”: {
“location”: “[resourceGroup().location]”,
“virtualNetworkName”: “VNetName”,
“addressPrefix”: “10.6.0.0/16”,
“subnet1Name”: “default”,
“subnet1Prefix”: “10.6.0.0/24”,
“publicIPAddressType”: “Dynamic”,
“vnetID”: “[resourceId(‘Microsoft.Network/virtualNetworks’,variables(‘virtualNetworkName’))]”,
“subnet1Ref”: “[concat(variables(‘vnetID’),’/subnets/’,variables(‘subnet1Name’))]”,
“userImageName”: “[concat(‘http://’,parameters(‘userImageStorageAccountName’),’.blob.core.windows.net/’,parameters(‘userImageStorageContainerName’),’/’,parameters(‘userImageVhdName’))]”
},
“resources”: [
{
“apiVersion”: “2015-05-01-preview”,
“type”: “Microsoft.Network/publicIPAddresses”,
“name”: “[concat(parameters(‘vmNameBase’),copyIndex(),’-PublicIP’)]”,
“copy”: {
“name”: “publicIPAddressCopy”,
“count”: “[parameters(‘vmCount’)]”
},
“location”: “[variables(‘location’)]”,
“properties”: {
“publicIPAllocationMethod”: “[variables(‘publicIPAddressType’)]”,
“dnsSettings”: {
“domainNameLabel”: “[concat(parameters(‘vmNameBase’),copyIndex())]”
}
}
},
{
“apiVersion”: “2015-05-01-preview”,
“type”: “Microsoft.Network/networkInterfaces”,
“name”: “[concat(parameters(‘vmNameBase’),copyIndex(),’-NIC’)]”,
“location”: “[variables(‘location’)]”,
“copy”: {
“name”: “networkInterfacesCopy”,
“count”: “[parameters(‘vmCount’)]”
},
“dependsOn”: [
“[concat(‘Microsoft.Network/publicIPAddresses/’, parameters(‘vmNameBase’),copyIndex(),’-PublicIP’)]”
],
“properties”: {
“ipConfigurations”: [
{
“name”: “ipconfig1”,
“properties”: {
“privateIPAllocationMethod”: “Dynamic”,
“publicIPAddress”: {
“id”: “[resourceId(‘Microsoft.Network/publicIPAddresses/’,concat(parameters(‘vmNameBase’),copyIndex(),’-PublicIP’))]”
},
“subnet”: {
“id”: “[variables(‘subnet1Ref’)]”
}
}
}
]
}
},
{
“apiVersion”: “2015-06-15”,
“type”: “Microsoft.Compute/virtualMachines”,
“name”: “[concat(parameters(‘vmNameBase’),copyIndex())]”,
“copy”: {
“name”: “vmCopy”,
“count”: “[parameters(‘vmCount’)]”
},
“location”: “[variables(‘location’)]”,
“dependsOn”: [
“[concat(‘Microsoft.Network/networkInterfaces/’,parameters(‘vmNameBase’),copyIndex(),’-NIC’)]”,
“[concat(‘Microsoft.Network/publicIPAddresses/’, parameters(‘vmNameBase’),copyIndex(),’-PublicIP’)]”
],
“properties”: {
“hardwareProfile”: {
“vmSize”: “[parameters(‘vmSize’)]”
},
“osProfile”: {
“computername”: “[concat(parameters(‘vmNameBase’),copyIndex())]”,
“adminUsername”: “[parameters(‘adminUsername’)]”,
“adminPassword”: “[parameters(‘adminPassword’)]”
},
“storageProfile”: {
“osDisk”: {
“name”: “[concat(parameters(‘vmNameBase’),copyIndex(),’-osDisk’)]”,
“osType”: “[parameters(‘osType’)]”,
“caching”: “ReadWrite”,
“createOption”: “FromImage”,
“image”: {
“uri”: “[variables(‘userImageName’)]”
},
“vhd”: {
“uri”: “[concat(‘http://’,parameters(‘userImageStorageAccountName’),’.blob.core.windows.net/vhds/’,parameters(‘vmNameBase’), copyIndex(),’-osDisk.vhd’)]”
}
}
},
“networkProfile”: {
“name”: “[concat(parameters(‘vmNameBase’),copyIndex(),’-networkProfile’)]”,
“networkInterfaces”: [
{
“id”: “[resourceId(‘Microsoft.Network/networkInterfaces/’,concat(parameters(‘vmNameBase’),copyIndex(),’-NIC’))]”
}
]
},
“diagnosticsProfile”: {

“bootDiagnostics”: {
“enabled”: “true”,
“storageUri”: “[concat(‘http://’,parameters(‘userImageStorageAccountName’),’.blob.core.windows.net’)]”
}
}
}
}
]
}

——————————————————-

The “copyIndex()” command is what tells the ARM engine to use the value from the loop.   Using “DependsOn” with this, means that Computer1 will depend upong Computer1-NIC and Computer1-PublicIP, so nothing will be built if the other parts aren’t ready for it.

This template is setup to be used with a user image, something custom that I’ve put up there that’s already sysprepped and ready-to-go.  I haven’t yet made this add the resources for the extensions or gotten it to join the domain after being built, but at least I have the VMs running now.

Advertisements

Network Security Groups

Two posts in a day?   Yes….because I’m afraid if I wait until Monday, I’ll forget this stuff.

I’ve been working with Azure Network Security Groups (NSG) today.   For the uninitiated, imagine firewall rules that you can apply to subnets within your Azure network or to specific VMs.   NSG can basically replace all the ACLs on your VM endpoints, allow you to more fully control network traffic between your subnets, and can give you granular control on your VMs.   I envision replacing all of my endpoint ACLs and all of my Windows Firewall configuration with these, once I get them right.

What did I learn today?

  1. The moment you apply an NSG to a subnet, the endpoint ACLs on any VMs within the subnet are basically deemed useless.  If you have an endpoint ACL that restrict connections, as soon as you turn on the NSG, if you don’t have the restriction configured, your connection is wide open to the Internet.
  2. Each rule is made up of a Name, Direction, Action, Source Address, Source Port, Destination Address, Destination Port, and Protocol.   This is like every other firewall rule I’ve ever put in.   One tip here, though, is that on Inbound rules, if you’re wanting to open, say, port 443, you need to put that on the Destination Port.   Leave Source Port at “Any” or “*”.    I was putting the ports in both parts, but it wouldn’t let inbound connections work at all that way.
  3. The new Azure portal is pretty nice in that you can open a subnet, see all the VMs in the subnet, and then use that to browse into the endpoints.    Much easier than in the old portal, which I don’t think could show you what VMs were on what subnets at all.
  4. Reiterated to me that you always do staging first.   I left off one endpoint from one NSG and I basically crippled SQL connections to a db server there.   Do your homework first and be careful.

These worked as advertised and I think it’s going to be the right path for me.   I’m still trying to figure out how to do things on VM-level NSGs for stuff like “RPC Dynamic Ports” and ICMP, but at least I can do some network-level stuff now.    Pretty cool.

End goal will be to have the subnet NSG and some templated VM NSG in such a condition that I can just add a VM, assign it to the right subnet, give it a VM NSG, and never have to worry about Windows Firewall on the machine.  Hell, I could even disable Windows Firewall altogether if I’m lucky.   Good times.

Formatting Disks After Deployment

I don’t usually post code snippets or whatever, but I think it’s time for me to do so.   I have some sweet little ones here and there, and I might as well publish them this way so others can use them if they need them.

One of the first difficulties I had when deploying new VMs was formatting all the disks that I attached to the machine after it was deployed.   In Azure, the system disk always formats to be the C: drive, and there’s always a D: drive attached for “temporary storage”.    However, as I was automating my server builds, I didn’t really have a way to format additional data disks that I attached.

Enter the Custom Script Extension.   This extension allows you to push a PowerShell script into a VM and have it just run.   It’s pretty slick in that it runs locally, just as if you were logged into the machine, and it puts some nice logging under

“C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension”

so you can find out what happened during the execution.

In my deployment script, after the VM is built, I simply pass in the following script to format all the additional drives and assign them the next available drive letters.

----------------------------------------------
Function SetupDisks()
{
 # This will initialize and format any uninitialized disks on the system and assign the next available drive letters to them
 
 $diskstoformat = get-disk | where-object {$_.numberofpartitions -eq 0} | sort {$_.number}
Foreach($disk in $diskstoformat)
 {
 $disknum = $disk.number
 $label = 'Data'+$disknum
$disk | Initialize-Disk -PartitionStyle MBR -ErrorAction SilentlyContinue

new-partition -disknumber $disknum -usemaximumsize -assigndriveletter:$False
$part = get-partition -disknumber $disknum -number 1
if($disknum -gt 1)
{
$part | Format-Volume -FileSystem NTFS -NewFileSystemLabel $label -AllocationUnitSize ‘65536’ -Confirm:$false -Force
}
else
{
$part | Format-Volume -FileSystem NTFS -NewFileSystemLabel $label -Confirm:$false -Force
}
end
$part | Add-PartitionAccessPath -AssignDriveLetter
}
}
———————————————
Taking a look at this, here’s what’s happening:

  1. First, I pull all the disks that have no partitions.
  2. For each one, I assign it a number and label, then initialize it.
  3. Next I create a single maximum-sized partition on the disk.
  4. If I’m on the first disk, I just format it with normal NTFS ….otherwise, I change the allocation size to 65536 to make better use of the disk.   (This isn’t necessary generally, but I do it because only my SQL VMs have more than one data disk, and I use those to store my data, logs, and backups.    My “standard” VM has an attached data disk, which I don’t use the allocation on.)
  5. Finally, I assign a drive letter.

This works every time.   What took me the longest was figuring out how to get the disks to initialize in a reliable way.   Once I got the initialization down, it was pretty straightforward.