Using Azure templateSpecs with a Custom UI
Published Aug 02 2022 07:58 AM 7,413 Views
Microsoft

TemplateSpecs provide a great way to govern deployments in your organization by sharing templates users can use to deploy approved or prescribed workloads.  While TemplateSpecs are commonly used in developer and devops scenarios they are also useful for end users to deploy canonical workloads through the Azure Portal.  By default, the Portal will create a simple user experience for any templateSpec you create and deploy through the Portal.  To provide a richer, more robust experience you can also create your own user interface for deploying templateSpecs via the Azure Portal.

 

With a customized user experience, you can do things like select existing resources from a list, restrict locations, or even make API calls to Azure to guide the experience.  I'll show you a sample that does just that which is only sampling of what's possible.  All the code for this sample can be found here.

 

There are 2 files needed to complete the task; one is the template itself, main.bicep, and the other is uiFormDefinition.json - a file that defines the user interface.  Once you have those files you can publish/create the templateSpec to make it available for deployment in the portal.  If you're new to templateSpecs the documentation can be found here and for a more hands-on experience there is also a learn module available.

 

The first step is to identify the template you want to share for users to deploy.  Any template will do and the step for creating the UI is really just about crafting the experience you want, based on the parameters defined in the template.  For the rest of the blog, I'll focus on that user experience.

 

Specifically, the user experience for the templateSpec is defined by the uiFormDefinition.json file.  The schema for this file is the latest used by the Azure Portal and is based on the createUiDefinition.json file used by the Azure Marketplace.  While the schema is slightly different, the capabilities are largely the same and the documentation available for user-interface for createUiDefinition elements and functions can be used to understand the capabilities of the uiForm.  Additionally, there is a sandbox you can use to develop and test your uiForm.

 

The Sample Scenario

In this sample I have a simple virtual machine template.  The VM will require an existing virtual network and we want the VM to only be deployed in certain locations.  Here's a walkthrough of the relevant parts of each section of the file.

 

Basics

The basics section of the file is the first tab that will show in the Azure Portal's deployment experience.  There are certain controls you will always have in this section but you can add other simple controls as well.  For a really simple template you may only need this section.  Adding other sections allow you to group together similar parameters to simplify the experience.  In our basics section we only have the resourceScope control that determines the scope of the deployment (resourceGroup, subscription or managementGroup).  Using this control we can also list or restrict locations that are allowed by using the allowedValues property.  Here's a snippet:

 

 

 

{
	"name": "resourceScope",
	"type": "Microsoft.Common.ResourceScope",
	"location": {
		"resourceTypes": [
			"microsoft.resources/resourcegroups"
		],
		"allowedValues": [
			"australiacentral",
			"australiaeast",
			"australiasoutheast"
		]
	}
}

 

 

 

 

 

With the resourceTypes property you can restrict the locations available to those where the specified resource types are available.  In other words, if your template contains resourcse are not globally available, you don't have to know which locations to allow - this property will do it for you.  In this sample all of the resources are globally available so using resourceGroups as the resourceTypes is a simple solution to make every location available.  The allowedValues further restrict the list to those that are allowed by the templateSpec author.  You can use different combinations of those properties to enable a number of different scenarios and the end result is a list of the intersecting set of locations from each filter.

 

Virtual Machine Settings

The next section uses a number of controls to prompt for simple values such as the VM name, credentials and VM size.  There is nothing terribly unique in this section but there are a number of built-in controls leveraged to tailor the experience with a little extra validation added in.  For example, the PasswordBox control uses a regular expression to enforce custom password complexity and length.  You can also supply a validation message when the rules are not met.

 

 

 

 

{
	"name": "adminPassword",
	"type": "Microsoft.Common.PasswordBox",
	"label": {
		"password": "Admin Password",
		"confirmPassword": "Confirm password"
	},
	"toolTip": "Password for the Virtual Machine.",
	"constraints": {
		"required": true,
		"regex": "^(?=.*[A-Z])(?=.*[.!@#$%^&*()-_=+])(?=.*[0-9])(?=.*[a-z]).{12,40}$",
		"validationMessage": "Password must be between 12-40 characters in length and contain the following: uppercase letter, lowercase letter, number, and one of the following '.!@#$%^&*()-_=+' characters."
	},
	"visible": true
},

 

 

 

 

 

 

 

 

Virtual Network Settings

Network settings can be some of the most complex to deal with in a deployment.  There are built-in controls to help with this complexity but there is a lot of detail for end-users to manage - and in many organizations end-users may not have permission to do some of the things you can do with the networking control.  In our scenario we simply want the user to select an existing subnet to deploy the VM to and to select a DNS name (that we'll validate) for the public IP address.

 

To select the existing subnet we'll use a few controls to list the virtualNetworks in the region and then list the subnets within the selected virtualNetwork.  The first control is the ResourceSelector which lists all of the resources of the specified type and filters.

 

 

 

 

 

 

 

{
	"name": "vnetSelector",
	"type": "Microsoft.Solutions.ResourceSelector",
	"label": "Virtual Network",
	"resourceType": "Microsoft.Network/virtualNetworks",
	"options": {
		"filter": {
			"subscription": "onBasics",
			"location": "onBasics"
		}
	}
},

 

 

 

 

 

Note, that you can filter resources based on criteria from the basics blade or you could add your own.  For example, if you wanted to geo-pair resources with a separate region, you could use the value of that control as a filter.

 

The next step is to list the subnets, after a virtualNetwork is selected.  For this we use the ArmApiControl and query the ARM API with a simple GET for the list.

 

 

 

 

 

 

 

{
	"name": "subnets",
	"type": "Microsoft.Solutions.ArmApiControl",
	"request": {
		"method": "GET",
		"path": "[concat(steps('basics').resourceScope.subscription.id, '/resourceGroups/', last(take(split(steps('networkSettings').vnetSelector.id, '/'), 5)), '/providers/Microsoft.Network/virtualNetworks/', steps('networkSettings').vnetSelector.name,'/subnets?api-version=2022-01-01')]"
	}
},

 

 

 

 

 

 

 

 

The response from that API request, or rather the value of the control can be used to populate a DropDown control with whatever values are returned in the response.  In this case we need just need the name for the label of an entry in the dropdown and then we'll use the full resourceId for the value.  The user will see the subnet name and then the resourceId will be passed to the template for deployment.  The common javascript map() function can be used to parse a complex JSON response and create the necessary code for the dropdown control.

 

 

 

 

{
	"name": "subnetList",
	"type": "Microsoft.Common.DropDown",
	"label": "Subnet",
	"filter": true,
	"constraints": {
		"allowedValues": "[map(steps('networkSettings').subnets.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\"}')))]",
		"required": true
	},
	"visible": true
},

 

 

 

 

One nice part about all of these control dependencies is that you don't need to specify any sequence or dependency, that's all handled by the UIForm.

 

Public IP Settings

The UIForm also has a control for creating or selecting a publicIpAddress.  This control, like the other built-in resource controls allows access to the full capabilities of the resource when created.  In our sample, we don't want the user to be able to use certain capabilities of the publicIPAddress but we do want to be able to validate the domainNameLable used prior to deployment.  For this validation we again use the ArmApiControl to check DNS availability.  The first control that collects the desired domain name is just a TextBox control.  There is a regex that is used to check the value in the TextBox and then the ArmApiControl will check availability of the name in Azure.

 

 

 

{
	"name": "dnsLabelPrefix",
	"type": "Microsoft.Common.TextBox",
	"label": "Dns Label Prefix",
	"toolTip": "Unique DNS Name for the Public IP used to access the Virtual Machine.",
	"subLabel": "[concat('.',steps('basics').resourceScope.location.name,'.cloudapp.azure.com')]",
	"constraints": {
		"required": true,
		"validations": [
			{
				"regex": "^$|^[a-z][a-z0-9-]{1,61}[a-z0-9]$",
				"message": "The domain name is invalid. It can contain only lowercase letters, numbers and hyphens. The first character must be a letter. The last character must be a letter or number. The value must be between 3 and 63 characters long."
			},
			{
				"isValid": "[steps('networkSettings').checkDNS.available]",
				"message": "The specificed name is not available."
			}
		]
	}
}

 

 

 

 

This next snippet shows the ArmApiControl calling the checkNameAvailability api and the result of that us used on line 15 in the previous code snippet above.

 

 

 

{
	"name": "checkDNS",
	"type": "Microsoft.Solutions.ArmApiControl",
	"request": {
		"method": "GET",
		"path": "[concat(steps('basics').resourceScope.subscription.id, '/providers/Microsoft.Network/locations/', steps('basics').resourceScope.location.name, '/CheckDnsNameAvailability?domainNameLabel=', steps('networkSettings').dnsLabelPrefix, '&api-version=2021-05-01')]"
	}
}

 

 

 

Outputs

The final section of the file is the outputs section.  Here you define the mapping between the parameters in the template and the controls used for collecting those values.  Suffice it to say, that for each parameter in your template that does not have a defaultValue, you must supply a value from the UIForm.  This is done with the parameters object.

 

 

 

"outputs": {
	"parameters": {
		"location": "[steps('basics').resourceScope.location.name]",
		"vmName": "[steps('vmSettings').vmName]",
		"adminUsername": "[steps('vmSettings').adminUsername]",
		"adminPasswordOrKey": "[steps('vmSettings').adminPassword]",
		"ubuntuOSVersion": "[steps('vmSettings').OSVersion]",
		"vmSize": "[steps('vmSettings').vmSize]",
		"dnsLabelPrefix": "[steps('networkSettings').dnsLabelPrefix]",
		"subnetId": "[steps('networkSettings').subnetList]"
	},
	"kind": "ResourceGroup",
	"location": "[steps('basics').resourceScope.location.name]",
	"resourceGroupId": "[steps('basics').resourceScope.resourceGroup.id]"
}

 

 

 

The remaining properties in the outputs are needed to scope the deployment of the template and will be "standard set" in all of your files.

 

Try It! (In the Sandbox)

The simplest step to see the UIForm in action is to copy and paste this file into the sandbox.  One edit you may wish to make is to remove the allowedValues from the resourceScope control at the top of the file.  Recall that this will filter the list of virtualNetworks to the allowed locations so if you don't have any available in those locations you won't see the subnet control in action.

 

Create & Deploy the templateSpec

To see everything working end to end, here are a few simple steps to make this available in your subscription.  First, copy main.bicep and uiFormDefinition.json to your computer or cloud shell.  Then create the templateSpec using Azure PowerShell or the Azure CLI.

 

Create with PowerShell

 

 

 

New-AzResourceGroup templateSpecs -Location australiasoutheast

New-AzTemplateSpec -ResourceGroupName templateSpecs `
                   -Name sample-vm `
                   -DisplayName "Ubuntu VM with PublicIP" `
                   -Description "Ubuntu VM with PublicIP, requires an existing virtualNetwork" `
                   -Version "1.0" `
                   -Location australiasoutheast `
                   -TemplateFile .\main.bicep `
                   -UIFormDefinitionFile C:\Users\bmoore\source\repos\sandbox\.blog\uiforms\uiFormDefinition.json `
                   -Verbose `

 

 

 

Create with CLI

 

 

 

az group create -n templateSpecs -l australiasoutheast

az ts create \
  --resource-group templateSpecs \
  --name sample-vm \
  --display-name "Ubuntu VM with PublicIP" \
  --description "Ubuntu VM with PublicIP, requires an existing virtualNetwork" \
  --version "1.0" \
  --location australiasoutheast \
  --template-file main.bicep \
  --ui-form-definition uiFormDefinition.json

 

 

 

Deploy in Portal

Once the templateSpec is created you can list the sample in the Azure Portal by going to the templateSpec blade.  You should see it in the list of templateSpecs in the subscription where it was created.

 

bmooremsft_3-1659134286192.png

 

 

Once you click deploy, you'll be redirected to the custom user experience defined in the templateSpec.

 

That's all there is to it, as always if you have feedback, let us know.

 

 

 

Co-Authors
Version history
Last update:
‎Aug 02 2022 10:08 AM
Updated by: