The Ultimate ARM Template – Part 2 (Parameters)

When you think of a template that will give you the most flexibility or reuse the main thing you’ll need is the ability to change what that template does and you do that with parameters. Variables help make this easier to do and read. So by allowing the consumer of the template to provide parameters for different options, you maximize reuse. There is a balance here in that you don’t want to provide parameters for everything as that will make things difficult to consume but there are things you can do to achieve a good balance. To be honest, it’s really all about how you use defaultValues…

Default Values

Default values essentially make a parameter “optional” in that the consumer of a template does not have to provide a value. Given this, you want to make sure that whatever defaultValue you provide will work in the majority of, or the most common cases. For example, if the parameter value needs to be globally unique, don’t provide a literal value, generate a unique one. However, idempotency is also important. The uniqueString() function generates a deterministic value and it instrumental in this approach. Contrast that with the newGuid() function that will generate a new GUID every time the template is deployed. This sample below is a good balance of idempotency and uniqueness – the expression creates a new storageAccount name for each VM. So this template could be deployed repeatedly to the same resourceGroup and not have to worry about conflicts (for example with the storageAccount SKU).


    "storageAccountName": {
      "type": "string",
      "defaultValue": "[concat('storage', uniqueString(parameters('vmName'), resourceGroup().id))]",
      "metadata": {
        "description": "Name of the storage account"
      }
    },

The downside is that it will create a storageAccount for each VM which isn’t likely necessary, but… this is only default. So the template can still be used for sharing a storageAccount, but since it’s generally not possible for the template author to determine the defaultValues needed for that scenario, the default/simple case is the scenario to optimize for here.

Default Values Working Together

Another thing to consider is how different parameters work together and how the defaultValues should too. In the ultimate template the consumer has the option of using new or existing resources for certain parts of the deployment. In this example, you may want to leverage an existing storageAccount for boot diagnostics on the VM. Now, again, I can’t reasonably assume what that existing storage account might be unless I’m in a constrained environment. So to make the template flexible and easy to use, the defaultValues work together to deploy a new storageAccount.

    
    "storageNewOrExisting": {
      "type": "string",
      "defaultValue": "new",
      "allowedValues": [
        "new",
        "existing",
        "none"
      ],
      "metadata": {
        "description": "Determines whether or not a new storage account should be provisioned.  'none' disables boot diags."
      }
    },
    "storageAccountName": {
      "type": "string",
      "defaultValue": "[concat('storage', uniqueString(parameters('vmName'), resourceGroup().id))]",
      "metadata": {
        "description": "Name of the storage account"
      }
    },
    "storageAccountType": {
      "type": "string",
      "defaultValue": "Standard_LRS",
      "allowedValues": [
        "Standard_LRS",
        "Standard_GRS",
        "Standard_RAGRS"
      ],
      "metadata": {
        "description": "Storage account type for boot diagnostics, only LRS/GRS skus are allowed."
      }
    },
    "storageAccountResourceGroupName": {
      "type": "string",
      "defaultValue": "[resourceGroup().name]",
      "metadata": {
        "description": "Name of the resource group for the existing storage account"
      }
    },

Here’s how they work together:

  • storageAccountNewOrExisting – this determines the scenario
  • storageAccountName – generates a unique name for each deployment
  • storageAccountSku – this doesn’t matter much for the scenario, but is the most cost efficient since we always have a new account
  • storageAccountResourceGroupName – perhaps only the one of these that’s not obvious but in the case of an existing storageAccount, that resource may be in a different resourceGroup – so the resourceId() function that references it needs the resourceGroup parameter. IOW, in the “new” scenario, this will be the current resourceGroup and then supplied by the user in the non-default case. Defaulting to the current resourceGroup simplifies the reference to it (more on this below).

So as a consumer of this template if I want to deploy the scenario with a new storageAccount, I can rely on the defaultValues of all the parameters that relate to it, to simplify my deployment. Also, note the parameter for new/existing also allows for a value of “none” if I want to disable boot diags. Two things to call out here:

  1. I didn’t need an extra parameter (say a boolean) for enabling disabling. On one hand that may be “overloading” the parameter but it’s still very readable, understandable and uses less code overall.
  2. When I select “none” I don’t need to worry about all the other parameters, I can use the default because the others will be ignored. That won’t be obvious in every deployment experience, say the Portal’s Template Deployment blade, but in scenarios where you control the user experience (Marketplace, Managed Apps and the forth coming templateSpec) the experience can be exactly as you’d expect.

Finally, this approach allows me to simplify the creation of resourceIds – I don’t need an if() condition around the new or existing case, because of the defaultValues, I can construct the resourceId in a consistent manner. This always works in the common case because of the defaultValue for the storageAccountResourceGroup name.

"storageAccountId": "[resourceId(parameters('storageAccountResourceGroupName'), 'Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]",

Parameter Validation

There is a trick you can use to validate parameter values and even validate parameter sets. It’s not the most elegant but it is possible and it will allow you to “fail fast” of something is wrong with the deployment. Here’s an example: In the ultimate template I have the option of using a managedIdentity on the VM. This could be a userAssigned or systemAssigned identity. If it’s systemAssigned, that’s simple, I just set one parameter value. However, if it’s userAssigned, I need to provide a value for the id of the identity – so I want to ensure that the user did not select a userAssigned identity and fail to provide it. Here’s what that looks like:

    "managedIdentity": {
      "type": "string",
      "defaultValue": "None",
      "allowedValues": [
        "UserAssigned",
        "SystemAssigned",
        "None"
      ],
      "metadata": {
        "description": "Managed identity type, if UserAssigned, the value of the UserAssignedIdentity parameter must also be set."
      }
    },
    "userAssignedIdentity": {
      "type": "string",
      "defaultValue": "",
      "metadata": {
        "description": "The resourceId of the user assigned managed identity to assign to the virtual machine."
      }
    },
    "validateManagedIdentity": {
      "type": "bool",
      "allowedValues": [
        true
      ],
      "defaultValue": "[if(and(equals(parameters('managedIdentity'),'UserAssigned'), empty(parameters('userAssignedIdentity'))), bool('false'), bool('true'))]",
      "metadata": {
        "description": "Check to ensure that if a managedIdentity type is UserAssigned that UserAssignedIdenity parameters is not empty."
      }
    },

The “validation” starts on line 20 with the validateManagedIdentity parameter. This parameter is a boolean type with only one allowedValue, it must validate successfully, or in other words it must be “true”. The defaultValue has a condition that makes sure the condition is met, in this case if the userAssignedIdentity is empty and the type is UserAssigned that would fail the validation so we set the defaultValue to “false”. Since the only allowedValue is true, this will cause template validation to fail and deployment will not continue. This way do don’t end up with a partial deployment because it never starts.

Now, this is not perfect – it’s possible for someone to override the defaultValue of the validateManagedIdentity parameter and just set it to true, but if you have a scenario where you can control the parameter input (Marketplace, Managed Apps and the forth coming templateSpec) then it works as expected. It’s helpful even if it cannot be completely enforced.

Do Not Default

The next thing to think about – a few cases where you do not to default parameters values:

  • Passwords for sure 😉 or any secrets – secureString, secureObject – aside from empty values to make the secret optional. I think we’ve all heard and maybe learned, about putting secrets in source control [remember templates should be under source code control] so won’t go into detail. And this is one case where the arm-ttk will flag the practice for you.
  • Non-secure credentials, e.g. user names, yeah it’s not a secret and it doesn’t need to be secure but it increases surface area. You might find cases where this is helpful, I’ll still say don’t do it. When you look at the this template you’ll find there are very few values that are required, so adding one for increased security is worth the cost, IMO.
  • Anything that’s not likely to work for the common case – this was mentioned above briefly, but don’t use a literal value for something that needs to be unique.

Ok, I thought I was going to get to variables in this post but the longer I make them the less likely I am to do them, so I’ll wrap this one here and save variables for the next post. You can always find the full template here.

As always, let me know if there’s something you’d like to see (here or via any of the social icons at the top) and I’ll add it to the list.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: