Azure Functions 的 ARM 模板,具有针对不同环境和插槽的许多 appSettings

ARM Templates for Azure Functions with many appSettings for different environments and slots

我有两个使用部署槽、阶段和生产的 Azure Function 应用程序。这两个 Azure Function 应用程序在应用程序设置中有大约 50~ key:value 对,用于定义各种 API 键、应用程序行为、连接字符串等

我想将这两个 Azure Function 应用程序部署到五个不同的环境(CI、DEV、QA、STG、PROD)。我相信使用 ARM 模板将这些资源部署到 Azure 是比 Azure CLI 更好的选择。我将在我的 Azure DevOps 发布管道中创建任务来实现这一点。

为了将ARM模板分解成易于维护的东西,我想为每个环境创建一个ARM模板参数文件。为 Azure 函数定义部署文件时,要定义的属性之一是 siteConfig object,您可以在其中使用 NameValuePair 对象定义 appSettings 对象。对于每个环境,阶段和生产槽将具有不同的 API 键、连接字符串和应用程序行为。我的部署文件使用生产槽和阶段槽创建 Azure Function 应用程序。在部署文件中,我必须提供两次 appSettings NameValuePair 对象。然后,我必须为每个环境创建 5 个不同的参数文件。将其乘以 2,因为我有两个插槽。

参数文件中定义的所有参数是否也必须在参数对象中的部署模板文件中定义?

我能否只从参数文件中传入一个带有 NameValuePairs 的对象数组,这样我就不必在部署文件的顶部和 siteConfig.appSettings 下定义整个参数列表功能应用程序?

The documentation here 表明您只能提供字符串数组或具有许多 key:value 的单个对象。但是 appSettings 是一个对象数组,其中每个对象有 3 key:value 对。

这是资源在部署文件中的样子。我想简单地引用参数文件中的整个对象数组,但看起来文档指出我在部署文件的顶部定义了所有 50~ 参数,然后在由 Azure CLI 或Azure DevOps 任务。

        {
            "type": "Microsoft.Web/sites",
            "apiVersion": "2018-11-01",
            "name": "[parameters('function-app-name')]",
            "location": "[parameters('location')]",
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "kind": "functionapp",
            "properties": {
                "enabled": true,
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
                "siteConfig": {
                    "appSettings": [] # I need to provide an array of objects here
                 }
            }
       }

除了我的抱怨之外...我不敢相信我将不得不为所有五个环境及其两个具有两个槽的 Azure Functions 创建 20 个参数文件。有没有更好的方法来使用 ARM 模板和参数文件及其独特的应用程序设置部署到我的所有环境及其部署槽?

更新:

我能够拼凑各种方法来创建特定于环境的 ARM 模板,并得出以下结果,但存在一些不便的问题。首先,我将解释我现在所处的位置,然后提出与设计相关的问题。

在我的部署模板中,我定义了两个参数。他们在这里:

        "deploymentEnvironment": {
            "type": "string",
            "allowedValues": [
                "CI",
                "DEV",
                "QA",
                "TRN",
                "STG",
                "PROD"
            ],
            "metadata": {
                "description": "Type of environment being deployed to. AKA \"Stage\" in release definition."
            }
        },
        "applicationSettings": {
            "type": "object",
            "metadata": {
                "description": "Application settings from function.parameters.json"
            }
        }

我的function.parameters.json有这样的结构:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "applicationSettings": {
            "value": {
                "CI": {
                    "appsetting1": "",
                    "appsetting2": ""
                },
                "DEV": {
                    "appsetting1": "",
                    "appsetting2": ""            },
                "QA": {
                    "appsetting1": "",
                    "appsetting2": ""
                }
            }
        }
    }
}

对于每个环境,我都放置了所有连接字符串、apikeys 和应用程序设置。

对于函数应用程序的生产槽,您可以添加一个 "resources" 属性 以对其应用配置。这是整个功能应用程序部署:

        {
            "name": "[parameters('function-app-name')]",
            "type": "Microsoft.Web/sites",
            "apiVersion": "2018-11-01",
            "kind": "functionapp",
            "location": "[parameters('location')]",
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "properties": {
                "enabled": true,
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
            },
            "dependsOn": [
                "[resourceId('Microsoft.Insights/components/', variables('applicationInsightsName'))]",
                "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
                "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
            ],
            "resources": [
                {
                    "name": "appsettings",
                    "type": "config",
                    "apiVersion": "2018-11-01",
                    "properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
                    "dependsOn": [
                        "[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
                    ]
                }
            ]
        }

接下来是定义阶段槽部署资源。这是:

        {
            "type": "Microsoft.Web/sites/slots",
            "apiVersion": "2018-11-01",
            "name": "[concat(parameters('function-app-name'), '/stage')]",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
            ],
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "kind": "functionapp",
            "properties": {
                "enabled": true,
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
            },
            "resources": [
                {
                    "name": "appsettings",
                    "type": "config",
                    "apiVersion": "2018-11-01",
                    "properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
                    "dependsOn": [
                        "[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]",
                        "[resourceId('Microsoft.Web/sites/slots/', parameters('function-app-name'), 'stage')]"
                    ]
                }
            ]
        }

有了这个解决方案,我就不必为每个环境准备一堆 parameters.json 文件了。

问题...

在 parameters.json 中定义所有应用程序设置意味着我无法使用模板函数来获取连接字符串或 Azure Key Vault 值。

这是我开始将一些应用程序设置移动到部署模板以使用模板功能的时候。因此,我没有在 parameters.json 文件中设置 APPINSIGHTS_INSTRUMENTATIONKEY 和其他 AzureWebJobs* 应用程序设置,而是在 Microsoft 的 "properties" 对象中提供了 siteConfig object。 Web/Sites 资源 和 Microsoft.Web/Sites/Slots 资源.

这真是太糟糕了 - 当部署 运行 时,它使用函数应用程序应用了 siteConfig.appsettings 值,然后当它应用 parameters.json 文件时,它 删除了应用程序设置并仅应用了 json 中的设置,而不是将它们合并在一起 。这是一个巨大的失望。在我使用 AzureCLI 进行的初始测试中,我使用此命令 az functionapp config appsettings set --name $functionAppName --resource-group $resourceGroupName --settings $settingsFile --slot $slot 来测试不在 json 文件中的应用程序设置会发生什么,并且很高兴它从未删除应用程序设置。 powershell 命令获取和设置值,很好地合并它并且从不删除。但是 ARM API 删除了所有这些名称值对并仅应用定义的内容。 这意味着我无法使用模板函数来创建动态应用程序设置和 json 文件来应用静态应用程序设置。

到目前为止,我觉得进行像样的 ARM 模板部署的唯一方法是部署没有 siteConfig 对象的资源或配置资源以应用应用程序设置,然后使用 Azure CLI 部署应用程序设置。我想我可以学习如何使用 Azure CLI 或 Azure DevOps 管道任务检索 Key Vault 机密,但最好将所有内容都放在一个 ARM 模板中。

作为参考,这是我尝试使用动态生成的 appSettings 和配置资源来定义更多 appsettings 时的整个部署模板。

{
    "$schema": "https://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "function-app-name": {
            "defaultValue": "functionappname",
            "type": "String",
            "metadata": {
                "description": "The name of the function app that you wish to create."
            }
        },
        "sku": {
            "type": "string",
            "allowedValues": [
                "S1",
                "S2",
                "S3"
            ],
            "defaultValue": "S3",
            "metadata": {
                "description": "The pricing tier for the hosting plan."
            }
        },
        "storageAccountType": {
            "type": "string",
            "defaultValue": "Standard_LRS",
            "metadata": {
                "description": "Storage Account type"
            }
        },
        "location": {
            "type": "string",
            "defaultValue": "southcentralus",
            "metadata": {
                "description": "Location for all resources."
            }
        },
        "deploymentEnvironment": {
            "type": "string",
            "allowedValues": [
                "CI",
                "DEV",
                "QA",
                "TRN",
                "STG",
                "PROD"
            ],
            "metadata": {
                "description": "Type of environment being deployed to."
            }
        },
        "applicationSettings": {
            "type": "object",
            "metadata": {
                "description": "Application settings from function.parameters.json"
            }
        }
    },
    "variables": {
        "storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]",
        "appServicePlanName": "[concat('ASP-', uniquestring(resourceGroup().id))]",
        "applicationInsightsName": "[concat('appInsights-', uniquestring(resourceGroup().id))]",
        "projectName": "DV"
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2019-04-01",
            "name": "[variables('storageAccountName')]",
            "kind": "Storage",
            "location": "[parameters('location')]",
            "sku": {
                "name": "[parameters('storageAccountType')]"
            },
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            }
        },
        {
            "name": "[variables('appServicePlanName')]",
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2019-08-01",
            "location": "[parameters('location')]",
            "properties": {
            },
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "sku": {
                "Name": "[parameters('sku')]",
                "capacity": 2
            },
            "dependsOn": [
                "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
            ]
        },
        {
            "name": "[variables('applicationInsightsName')]",
            "apiVersion": "2015-05-01",
            "type": "Microsoft.Insights/components",
            "kind": "web",
            "location": "[parameters('location')]",
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "properties": {
                "Application_Type": "web"
            }
        },
        {
            "name": "[parameters('function-app-name')]",
            "type": "Microsoft.Web/sites",
            "apiVersion": "2018-11-01",
            "kind": "functionapp",
            "location": "[parameters('location')]",
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "properties": {
                "enabled": true,
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
                "siteConfig": {
                    "appSettings": [
                        {
                            "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
                            "value": "[reference(concat('microsoft.insights/components/', variables('applicationInsightsName'))).InstrumentationKey]"
                        },
                        {
                            "name": "AzureWebJobsDashboard",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
                        },
                        {
                            "name": "AzureWebJobsStorage",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
                        },
                        {
                            "name": "FUNCTIONS_EXTENSION_VERSION",
                            "value": "~1"
                        }
                    ]
                }
            },
            "dependsOn": [
                "[resourceId('Microsoft.Insights/components/', variables('applicationInsightsName'))]",
                "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
                "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
            ],
            "resources": [
                {
                    "name": "appsettings",
                    "type": "config",
                    "apiVersion": "2018-11-01",
                    "properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
                    "dependsOn": [
                        "[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
                    ]
                }
            ]
        },
        {
            "type": "Microsoft.Web/sites/slots",
            "apiVersion": "2018-11-01",
            "name": "[concat(parameters('function-app-name'), '/stage')]",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
            ],
            "tags": {
                "project": "[variables('projectName')]",
                "env": "[parameters('deploymentEnvironment')]"
            },
            "kind": "functionapp",
            "properties": {
                "enabled": true,
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
                "siteConfig": {
                    "appSettings": [
                        {
                            "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
                            "value": "[reference(concat('microsoft.insights/components/', variables('applicationInsightsName'))).InstrumentationKey]"
                        },
                        {
                            "name": "AzureWebJobsDashboard",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
                        },
                        {
                            "name": "AzureWebJobsStorage",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
                        },
                        {
                            "name": "FUNCTIONS_EXTENSION_VERSION",
                            "value": "~1"
                        }
                    ]
                },
                "resources": [
                    {
                        "name": "appsettings",
                        "type": "config",
                        "apiVersion": "2018-11-01",
                        "properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
                        "dependsOn": [
                            "[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
                        ]
                    }
                ]
            }
        }
    ]
}

更新二:

I raised a github issue to have them fix the problem with ARM templates replacing all of the application settings on each deployment. FWIW - I also voted on some Azure feedback post

抱歉,我没有太多时间来回答,你有一堆主要与 "what's the best way to..." 相关的问题,答案总是 "it depends".

我发现一件事更容易管理,而不是使用 siteConfig 来设置所有应用程序设置,您可以创建类型为 Microsoft.Web/sites/config 的顶级资源(我发现有时这对您很有用可以在站点创建后创建它们,因此如果您在其他地方有尚未设置的依赖项,将配置和站点分开会很方便。

"parameters": {
  "appSettings": {
    "type": "object",
    "defaultValue": {
      "property1": "value1",
      "property2": "value2"
    }
  }
}

"resources": [
  {
    "type": "Microsoft.Web/sites",
    "apiVersion": "2018-11-01",
    "name": "[parameters('function-app-name')]",
    "location": "[parameters('location')]",
    "kind": "functionapp",
    "properties": {
      "enabled": true,
      "serverFarmId": "..."
    }
  },
  {
    "type": "Microsoft.Web/sites/config",
    "name": "[concat(parameters('function-app-name'), '/appsettings')]",
    "apiVersion": "2018-11-01",
    "properties": "[parameters('appSettings')]"
    "dependsOn": [ "[resourceId('Microsoft.Web/sites/sites', parameters('function-app-name'))]",
  }
]

上面的一个缺点是你不能在参数部分使用某些函数,所以你不能使用 listKeys() 来获取资源的键,所以它只是有时有用,或者像这个例子,如果你想添加对同样在同一模板中创建的应用洞察的引用,如果你将设置作为参数传递,这是不可能的。

  {
    "type": "Microsoft.Web/sites/config",
    "name": "[concat(parameters('function-app-name'), '/appsettings')]",
    "apiVersion": "2018-11-01",
    "properties": {
      "property1": "value1",
      "property2": "value2",
      "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey]"
    }
    "dependsOn": [ 
      "[resourceId('Microsoft.Web/sites/sites', parameters('function-app-name'))]",
      "[resourceId('microsoft.insights/components', variables('appInsightsName'))]"
  }

您真的应该在部署时解决所有可能的问题,因此可以将存储帐户(例如)连接字符串安全地添加到模板中,并且仅在部署时解决。

另一个方便的提示是使用密钥保管库存储任何安全凭据、api 密钥、连接字符串等 无法 在模板中解析的内容。你提到需要它们,但随后你将它们提交给模板中的源代码控制......好吧,它们不会保密很长时间(另一个提示,确保它们都使用 securestring 而不是字符串类型,否则门户网站会将它们暴露在资源组的部署日志)。您可以像这样从应用设置访问密钥保管库:

"secretConnectionString": "[concat('@Microsoft.KeyVault(SecretUri=https://', variables('vaultName'), '.vault.azure.net/secrets/my-connection-string/)')]",

但要使上述工作正常,您需要授予您的应用程序对保管库的读取权限 "vaultName",如果您使用托管服务身份,这应该没问题。

回答这篇文章:

Is it also true that all parameters defined in the parameter file have to be defined in the deployment template file in the parameters object?

是的,参数文件中的所有内容都需要在部署文件中定义。反之则不然。部署文件中定义的所有内容都不需要在参数文件中定义。部署文件中的定义可以有默认值:

"location": {
  "type": "string",
  "defaultValue": "Central US",
  "metadata": {
    "description": "Specifies the Azure location where the key vault should be created."
  }
},

或者,可以在发布任务中将参数作为覆盖参数传入。

可以将静态配置与部署时引用相结合。 您使用 union template function to combine your static configuration (object or array) with some deployment-time value that you wrap using the json template function.

在下面的例子中我结合了:

  • 静态基础配置对象
  • 静态服务特定配置对象
  • 部署时 Application Insights 配置值
[union(
  variables('appServiceBaseConfig'), 
  variables('appService1'), 
  json(
    concat(
      '{\"APPINSIGHTS_INSTRUMENTATIONKEY\":\"', 
      reference(concat('microsoft.insights/components/', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey,
       '\"}')
    )
  )
]

只需向此线程添加输入以添加我对此问题的解决方案。我不确定覆盖应用程序设置的问题是否已在普通 ARM 模板中得到修复,但发布管道步骤允许您设置在部署应用程序时部署的其他应用程序设置。因此,我有动态应用程序 settings/the 应用程序设置,我的所有 azure 函数都需要在 ARM 模板中的创建集上定义,然后我在发布管道中设置了额外的应用程序设置(使用带有秘密变量的变量组来隐藏它们的值)。像这样:

对于来到此主题的任何人,我想提供一种替代上述建议的方法,即使用 union。我首先选择了那个选项,但开始发现很难使用形成 json 的字符串连接,所以想提供这个替代方案:

[union(
  variables('appServiceBaseConfig'), 
  variables('appService1'), 
  createObject(
    'APPINSIGHTS_INSTRUMENTATIONKEY', reference(concat('microsoft.insights/components/', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey,
    'MY_VALUE', 'xyz'
  )
 )
]

这个额外的建议答案的原因是,如果您有多个键值对,createObject 函数可以更容易地构造对象。

注意: 如果您按照 this note in the docs 使用 Powershell cli,ARM 目前仅支持这样的多行函数。在 Azure DevOps 中部署时我花了一些时间才弄明白这个问题:facepalm: