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:
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.
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:
Here is an example I created based on one at this GitHub site. Note the error message for “Virtual Machine name” indicating that the entry does not match validation requirements.
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.
Note that I was able to specify informative error messages.
{
"name": "vmName",
"type": "Microsoft.Common.TextBox",
"label": "Virtual Machine name",
"toolTip": "The name of the Virtual Machine.",
"defaultValue": "gary-vm",
"constraints": {
"required": true,
"validations": [
{
"regex": "^[a-z0-9A-Z-]{4,15}$",
"message": "The VM name must start with 'gary,' be between 4 and 15 characters long and contain letters, numbers and hyphens only."
},
{
"isValid": "[startsWith(steps('basics').vmName,'gary')]",
"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:
"toolTip": {
"virtualNetwork": "Creating a new VNet is not allowed, attempts to do so will fail",
"subnets": "Must select an existing subnet"
}
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.
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.
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.
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.
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.
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.
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:
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:
Even though you may be able to successfully retrieve your UI and ARM files from a private GitHub repository via a browser, it will fail when using the “Deploy to Azure” button. That’s because the code behind that employs JavaScript, and in doing so mimics a cross-origin resource sharing (CORS) attack. You will get an error message similar to the one shown below:
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.
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.
{
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "[concat(parameters('vmName'),'/CustomScriptExtension')]",
"apiVersion": "2015-05-01-preview",
"location": "[resourceGroup().location]",
"dependsOn": [
"[concat('Microsoft.Compute/virtualMachines/', parameters('vmName'))]"
],
"properties": {
"publisher": "Microsoft.Compute",
"type": "CustomScriptExtension",
"protectedSettings": {
"fileUris": [ "[parameters('scriptFile')]"],
"storageAccountName": "customscriptsgary",
"storageAccountKey": "[parameters('storageAccountKey')]"
},
"typeHandlerVersion": "1.7",
"autoUpgradeMinorVersion": true,
"settings": {
"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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.