Search code examples
azureazure-resource-managerazure-bicepazure-front-door

How to deploy App Service with managed SSL certificate using ARM


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?


Solution

  • 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
      }
    }]