Original Post Read More
A customer of mine wanted to provide certain internal users the ability to create specific Azure resources, in this case Windows-based virtual machines (VMs), in a very controlled manner. Some of their requirements included:
A completely custom user interface served up via the Azure Portal.
Integration with Azure Key Vault for certain sensitive values (e.g., passwords) so that they are not displayed, and the user cannot enter.
Enforcement and validation of naming conventions on various fields, with appropriate interactive guidance and feedback. For example, all VMs must start with the letters “VM_.”
Limitations on certain fields (e.g., regions allowed size/family of VM, etc.).
Forced usage of existing VNets and resource groups.
All scripts should be contained in a private GitHub repository.
Ability to provide data via custom parameters on the portal interface that gets passed into a PowerShell script, which gets automatically invoked upon VM creation. An example would be allowing the user creating the VM to specify via a “Yes/No” parameter whether Microsoft Internet Information Services (IIS) should be installed upon VM creation.
Piecing all of these requirements together entailed stitching together a number of new (to me) technologies such as nested ARM deployments, Azure Managed Applications, etc. This article will take you though these one by one and explain some of my “School of Hard Knocks” lessons learned along the way.
Custom User Interface
One of the first was the custom user interface definition specification that is available as part of Azure Managed Applications.
Creating a “uiFormDefinition.json” file is a simple yet powerful way to provide custom user interfaces for your end users. Some of the capabilities include:
Separation of your user interface into separate “steps” for each logical group, which manifest themselves as unique tabbed sections in the UI. For example, one step could contain all the networking configuration options available to the end user, while another the disk options.
Ability to filter the available locations to only those locations that support the resource types to deploy, provide an array of the resource types. If you provide more than one resource type, only those locations that support all of the resource types are returned.
Deployment of services via Managed Identities (in preview as of December 14th, 2021).
Custom built-in user interface elements. They are grouped into several distinct units:
Common contains typical elements such as checkboxes, dropdowns, textboxes, etc. It also has some more specific ones like ones for passwords, file uploads, tags, etc.
Compute has elements specific to VM creation, such as a field geared toward picking the VM’s size.
KeyVault has one to help select a certificate.
ManagedIdentity has one to select an identity.
Network provides elements to assist in creation or selection of public IPs and virtual networks.
Solutions is aimed at capabilities specific to Managed Applications, such as the ability call get results from an Azure Resource Manager API call.
Storage makes it possible for the user to select and create storage accounts and select blobs more easily.
Custom built-in user interface functions. There are numerous functions available for operations such as:
String manipulation and logic.
Data manipulation and logic.
Dates and times manipulation and logic.
Collection manipulation and logic.
Element-specific validations and constraints. This includes simple things, like forcing a naming convention, but also validations specific to the given control, such as having the File Upload element only allow certain file types/extensions (e.g., “.doc,.docx,.xml,application/msword”).
My entire “uiFormDefinition.json” example can be found here. I’ll point out a few areas of interest in this section.
I placed several constraints on the name of the virtual machine.
The name must be between 4 and 15 characters long.
It can only contain letters, numbers, and hyphens.
It must start with the letters “gary.”
Note that I was able to specify informative error messages.
“label”: “Virtual Machine name”,
“toolTip”: “The name of the Virtual Machine.”,
“message”: “The VM name must start with ‘gary,’ be between 4 and 15 characters long and contain letters, numbers and hyphens only.”
“message”: “Must start with ‘gary’.”
I was able to create custom “tool tips” to provide additional information to the user. For example, hovering over the “ⓘ” symbol shows the configured tool tip:
Here is the code that specifies the tool tip:
“virtualNetwork”: “Creating a new VNet is not allowed, attempts to do so will fail”,
“subnets”: “Must select an existing subnet”
Azure Key Vault Access
Because we don’t have a parameter file, we’ll need to used linked or nested templates as described in this section. I used a nested template just to keep all the code together. Note that nested templates add complexity to debugging as there are multiple deployments to explore.
My deployment ARM template is here. In it you can see how I pulled both the administrator password and the Storage Account access key from an Azure Key Vault.
Make sure you allow the user creating the VM to have access to the Key Vault for ARM deployments only by following the instructions here.
The goal is to lock down what the user is able to do on the Azure Portal as much as possible. For example, we don’t want her to be able to just go to the standard Virtual Machine creation user interface and spin up a VM from there. Be aware that you may have to add additional roles depending on what kind of virtual machine you create – for example, if you have boot diagnostics enabled you’ll need to add a “Storage Contributor” role to the user’s resource group. Other VM features might drag along similar requirements.
Azure Role-Based Access Control (RBAC)
In order to create a Virtual Machine, even from our custom UI, the end user must have “Virtual Machine Contributor” permission. Here’s how I scoped down what that user can do.
Create Unique Resource Group
I created a resource group just for the given user called “gary-vm-rg,” and designated the “Virtual Machine Contributor” access at that level rather than at the broader subscription level. That is the only role assigned to the user for the resource group.
In doing so, the only resource group the user can choose is “gary-vm-rg.” Trying to create a new resource group results in the following error message. Unfortunately, it’s not possible to configure the user interface to not show a “Create new” link below the resource group.
Virtual Network Access
Because the user only has “Virtual Machine Contributor” access at the resource group level, he is not able to either create a new or select an existing virtual network. To enable this, I created a new resource group called “VNets” containing the target virtual networks, and assigned the user two roles at that level.
Reader. This allows the user to view the virtual networks within the “VNets” resource group.
A custom role that I called “Subnet Joiner.” This is necessary to allow the network interface created to join the virtual network’s subnet. The only action permitted for this custom role is “Microsoft.Network/virtualNetworks/subnets/join/action.”
Note that I could have also done this at the virtual network level itself to further tighten access.
As with resource groups, it currently is not possible to hide the “Create new” link next to the virtual network field. Unlike with resource groups, which gives an error immediately if the user does not have permission to create one, attempts to create a new virtual network will pass validation and fail during deployment.
Private GitHub Access
Creating a “Deploy to Azure” for custom UI and ARM scripts is easy, and described well here. However, it relies on the GitHub repository being public, not private. There is a note in that section that references an external article describing how to do so using a private repository using a custom Azure Function, but the article involves passing the GitHub personal access token in as an HTTP Get parameter, which largely defeats the purpose of having a private repository.
I created a new Azure Function that pulls the GitHub token out of Azure Key Vault, making the solution much more secure.
GitHub Proxy Function Application
My revamped GitHub Proxy Function can be found here. It is pretty simple – it just takes in a URL to a file in the repository as an HTTP Get parameter called “gitHubURL” whose value is a URL to the raw GitHub version of a file. For example:
You can test out your usage of the function from a browser; for example:
To use the new proxy from the “Deploy to Azure” button I had to redo the URL for the link. You can see the new URL from README.md file. The new URL is:
[![Deploy To Azure](https://github.com/gamullen/Custom-VM-Creation/blob/master/deploytoazure.svg?sanitize=true)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fgithubproxy.azurewebsites.net%2Fapi%2FHttpTrigger%3Fgithuburi%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgamullen%2FCustom-VM-Creation%2Fmaster%2Fazuredeploy.json%3F/uiFormDefinitionUri/https%3A%2F%2Fgithubproxy.azurewebsites.net%2Fapi%2FHttpTrigger%3Fgithuburi%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgamullen%2FCustom-VM-Creation%2Fmaster%2FuiFormDefinition.json)
Azure Key Vault
As mentioned above, I reworked the original Function App to pull the GitHub access token from Azure Key Vault. Doing so is quite simple:
string keyVaultURL = “https://keyvaultgary.vault.azure.net/”;
var kvClient = new SecretClient(new Uri(keyVaultURL), new DefaultAzureCredential());
KeyVaultSecret secret = kvClient.GetSecret(secretName);
Note that in order to give the Function App access to my Azure Key Vault I had to create a managed identity for the Function App and configure access from within the Key Vault so that identity could see secrets.
You can generate a personal access token for the GitHub Proxy Function App by navigating to “Settings / Developer settings / Personal access tokens” in GitHub. Here is a screenshot of the token I created:
To get around this we must add the URLs for the Azure Portal to the list of allowed origins for our Function App as shown below:
After doing so the “Deploy to Azure” button should work fine.
Portal Parameter Passing to PowerShell Script
This was among the easiest of the requirements to solve.
I used the CustomScriptExtension to execute my PowerShell script. The script itself get pulled from a Storage Account container. I pull the Storage Account key from Azure Key Vault to keep it secure.
The parameter “scriptParameter” matches a Microsoft.Common.TextBox element from the custom UI. You could also make it a Microsoft.Common.CheckBox for a true/false value. As you can see below, I simply append it to the script to be called. If I had multiple parameters I could just keep adding more, separated by spaces. The PowerShell script just accesses them via the “args” array.
“fileUris”: [ “[parameters(‘scriptFile’)]”],
“commandToExecute”: “[concat(‘powershell -ExecutionPolicy Unrestricted -file ‘, parameters(‘scriptName’), ‘ ‘, parameters(‘scriptParameter’))]”
This article describes a powerful, easy to use, and secure set of technologies that make it possible to create a custom portal experience for users. Sensitive values are stored in Azure Key Vault to protect them.