{ "metadata": { "kernelspec": { "name": "powershell", "display_name": "PowerShell" }, "language_info": { "name": "powershell", "codemirror_mode": "shell", "mimetype": "text/x-sh", "file_extension": ".ps1" } }, "nbformat_minor": 2, "nbformat": 4, "cells": [ { "cell_type": "markdown", "source": [ "# Create Azure Sql Virtual Machine using Az-Cli" ], "metadata": { "azdata_cell_guid": "420658c9-5177-4220-9b7e-34429d2e526d" } }, { "cell_type": "markdown", "source": [ "## PS Version and Modules\r\n", "This notebook uses a PowerShell Notebook kernel (YouTube video) and requires PS v5.1 or greater. It also uses the latest version of Azure CLI. To install, see https://aka.ms/installazurecliwindows. Update instructions can be found at https://aka.ms/doc/UpdateAzureCliMsi" ], "metadata": { "azdata_cell_guid": "e0d6402d-4f1c-4e37-923e-1e9483431587" } }, { "cell_type": "code", "source": [ "# Check that PowerShell >= v5.1 and Azure Command Line Interface (Az CLI) are installed\r\n", "if ($PSVersionTable.PSVersion.Major -lt 5)\r\n", "{\r\n", " Write-Warning \"Please install latest version of PowerShell. Do not continue executing the rest of the notebook.\"\r\n", "}\r\n", "else {\r\n", " $psVersion = $PSVersionTable.PSVersion.ToString()\r\n", " Write-Output \"Found PS version... $psVersion\"\r\n", " # Check for Azure CLI\r\n", " $azCli = (&az --version) \r\n", " if ($azCli) { \r\n", " $azVersion = $azCli[0].Split()[-1]\r\n", " Write-Output \"Found Az CLI version... $azVersion\"\r\n", " }\r\n", "}" ], "metadata": { "azdata_cell_guid": "5a6893cf-e29d-4855-8771-0101bb6e55cd", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Set Variables for the Notebook" ], "metadata": { "azdata_cell_guid": "8998da5f-e4aa-4b32-aadf-51077748c56d" } }, { "cell_type": "code", "source": [ "# ADP Resource \r\n", "$Env:BOOTSTRAP_Subscription = \"\" # Azure Subscription ID/Name # The bacpac files and ADP Resources are assumed to be in the same subscription\r\n", "$Env:BOOTSTRAP_ResourceGroup = \"TestResourceGroup\" # Azure Resource Group which contains the ADP Resources\r\n", "\r\n", "# SQL Server \r\n", "$VMName = \"TestVM\" # Name of the Virtual Machine to be created\r\n", "$ImageSku = \"Enterprise\" # Choose your Image sku (see appendix)\r\n", "$Location = \"East US\" # see Appendix for a list of location settings\r\n", "$VmUsername = \"\" # Username of Virtual Machine to be created\r\n", "$VmPassword = \"\" # Password of Virtual Machine to be created\r\n", "\r\n", "# Set Variables for ADP Resources\r\n", "$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\" \r\n", "$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n", "$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"Vnet\"" ], "metadata": { "azdata_cell_guid": "a6f7e1bb-6517-4c43-a63b-d70a5e8627ba", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Notebook Functions\r\n", "Defines logical functions for the rest of the notebook. Function blocks are combined in a single cell that can be collapsed for readability or expanded for further examination. Nothing is executed until called later in the notebook. As a result, this cell is a requirement for any of the other cells below it. " ], "metadata": { "azdata_cell_guid": "c0f151bc-d1a3-4d41-8c93-b67f7a068be0" } }, { "cell_type": "code", "source": [ "# Expand cell to view framework\r\n", "\r\n", "function Login-Azure\r\n", "{ \r\n", " # query azure locations to test for existing az login session exists with valid access tocken\r\n", " $azureLocations = az account list-locations -o JSON 2>$null | ConvertFrom-Json\r\n", " \r\n", " if (!$azureLocations){ #If there are no az locations, there is no existing az login session\r\n", " $subscriptions = az login -o JSON | ConvertFrom-Json # Login \r\n", " }\r\n", " else {\r\n", " $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n", " }\r\n", "\r\n", " if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n", " {\r\n", " $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n", " if (!$specified_Subscription) #if specified subscription is not valid\r\n", " { \r\n", " $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n", " Write-Host \"Refer below for the list of subscriptions for logged in account '$($currentUser.UPN)'`n\"\r\n", " az account list --query \"[].{Name:name,SubscriptionID:id}\" -o table # list subscriptions under current logged in account\r\n", " }\r\n", " else { # if specified subscription is valid\r\n", " Write-Output \"Using subscription... '$($specified_Subscription.name)' ... '$($specified_Subscription.id)'\" \r\n", " }\r\n", " }\r\n", " else { # if no subscription is specified, users are given a gridview to select subscription from\r\n", "\r\n", " $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n", " $SubscriptionId = $selectedSubscription.Id\r\n", " $Subscription = $selectedSubscription.Name \r\n", " $Env:BOOTSTRAP_Subscription = $subscription \r\n", " Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n", " } \r\n", "}\r\n", "\r\n", "function Verify-ADPResources\r\n", "{ \r\n", " [CmdletBinding()]\r\n", " param(\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Subscription,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$ADPResourceGroupName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$FunctionName, \r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$BatchAccountName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$VNetName,\r\n", " [Parameter (Mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$ApplicationName=\"SqlPackageWrapper\", \r\n", " [Parameter (Mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$ApplicationPackageVersionName=\"1\",\r\n", " [Parameter (Mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$SubNetName=\"default\" \r\n", " ) \r\n", "\r\n", "# validate Subscription\r\n", "$specified_Subscription= az account show --subscription $Subscription -o json | ConvertFrom-Json\r\n", "if(!$specified_Subscription){\r\n", " $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user information\r\n", " Write-Host \"Refer below for the list of subscriptions for logged in account '$($currentUser.UPN)'`n\"\r\n", " az account list --query \"[].{Name:name,SubscriptionID:id}\" -o table # list subscriptions under current logged in account\r\n", " return } \r\n", "# validate ResourceGroup \r\n", "$specified_ResourceGroup= az group show -n $ADPResourceGroupName --subscription $Subscription -o json | ConvertFrom-Json\r\n", "if(!$specified_ResourceGroup) { \r\n", " return\r\n", " } \r\n", "\r\n", "$Installed = [ordered]@{} # ordered hash to store status of installation\r\n", "$countError=0\r\n", "\r\n", "#Verify if VNet exists \r\n", "$specified_VNet= az network vnet show -n $VNetName -g $ADPResourceGroupName --subscription $Subscription -o JSON 2>$null |ConvertFrom-Json \r\n", "if(!$specified_VNet) {\r\n", " $Installed['VNET']=\"Not Found\"\r\n", " $countError++\r\n", "} \r\n", "else { \r\n", " $existingVnetSubnet = az network vnet subnet show -n $SubNetName --vnet-name $VNetName -g $ADPResourceGroupName --subscription $Subscription -o JSON 2>$null |ConvertFrom-Json\r\n", " if(!$existingVnetSubnet){\r\n", " $Installed['VNET']=\"Default Subnet under\"+ $VNetName + \"Not Found\"\r\n", " $countError++\r\n", " }\r\n", " else {\r\n", " $Installed['VNET']=\"Installed\"\r\n", " }\r\n", " }\r\n", "\r\n", "#Verify if FunctionApp Exists\r\n", "$specified_FunctionApp = az functionapp show -n $FunctionName -g $ADPResourceGroupName --subscription $Subscription -o JSON 2>$null | ConvertFrom-Json\r\n", "if(!$specified_FunctionApp)\r\n", "{\r\n", " $Installed['FunctionApp']=\"Not Installed\"\r\n", " $countError++\r\n", "}\r\n", "else\r\n", "{\r\n", " $Installed['FunctionApp']=\"Installed\"\r\n", "} \r\n", "\r\n", "#check if Batch account exists\r\n", "$specified_BatchAccount = az batch account show -n $BatchAccountName -g $ADPResourceGroupName --subscription $Subscription -o JSON 2>$null | ConvertFrom-Json\r\n", "if(!$specified_BatchAccount)\r\n", "{\r\n", " $Installed['Batch']=\"Not Installed\"\r\n", " $countError++\r\n", "}\r\n", "else\r\n", "{\r\n", " $appPackageInstalled = az batch application package show --application-name $ApplicationName --version-name $ApplicationPackageVersionName -n $BatchAccountName -g $ADPResourceGroupName --subscription $Subscription -o JSON 2>$null | ConvertFrom-Json\r\n", " $connectedToStorage= $specified_BatchAccount.autoStorage \r\n", " if($connectedToStorage -and $appPackageInstalled){ # BatchAccount connected to storageaccount and applicationpackage is installed\r\n", " $Installed['Batch']=\"Installed\"\r\n", " $Installed['Batch_ApplicationPackage']=\"Installed\"\r\n", " $Installed['Batch_StorageAccount']=\"Connected to storage- \"+$connectedToStorage.storageAccountId.Split(\"/\")[-1]\r\n", " }\r\n", " if(!$connectedToStorage)\r\n", " {\r\n", " $Installed['Batch_StorageAccount']='Not Found'\r\n", " $countError++\r\n", " } \r\n", " if(!$appPackageInstalled)\r\n", " {\r\n", " $Installed['Batch_ApplicationPackage']=\"Not Found\"\r\n", " $countError++\r\n", " } \r\n", "}\r\n", "if ($countError -gt 0){\r\n", " Write-Output \"ADP Resources are not installed correctly. Please refer the list below and use the Bootstrap NB to install ADP Resources\"\r\n", "}\r\n", "$Installed\r\n", "if ($countError -eq 0){\r\n", " Write-Output \"`nFound all ADP Resources.\"\r\n", "}\r\n", "}\r\n", "\r\n", "function Prepare-InputForImportFunction\r\n", "{ \r\n", " [CmdletBinding()]\r\n", " param(\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Subscription,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$ADPResourceGroupName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$FunctionName, \r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$BatchAccountName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$BackupFiles_StorageAccount,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$BackupFiles_ContainerName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$VNetName, \r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$TargetRGName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$SqlServerName,\r\n", " [Parameter (Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$SqlServerPassword\r\n", " )\r\n", " \r\n", " $Result = @{}\r\n", " # Build Header \r\n", " ## get Function key\r\n", " $FunctionAppID =az functionapp show -n $FunctionName -g $ADPResourceGroupName --subscription $Subscription --query \"[id]\" -o JSON 2>$null | ConvertFrom-Json\r\n", " $DefaultHostKey = az rest --method post --uri \"$FunctionAppID/host/default/listKeys?api-version=2018-11-01\" --query \"[functionKeys.default]\" -o JSON 2>$null | ConvertFrom-Json\r\n", " ## Build Json Object for Headers\r\n", " $headers = @{\r\n", " 'x-functions-key' = $DefaultHostKey\r\n", " }\r\n", " $Result['Header']=$headers\r\n", "\r\n", " # Build string for Function URL \r\n", " $specified_Subscription= az account show --subscription $Subscription -o json |ConvertFrom-Json #Get SpecifiedSubscriptionID\r\n", " $SubscriptionID= $specified_Subscription.id\r\n", " $FunctionUrl = 'https://'+ $FunctionName +'.azurewebsites.net/api/subscriptions/'+ $SubscriptionID +'/resourceGroups/' + $ADPResourceGroupName + '/Import'\r\n", " $Result['FunctionURL']=$FunctionUrl\r\n", "\r\n", " # Set parameter variables for Body\r\n", " ## Get BatchAccountURL \r\n", " $specified_Batch = az batch account show -n $BatchAccountName -g $ADPResourceGroupName --subscription $Subscription -o JSON 2>$null | ConvertFrom-Json\r\n", " $BatchAccountURL = 'https://' + $specified_Batch.accountEndpoint\r\n", "\r\n", " ## Get default SubNet ID for specified VNet\r\n", " $specified_VNet_SubNet = az network vnet subnet show -g $ADPResourceGroupName --vnet-name $VNetName -n 'default' --subscription $Subscription -o JSON |ConvertFrom-Json\r\n", " $VNetSubNetID = $specified_VNet_SubNet.id\r\n", "\r\n", " ## Create access token to source sql server\r\n", " $targetAccessToken = az account get-access-token --resource=https://database.windows.net --query accessToken\r\n", " $targetAccessToken\r\n", "\r\n", " ## Build JSon object for Body\r\n", " $Body = @{\r\n", " batchAccountUrl = $BatchAccountURL\r\n", " VNetSubnetId= $VNetSubNetID\r\n", " storageAccountName = $BackupFiles_StorageAccount\r\n", " containerName = $BackupFiles_ContainerName\r\n", " targetSqlServerResourceGroupName = $TargetRGName\r\n", " targetSqlServerName = $SQLServerName \r\n", " userName = $SqlServerLogin \r\n", " targetAccessToken = $targetAccessToken\r\n", " sqlAdminPassword = $SqlServerPassword\r\n", " }\r\n", " $json = $Body | ConvertTo-Json\r\n", " $Result['Body']=$json\r\n", "\r\n", " $Result\r\n", " \r\n", "}\r\n", "\r\n", "function Provision-FuncRBAC {\r\n", " [CmdletBinding()]\r\n", " param (\r\n", " [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Subscription,\r\n", " [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$ResourceGroupName,\r\n", " [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$FunctionName,\r\n", " [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$ScopeRGName,\r\n", " [Parameter(Mandatory=$false)][ValidateNotNullOrEmpty()][string]$Role=\"Contributor\"\r\n", " )\r\n", "\r\n", " # Get the scope resource group's ID\r\n", " $scopeID = az group show --resource-group $ScopeRGName --subscription $Subscription --query \"[id]\" -o JSON | ConvertFrom-Json \r\n", " if(!$scopeID) {\r\n", " Write-Output \"Provision-FuncRBAC failed.\" \r\n", " return }\r\n", " else { Write-Output \"Found scope '$ScopeRGName' with ID... '$scopeID'\"\r\n", " }\r\n", "\r\n", " # Get the az function principal id\r\n", " $app_PrincipalID = az functionapp show -n $FunctionName --resource-group $ResourceGroupName --subscription $Subscription --query \"[identity.principalId]\" -o JSON | ConvertFrom-Json \r\n", " if(!$app_PrincipalID) {\r\n", " Write-Output \"Provision-FuncRBAC failed.\" \r\n", " return }\r\n", " else { Write-Output \"Found principal id of Azure function '$FunctionName'... '$app_PrincipalID'\"\r\n", " }\r\n", "\r\n", " # Verify if a role assignment has been created for function\r\n", " $app_RoleAssignmentDefinition= az role assignment list --subscription $Subscription --assignee $app_PrincipalID --scope $scopeID --query \"[].roleDefinitionName\" -o JSON 2>$null | ConvertFrom-Json\r\n", "\r\n", " if($app_RoleAssignmentDefinition -eq $Role)\r\n", " {\r\n", " Write-Output \"Found Role Assignment for Principal ID.. '$app_PrincipalID' with Role.. '$app_RoleAssignmentDefinition' . No work needed\"\r\n", " }\r\n", " else\r\n", " {\r\n", " # Continue to setup RBAC, once we verify an assignment is not setup and all the resources exist\r\n", " Write-Output \"Creating new role assignment by running: 'az functionapp identity assign -n $FunctionName --role $Role -g $ResourceGroupName --scope $scopeID --subscription $Subscription'\"\r\n", " Write-Warning \"If your account does not have the access to assign new roles as Owner or User Access Administrator for the resource group, than you will need to contact your Azure AD Administrator to assign a service principle using the commands above\"\r\n", " az functionapp identity assign -n $FunctionName --role $Role -g $ResourceGroupName --scope $scopeID --subscription $Subscription \r\n", " }\r\n", "}\r\n", "Write-Host \"Helper Functions Created successfully\" " ], "metadata": { "azdata_cell_guid": "ea9ddf9c-4c09-4db3-94ea-e01bc5f105c9", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Connect to Azure Account\r\n", "Run the below cell to login to an Azure account. Be sure to check the Windows Taskbar for a login dialog box underneath the notebook or other windows or by pressing Alt+TAB." ], "metadata": { "azdata_cell_guid": "2a5a155f-556d-451d-a0d4-b802017946df" } }, { "cell_type": "code", "source": [ "Login-Azure" ], "metadata": { "azdata_cell_guid": "77067078-7ec5-4d07-803b-5c52dfcf06f0", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Verify ADP Resources \r\n", "Verify if ADP resources exists in specified Resource Group" ], "metadata": { "azdata_cell_guid": "717960de-4baf-4b78-bcec-3a8e386b42b7" } }, { "cell_type": "code", "source": [ "Verify-ADPResources -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n", " -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET " ], "metadata": { "azdata_cell_guid": "22e8a9ed-d2e0-46eb-92b0-8c1108b60b67", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Create a resource group\r\n", "\r\n", "In Azure, all resources are allocated in a resource management group. Resource groups provide logical groupings of resources that make them easier to work with as a collection" ], "metadata": { "azdata_cell_guid": "73e7c7f9-f518-4b0a-87d1-d43c6a10cbad" } }, { "cell_type": "code", "source": [ "$rsgExists = az group exists -n $Env:BOOTSTRAP_ResourceGroup\r\n", "if ($rsgExists -eq 'false') {\r\n", " az group create -l $Location -n $Env:BOOTSTRAP_ResourceGroup\r\n", "}" ], "metadata": { "azdata_cell_guid": "7e95158c-9ddc-4b5e-b289-2086a2d03c15", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Create a virtual machine\r\n", "\r\n", "" ], "metadata": { "azdata_cell_guid": "222d6b8d-9789-4416-b512-63b1ce6f11e0" } }, { "cell_type": "code", "source": [ "az vm create -n sql -g $Env:BOOTSTRAP_ResourceGroup --image MicrosoftSQLServer:SQL2017-WS2016:Standard:14.0.1000200 --location $Location --admin-username $VmUsername --admin-password $VmPassword --verbose" ], "metadata": { "azdata_cell_guid": "37ab908d-839a-45e9-89cf-78ed2452debe", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "## Get VM information with queries\r\n", "Now that a VM has been created, detailed information about it can be retrieved. The common command for getting information from a resource is _show_" ], "metadata": { "azdata_cell_guid": "9de4dd69-c9e3-4e10-89f1-cc785ea712b4" } }, { "cell_type": "code", "source": [ "az vm show --name TutorialVM1 --resource-group TutorialResources" ], "metadata": { "azdata_cell_guid": "51a4a092-1697-476c-934f-75a1a30dab7f", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "Lot of information can be seen, which can be difficult to parse visually. The returned JSON contains information on authentication, network interfaces, storage, and more. Most importantly, it contains the Azure object IDs for resources that the VM is connected to. Object IDs allow accessing these resources directly to get more information about the VM's configuration and capabilities.\r\n", "\r\n", "In order to extract the object ID , the --query argument is used. Queries are written in the JMESPath query language. Start with getting the network interface controller (NIC) object ID" ], "metadata": { "azdata_cell_guid": "902e834e-bbbf-40f4-80db-48d3b4b90f99" } }, { "cell_type": "code", "source": [ "az vm show --name TutorialVM1 \\\r\n", " --resource-group TutorialResources \\\r\n", " --query 'networkProfile.networkInterfaces[].id' \\\r\n", " --output tsv" ], "metadata": { "azdata_cell_guid": "2bc8e0c7-d936-42ce-9e6a-e66207cd78f0", "tags": [ "hide_input" ] }, "outputs": [], "execution_count": null }, { "cell_type": "markdown", "source": [ "### Appendix: Locations\r\n", "See the Azure locations page for a complete list of Azure regions along with their general physical location. The following is a list of common North American location settings for this guide:\r\n", "\r\n", "#### US Regions\r\n", "| Setting | Location |\r\n", "| ------------ | --------- |\r\n", "| Central US | Iowa |\r\n", "| East US | Virginia |\r\n", "| East US 2 | Virginia |\r\n", "| North Central US | Illinois |\r\n", "| South Central US | Texas |\r\n", "| West US 2 | Washington |\r\n", "| West Central US | Wyoming |\r\n", "| West US | California | \r\n", "| Canada Central | Toronto |\r\n", "| Canada East | Quebec City |\r\n", "| Brazil South | Sao Paulo |\r\n", "| Mexico Central | Queretaro |\r\n", "\r\n", "### Appendix: Storage Skus\r\n", "Use these as values for provisioning storage skus. \r\n", "\r\n", "Data for table taken from SKU Types page but is subject to change. Not all skus are listed here. SKU type names are case-sensitive.\r\n", "\r\n", "| Name |\r\n", "| -----|\r\n", "| Developer |\r\n", "| Enterprise |\r\n", "| Express |\r\n", "| Standard |\r\n", "| Web |" ], "metadata": { "azdata_cell_guid": "a40b46e4-4b92-4ada-b022-4342ce1b35f2" } } ] }