Configure app and web server logs to a storage account in ARM template

15 Mar 2020

Azure App Service allows application and Web server logging to file system or blob storage through simple configuration wizard in Azure portal. But, when you want automate and do the same from ARM template, there are couple of challenges that needs to be addressed. In this post, I'll discuss about the challenges and provide you a working sample.

Following screenshot shows the app server logs configuration that we wish to set from the ARM template

app server logs configuration in azure portal

Challenge 1: Couldn't find the properties related to app server logs in Export template ?

We often use Export Template option in app service blade (Azure portal) to find the properties that you can set during ARM deployments. This works out well for most configurations. But for a surprise, app server logs related entries are not exported. Looking into the Azure Resource Explorer in Azure portal too results in missing information. This raises a question 'Can app server logs be configured through ARM ?'. Luckily, the answer is Yes. 

Apparently, Azure has a Azure Resource Explorer (preview) tool through which you can see entire Azure resources in template format. Browse to https://resources.azure.com/, and search for the app service. Selecting your application will show you entire detailed ARM template with all logs related information in a dedicated 'logs' node. From there you'll know that the logs can be configured with the schema structure that isn't listed anywhere in the Azure ARM documentation.

{
    "type": "Microsoft.Web/sites/config", // logs
    "apiVersion": "2018-11-01",
    "location": "[variables('location')]",
    "name": "[concat(variables('webAppName'), '/logs')]",
    "dependsOn": [
        "logsStorageBlobsLoop",
        "[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
    ],
    "properties": {
        "applicationLogs": {
            "fileSystem": {
                "level": "Off"
            },
            "azureTableStorage": {
                "level": "Off",
                "sasUrl": null
            },
            "azureBlobStorage": {
                "level": "[parameters('diagnosticsLogsLevel')]",
                "sasUrl": "[parameters('sasUrl1')]",
                "retentionInDays": "[parameters('diagnosticsLogsRetentionInDays')]"
            }
        },
        "httpLogs": {
            "fileSystem": {
                "retentionInMb": 35,
                "retentionInDays": 10,
                "enabled": false
            },
            "azureBlobStorage": {
                "enabled": "[parameters('httpLoggingEnabled')]",
                "sasUrl": "[parameters('sasUrl2')]",
                "retentionInDays": "[parameters('diagnosticsLogsRetentionInDays')]"
            }
        },
        "failedRequestsTracing": {
            "enabled": "[parameters('httpLoggingEnabled')]"
        },
        "detailedErrorMessages": {
            "enabled": "[parameters('detailedErrorLoggingEnabled')]"
        }
    }
}

So, through above ARM properties structure you would be able configure the logs successfully.

Challenge 2: Configuring blob storage URL dynamically

In above code, you'll notice that the storage URL should be configured through the parameters like sasUrl1, sasUrl2 etc., This pretty much sounds like an hard coding and this forces us to create the required storage account & blobs first and provide those as the parameter to your web app service ARM template.

Due this challenge, many have opted to move on to Azure Power shell scripts that allows you to programmatically pull in those information and provide it to the next template. But if you want to do it through the declarative syntax through ARM templates, it is feasible using Azure funtion - listAccountSas which returns object from which you can query account SasToken.

Following ARM sample shows on how to dynamically create the required multiple log storage account blobs within the current resourceGroup and assign its sasUrl to the app service

"variables": {
    //...
    "storageAccountName": "[toLower(concat(parameters('logsStorageDescriptor'), resourceGroup().name))]",
    "appLogsBlobContainer": "[toLower(concat(parameters('WebAppDescriptor'),'-','logs'))]",
    "iisLogBlobContainer": "[toLower(concat(parameters('WebAppDescriptor'),'-','iislogs'))]",
    "listAccountSasRequestContent": {
        "signedServices": "bfqt",
        "signedPermission": "rwdlacup",
        "signedStart": "2020-03-10T07:39:59Z",
        "signedExpiry": "2220-03-10T07:39:59Z",
        "signedResourceTypes": "sco"
    },
    "logsBlobNames": "[createArray(variables('appLogsBlobContainer'), variables('iisLogBlobContainer'))]"
}
{
    "apiVersion": "2019-06-01",
    "type": "Microsoft.Storage/storageAccounts", // For storing logs in a separate storage account within that particular resource group
    "name": "[variables('storageAccountName')]",
    "location": "[variables('location')]",
    "sku": {
        "name": "[parameters('logsStorageAccountType')]"
    },
    "kind": "[parameters('logsStorageAccountKind')]"
},
{
    "name": "[concat(variables('storageAccountName'),'/default/', variables('logsBlobNames')[copyIndex()])]",
    "type": "Microsoft.Storage/storageAccounts/blobServices/containers",
    "apiVersion": "2019-06-01",
    "dependsOn": [
        "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
    ],
    "properties": {
        "publicAccess": "None"
    },
    "copy": {
        "name": "logsStorageBlobsLoop",
        "count": "[length(variables('logsBlobNames'))]"
    }
},
{
    "type": "Microsoft.Web/sites/config", // WebApp configurations
    "apiVersion": "2018-11-01",
    "name": "[concat(variables('webAppName'), '/web')]",
    "location": "[variables('location')]",
    "dependsOn": [
        "[resourceId('Microsoft.Web/sites', variables('webAppName'))]",
        "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
    ],
    "properties": {
        // ...
        "httpLoggingEnabled": "[parameters('httpLoggingEnabled')]",
        "logsDirectorySizeLimit": 35,
        "detailedErrorLoggingEnabled": "[parameters('detailedErrorLoggingEnabled')]",
        "requestTracingEnabled": false
        // ...
    }
},
{
    "type": "Microsoft.Web/sites/config", // logs
    "apiVersion": "2018-11-01",
    "location": "[variables('location')]",
    "name": "[concat(variables('webAppName'), '/logs')]",
    "dependsOn": [
        "logsStorageBlobsLoop",
        "[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
    ],
    "properties": {
        "applicationLogs": {
            "fileSystem": {
                "level": "Off"
            },
            "azureTableStorage": {
                "level": "Off",
                "sasUrl": null
            },
            "azureBlobStorage": {
                "level": "[parameters('diagnosticsLogsLevel')]",
                "sasUrl": "[concat(reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').primaryEndpoints.blob, variables('appLogsBlobContainer'), '?', listAccountSas(variables('storageAccountName'), '2019-06-01', variables('listAccountSasRequestContent')).accountSasToken)]",
                "retentionInDays": "[parameters('diagnosticsLogsRetentionInDays')]"
            }
        },
        "httpLogs": {
            "fileSystem": {
                "retentionInMb": 35,
                "retentionInDays": 10,
                "enabled": false
            },
            "azureBlobStorage": {
                "enabled": "[parameters('httpLoggingEnabled')]",
                "sasUrl": "[concat(reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').primaryEndpoints.blob, variables('iisLogBlobContainer'), '?', listAccountSas(variables('storageAccountName'), '2019-06-01', variables('listAccountSasRequestContent')).accountSasToken)]",
                "retentionInDays": "[parameters('diagnosticsLogsRetentionInDays')]"
            }
        },
        "failedRequestsTracing": {
            "enabled": "[parameters('httpLoggingEnabled')]"
        },
        "detailedErrorMessages": {
            "enabled": "[parameters('detailedErrorLoggingEnabled')]"
        }
    }
}

listAccountSasRequestContent variable defines the list of access permissions that are required in from the mentioned blob.

Challenge 3: Still my app server log settings is not configured properly

I've spent quiet lot of time over here, trying to figure-out Why the settings aren't getting configured as expected. No errors were thrown by the ARM during deployment.

The app server logs configuration works by setting following four properties in the appSettings (Configuration section in app service - Azure portal); 
1. DIAGNOSTICS_AZUREBLOBCONTAINERSASURL
2. DIAGNOSTICS_AZUREBLOBRETENTIONINDAYS
3. WEBSITE_HTTPLOGGING_CONTAINER_URL
4. WEBSITE_HTTPLOGGING_RETENTION_DAYS

It turns out that the log sasUrl are automatically created lot earlier in the ARM deployment, and it was cleared OFF when appsettings are again set from your ARM template. So, you need to set a proper dependsOn section for the logs resource like below

{
    "type": "Microsoft.Web/sites/config", // logs
    "apiVersion": "2018-11-01",
    "location": "[variables('location')]",
    "name": "[concat(variables('webAppName'), '/logs')]",
    "dependsOn": [
        "logsStorageBlobsLoop",
        "[resourceId('Microsoft.Web/sites', variables('webAppName'))]",
        "[resourceId('Microsoft.Web/sites/config', variables('webAppName'), 'appsettings')]"
    ],
    // ...
}

After above dependency, everything was dynamically set as expected.

Hope this helps!