How to deploy App Service with managed SSL certificate using ARM - azure

I want create an Azure App Service with a custom hostname binding and a managed SSL certificate.
When I create a single Bicep-template, the certificate resource can only be deployed if the hostname binding is already created. But to create a hostname binding, I need the certificate thumbprint.
Updating the hostname binding in the same template also is not possible, as a resource can only exist once in a template.
// hostname bindings must be deployed one by one to prevent Conflict (HTTP 429) errors.
#batchSize(1)
resource customHostnameWithoutSsl 'Microsoft.web/sites/hostnameBindings#2019-08-01' = [for fqdn in customHostnames: {
name: '${webAppService.name}/${fqdn}'
properties: {
siteName: webAppService.name
hostNameType: 'Verified'
sslState: 'Disabled'
}
}]
// Managed certificates can only be created once the hostname is added to the web app.
resource certificates 'Microsoft.Web/certificates#2022-03-01' = [for (fqdn, i) in customHostnames: {
name: '${fqdn}-${webAppName}'
location: location
properties: {
serverFarmId: appServicePlanResourceId
canonicalName: fqdn
}
dependsOn: [ ]
}]
// sslState and thumbprint can only be set once the managed certificate is created
#batchSize(1)
resource customHostname 'Microsoft.web/sites/hostnameBindings#2019-08-01' = [for (fqdn, i) in customHostnames: {
name: '${webAppService.name}/${fqdn}'
properties: {
siteName: webAppService.name
hostNameType: 'Verified'
sslState: 'SniEnabled'
thumbprint: certificates[i].properties.thumbprint
}
}]
Is there another way to create a single deployment template to deploy an Azure App Service with a managed SSL certificate for the custom hostname?

Updating the hostname binding in the same template also is not possible, as a resource can only exist once in a template.
To prevent this error, the resource can be deployed using a Bicep module (or ARM nested template).
Then the solution becomes this:
webApp.bicep
#description('The name of the App Service Plan that this web app will be deployed to.')
param appServicePlanResourceId string
#description('The location that the resource will be deployed to')
param location string = resourceGroup().location
#description('The custom hostnames that you wish to add.')
param customHostnames array = []
#description('Deploy hostnames without SSL binding before creating the certificate. Required when hostname is not present yet.')
param redeployHostnames bool = false
resource webAppService 'Microsoft.Web/sites#2020-12-01' = {
...
}
// hostname bindings must be deployed one by one to prevent Conflict (HTTP 429) errors.
#batchSize(1)
resource customHostnameWithoutSsl 'Microsoft.web/sites/hostnameBindings#2019-08-01' = [for fqdn in customHostnames: if (redeployHostnames) {
name: '${webAppService.name}/${fqdn}'
properties: {
siteName: webAppService.name
hostNameType: 'Verified'
sslState: 'Disabled'
}
}]
// certificates must be bound via module/nested template, because each resource can only occur once in every template
// in this case the hostnameBindings would occur twice otherwise.
module certificateBindings './bindCertificateToHostname.bicep' = {
name: '${deployment().name}-ssl'
params: {
appServicePlanResourceId: appServicePlanResourceId
customHostnames: customHostnames
location: location
webAppName: webAppService.name
}
dependsOn: customHostnameWithoutSsl
}
bindCertificateToHostname.bicep
param webAppName string
param location string
param appServicePlanResourceId string
param customHostnames array
// Managed certificates can only be created once the hostname is added to the web app.
resource certificates 'Microsoft.Web/certificates#2022-03-01' = [for (fqdn, i) in customHostnames: {
name: '${fqdn}-${webAppName}'
location: location
properties: {
serverFarmId: appServicePlanResourceId
canonicalName: fqdn
}
}]
// sslState and thumbprint can only be set once the managed certificate is created
#batchSize(1)
resource customHostname 'Microsoft.web/sites/hostnameBindings#2019-08-01' = [for (fqdn, i) in customHostnames: {
name: '${webAppName}/${fqdn}'
properties: {
siteName: webAppName
hostNameType: 'Verified'
sslState: 'SniEnabled'
thumbprint: certificates[i].properties.thumbprint
}
}]

One of the workaround you can follow to achieve the above requirement ;
To deploy an app service with SSL certificate for the custom domain you can follow the complete configuration and template which is suggested by #bmoore-msft on this GitHub sample:-
Sample template.json:-
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2019-08-01",
"name": "[variables('appServicePlanName')]",
"location": "[parameters('location')]",
"properties": {
"name": "[variables('appServicePlanName')]"
},
"sku": {
"name": "P1",
"tier": "Premium",
"size": "1",
"family": "P",
"capacity": "1"
}
},
{
"type": "Microsoft.Web/sites",
"apiVersion": "2019-08-01",
"name": "[parameters('webAppName')]",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverFarms', variables('appServicePlanName'))]"
],
"properties": {
"name": "[parameters('webAppName')]",
"serverFarmId": "[resourceId('Microsoft.Web/serverFarms', variables('appServicePlanName'))]"
}
},
{
"condition": "[variables('enableSSL')]",
"type": "Microsoft.Web/certificates",
"apiVersion": "2019-08-01",
"name": "[variables('certificateName')]",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('webAppName'))]"
],
"properties": {
"keyVaultId": "[parameters('existingKeyVaultId')]",
"keyVaultSecretName": "[parameters('existingKeyVaultSecretName')]",
"serverFarmId": "[resourceId('Microsoft.Web/serverFarms', variables('appServicePlanName'))]"
}
},
{
"type": "Microsoft.Web/sites/hostnameBindings",
"name": "[concat(parameters('webAppName'), '/', parameters('customHostname'))]",
"apiVersion": "2019-08-01",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Web/certificates', variables('certificateName'))]"
],
"properties": {
"sslState": "[if(variables('enableSSL'), 'SniEnabled', json('null'))]",
"thumbprint": "[if(variables('enableSSL'), reference(resourceId('Microsoft.Web/certificates', variables('certificateName'))).Thumbprint, json('null'))]"
}
}
NOTE:- I am not able to test it with custom domain due to of some provision issue with our account
For more information please refer this SO THREAD| How to configure an App Service Managed Certificate

Related

Get VirtualMachineScaleSet instanceview for a ResourceGroup

I am trying to get instanceview objects(VirtualMachineScaleSetInstanceViewInner) of all Azure's VirtualMachineScaleSets under a Subscription and this requires both ResourceGroup Name and Vmss name together.
azureResourceManager.virtualMachines().manager().serviceClient().getVirtualMachineScaleSets().getInstanceView(resourceGroupName, virtualMachineScaleSet.name(), Context.NONE);
How do I get specific VirtualMachineScaleSets under a ResourceGroup? I only see AzureResourceManager.ResourceGroups() and AzureResourceManager.virtualMachineScaleSets(), but nothing that gets virtualMachineScaleSets under a ResourceGroup.
Thanks
I have tried to get the VMSS instance in a Resource group by using the below RestApi:
GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}?api-version=2022-08-01
output:
Body:
{
"name": "rajtestVMSS",
"id": "/subscriptions/*********/resourceGroups/abcaaaa/providers/Microsoft.Compute/virtualMachineScaleSets/rajtestVMSS",
"type": "Microsoft.Compute/virtualMachineScaleSets",
"location": "westus2",
"tags": {
"azsecpack": "nonprod",
"platformsettings.host_environment.service.platform_optedin_for_rootcerts": "true"
},
"sku": {
"name": "Standard_D2s_v3",
"tier": "Standard",
"capacity": 2
},
"properties": {
"singlePlacementGroup": false,
"upgradePolicy": {
"mode": "Manual"
},
"scaleInPolicy": {
"rules": [
"Default"
]
},
"virtualMachineProfile": {
"osProfile": {
"computerNamePrefix": "rajtestvm",
"adminUsername": "rajtest",
"windowsConfiguration": {
"provisionVMAgent": true,
"enableAutomaticUpdates": true,
"enableVMAgentPlatformUpdates": false
------------------
------------------
------------------
You can use the below java code to get the VirtualMachine ScaleSet instanceview for a ResourceGroup.
import com.azure.core.util.Context;
public final class Main {
public static void getAVirtualMachineScaleSet(com.azure.resourcemanager.AzureResourceManager azure) {
azure
.virtualMachines()
.manager()
.serviceClient()
.getVirtualMachineScaleSets()
.getByResourceGroupWithResponse("myResourceGroup", "myVirtualMachineScaleSet", null, Context.NONE);
}
}
Thanks to #XiaofeiCao for the github link to know more about Azure Resource Manager client library for Java.

Deploy keyVault with private endpoint blocked by policy

I need to create policy assignment which will block keyVault deployments without privateEndpoint configured. I tested built-in policy "[Preview]: Azure Key Vaults should use private link" with "Audit" effect and it works fine.
But when I change effect to "Deny" then my deployment is blocked as I'm deploying two resources (keyVault and privateEndpoint) separately. From what I understood from docs (https://learn.microsoft.com/en-us/azure/governance/policy/concepts/effects#deny) the resource is evaluated before sending to Resource provided. Which implicates the policy is not aware of private endpoint (as it's separate resource).
Have anyone faced similar problem and managed to handle it?
I'm pasting my template below:
resource keyVaultPrivateLink 'Microsoft.KeyVault/vaults#2019-09-01' = {
name: kvName
location: location
properties: {
enabledForTemplateDeployment: true
tenantId: tenant
enableRbacAuthorization: true
enablePurgeProtection: true
enableSoftDelete: true
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Deny'
virtualNetworkRules: [
{
id: subnetId
}
]
}
sku: {
name: 'standard'
family: 'A'
}
}
}
resource keyVaultPrivateEndpoint 'Microsoft.Network/privateEndpoints#2020-03-01' = {
name: 'pewetkvwetprivatelink'
location: location
properties: {
subnet: {
id: subnetId
}
privateLinkServiceConnections: [
{
name: 'kvwetprivatelink'
properties: {
privateLinkServiceId: keyVaultPrivateLink.id
groupIds: [
'vault'
]
}
}
]
}
}
The error code received:
{
"error": {
"code": "InvalidTemplateDeployment",
"message": "The template deployment failed because of policy violation. Please see details for more information.",
"details": [
{
"code": "RequestDisallowedByPolicy",
"target": "keyVault-name",
"message": "Resource 'kvwetprivatelink' was disallowed by policy. Policy identifiers: '[{\"policyAssignment\":{\"name\":\"Audit KeyVault Initiative\",\"id\":\"/subscriptions/***/providers/Microsoft.Authorization/policyAssignments/Audit KeyVault Initiative\"},\"policyDefinition\":{\"name\":\"[Preview]: Azure Key Vaults should use private link\",\"id\":\"/providers/Microsoft.Authorization/policyDefinitions/a6abeaec-4d90-4a02-805f-6b26c4d3fbe9\"},\"policySetDefinition\":{\"name\":\"Audit KeyVault Initiative\",\"id\":\"/subscriptions/***/providers/Microsoft.Authorization/policySetDefinitions/Audit KeyVault Initiative\"}}]'.",
"additionalInfo": [
{
"type": "PolicyViolation",
"info": {
"policyDefinitionDisplayName": "[Preview]: Azure Key Vaults should use private link",
"policySetDefinitionDisplayName": "Audit KeyVault Initiative",
"evaluationDetails": {
"evaluatedExpressions": [
{
"result": "True",
"expressionKind": "Field",
"expression": "type",
"path": "type",
"expressionValue": "Microsoft.KeyVault/vaults",
"targetValue": "Microsoft.KeyVault/vaults",
"operator": "Equals"
},
{
"result": "True",
"expressionKind": "Count",
"expression": "Microsoft.KeyVault/vaults/privateEndpointConnections[*]",
"path": "properties.privateEndpointConnections[*]",
"expressionValue": 0,
"targetValue": 1,
"operator": "Less"
}
]
},
"policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/a6abeaec-4d90-4a02-805f-6b26c4d3fbe9",
"policySetDefinitionId": "/subscriptions/***/providers/Microsoft.Authorization/policySetDefinitions/Audit KeyVault Initiative",
"policyDefinitionReferenceId": "[[Preview]: Azure Key Vaults should use private link",
"policySetDefinitionName": "Audit KeyVault Initiative",
"policyDefinitionName": "a6abeaec-4d90-4a02-805f-6b26c4d3fbe9",
"policyDefinitionEffect": "Deny",
"policyAssignmentId": "/subscriptions/***/providers/Microsoft.Authorization/policyAssignments/Audit KeyVault Initiative",
"policyAssignmentName": "Audit KeyVault Initiative",
"policyAssignmentDisplayName": "Audit KeyVault Initiative",
"policyAssignmentScope": "/subscriptions/***"
}
}
]
}
]
}
}
And policy definition:
{
"properties": {
"displayName": "[Preview]: Azure Key Vaults should use private link",
"policyType": "BuiltIn",
"mode": "Indexed",
"description": "Azure Private Link lets you connect your virtual networks to Azure services without a public IP address at the source or destination. The Private Link platform handles the connectivity between the consumer and services over the Azure backbone network. By mapping private endpoints to key vault, you can reduce data leakage risks. Learn more about private links at: https://aka.ms/akvprivatelink.",
"metadata": {
"version": "1.0.0-preview",
"category": "Key Vault",
"preview": true
},
"parameters": {
"effect": {
"type": "String",
"metadata": {
"displayName": "Effect",
"description": "Enable or disable the execution of the policy"
},
"allowedValues": [
"Audit",
"Deny",
"Disabled"
],
"defaultValue": "Audit"
}
},
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.KeyVault/vaults"
},
{
"count": {
"field": "Microsoft.KeyVault/vaults/privateEndpointConnections[*]",
"where": {
"field": "Microsoft.KeyVault/vaults/privateEndpointConnections[*].privateLinkServiceConnectionState.status",
"equals": "Approved"
}
},
"less": 1
}
]
},
"then": {
"effect": "[parameters('effect')]"
}
}
},
"id": "/providers/Microsoft.Authorization/policyDefinitions/a6abeaec-4d90-4a02-805f-6b26c4d3fbe9",
"type": "Microsoft.Authorization/policyDefinitions",
"name": "a6abeaec-4d90-4a02-805f-6b26c4d3fbe9"
}
To clear the confusion here , As per the Microsoft Documentation shared by you it says :
When creating or updating a matched resource in a Resource Manager
mode, deny prevents the request before being sent to the Resource
Provider. The request is returned as a 403 (Forbidden).
Which means that neither the KeyVault or Private Endpoint can be created in the same template if the effect is set to Deny. The Effect should be Audit only for the policy to be effective properly.
I tested this using portal and its same as the template :
Scenario 1: Effect: Deny
Even if I add a private endpoint while deploying the Keyvault, the validation fails.
Scenario 2: Effect:Audit
I tried creating a keyvault without private endpoint evenif the validation passes , after clicking on create it fails as per policy.
If I create with a private endpoint then it successfully deploys.

Azure Resource Manager Template: How to get connection string for a resource in a different ResourceGroup?

I want to deploy a AzureKeyVault that contains AzureStorageAccount connection string from another resource group. I know you can do this if AzureKeyVault and AzureStorageAccount are in the same resource group, like in the following:
"resources": [
{
"type": "Microsoft.KeyVault/vaults/secrets",
"name": "secretName",
"properties": {
"value": "[concat('DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=', 'StorageAccountName', ';AccountKey=', first(listKeys(resourceId('Microsoft.Storage/storageAccounts', 'StorageAccountName'), variables('storageApiVersion')).keys).value)]"
},
"dependsOn": []
},
{
"type": "Microsoft.Storage/storageAccounts",
"name": "StorageAccountName",
...
},
{
"type": "Microsoft.KeyVault/vaults",
"name": "KeyVaultName",
...
}
]
My question is how can an change this to get the connection string from a different resource group that has already been deployed?
use the built-in functionality of the resourceId() function:
resourceId(%different_rg_name%, 'Microsoft.Storage/storageAccounts', 'StorageAccountName')
https://learn.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-resource#resourceid

Enable Azure StorageV2 static website (preview) feature using ARM template

Im trying to write an ARM template that creates a storage account with the new static website (preview) feature:
When I go to the Automation Script blade I don't see any related settings within the ARM template:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccounts_spastore_name": {
"defaultValue": "spastore",
"type": "String"
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"sku": {
"name": "Standard_LRS",
"tier": "Standard"
},
"kind": "StorageV2",
"name": "[parameters('storageAccounts_spastore_name')]",
"apiVersion": "2018-02-01",
"location": "westeurope",
"tags": {
"purpose": "example"
},
"scale": null,
"properties": {
"networkAcls": {
"bypass": "AzureServices",
"virtualNetworkRules": [],
"ipRules": [],
"defaultAction": "Allow"
},
"supportsHttpsTrafficOnly": false,
"encryption": {
"services": {
"file": {
"enabled": true
},
"blob": {
"enabled": true
}
},
"keySource": "Microsoft.Storage"
},
"accessTier": "Hot"
},
"dependsOn": []
}
]
}
I also don't see any related settings within the Azure Resource Explorer. I am aware that I have to use a newer API version as well but I don't know how to enable the feature using an ARM Template?
I don't think you can (at least as of today). ARM templates are meant for controlling the Control Plane whereas Static Websites Settings feature is exposed as part of Data Plane which is accessed by Storage Service REST API.
With the announcement of RBAC (and Azure AD roles) for Azure Storage, I am seeing some of the operations from Storage Service REST API becoming available in Storage Resource Provider API, so my guess is that sooner or later this functionality will be exposed there as well. Then you should be able to configure it through ARM templates.
It is ugly but you can do it with a deployment script in arm/bicep:
param deploymentScriptTimestamp string = utcNow()
param indexDocument string = 'index.html'
param errorDocument404Path string = 'error.html'
var storageAccountContributorRoleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities#2018-11-30' = {
name: 'DeploymentScript'
location: location
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments#2020-04-01-preview' = {
scope: storageAccount
name: guid(resourceGroup().id, storageAccountContributorRoleDefinitionId)
properties: {
roleDefinitionId: storageAccountContributorRoleDefinitionId
principalId: managedIdentity.properties.principalId
}
}
resource deploymentScript 'Microsoft.Resources/deploymentScripts#2020-10-01' = {
name: 'deploymentScript'
location: location
kind: 'AzurePowerShell'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
dependsOn: [
roleAssignment
storageAccount
]
properties: {
azPowerShellVersion: '3.0'
scriptContent: '''
param(
[string] $ResourceGroupName,
[string] $StorageAccountName,
[string] $IndexDocument,
[string] $ErrorDocument404Path)
$ErrorActionPreference = 'Stop'
$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -AccountName $StorageAccountName
$ctx = $storageAccount.Context
Enable-AzStorageStaticWebsite -Context $ctx -IndexDocument $IndexDocument -ErrorDocument404Path $ErrorDocument404Path
'''
forceUpdateTag: deploymentScriptTimestamp
retentionInterval: 'PT4H'
arguments: '-ResourceGroupName ${resourceGroup().name} -StorageAccountName ${accountName} -IndexDocument ${indexDocument} -ErrorDocument404Path ${errorDocument404Path}'
}
}
see: azure example for static website, resource templates and scripts

VSTS deployment fails with "Authorization failed for ... of type 'Microsoft.Storage/storageAccounts/providers/locks'"

I'm deploying an ARM template with VSTS which contains a lock (in my case lock on a Storage Account for a Function App)
{
"parameters": {
"name": {
"type": "string"
},
"storageName": {
"type": "string"
},
"location": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "2015-05-01-preview",
"type": "Microsoft.Storage/storageAccounts",
"name": "[parameters('storageName')]",
"location": "[parameters('location')]",
"properties": {
"accountType": "Standard_LRS"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts/providers/locks",
"name": "[concat(parameters('storageName'), '/Microsoft.Authorization/', parameters('storageName'))]",
"apiVersion": "2015-01-01",
"dependsOn": [
"[concat('Microsoft.Storage/storageAccounts/', parameters('storageName'))]"
],
"properties": {
"level": "CannotDelete",
"notes": "One or more function apps were linked to this storage account. You can see all the function apps linked to the account under 'files' or 'shares'."
}
}
]
},...
That just works fine when deploying from VS or from command line with my credentials.
However when deploying from a VSTS release definition, the deployment fails with:
Resource Microsoft.Resources/deployments 'myFunctionApp' failed with message '{
"error": {
"code": "InvalidTemplateDeployment",
"message": "The template deployment failed with error: 'Authorization failed for template resource 'myFunctionAppStorage/Microsoft.Authorization/myFunctionAppStorage' of type 'Microsoft.Storage/storageAccounts/providers/locks'. The client '***VSTS service principal Id***' with object id '***VSTS service principal Id***' does not have permission to perform action 'Microsoft.Authorization/locks/write' at scope '/subscriptions/*** subscription ***/resourceGroups/*** resource group ***/providers/Microsoft.Storage/storageAccounts/myFunctionAppStorage/providers/Microsoft.Authorization/locks/myFunctionAppStorage'.'."
}
}
When I remove the Microsoft.Storage/storageAccounts/providers/locks section from the template, the VSTS deployment works. But then the storage account would bear no lock preventing a deletion.
Contributor role - which is assigned when VSTS creates the Service Principal in the AAD connected to the Resource Groups Subscription - is not sufficient for placing the lock. Assign Owner and the lock can be placed with the VSTS deployment process.

Resources