0
votes

I have developed and ARM Template that will build multiple VMs in parallel. Each machine has an OS Disk, and a vNIC. I can use PowerShell to deploy the ARM Template or I can upload the ARM Template and deploy it using the "Deploy Custom Template" feature of the Azure Portal.

However I cannot quite seem to get the Syntax correct to add in the JsonADDomainExtension with a copy loop so that all machines get added to my domain in the same template.

At the moment all my resources are in the one Resource Group. I have pre-deployed a vNET with a Subnet, and a storage account.

Below is the ARM Template that works deploying multiple machines at once.

{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters":
    {
    "adminUsername":
        {
        "type": "string",
        "metadata": {"description": "Administrator username for the Virtual Machine."}
        },
    "adminPassword":
         {
        "type": "securestring",
        "metadata": {"description": "Password for the Virtual Machine.Should contain any 3 of: 1 Lower Case, 1 Upper Case, 1 Number and 1 Special character."}
        },
    "numberOfInstances":
        {
        "type": "int",
        "defaultValue": 3,
        "minValue": 2,
        "maxValue": 5,
        "metadata": {"description": "Number of VMs to deploy, limit 5 since this sample is using a single storage account."}
        },
    "StartOfCount":
        {
        "type": "int",
        "defaultValue": 1,
        "metadata": {"description": "Number that you want to start the machine name numbering from, EG to create 5 VMs with the name starting at 045 (W2016SRV-045-vm) you would enter 45 here, or to start at 1 just enter 1 here, this wil give you a machine name like 'W2016SRV-001-vm'."}
        },
    "vmNamePrefix": 
        {
            "type": "string",
            "defaultValue": "W2016SRV-",
            "maxLength": 9,
            "metadata": {"description": "The VM name prefix maximum 9 characters. This allows for three digits in the name and trailing '-vm'. EG: 'W2016SRV-045-vm'."}
            },
    "vmSize": 
        {
        "type": "string",
        "defaultValue": "Standard_B2ms",
        "metadata": 
            {
            "description": "The size(T-shirt) for the VM. (Standard_D8s_v3)",
            "SNC::Parameter::Metadata": {"referenceType": "Microsoft.Compute/virtualMachines/vmSize"}
            }
        },
    "vmLocation": 
        {
        "type": "string",
        "defaultValue": "AustraliaSoutheast",
        "metadata": 
            {
            "description": "Location or datacenter where the VM will be placed.",
            "SNC::Parameter::Metadata": {"referenceType": "Microsoft.Azure/region"}
            }
        },
    "domainToJoin": 
        {
        "type": "string",
        "metadata": {"description": "The FQDN of the AD domain"}
        },
    "domainUsername": 
        {
        "type": "string",
        "metadata": {"description": "Username of the account on the domain"}
        },
    "domainPassword": 
        {
        "type": "securestring",
        "metadata": {"description": "Password of the account on the domain"}
        },
    "ouPath": 
        {
        "type": "string",
        "metadata": {"description": "Specifies an organizational unit (OU) for the domain account. Enter the full distinguished name of the OU in quotation marks. Example: 'OU=testOU; DC=domain; DC=Domain; DC=com"}
        },
    "sizeOfDiskInGB": 
        {
        "type": "int",
        "defaultValue": 200,
        "metadata": {"description": "The disk size for the OS drive in the VM, in GBs. Default Value is 500"}
        },
    "existingBootDiagStorageResourceGroup": 
        {
        "type": "string",
        "metadata": {"description": "Storage account resource group name where boot diagnistics will be stored"}
        },
    "existingBootDiagStorageName": 
        {
        "type": "string",
        "metadata": 
            {
            "description": "Storage account name where boot diagnistics will be stored. It should be at the same location as the VM.",
            "SNC::Parameter::Metadata": {"referenceType": "Microsoft.Storage/storageAccounts"}
            }
        },
    "existingvNetResourceGroup": 
        {
        "type": "string",
        "metadata": 
            {
            "description": "Resource Group of the Existing Virtual Network.",
            "SNC::Parameter::Metadata": {"referenceType": "Microsoft.Resources/resourceGroups"}
            }
        },
    "existingvNetName": 
        {
        "type": "string",
        "metadata": 
            {
            "description": "Existing Virtual Network to connect to Network Interface to.It should be at the same location as the VM.",
            "SNC::Parameter::Metadata": {"referenceType": "Microsoft.Network/virtualNetworks"}
            }
        },
    "subnetName": 
        {
        "type": "string",
        "metadata": 
            {
            "description": "The Subnet for the VM.",
            "SNC::Parameter::Metadata": {"referenceType": "Microsoft.Network/subNets"}
            }
        }
    },
"variables": 
    {
    "storageAccountType": "Standard_LRS",
    "vnetID": "[resourceId(parameters('existingvNetResourceGroup'), 'Microsoft.Network/virtualNetworks', parameters('existingvNetName'))]",
    "subnetRef": "[concat(variables('vnetID'),'/subnets/', parameters('subnetName'))]",
    "StartOfCountString": "[String(Parameters('StartOfCount'))]"
    },
"resources": 
    [
        {
        "apiVersion": "[providers('Microsoft.Network','networkInterfaces').apiVersions[0]]",
        "type": "Microsoft.Network/networkInterfaces",
        "name": "[concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm-vNic')]",
        "location": "[parameters('vmLocation')]",
        "copy": {
            "name": "nicLoop",
            "count": "[parameters('numberOfInstances')]"
            },
        "properties": 
            {
            "ipConfigurations": 
                [
                    {
                    "name": "Prod",
                    "properties": 
                        {
                        "privateIPAllocationMethod": "Dynamic",
                        "subnet": {"id": "[variables('subnetRef')]"}
                        }
                    }
                ]
            }
        },
        {
        "apiVersion": "[providers('Microsoft.Compute','virtualMachines').apiVersions[0]]",
        "type": "Microsoft.Compute/virtualMachines",
        "name": "[concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm')]",
        "location": "[parameters('vmLocation')]",
        "copy": {
            "name": "vmLoop",
            "count": "[parameters('numberOfInstances')]"
            },
        "dependsOn": ["nicLoop"],
        "tags": 
            {
            "Project": "***"
            },
        "properties": 
            {
            "hardwareProfile": {"vmSize": "[parameters('vmSize')]"},
            "osProfile": 
                {
                "computerName": "[concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm')]",
                "adminUsername": "[parameters('adminUsername')]",
                "adminPassword": "[parameters('adminPassword')]",
                "windowsConfiguration": {"enableAutomaticUpdates": false}
                },
            "storageProfile": 
                {
                "imageReference": {
                    "id": "/subscriptions/***/resourceGroups/***/providers/Microsoft.Compute/images/***"
                },
                "osDisk": 
                    {
                    "name": "[concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm-osDisk')]",
                    "createOption": "FromImage",
                    "diskSizeGB": "[parameters('sizeOfDiskInGB')]",
                    "caching": "ReadWrite",
                    "managedDisk": {"storageAccountType": "[variables('storageAccountType')]"}
                    }
                },
            "networkProfile": {"networkInterfaces": [{"id": "[resourceId('Microsoft.Network/networkInterfaces',concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm-vNic'))]"}]},
            "diagnosticsProfile": 
                {
                "bootDiagnostics": 
                    {
                    "enabled": true,
                    "storageUri": "[reference(resourceId(parameters('existingBootDiagStorageResourceGroup'), 'Microsoft.Storage/storageAccounts', parameters('existingBootDiagStorageName')), '2015-06-15').primaryEndpoints['blob']]"
                    }
                }
            }
        }
    ]
}

However, If I add in this to the resources bit at the bottom then it doesn't work.

{
    "apiVersion": "2016-03-30",
    "type": "Microsoft.Compute/virtualMachines/extensions",
    "name": "[concat(parameters('vmNamePrefix'), padLeft(copyIndex(variables('StartOfCountString')),3, '0'), '/JoinDomain')]",
    "location": "[resourceGroup().location]",
    "copy": {
        "name": "DomainJoinLoop",
        "count": "[parameters('numberOfInstances')]"
    },
    "dependsOn": "[concat('Microsoft.Compute/virtualMachines/', concat(parameters('vmNamePrefix'), padLeft(copyIndex(variables('StartOfCountString')),3, '0')))]",
    "properties": {
        "publisher": "Microsoft.Compute",
        "type": "JsonADDomainExtension",
        "typeHandlerVersion": "1.3",
        "autoUpgradeMinorVersion": true,
        "settings": {
            "Name": "[parameters('domainToJoin')]",
            "User": "[concat(parameters('domainToJoin'), '\\', parameters('domainUsername'))]",
            "OUPath": "[parameters('ouPath')]",
            "Restart": "true",
            "Options": "3"
        },
        "protectedsettings": {
            "Password": "[parameters('domainPassword')]"
        }
    }
}

In the copyIndex portions of the code I've tried both variables and parameters to set the offset for the copyIndex. I've also tried hard coding it. I keep getting an error like this.

{"telemetryId":"649e0a7f-2c22-41f9-bede-c13618f5053a", "bladeInstanceId":"Blade_b45c7efadff74340820345aa2e9d76c1_26_0", "galleryItemId":"Microsoft.Template","createBlade": "DeployToAzure", "code":"InvalidRequestContent", "message":"The request content was invalid and could not be deserialized: 'Error converting value \"[concat('Microsoft.Compute/virtualMachines/', concat(parameters('vmNamePrefix'), padLeft(copyIndex(variables('StartOfCountString')),3, '0')))]\" to type 'System.String[]'. Path 'properties.template.resources[1].resources[0].dependsOn', line 259, position 171.'."}

The reason that I want to set an offset for the copy index is so that I can build say ten machines like W2016SRV-001, 002, ... 010, and then later on build more machines starting at 011, 012, 013, etc.

There are other ways around it but I really want to be able to do this in an ARM Template and I'm not yet aware after all my extensive googling why it shouldn't be possible.

...............................................................................

OK so after editing the template as suggested by @4c74356b41 I'm now getting a completely new and different error when I try and deploy the template through the portal.

{"telemetryId":"649e0a7f-2c22-41f9-bede-c13618f5053a",
"bladeInstanceId":"Blade_b45c7efadff74340820345aa2e9d76c1_26_0",
"galleryItemId":"Microsoft.Template","createBlade":"DeployToAzure",
"code":"InvalidTemplate",
"message":"Deployment template validation failed: 'The template resource 
'[concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'),
'-vm/JoinDomain')]' 
at line '250' column '13' is not valid. Copying nested resources is not supported. 
Please see https://aka.ms/arm-copy/#looping-on-a-nested-resource for usage details.'."}

So following the link in the error message and reading some more ... what I needed to do was drop the Domain Join Extension out as a child of the VM and have it as a top level resource. Woot woot it worked.

So now the end of my ARM Template looks like this.

            *** VM bits are here ***
                }
            }
        }, <-- end of the VM bits.
        {
        "apiVersion": "2016-03-30",
        "type": "Microsoft.Compute/virtualMachines/extensions",
        "name": "[concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm/JoinDomain')]",
        "location": "[resourceGroup().location]",
        "copy": {
            "name": "DomainJoinLoop",
            "count": "[parameters('numberOfInstances')]"
            },
        "dependsOn": [ "vmLoop" ],
        "properties": 
            {
            "publisher": "Microsoft.Compute",
            "type": "JsonADDomainExtension",
            "typeHandlerVersion": "1.3",
            "autoUpgradeMinorVersion": true,
            "settings":
                {
                "Name": "[parameters('domainToJoin')]",
                "User": "[concat(parameters('domainToJoin'), '\\', parameters('domainUsername'))]",
                "OUPath": "[parameters('ouPath')]",
                "Restart": "true",
                "Options": "3"
                },
            "protectedsettings": {"Password": "[parameters('domainPassword')]"}
            }
        }

    ],
"outputs":
    {}
}

Multiple VMs deployed in parallel and all joined to the domain without issue.

Sooo my lessons learned are these and I hope that they help someone else in the future.

  1. You cannot use copyIndex on a Child resource.
  2. Make sure that you name your DomainJoin Extension properly.
1
Thanks. It helps, but why the extension does not specify the VM that each of the extensions is deployed to?Bomin

1 Answers

0
votes

Compare your vm name and extension name:

concat(parameters('vmNamePrefix'), padLeft(copyIndex(parameters('StartOfCount')),3, '0'), '-vm')
concat(parameters('vmNamePrefix'), padLeft(copyIndex(variables('StartOfCountString')),3, '0'), '/JoinDomain')

besides the fact you are using an unneeded variable you are missing -vm. The extension name has to be in the format parent_name/extension_name else it wouldn't know to which parent resource this extension belongs (it must always belong to some vm).

Your dependsOn is also wrong, you can simplify it to this:

"dependsOn": [ "vmLoop" ]