Dive into ARM template from a Function App

  • Thread starter Thread starter theringe
  • Start date Start date
T

theringe

We use ARM templates to create Function Apps (or other Azure resources) to systematically manage related resources and highly customize some properties of these resources. However, without understanding the dependencies between Azure resources (e.g., Function App depends on Storage Account), it is difficult to customize the templates. This article will briefly describe the resource dependencies in ARM templates using Function App as an example.



TOC

Resource Structure of Function App

ARM's dependsOn Property

ARM's Run-time Functions

ARM's Nested Deployment

Complete Example




Resource Structure of Function App

In short, a Function App is an abstract concept composed of Application, App Service Plan, and Storage. Their roles are as follows:


  • Application: The Function App itself, responsible for executing various user-specified triggers and getting/putting related data from/to other services.
  • App Service Plan: The collection of hardware resources that actually host the Function App. The serverless feature of Function App is actually transparent to the end user, but there are still instances responsible for loading and executing these applications.
  • Storage (i.e., Storage Account): The Function App uses blob/file to save the application's related settings, actual code, and other data.

From the above description, we can see that creating this series of resources requires a specific order. Storage and App Service Plan must be created before the Function App.



ARM's dependsOn Property

Please see the following ARM template snippet:


theringe_0-1725263554070.png



When we closely examine the creation process of a Storage Account, we find that it is also an abstract concept containing multiple services (e.g., blob, file, table, queue). The actual file is a more refined resource that needs to be created. Correctly, the file service must be created before the file share, leading to the creation order: Storage Account before File Service before File Shares.



In other words, the creation of File Shares depends on the existence of File Service, and the creation of File Service depends on the existence of Storage Account.



The so-called "depends on" is the dependsOn property in the ARM template. In the above example, we can see the dependency behavior of these three resources. Through the description of dependsOn, we can ensure the order of resource creation during the process, avoiding issues of missing resources.



ARM's Run-time Functions

Can we use the same method to specify that App Service Plan and Function App are created after Storage? The answer is no. The reason is that the authorization and communication between Function App and Storage use Connection String by default, which is randomly generated by the system during the creation of Storage. This makes it impossible for the ARM template to pre-specify the Connection String of Storage (and it is also not secure).



Instead, ARM uses functions like listKeys to dynamically obtain the related Connection String from the already created Storage resource and use it as a parameter for the Function App to communicate with Storage in the future. Refer to the following ARM template snippet:


theringe_1-1725263708508.png



The problem is that the listKeys function is called a run-time function, and its execution order is the highest priority in the entire deployment. In the same deployment, run-time functions do not consider the order of dependsOn and execute immediately, returning results. For details, refer to this article: Resource Not Found (dependsOn is not working) – ARM Template Sandbox (bmoore-msft.blog)



Is there a way to solve this problem? The answer is yes. Note that the run-time functions mentioned earlier are executed first only in the same deployment. If we modify the deployment to a nested format, we can avoid this issue.



ARM's Nested Deployment

Please see the following ARM template snippet:


theringe_0-1725265016694.png

In the original resources, besides creating the following three resources, we can also create another deployment, i.e., "Microsoft.Resources/deployments"

  • "Microsoft.Storage/storageAccounts"
  • "Microsoft.Storage/storageAccounts/fileservices"
  • "Microsoft.Storage/storageAccounts/fileServices/shares"



By adding the dependsOn property, we can ensure that all execution content of ASPResourcesDeployment, including run-time functions, will be executed only after the creation of Storage.



Complete Example




Code:
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "resourcePrefix": {
      "type": "string",
      "defaultValue": "[concat('ringe', '-', uniqueString(newGuid()))]",
      "metadata": {
        "description": "Do not modify this name"
      }
    }
  },
  "variables": {
    "storageAccountName": "[concat(replace(parameters('resourcePrefix'),'-', ''), 'sa')]",
    "storageAccountSkuName": "Standard_LRS",
    "storageAccountSupportsHttpsTrafficOnly": true,
    "storageAccountMinimumTlsVersion": "TLS1_2",
    "storageAccountDefaultToOAuthAuthentication": true,
    "storageAccountIsShareSoftDeleteEnabled": true,
    "storageAccountShareSoftDeleteRetentionDays": 7,
    "storageAccountFileShareName": "ringe-file",
    "storageAccountFileShareShareQuota": 5120,
    "storageAccountFileShareEnabledProtocols": "SMB",
    "planName": "[concat(parameters('resourcePrefix'), '-', 'plan')]",
    "planKind": "linux",
    "planSkuTier": "Dynamic",
    "planSkuName": "Y1",
    "planWorkerSize": "0",
    "planWorkerSizeId": "0",
    "planNumberOfWorkers": "1",
    "planReserved": true,
    "functionName": "[concat(parameters('resourcePrefix'), '-', 'func')]",
    "functionKind": "functionapp,linux",
    "functionSiteConfigAppSettingsFUNCTIONS_EXTENSION_VERSION": "~4",
    "functionSiteConfigAppSettingsFUNCTIONS_WORKER_RUNTIME": "node",
    "functionSiteConfigAppSettingsWEBSITE_CONTENTSHARE": "ringe-func",
    "functionSiteConfigUse32BitWorkerProcess": false,
    "functionSiteConfigFtpsState": "FtpsOnly",
    "functionSiteConfigLinuxFxVersion": "Node|18",
    "functionClientAffinityEnabled": false,
    "functionPublicNetworkAccess": "Enabled",
    "functionHttpsOnly": true,
    "functionServerFarmId": "[concat('subscriptions/', subscription().subscriptionId, '/resourcegroups/', resourceGroup().name, '/providers/Microsoft.Web/serverfarms/', variables('planName'))]",
    "scriptName": "[concat(replace(parameters('resourcePrefix'),'-', ''), 'sh')]"
  },
  "resources": [
    {
      "comments": "Storage Account",
      "apiVersion": "2022-05-01",
      "type": "Microsoft.Storage/storageAccounts",
      "location": "[resourceGroup().location]",
      "name": "[variables('storageAccountName')]",
      "tags": {},
      "sku": {
          "name": "[variables('storageAccountSkuName')]"
      },
      "properties": {
          "supportsHttpsTrafficOnly": "[variables('storageAccountSupportsHttpsTrafficOnly')]",
          "minimumTlsVersion": "[variables('storageAccountMinimumTlsVersion')]",
          "defaultToOAuthAuthentication": "[variables('storageAccountDefaultToOAuthAuthentication')]"
      }
    },
    {
      "comments": "Storage Account: fileservices",
      "apiVersion": "2022-05-01",
      "type": "Microsoft.Storage/storageAccounts/fileservices",
      "name": "[concat(variables('storageAccountName'), '/default')]",
      "properties": {
        "shareDeleteRetentionPolicy": {
          "enabled": "[variables('storageAccountIsShareSoftDeleteEnabled')]",
          "days": "[variables('storageAccountShareSoftDeleteRetentionDays')]"
        }
      },
      "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
      ]
    },
    {
      "comments": "Storage Account: fileshares",
      "apiVersion": "2021-04-01",
      "type": "Microsoft.Storage/storageAccounts/fileServices/shares",
      "location": "[resourceGroup().location]",
      "name": "[concat(variables('storageAccountName'), '/default/', variables('storageAccountFileShareName'))]",
      "dependsOn": [
        "[resourceId('Microsoft.Storage/storageAccounts/fileServices', variables('storageAccountName'), 'default')]",
        "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
      ],
      "properties": {
          "shareQuota": "[variables('storageAccountFileShareShareQuota')]",
          "enabledProtocols": "[variables('storageAccountFileShareEnabledProtocols')]"
      }
    },
    {
      "comments": "ASP nested resources due to Storage Account",
      "apiVersion": "2017-05-10",
      "type": "Microsoft.Resources/deployments",
      "name": "ASPResourcesDeployment",
      "dependsOn": [
        "[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
      ],
      "properties": {
        "mode": "Incremental",
        "template": {
          "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
          "contentVersion": "1.0.0.0",
          "parameters": {},
          "variables": {},
          "resources": [
            {
              "comments": "ASP",
              "apiVersion": "2018-11-01",
              "type": "Microsoft.Web/serverfarms",
              "location": "[resourceGroup().location]",
              "name": "[variables('planName')]",
              "kind": "[variables('planKind')]",
              "sku": {
                "Tier": "[variables('planSkuTier')]",
                "Name": "[variables('planSkuName')]"
              },
              "tags": {},
              "dependsOn": [],
              "properties": {
                "name": "[variables('planName')]",
                "workerSize": "[variables('planWorkerSize')]",
                "workerSizeId": "[variables('planWorkerSizeId')]",
                "numberOfWorkers": "[variables('planNumberOfWorkers')]",
                "reserved": "[variables('planReserved')]"
              }
            },
            {
              "comments": "Function App",
              "apiVersion": "2018-11-01",
              "type": "Microsoft.Web/sites",
              "location": "[resourceGroup().location]",
              "name": "[variables('functionName')]",
              "kind": "[variables('functionKind')]",
              "tags": {},
              "dependsOn": [
                "[concat('Microsoft.Web/serverfarms/', variables('planName'))]"
              ],
              "properties": {
                "name": "[variables('functionName')]",
                "siteConfig": {
                  "appSettings": [
                    {
                      "name": "FUNCTIONS_EXTENSION_VERSION",
                      "value": "[variables('functionSiteConfigAppSettingsFUNCTIONS_EXTENSION_VERSION')]"
                    },
                    {
                      "name": "FUNCTIONS_WORKER_RUNTIME",
                      "value": "[variables('functionSiteConfigAppSettingsFUNCTIONS_WORKER_RUNTIME')]"
                    },
                    {
                      "name": "AzureWebJobsStorage",
                      "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts',variables('storageAccountName')),'2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                    },
                    {
                      "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                      "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts',variables('storageAccountName')),'2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                    },
                    {
                      "name": "WEBSITE_CONTENTSHARE",
                      "value": "[variables('functionSiteConfigAppSettingsWEBSITE_CONTENTSHARE')]"
                    }
                  ],
                  "cors": {
                    "allowedOrigins": []
                  },
                  "use32BitWorkerProcess": "[variables('functionSiteConfigUse32BitWorkerProcess')]",
                  "ftpsState": "[variables('functionSiteConfigFtpsState')]",
                  "linuxFxVersion": "[variables('functionSiteConfigLinuxFxVersion')]"
                },
                "clientAffinityEnabled": "[variables('functionClientAffinityEnabled')]",
                "virtualNetworkSubnetId": null,
                "publicNetworkAccess": "[variables('functionPublicNetworkAccess')]",
                "httpsOnly": "[variables('functionHttpsOnly')]",
                "serverFarmId": "[variables('functionServerFarmId')]"
              }
            }
          ],
          "outputs": {}
        }
      }
    }
  ],
  "outputs": {}
}



We can use a simple method to publish this template (please note that you can replace "ringe" appears from template and CLI with your project name).



Code:
az group create --name ringe-test-rg --location westus
az deployment group create --resource-group ringe-test-rg --template-file ringe.json

Continue reading...
 
Back
Top