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

Ant*_*otz 9 azure azure-resource-manager azure-cli azure-deployment-slots azure-functions

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

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

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

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

我可以只从参数文件中传入一个带有 NameValuePairs 的对象数组,这样我就不必在顶部的部署文件中以及函数应用的 siteConfig.appSettings 下定义完整的参数列表吗?

此处的文档显示您只能提供字符串数组或具有多个键值的单个对象。但是 appSettings 是一个对象数组,其中每个对象都有 3 个键值对。

这就是资源在部署文件中的样子。我想简单地从参数文件中引用整个对象数组,但看起来文档说明我在部署文件的顶部定义了所有 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
                 }
            }
       }

Run Code Online (Sandbox Code Playgroud)

除了我的抱怨......我不敢相信我将不得不为所有五个环境及其两个具有两个插槽的 Azure 函数创建 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"
            }
        }
Run Code Online (Sandbox Code Playgroud)

我的 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": ""
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

对于功能应用的生产槽,您可以添加一个“资源”属性,将配置应用于它。这是整个函数应用部署:

        {
            "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'))]"
                    ]
                }
            ]
        }
Run Code Online (Sandbox Code Playgroud)

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

        {
            "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')]"
                    ]
                }
            ]
        }
Run Code Online (Sandbox Code Playgroud)

使用此解决方案,我不必为每个环境准备一堆 parameters.json 文件。

问题...

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

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

这是真正的麻烦 - 当部署运行时,它使用函数应用程序应用了 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'))]"
                        ]
                    }
                ]
            }
        }
    ]
}
Run Code Online (Sandbox Code Playgroud)

更新 2:

我提出了一个 github 问题,让他们用 ARM 模板替换每个部署上的所有应用程序设置来解决问题。 FWIW -我还对一些 Azure 反馈帖子进行了投票

Dan*_*itt 7

抱歉,我没有太多时间来回答,您有很多问题主要与“什么是……的最佳方法”有关,而答案始终是“视情况而定”。

我发现更容易管理的一件事是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'))]",
  }
]
Run Code Online (Sandbox Code Playgroud)

上面的一个缺点是,你不能在 params 部分使用某些函数,所以你不能使用 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'))]"
  }
Run Code Online (Sandbox Code Playgroud)

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

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

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

但是要使上述工作正常运行,您需要为应用程序提供对保管库“vaultName”的读取访问权限,如果您使用托管服务标识,这应该没问题。


sor*_*usy 6

可以将静态配置与部署时参考结合起来。您可以使用union 模板函数将静态配置(对象或数组)与使用json 模板函数包装的某些部署时值组合起来。

在下面的例子中我结合:

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