Search code examples
azureazure-web-app-serviceazure-storageazure-resource-managerazure-storage-files

Can not deploy ARM template of Azure App Service with Linux that has mounted storage


We are trying to deploy Azure Linux App Service, that also has mounted storage. Here's our ARM template

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "Environment": {
            "type": "String",
            "allowedValues": [
                "dev",
                "stg",
                "prd"
            ]
        },
        "Region": {
            "type": "string",
            "allowedValues": [
                "eu",
                "we"
            ]
        },
        "MagentoMediaFileShareName": {
            "type": "string"
        },
        "StorageAccountName": {
            "type": "string"
        },
        "StorageAccountAccessKey": {
            "type": "securestring"
        },
        "MagentoMediaMountPath": {
            "type": "string",
            "defaultValue": "/var/www/html/pub/external_media"
        }
    },
    "variables": {
        "Location": "[resourceGroup().location]",
        "ResourcePrefix": "[format('ariva-{0}-{1}-magento', parameters('Environment'), parameters('Region'))]",
        "WebSiteName": "[concat(variables('ResourcePrefix'), '-web')]",
        "ServicePlanId": "[format('/subscriptions/{0}/resourceGroups/ariva-{1}-{2}/providers/Microsoft.Web/serverfarms/ariva-{1}-{2}-asp', subscription().subscriptionId,  parameters('Environment'), parameters('Region'))]"
    },
    "resources": [
        {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2020-12-01",
            "name": "[variables('WebSiteName')]",
            "location": "[variables('Location')]",
            "kind": "app,linux,container",
            "properties": {
                "enabled": true,
                "hostNameSslStates": [
                    {
                        "name": "[concat(variables('WebSiteName'), '.azurewebsites.net')]",
                        "sslState": "Disabled",
                        "hostType": "Standard"
                    },
                    {
                        "name": "[concat(variables('WebSiteName'), '.scm.azurewebsites.net')]",
                        "sslState": "Disabled",
                        "hostType": "Repository"
                    }
                ],
                "serverFarmId": "[variables('ServicePlanId')]",
                "reserved": true,
                "isXenon": false,
                "hyperV": false,
                "siteConfig": {
                    "numberOfWorkers": 1,
                    "linuxFxVersion": "DOCKER|mcr.microsoft.com/appsvc/staticsite:latest",
                    "acrUseManagedIdentityCreds": false,
                    "alwaysOn": false,
                    "http20Enabled": false,
                    "functionAppScaleLimit": 0,
                    "minimumElasticInstanceCount": 1
                },
                "scmSiteAlsoStopped": false,
                "clientAffinityEnabled": false,
                "clientCertEnabled": false,
                "clientCertMode": "Required",
                "hostNamesDisabled": false,
                "customDomainVerificationId": "1071794BD68C78EC0A4569F03C034F6E1B21BD4E6D35725D99523AC00AE12AA1",
                "containerSize": 0,
                "dailyMemoryTimeQuota": 0,
                "keyVaultReferenceIdentity": "SystemAssigned",
                "httpsOnly": false,
                "redundancyMode": "None",
                "storageAccountRequired": false
            }
        },
        {
            "type": "Microsoft.Web/sites/basicPublishingCredentialsPolicies",
            "apiVersion": "2020-12-01",
            "name": "[concat(variables('WebSiteName'), '/ftp')]",
            "location": "[variables('Location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', variables('WebSiteName'))]"
            ],
            "properties": {
                "allow": true
            }
        },
        {
            "type": "Microsoft.Web/sites/basicPublishingCredentialsPolicies",
            "apiVersion": "2020-12-01",
            "name": "[concat(variables('WebSiteName'), '/scm')]",
            "location": "[variables('Location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', variables('WebSiteName'))]"
            ],
            "properties": {
                "allow": true
            }
        },
        {
            "type": "Microsoft.Web/sites/config",
            "apiVersion": "2020-12-01",
            "name": "[concat(variables('WebSiteName'), '/web')]",
            "location": "[variables('Location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', variables('WebSiteName'))]"
            ],
            "properties": {
                "numberOfWorkers": 1,
                "defaultDocuments": [
                    "Default.htm",
                    "Default.html",
                    "Default.asp",
                    "index.htm",
                    "index.html",
                    "iisstart.htm",
                    "default.aspx",
                    "index.php",
                    "hostingstart.html"
                ],
                "netFrameworkVersion": "v4.0",
                "linuxFxVersion": "DOCKER|mcr.microsoft.com/appsvc/staticsite:latest",
                "requestTracingEnabled": false,
                "remoteDebuggingEnabled": false,
                "remoteDebuggingVersion": "VS2019",
                "httpLoggingEnabled": false,
                "acrUseManagedIdentityCreds": false,
                "logsDirectorySizeLimit": 35,
                "detailedErrorLoggingEnabled": false,
                "publishingUsername": "[concat('$', variables('WebSiteName'))]",
                "azureStorageAccounts": {
                    "magento-media": {
                        "type": "AzureFiles",
                        "accountName": "[parameters('StorageAccountName')]",
                        "shareName": "[parameters('MagentoMediaFileShareName')]",
                        "mountPath": "[parameters('MagentoMediaMountPath')]",
                        "accessKey": "[parameters('StorageAccountAccessKey')]"
                    }
                },
                "scmType": "None",
                "use32BitWorkerProcess": true,
                "webSocketsEnabled": true,
                "alwaysOn": true,
                "managedPipelineMode": "Integrated",
                "virtualApplications": [
                    {
                        "virtualPath": "/",
                        "physicalPath": "site\\wwwroot",
                        "preloadEnabled": false
                    }
                ],
                "loadBalancing": "LeastRequests",
                "experiments": {
                    "rampUpRules": []
                },
                "autoHealEnabled": false,
                "vnetRouteAllEnabled": false,
                "vnetPrivatePortsCount": 0,
                "localMySqlEnabled": false,
                "ipSecurityRestrictions": [
                    {
                        "ipAddress": "Any",
                        "action": "Allow",
                        "priority": 1,
                        "name": "Allow all",
                        "description": "Allow all access"
                    }
                ],
                "scmIpSecurityRestrictions": [
                    {
                        "ipAddress": "Any",
                        "action": "Allow",
                        "priority": 1,
                        "name": "Allow all",
                        "description": "Allow all access"
                    }
                ],
                "scmIpSecurityRestrictionsUseMain": false,
                "http20Enabled": true,
                "minTlsVersion": "1.2",
                "scmMinTlsVersion": "1.0",
                "ftpsState": "AllAllowed",
                "preWarmedInstanceCount": 0,
                "functionAppScaleLimit": 0,
                //"healthCheckPath": "/health_check.php",
                "functionsRuntimeScaleMonitoringEnabled": false,
                "minimumElasticInstanceCount": 1
            }
        },
        {
            "type": "Microsoft.Web/sites/hostNameBindings",
            "apiVersion": "2020-12-01",
            "name": "[concat(variables('WebSiteName'), '/', variables('WebSiteName'), '.azurewebsites.net')]",
            "location": "[variables('Location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites', variables('WebSiteName'))]"
            ],
            "properties": {
                "siteName": "[variables('WebSiteName')]",
                "hostNameType": "Verified"
            }
        }
    ]
}

If we execute this exact template, we will receive the following error

{
    "status": "Failed",
    "error": {
        "code": "BadRequest",
        "message": "Required parameter AccessKey is missing.",
        "details": [
            {
                "message": "Required parameter AccessKey is missing."
            },
            {
                "code": "BadRequest"
            },
            {}
        ]
    }
}

We receive the same error if we have storage manually mounted through portal and try to change the configurations or add deployment slots.

As soon as I remove the mounted storage configuration, everything works just fine.

We need to have this in our DevOps pipeline, it's not acceptable that we unmount-remount the storage manually before and after the deployments. That would cause major issues and downtime. What can we do to overcome the issue?

UPDATE 1

I've tried to create a totally separate web app and storage account all manually. As soon as I hooked the storage mount and tried to enable health checks for instance, I got the same error. I think there's a bug in Azure.

enter image description here

UPDATE 2

Here is the template that deploys the dependent storage account

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "Environment": {
            "type": "String",
            "allowedValues": [
                "dev",
                "stg",
                "prd"
            ]
        },
        "Region": {
            "type": "string",
            "allowedValues": [
                "eu",
                "we"
            ]
        }
    },
    "variables": {
        "Location": "[resourceGroup().location]",
        "StorageAccountName": "[format('ariva{0}{1}magentostorage', parameters('Environment'), parameters('Region'))]",
        "MagentoMediaFileShareName": "magento-media"
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2021-04-01",
            "name": "[variables('StorageAccountName')]",
            "location": "[variables('Location')]",
            "sku": {
                "name": "Standard_LRS",
                "tier": "Standard"
            },
            "kind": "StorageV2",
            "properties": {
                "allowCrossTenantReplication": true,
                "minimumTlsVersion": "TLS1_2",
                "allowBlobPublicAccess": true,
                "allowSharedKeyAccess": true,
                "networkAcls": {
                    "resourceAccessRules": [],
                    "bypass": "AzureServices",
                    "virtualNetworkRules": [],
                    "ipRules": [],
                    "defaultAction": "Allow"
                },
                "supportsHttpsTrafficOnly": true,
                "encryption": {
                    "services": {
                        "file": {
                            "keyType": "Account",
                            "enabled": true
                        },
                        "blob": {
                            "keyType": "Account",
                            "enabled": true
                        }
                    },
                    "keySource": "Microsoft.Storage"
                },
                "accessTier": "Hot"
            }
        },
        {
            "type": "Microsoft.Storage/storageAccounts/blobServices",
            "apiVersion": "2021-04-01",
            "name": "[concat(variables('StorageAccountName'), '/default')]",
            "dependsOn": [
                "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]"
            ],
            "sku": {
                "name": "Standard_LRS",
                "tier": "Standard"
            },
            "properties": {
                "changeFeed": {
                    "enabled": false
                },
                "restorePolicy": {
                    "enabled": false
                },
                "containerDeleteRetentionPolicy": {
                    "enabled": true,
                    "days": 7
                },
                "cors": {
                    "corsRules": []
                },
                "deleteRetentionPolicy": {
                    "enabled": true,
                    "days": 7
                },
                "isVersioningEnabled": false
            }
        },
        {
            "type": "Microsoft.Storage/storageAccounts/fileServices",
            "apiVersion": "2021-04-01",
            "name": "[concat(variables('StorageAccountName'), '/default')]",
            "dependsOn": [
                "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]"
            ],
            "sku": {
                "name": "Standard_LRS",
                "tier": "Standard"
            },
            "properties": {
                "shareDeleteRetentionPolicy": {
                    "enabled": true,
                    "days": 7
                }
            }
        },
        {
            "type": "Microsoft.Storage/storageAccounts/queueServices",
            "apiVersion": "2021-04-01",
            "name": "[concat(variables('StorageAccountName'), '/default')]",
            "dependsOn": [
                "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]"
            ],
            "properties": {
                "cors": {
                    "corsRules": []
                }
            }
        },
        {
            "type": "Microsoft.Storage/storageAccounts/tableServices",
            "apiVersion": "2021-04-01",
            "name": "[concat(variables('StorageAccountName'), '/default')]",
            "dependsOn": [
                "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]"
            ],
            "properties": {
                "cors": {
                    "corsRules": []
                }
            }
        },
        {
            "type": "Microsoft.Storage/storageAccounts/fileServices/shares",
            "apiVersion": "2021-04-01",
            "name": "[concat(variables('StorageAccountName'), '/default/', variables('MagentoMediaFileShareName'))]",
            "dependsOn": [
                "[resourceId('Microsoft.Storage/storageAccounts/fileServices', variables('StorageAccountName'), 'default')]",
                "[resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName'))]"
            ],
            "properties": {
                "accessTier": "TransactionOptimized",
                "shareQuota": 5120,
                "enabledProtocols": "SMB"
            }
        }
    ],
    "outputs": {
        "MagentoMediaFileShareName": {
            "type": "string",
            "value": "[variables('MagentoMediaFileShareName')]"
        },
        "StorageAccountName": {
            "type": "string",
            "value": "[variables('StorageAccountName')]"
        },
        "StorageAccountAccessKey": {
            "type": "securestring",
            "value": "[listKeys(variables('StorageAccountName'), '2019-04-01').keys[0].value]"
        }
    }
}

UPDATE 3 - temporary workaround

We were able to find a temporary workaround. Instead of deploying the file mount through the ARM template, we've removed that bit from ARM template, after, we use the following build step in our DevOps pipeline to mount the storage through azure CLI

 - task: AzureCLI@2
   displayName: 'Attach media volume'
   inputs:
     azureSubscription: '${{ parameters.azureSubscription }}'
     scriptType: pscore
     scriptLocation: inlineScript
     inlineScript: |
       az webapp config storage-account add `
         --resource-group ${{ parameters.resourceGroup }} `
         --name "$(WebSiteName)" `
         --access-key "$(StorageAccountAccessKey)" `
         --custom-id magento-media `
         --storage-type AzureFiles `
         --share-name "$(MagentoMediaFileShareName)" `
         --account-name "$(StorageAccountName)" `
         --mount-path "/var/www/html/pub/external_media"

This doesn't solve the problem of course.

  1. Doing so, ARM template will essentially remove the mount and then it will be deployed by the script, causing downtime
  2. When mount is in place, we still can't change any of the WebApp properties through the portal and we will get the error that AccessKey is missing

Solution

  • azureStorageAccounts is not part of Microsoft.Web sites/config 2020-12-01 however it is under Microsoft.Web sites 2020-12-01 SiteConfig property. This may be the reason accessKey wasn't found, wasn't uploaded. I would move azureStorageAccounts under siteConfig