Search code examples
azure-devopsazure-web-app-serviceazure-resource-managerazure-bicep

Bicep / ARM library for App Services with optional certificates


I am currently developing a bicep module to enable the team to deploy Azure App Services via Azure DevOps. It's working when I specify the certificate via the customHostnameConfiguration Json property which is passes through to the AppServices.bicep file but when I do not pass it in for App Services that do not require certificate bindings its failing.

I am trying to do a conditional deployment using the Microsoft.Web/certificates@2022-03-01 resource provider, specifically using the if (!empty(customHostnameConfiguration)) condition. I have also implemented a ternary operator on the resource properties such as name: contains(customHostnameConfiguration, 'appServiceCertificateName') ? customHostnameConfiguration.appServiceCertificateName : ''

When I specify the certificate details within the customHostnameConfiguration json property the function app deploys, creates the key vault certificate reference and binds it to the specific hostname.If I do not provide the customHostnameConfiguration json property the function app is created but it still tries to deploy the certificate and hostname binding and fails as the Microsoft.Web/certificates@2022-03-01 name property is an empty string. If I provide a default value it also fails as it tries to evaluate the certificate.

My question is how do I have a conditional deployment of the Microsoft.Web/certificates and Microsoft.Web/sites/hostNameBindings@2021-03-01 when they are not required in my App Service library?

Thanks

App Service without certificate Json

   "basicFunctionAppConfiguration": {
        "value": {
            "name": "iac-dev-functionapp",
            "kind": "functionapp",
            "appServiceEnvironment": {
                "ResourceGroupName": "rg-eun-h1s01-dev-iac",
                "Name": "ase-eun-h1s01-dev-iac"
            }               
        }
    }

App Service with certificate Json

"functionAppCustomDomain": {
            "value": {
                "name": "iac-dev-functionappcustomdomain",
                "kind": "functionapp",
                "appServiceEnvironment": {
                    "resourceGroupName": "rg-eun-h1s01-dev-iac",
                    "name": "ase-eun-h1s01-dev-iac"
                },
                "customHostnameConfiguration": {
                    "keyVaultName": "kv-eun-h1s01-dev-odp",
                    "keyVaultResourceGroup": "rg-eun-h1s01-dev-odp",
                    "keyVaultCertificateSecretName": "New-Wildcard-Company-com/{HiddenGUID}",
                    "customHostName": "mytestapp.mycompany.com",
                    "appServiceCertificateName": "companycert"
                }                
            }
        } 

Main.bicep

module functionApp 'br/Compute:appservice:v0.1'= {
  name: 'functionapp-${basicFunctionAppConfiguration.name}-${uniqueString(basicFunctionAppConfiguration.name)}'
  params:{
    name: basicFunctionAppConfiguration.name
    location: location
    appServiceEnvironmentId: resourceId(basicFunctionAppConfiguration.appServiceEnvironment.ResourceGroupName, 'Microsoft.Web/hostingEnvironments', basicFunctionAppConfiguration.appServiceEnvironment.Name)
    appServicePlanId: appServicePlan.outputs.resourceId
    storageAccountId: resourceId(basicFunctionAppConfiguration.diagnosticSettings.storageAccount.resourceGroup, 'Microsoft.Storage/storageAccounts', basicFunctionAppConfiguration.diagnosticSettings.storageAccount.accountName)
    appInsightId: applicationInsights.outputs.resourceId
    kind: basicFunctionAppConfiguration.kind
  }
}


module functionAppCustomDomainKeyVault 'br/Compute:appservice:v0.1'= {
  name: 'functionapp-${functionAppCustomDomain.name}-${uniqueString(functionAppCustomDomain.name)}'
  params:{
    name: functionAppCustomDomain.name
    location: location
    appServiceEnvironmentId: resourceId(functionAppCustomDomain.appServiceEnvironment.resourceGroupName, 'Microsoft.Web/hostingEnvironments', functionAppCustomDomain.appServiceEnvironment.name)
    appServicePlanId: appServicePlan.outputs.resourceId
    storageAccountId: resourceId(functionAppCustomDomain.diagnosticSettings.storageAccount.resourceGroup, 'Microsoft.Storage/storageAccounts', functionAppCustomDomain.diagnosticSettings.storageAccount.accountName)
    customHostnameConfiguration: functionAppCustomDomain.customHostnameConfiguration
    appInsightId: applicationInsights.outputs.resourceId
    kind: functionAppCustomDomain.kind
  }
}

App Service bicep

@description('Required. Name of the site.')
param name string

@description('Optional. Location for all Resources.')
param location string = resourceGroup().location

@description('Required. The resource ID of the app service environment to use for this resource.')
param appServiceEnvironmentId string

@description('Required. The resource ID of the app service plan to use for the site.')
param appServicePlanId string

@description('Required. Resource ID of the app insight to leverage for this resource.')
param appInsightId string

@description('Required. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions.')
param storageAccountId string

@description('Optional. Resource ID of the diagnostic storage account.')
param diagnosticStorageAccountId string = ''

@description('Optional. Resource ID of log analytics workspace.')
param diagnosticWorkspaceId string = ''

@description('Required. Type of site to deploy.')
@allowed([
  'functionapp' // function app windows os
  'functionapp,linux' // function app linux os
  'functionapp,workflowapp' // logic app workflow
  'functionapp,workflowapp,linux' // logic app docker container
  'app' // normal web app
])
param kind string

@description('Optional. If client affinity is enabled.')
param clientAffinityEnabled bool = false

@description('Optional. If web sockets are enabled.')
param webSocketsEnabled bool = false

@description('Optional. The app settings-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING.')
param appSettingsKeyValuePairs object = {}

@description('Optional. The custom hostname and key vault object for custom hostname configuration. Get and List access policy we be generated for this app service system assigned managed identity')
param customHostnameConfiguration object = {}

@description('Optional. Tags of the resource.')
param tags object = {}

//Default Values
var clientCertEnabled = false
var clientCertMode = 'Optional'
var containerSize = -1
var customDomainVerificationId = ''
var dailyMemoryTimeQuota = -1
var enabled = true
var httpsOnly = true
var hyperV = false
var keyVaultAccessIdentityResourceId = 'SystemAssigned'
var redundancyMode = 'None'
var storageAccountRequired = true
var systemAssignedIdentity = true

// var clientCertExclusionPaths = ''
// var cloningInfo = {}
// var virtualNetworkSubnetId = ''

var defaultAppSettings = {
  FUNCTIONS_EXTENSION_VERSION: '~4'
  FUNCTIONS_WORKER_RUNTIME: 'dotnet'
}
var completeAppSettings = union(defaultAppSettings, appSettingsKeyValuePairs)

var siteConfig = {
  ftpsState: 'FtpsOnly'
  minTlsVersion: '1.2'
  netFrameworkVersion: '6.0'
  alwaysOn: true
  autoHealEnabled: true
  http20Enabled: true
  webSocketsEnabled: webSocketsEnabled 
}

//Diagnostics
var diagnosticLogsRetentionInDays = 365
var diagnosticSettingsName = 'diag-${name}-appservice-log'
var diagnosticMetricsToEnable = ['AllMetrics']

var diagnosticLogCategoriesToEnable = contains(kind, 'workflowapp') ? [
  'WorkflowRuntime'
  'FunctionAppLogs'
] : kind == 'functionapp' ? [
  'FunctionAppLogs'
] : [
  'AppServiceHTTPLogs'
  'AppServiceConsoleLogs'
  'AppServiceAppLogs'
  'AppServiceAuditLogs'
  'AppServiceIPSecAuditLogs'
  'AppServicePlatformLogs'
]

var diagnosticsLogsSpecified = [for category in filter(diagnosticLogCategoriesToEnable, item => item != 'allLogs'): {
  category: category
  enabled: true
  retentionPolicy: {
    enabled: true
    days: diagnosticLogsRetentionInDays
  }
}]

var diagnosticsLogs = contains(diagnosticLogCategoriesToEnable, 'allLogs') ? [
  {
    categoryGroup: 'allLogs'
    enabled: true
    retentionPolicy: {
      enabled: true
      days: diagnosticLogsRetentionInDays
    }
  }
] : diagnosticsLogsSpecified

var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: {
  category: metric
  timeGrain: null
  enabled: true
  retentionPolicy: {
    enabled: true
    days: diagnosticLogsRetentionInDays
  }
}]

//Identity
var identityType = systemAssignedIdentity ? 'SystemAssigned' : 'None'

var identity = identityType != 'None' ? {
  type: identityType
  userAssignedIdentities: null
} : null


//Telemetry
var enableDefaultTelemetry = true
resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) {
  name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name, location)}'
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: []
    }
  }
}

//KeyVault Certificate
resource appServiceCertificate 'Microsoft.Web/certificates@2022-03-01' = if (!empty(customHostnameConfiguration)) {
  name: contains(customHostnameConfiguration, 'appServiceCertificateName') ? customHostnameConfiguration.appServiceCertificateName : ''
  location: location
  properties: { 
    keyVaultId: resourceId(customHostnameConfiguration.keyVaultResourceGroup, 'Microsoft.KeyVault/vaults', customHostnameConfiguration.keyVaultName)
    keyVaultSecretName: contains(customHostnameConfiguration, 'keyVaultCertificateSecretName') ? customHostnameConfiguration.keyVaultCertificateSecretName : ''
    serverFarmId: appServicePlanId
  }
}

//Create the app service
resource app 'Microsoft.Web/sites@2021-03-01' = {
  name: name
  location: location
  kind: kind
  tags: tags
  identity: identity
  properties: {
    serverFarmId: appServicePlanId
    clientAffinityEnabled: clientAffinityEnabled
    httpsOnly: httpsOnly
    hostingEnvironmentProfile: !empty(appServiceEnvironmentId) ? {
      id: appServiceEnvironmentId
    } : null
    storageAccountRequired: storageAccountRequired
    keyVaultReferenceIdentity: !empty(keyVaultAccessIdentityResourceId) ? keyVaultAccessIdentityResourceId : null
    siteConfig: siteConfig
    clientCertEnabled: clientCertEnabled
    clientCertMode: clientCertMode
    containerSize: containerSize != -1 ? containerSize : null
    customDomainVerificationId: !empty(customDomainVerificationId) ? customDomainVerificationId : null
    dailyMemoryTimeQuota: dailyMemoryTimeQuota != -1 ? dailyMemoryTimeQuota : null
    enabled: enabled
    hostNamesDisabled: false
    hyperV: hyperV
    redundancyMode: redundancyMode
  }
}
    // hostNameSslStates: (!empty(customHostnameConfiguration)) ? customHostnameState : null
    //cloningInfo: !empty(cloningInfo) ? cloningInfo : null
    //virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : any(null)
    //clientCertExclusionPaths: !empty(clientCertExclusionPaths) ? clientCertExclusionPaths : null

resource hostNameBinding 'Microsoft.Web/sites/hostNameBindings@2021-03-01' = if (!empty(customHostnameConfiguration)) {
    name: contains(customHostnameConfiguration, 'customHostName') ? customHostnameConfiguration.customHostName : '${uniqueString(deployment().name, location)}-hostnamebinding' 
    parent: app
    properties: {
      siteName: contains(customHostnameConfiguration, 'customHostName') ? customHostnameConfiguration.customHostName : ''
      sslState: 'SniEnabled'
      thumbprint: !empty(appServiceCertificate) ? appServiceCertificate.properties.thumbprint : ''
    }
  }

//Set the default stack to dotnet
resource appSettingsStack 'Microsoft.Web/sites/config@2021-03-01' = { 
  name: 'metadata' 
  parent: app
  properties: { 
    CURRENT_STACK: 'dotnet' 
  } 
}

//Save the app settings
module app_appsettings 'br/Compute:appservice/appsettings:v0.1' = if (!empty(completeAppSettings)) {
  name: '${uniqueString(deployment().name, location)}-Site-Config-AppSettings'
  params: {
    appName: app.name
    kind: kind
    storageAccountId: storageAccountId
    appInsightId: appInsightId
    appSettingsKeyValuePairs: completeAppSettings
  }
}

//Diagnostics
resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(diagnosticStorageAccountId)) {
  name: !empty(diagnosticSettingsName) ? diagnosticSettingsName : '${name}-diagnosticSettings'
  properties: {
    storageAccountId: !empty(diagnosticStorageAccountId) ? diagnosticStorageAccountId : null
    workspaceId: !empty(diagnosticWorkspaceId) ? diagnosticWorkspaceId : null
    metrics: diagnosticsMetrics
    logs: diagnosticsLogs
  }
  scope: app
}

// Outputs     
@description('The name of the site.')
output name string = app.name

@description('The resource ID of the site.')
output appServiceResourceId string = app.id

@description('The resource group the site was deployed into.')
output resourceGroupName string = resourceGroup().name

@description('The principal ID of the system assigned identity.')
output systemAssignedPrincipalId string = systemAssignedIdentity && contains(app.identity, 'principalId') ? app.identity.principalId : ''

@description('The location the resource was deployed into.')
output location string = app.location

@description('Default hostname of the app.')
output defaultHostname string = app.properties.defaultHostName

Solution

  • So, the answer here is that you have to have a sub module for the certificate and hostnaem binding for this to work.

    AppService.bicep

    @description('Required. Name of the site.')
    param name string
    
    @description('Optional. Location for all Resources.')
    param location string = resourceGroup().location
    
    @description('Required. The resource ID of the app service environment to use for this resource.')
    param appServiceEnvironmentId string
    
    @description('Required. The resource ID of the app service plan to use for the site.')
    param appServicePlanId string
    
    @description('Required. Resource ID of the app insight to leverage for this resource.')
    param appInsightId string
    
    @description('Required. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions.')
    param storageAccountId string
    
    @description('Optional. Resource ID of the diagnostic storage account.')
    param diagnosticStorageAccountId string = ''
    
    @description('Optional. Resource ID of log analytics workspace.')
    param diagnosticWorkspaceId string = ''
    
    @description('Required. Type of site to deploy.')
    @allowed([
      'functionapp' // function app windows os
      'functionapp,linux' // function app linux os
      'functionapp,workflowapp' // logic app workflow
      'functionapp,workflowapp,linux' // logic app docker container
      'app' // normal web app
    ])
    param kind string
    
    @description('Optional. If client affinity is enabled.')
    param clientAffinityEnabled bool = false
    
    @description('Optional. If web sockets are enabled.')
    param webSocketsEnabled bool = false
    
    @description('Optional. The app settings-value pairs except for AzureWebJobsStorage, AzureWebJobsDashboard, APPINSIGHTS_INSTRUMENTATIONKEY and APPLICATIONINSIGHTS_CONNECTION_STRING.')
    param appSettingsKeyValuePairs object = {}
    
    @description('Optional. The custom hostname and key vault object for custom hostname configuration. Get and List access policy we be generated for this app service system assigned managed identity')
    param customHostnameConfiguration object = {}
    
    @description('Optional. Tags of the resource.')
    param tags object = {}
    
    //Default Values
    var clientCertEnabled = false
    var clientCertMode = 'Optional'
    var containerSize = -1
    var customDomainVerificationId = ''
    var dailyMemoryTimeQuota = -1
    var enabled = true
    var httpsOnly = true
    var hyperV = false
    var keyVaultAccessIdentityResourceId = 'SystemAssigned'
    var redundancyMode = 'None'
    var storageAccountRequired = true
    var systemAssignedIdentity = true
    
    // var clientCertExclusionPaths = ''
    // var cloningInfo = {}
    // var virtualNetworkSubnetId = ''
    
    var defaultAppSettings = {
      FUNCTIONS_EXTENSION_VERSION: '~4'
      FUNCTIONS_WORKER_RUNTIME: 'dotnet'
    }
    var completeAppSettings = union(defaultAppSettings, appSettingsKeyValuePairs)
    
    var siteConfig = {
      ftpsState: 'FtpsOnly'
      minTlsVersion: '1.2'
      netFrameworkVersion: '6.0'
      alwaysOn: true
      autoHealEnabled: true
      http20Enabled: true
      webSocketsEnabled: webSocketsEnabled 
    }
    
    //Diagnostics
    var diagnosticLogsRetentionInDays = 365
    var diagnosticSettingsName = 'diag-${name}-appservice-log'
    var diagnosticMetricsToEnable = ['AllMetrics']
    
    var diagnosticLogCategoriesToEnable = contains(kind, 'workflowapp') ? [
      'WorkflowRuntime'
      'FunctionAppLogs'
    ] : kind == 'functionapp' ? [
      'FunctionAppLogs'
    ] : [
      'AppServiceHTTPLogs'
      'AppServiceConsoleLogs'
      'AppServiceAppLogs'
      'AppServiceAuditLogs'
      'AppServiceIPSecAuditLogs'
      'AppServicePlatformLogs'
    ]
    
    var diagnosticsLogsSpecified = [for category in filter(diagnosticLogCategoriesToEnable, item => item != 'allLogs'): {
      category: category
      enabled: true
      retentionPolicy: {
        enabled: true
        days: diagnosticLogsRetentionInDays
      }
    }]
    
    var diagnosticsLogs = contains(diagnosticLogCategoriesToEnable, 'allLogs') ? [
      {
        categoryGroup: 'allLogs'
        enabled: true
        retentionPolicy: {
          enabled: true
          days: diagnosticLogsRetentionInDays
        }
      }
    ] : diagnosticsLogsSpecified
    
    var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: {
      category: metric
      timeGrain: null
      enabled: true
      retentionPolicy: {
        enabled: true
        days: diagnosticLogsRetentionInDays
      }
    }]
    
    //Identity
    var identityType = systemAssignedIdentity ? 'SystemAssigned' : 'None'
    
    var identity = identityType != 'None' ? {
      type: identityType
      userAssignedIdentities: null
    } : null
    
    
    //Telemetry
    var enableDefaultTelemetry = true
    resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) {
      name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name, location)}'
      properties: {
        mode: 'Incremental'
        template: {
          '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
          contentVersion: '1.0.0.0'
          resources: []
        }
      }
    }
    
    //Create the app service
    resource app 'Microsoft.Web/sites@2021-03-01' = {
      name: name
      location: location
      kind: kind
      tags: tags
      identity: identity
      properties: {
        serverFarmId: appServicePlanId
        clientAffinityEnabled: clientAffinityEnabled
        httpsOnly: httpsOnly
        hostingEnvironmentProfile: !empty(appServiceEnvironmentId) ? {
          id: appServiceEnvironmentId
        } : null
        storageAccountRequired: storageAccountRequired
        keyVaultReferenceIdentity: !empty(keyVaultAccessIdentityResourceId) ? keyVaultAccessIdentityResourceId : null
        siteConfig: siteConfig
        clientCertEnabled: clientCertEnabled
        clientCertMode: clientCertMode
        containerSize: containerSize != -1 ? containerSize : null
        customDomainVerificationId: !empty(customDomainVerificationId) ? customDomainVerificationId : null
        dailyMemoryTimeQuota: dailyMemoryTimeQuota != -1 ? dailyMemoryTimeQuota : null
        enabled: enabled
        hostNamesDisabled: false
        hyperV: hyperV
        redundancyMode: redundancyMode
      }
    }
        // hostNameSslStates: (!empty(customHostnameConfiguration)) ? customHostnameState : null
        //cloningInfo: !empty(cloningInfo) ? cloningInfo : null
        //virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : any(null)
        //clientCertExclusionPaths: !empty(clientCertExclusionPaths) ? clientCertExclusionPaths : null
    
      module appServiceCertificate 'br/Compute:appservice/appservicecertificates:v0.1' = if (!empty(customHostnameConfiguration)) {
        name: '${uniqueString(deployment().name, location)}-app-service-certificate'
        dependsOn: [app]
        params:{
          appServiceName: name
          appServicePlanId: appServicePlanId
          keyVaultName: customHostnameConfiguration.keyVaultName
          keyVaultResourceGroup: customHostnameConfiguration.keyVaultResourceGroup
          keyVaultCertificateSecretName: customHostnameConfiguration.keyVaultCertificateSecretName
          customHostName: customHostnameConfiguration.customHostName
          appServiceCertificateName: customHostnameConfiguration.appServiceCertificateName
        }
      }
    
    //Set the default stack to dotnet
    resource appSettingsStack 'Microsoft.Web/sites/config@2021-03-01' = { 
      name: 'metadata' 
      parent: app
      properties: { 
        CURRENT_STACK: 'dotnet' 
      } 
    }
    
    //Save the app settings
    module app_appsettings 'br/Compute:appservice/appsettings:v0.1' = if (!empty(completeAppSettings)) {
      name: '${uniqueString(deployment().name, location)}-Site-Config-AppSettings'
      params: {
        appName: app.name
        kind: kind
        storageAccountId: storageAccountId
        appInsightId: appInsightId
        appSettingsKeyValuePairs: completeAppSettings
      }
    }
    
    //Diagnostics
    resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(diagnosticStorageAccountId)) {
      name: !empty(diagnosticSettingsName) ? diagnosticSettingsName : '${name}-diagnosticSettings'
      properties: {
        storageAccountId: !empty(diagnosticStorageAccountId) ? diagnosticStorageAccountId : null
        workspaceId: !empty(diagnosticWorkspaceId) ? diagnosticWorkspaceId : null
        metrics: diagnosticsMetrics
        logs: diagnosticsLogs
      }
      scope: app
    }
    
    // Outputs     
    @description('The name of the site.')
    output name string = app.name
    
    @description('The resource ID of the site.')
    output appServiceResourceId string = app.id
    
    @description('The resource group the site was deployed into.')
    output resourceGroupName string = resourceGroup().name
    
    @description('The principal ID of the system assigned identity.')
    output systemAssignedPrincipalId string = systemAssignedIdentity && contains(app.identity, 'principalId') ? app.identity.principalId : ''
    
    @description('The location the resource was deployed into.')
    output location string = app.location
    
    @description('Default hostname of the app.')
    output defaultHostname string = app.properties.defaultHostName
    

    AppServiceCertificate.bicep

    @description('Required. Name of the app service to bind the key vault certificate.')
    param appServiceName string 
    
    @description('Required. Resource Id of the app service plan.')
    param appServicePlanId string 
    
    @description('Optional. Location for all Resources.')
    param location string = resourceGroup().location
    
    @description('Required. Name of the keyvault where the certificate is stored.')
    param keyVaultName string
    
    @description('Required. Name of the keyvault resource group where the certificate is stored.')
    param keyVaultResourceGroup string
    
    @description('Required. Name of the keyvault certificate including version in {keyvaultname}/{version} format')
    param keyVaultCertificateSecretName string
    
    @description('Required. Custom hostname for the app service')
    param customHostName string
    
    @description('Required. Certificate name for this app service. This is registered against the resource group')
    param appServiceCertificateName string
    
    resource appService 'Microsoft.Web/sites@2021-03-01' existing = {
      name: appServiceName
    }
    
    resource appServiceCertificate 'Microsoft.Web/certificates@2022-03-01' = {
      name: appServiceCertificateName
      location: location
      properties: { 
        keyVaultId: resourceId(keyVaultResourceGroup, 'Microsoft.KeyVault/vaults', keyVaultName)
        keyVaultSecretName: keyVaultCertificateSecretName 
        serverFarmId: appServicePlanId
      }
    }
    
    resource hostNameBinding 'Microsoft.Web/sites/hostNameBindings@2021-03-01' = {
      name: customHostName 
      parent: appService
      properties: {
        siteName: customHostName
        sslState: 'SniEnabled'
        thumbprint: !empty(appServiceCertificate) ? appServiceCertificate.properties.thumbprint : ''
      }
    }