快速入门:使用 Bicep 创建和发布 Azure 托管应用程序定义

此快速入门介绍如何使用 Bicep 以在服务目录中创建和发布 Azure 托管应用程序定义。 你的服务目录中的定义可供组织成员使用。

若要创建托管应用程序定义并将其发布到服务目录,请执行以下任务:

  • 使用 Bicep 开发模板,并将其转换为 Azure 资源管理器模板(ARM 模板)。 该模板定义托管应用程序部署的 Azure 资源。
  • 使用 Bicep build 命令将 Bicep 转换为 JSON。 将文件转换为 JSON 后,请验证代码的准确性。
  • 部署托管应用程序时,请定义门户的用户界面元素。
  • 创建包含必需 JSON 文件的 .zip 包。 该 .zip 包文件对于服务目录的托管应用程序定义具有 120 MB 的限制。
  • 发布托管应用程序定义,使其在服务目录中可用。

如果你的托管应用程序定义超过 120 MB,或者出于组织的合规性原因你想要使用自己的存储帐户,请转到快速入门:使用自己的存储来创建和发布 Azure 托管应用程序定义

还可以使用 Bicep 从服务目录部署托管应用程序定义。 有关详细信息,请转到快速入门:使用 Bicep 部署 Azure 托管应用程序定义

先决条件

若要完成本文中的任务,需要准备好以下项:

创建 Bicep 文件

每个托管应用程序定义均包含一个名为 mainTemplate.json 的文件。 该模板定义了要部署的 Azure 资源,与常规 ARM 模板没有什么不同。 可以使用 Bicep 开发模板,然后将 Bicep 文件转换为 JSON。

打开 Visual Studio Code,创建一个名称区分大小写的文件 mainTemplate.bicep 并保存它。

添加以下 Bicep 代码并保存文件。 它定义用于部署应用程序服务、应用服务计划和存储帐户的托管应用程序资源。

param location string = resourceGroup().location

@description('App Service plan name.')
@maxLength(40)
param appServicePlanName string

@description('App Service name prefix.')
@maxLength(47)
param appServiceNamePrefix string

@description('Storage account name prefix.')
@maxLength(11)
param storageAccountNamePrefix string

@description('Storage account type allowed values')
@allowed([
  'Premium_LRS'
  'Standard_LRS'
  'Standard_GRS'
])
param storageAccountType string

var appServicePlanSku = 'F1'
var appServicePlanCapacity = 1
var appServiceName = '${appServiceNamePrefix}${uniqueString(resourceGroup().id)}'
var storageAccountName = '${storageAccountNamePrefix}${uniqueString(resourceGroup().id)}'
var appServiceStorageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};Key=${storageAccount.listKeys().keys[0].value}'

resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: appServicePlanSku
    capacity: appServicePlanCapacity
  }
}

resource appServiceApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appServiceName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      appSettings: [
        {
          name: 'AppServiceStorageConnectionString'
          value: appServiceStorageConnectionString
        }
      ]
    }
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageAccountType
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
    allowSharedKeyAccess: false
    minimumTlsVersion: 'TLS1_2'
  }
}

output appServicePlan string = appServicePlan.name
output appServiceApp string = appServiceApp.properties.defaultHostName
output storageAccount string = storageAccount.properties.primaryEndpoints.blob

将 Bicep 转换为 JSON

使用 PowerShell 或 Azure CLI 生成 mainTemplate.json 文件。 转到用于保存 Bicep 文件的目录,然后运行 build 命令。

bicep build mainTemplate.bicep

若要了解详细信息,请转到 Bicep 生成

将 Bicep 文件转换为 JSON 后,mainTemplate.json 文件应与以下示例一致。 但 metadata 属性中的 versiontemplateHash 值可能不同。

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.27.1.19265",
      "templateHash": "1262990362980206722"
    }
  },
  "parameters": {
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "appServicePlanName": {
      "type": "string",
      "maxLength": 40,
      "metadata": {
        "description": "App Service plan name."
      }
    },
    "appServiceNamePrefix": {
      "type": "string",
      "maxLength": 47,
      "metadata": {
        "description": "App Service name prefix."
      }
    },
    "storageAccountNamePrefix": {
      "type": "string",
      "maxLength": 11,
      "metadata": {
        "description": "Storage account name prefix."
      }
    },
    "storageAccountType": {
      "type": "string",
      "allowedValues": [
        "Premium_LRS",
        "Standard_LRS",
        "Standard_GRS"
      ],
      "metadata": {
        "description": "Storage account type allowed values"
      }
    }
  },
  "variables": {
    "appServicePlanSku": "F1",
    "appServicePlanCapacity": 1,
    "appServiceName": "[format('{0}{1}', parameters('appServiceNamePrefix'), uniqueString(resourceGroup().id))]",
    "storageAccountName": "[format('{0}{1}', parameters('storageAccountNamePrefix'), uniqueString(resourceGroup().id))]"
  },
  "resources": [
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2023-12-01",
      "name": "[parameters('appServicePlanName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "[variables('appServicePlanSku')]",
        "capacity": "[variables('appServicePlanCapacity')]"
      }
    },
    {
      "type": "Microsoft.Web/sites",
      "apiVersion": "2023-12-01",
      "name": "[variables('appServiceName')]",
      "location": "[parameters('location')]",
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]",
        "httpsOnly": true,
        "siteConfig": {
          "appSettings": [
            {
              "name": "AppServiceStorageConnectionString",
              "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};EndpointSuffix={1};Key={2}', variables('storageAccountName'), environment().suffixes.storage, listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-04-01').keys[0].value)]"
            }
          ]
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]",
        "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
      ]
    },
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-04-01",
      "name": "[variables('storageAccountName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "[parameters('storageAccountType')]"
      },
      "kind": "StorageV2",
      "properties": {
        "accessTier": "Hot",
        "allowSharedKeyAccess": false,
        "minimumTlsVersion": "TLS1_2"
      }
    }
  ],
  "outputs": {
    "appServicePlan": {
      "type": "string",
      "value": "[parameters('appServicePlanName')]"
    },
    "appServiceApp": {
      "type": "string",
      "value": "[reference(resourceId('Microsoft.Web/sites', variables('appServiceName')), '2023-12-01').defaultHostName]"
    },
    "storageAccount": {
      "type": "string",
      "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2023-04-01').primaryEndpoints.blob]"
    }
  }
}

定义门户体验

作为发布者,你需要定义创建托管应用程序的门户体验。 createUiDefinition.json 文件生成门户的用户界面。 使用控件元素(如下拉框和文本框)来定义用户如何为每个参数提供输入。

在此例中,用户界面会提示你输入应用程序服务名称前缀、应用服务计划的名称、存储帐户前缀和存储帐户类型。 在部署期间,mainTemplate.json 中的变量使用 uniqueString 函数将包含 13 个字符的字符串追加到名称前缀,以便名称在 Azure 中是全局唯一的。

打开 Visual Studio Code,创建一个名称区分大小写的文件 createUiDefinition.json 并保存它。

将以下 JSON 代码添加到文件并保存它。

{
  "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#",
  "handler": "Microsoft.Azure.CreateUIDef",
  "version": "0.1.2-preview",
  "parameters": {
    "basics": [
      {}
    ],
    "steps": [
      {
        "name": "webAppSettings",
        "label": "Web App settings",
        "subLabel": {
          "preValidation": "Configure the web app settings",
          "postValidation": "Completed"
        },
        "elements": [
          {
            "name": "appServicePlanName",
            "type": "Microsoft.Common.TextBox",
            "label": "App Service plan name",
            "placeholder": "App Service plan name",
            "defaultValue": "",
            "toolTip": "Use alphanumeric characters or hyphens with a maximum of 40 characters.",
            "constraints": {
              "required": true,
              "regex": "^[a-z0-9A-Z-]{1,40}$",
              "validationMessage": "Only alphanumeric characters or hyphens are allowed, with a maximum of 40 characters."
            },
            "visible": true
          },
          {
            "name": "appServiceName",
            "type": "Microsoft.Common.TextBox",
            "label": "App Service name prefix",
            "placeholder": "App Service name prefix",
            "defaultValue": "",
            "toolTip": "Use alphanumeric characters or hyphens with minimum of 2 characters and maximum of 47 characters.",
            "constraints": {
              "required": true,
              "regex": "^[a-z0-9A-Z-]{2,47}$",
              "validationMessage": "Only alphanumeric characters or hyphens are allowed, with a minimum of 2 characters and maximum of 47 characters."
            },
            "visible": true
          }
        ]
      },
      {
        "name": "storageConfig",
        "label": "Storage settings",
        "subLabel": {
          "preValidation": "Configure the storage settings",
          "postValidation": "Completed"
        },
        "elements": [
          {
            "name": "storageAccounts",
            "type": "Microsoft.Storage.MultiStorageAccountCombo",
            "label": {
              "prefix": "Storage account name prefix",
              "type": "Storage account type"
            },
            "toolTip": {
              "prefix": "Enter maximum of 11 lowercase letters or numbers.",
              "type": "Available choices are Standard_LRS, Standard_GRS, and Premium_LRS."
            },
            "defaultValue": {
              "type": "Standard_LRS"
            },
            "constraints": {
              "allowedTypes": [
                "Premium_LRS",
                "Standard_LRS",
                "Standard_GRS"
              ]
            },
            "visible": true
          }
        ]
      }
    ],
    "outputs": {
      "location": "[location()]",
      "appServicePlanName": "[steps('webAppSettings').appServicePlanName]",
      "appServiceNamePrefix": "[steps('webAppSettings').appServiceName]",
      "storageAccountNamePrefix": "[steps('storageConfig').storageAccounts.prefix]",
      "storageAccountType": "[steps('storageConfig').storageAccounts.type]"
    }
  }
}

若要了解详细信息,请转到 CreateUiDefinition 入门

将文件打包

将这两个文件添加到名为 app.zip 的包文件中。 这两个文件必须都位于该 .zip 文件的根级别。 如果文件位于文件夹中,则当你创建托管应用程序定义时,你会收到一条错误消息,指出所需文件不存在。

app.zip 上传到 Azure 存储帐户,以便在部署托管应用程序的定义时使用它。 存储帐户名称在 Azure 中必须是全局唯一的,长度必须为 3-24 个字符,仅包含小写字母和数字。 在命令中,将占位符<pkgstorageaccountname> (包括尖括号 (<>)) 替换为唯一的存储帐户名称。

在 Visual Studio Code 中,打开新的 PowerShell 终端并登录到 Azure 订阅。

Connect-AzAccount -Environment AzureChinaCloud

此命令会打开默认浏览器,并提示登录到 Azure。 有关详细信息,请转到使用 Azure PowerShell 登录

连接后,运行以下命令。

New-AzResourceGroup -Name packageStorageGroup -Location chinanorth3

$pkgstorageparms = @{
  ResourceGroupName = "packageStorageGroup"
  Name = "<pkgstorageaccountname>"
  Location = "chinanorth3"
  SkuName = "Standard_LRS"
  Kind = "StorageV2"
  MinimumTlsVersion = "TLS1_2"
  AllowBlobPublicAccess = $true
  AllowSharedKeyAccess = $false
}

$pkgstorageaccount = New-AzStorageAccount @pkgstorageparms

$pkgstorageparms 变量使用 PowerShell splatting 来提高用于创建新存储帐户的命令中使用的参数值的可读性。 Splatting 可用于使用多个参数值的其他 PowerShell 命令。

创建存储帐户后,将角色分配存储 Blob 数据参与者添加到存储帐户范围中。 分配对 Microsoft Entra 用户帐户的访问权限。 根据 Azure 中的访问级别,你可能需要管理员分配的其他权限。 有关详细信息,请参阅分配用于访问 Blob 数据的 Azure 角色

向存储帐户添加角色后,需要几分钟才能在 Azure 中激活。 然后,可以创建创建容器和上传文件所需的上下文。

$pkgstoragecontext = New-AzStorageContext -StorageAccountName $pkgstorageaccount.StorageAccountName -UseConnectedAccount

New-AzStorageContainer -Name appcontainer -Context $pkgstoragecontext -Permission blob

$blobparms = @{
  File = "app.zip"
  Container = "appcontainer"
  Blob = "app.zip"
  Context = $pkgstoragecontext
}

Set-AzStorageBlobContent @blobparms

使用以下命令将包文件的 URI 存储在名为 packageuri 的变量中。 将在部署托管应用程序定义时使用该变量的值。

$packageuri=(Get-AzStorageBlob -Container appcontainer -Blob app.zip -Context $pkgstoragecontext).ICloudBlob.StorageUri.PrimaryUri.AbsoluteUri

创建托管应用程序定义

在本部分中,你将从 Microsoft Entra ID 获取标识信息、创建资源组并部署托管应用程序定义。

获取组 ID 和角色定义 ID

下一步是选择用于为客户管理资源的用户、安全组或应用程序。 此标识对受管理资源组的权限与所分配的角色相对应。 角色可以是任何 Azure 内置角色,例如所有者或参与者。

此示例使用安全组,并且 Microsoft Entra 帐户应是组的成员。 若要获取组的对象 ID,请将占位符<managedAppDemo> ((包括尖括号 (<>)) 替换为组的名称。 将在部署托管应用程序定义时使用该变量的值。

若要创建新的 Microsoft Entra 组,请转到管理 Microsoft Entra 组和组成员身份

$principalid=(Get-AzADGroup -DisplayName <managedAppDemo>).Id

接下来,获取希望将其访问权限授予用户、组或应用程序的 Azure 内置角色的角色定义 ID。 将在部署托管应用程序定义时使用该变量的值。

$roleid=(Get-AzRoleDefinition -Name Owner).Id

创建定义部署模板

使用 Bicep 文件在服务目录中部署托管应用程序定义。

打开 Visual Studio Code,创建一个名为 deployDefinition.bicep 的文件并保存它。

添加以下 Bicep 代码并保存文件。

param location string = resourceGroup().location

@description('Name of the managed application definition.')
param managedApplicationDefinitionName string

@description('The URI of the .zip package file.')
param packageFileUri string

@description('Publishers Principal ID that needs permissions to manage resources in the managed resource group.')
param principalId string

@description('Role ID for permissions to the managed resource group.')
param roleId string

var definitionLockLevel = 'ReadOnly'
var definitionDisplayName = 'Sample Bicep managed application'
var definitionDescription = 'Sample Bicep managed application that deploys web resources'

resource managedApplicationDefinition 'Microsoft.Solutions/applicationDefinitions@2021-07-01' = {
  name: managedApplicationDefinitionName
  location: location
  properties: {
    lockLevel: definitionLockLevel
    description: definitionDescription
    displayName: definitionDisplayName
    packageFileUri: packageFileUri
    authorizations: [
      {
        principalId: principalId
        roleDefinitionId: roleId
      }
    ]
  }
}

有关模板属性的详细信息,请转到 Microsoft.Solutions/applicationDefinitions

受管理资源组上的 lockLevel 可防止客户对此资源组执行不良操作。 目前,ReadOnly 是唯一受支持的锁定级别。 ReadOnly 指定客户只能读取受管理资源组中存在的资源。 授予对受管理资源组的访问权限的发布者标识不受该锁定级别控制。

创建参数文件

托管应用程序定义的部署模板需要用户输入多个参数。 部署命令会提示输入值,你也可以为值创建参数文件。 在此示例中,我们使用参数文件将参数值传递给部署命令。

在 Visual Studio Code 中,创建名为 deployDefinition-parameters.bicepparam 的新文件并保存它

将以下内容添加到参数文件并保存它。 然后,将 <placeholder values> (包括大括号(<>)) 替换为你的值。

using './deployDefinition.bicep'

param managedApplicationDefinitionName = 'sampleBicepManagedApplication'
param packageFileUri = '<placeholder for the packageFileUri>'
param principalId = '<placeholder for principalid value>'
param roleId = '<placeholder for roleid value>'

下表描述了托管应用程序定义的参数值。

参数
managedApplicationDefinitionName 托管应用程序定义的名称。 对于此例,请使用 sampleBicepManagedApplication
packageFileUri 输入 .zip 包文件的 URI。 使用 packageuri 变量的值。 格式为 https://yourStorageAccountName.blob.core.chinacloudapi.cn/appcontainer/app.zip
principalId 需要权限来管理受管理资源组中的资源的发布者主体 ID。 使用 principalid 变量的值。
roleId 对受管理资源组具有访问权限的角色 ID。 例如“所有者”、“参与者”、“读者”。 使用 roleid 变量的值。

若要获取变量值,请执行以下操作:

  • Azure PowerShell:在 PowerShell 中,输入 $variableName 以显示变量的值。
  • Azure CLI:在 Bash 中,输入 echo $variableName 以显示变量的值。

部署定义

部署托管应用程序的定义时,它将在服务目录中可用。 此过程不会部署托管应用程序的资源。

创建一个名为 bicepDefinitionGroup 的资源组并部署托管应用程序定义

New-AzResourceGroup -Name bicepDefinitionGroup -Location chinanorth3

$deployparms = @{
  ResourceGroupName = "bicepDefinitionGroup"
  TemplateFile = "deployDefinition.bicep"
  TemplateParameterFile = "deployDefinition-parameters.bicepparam"
  Name = "deployDefinition"
}

New-AzResourceGroupDeployment @deployparms

验证结果

运行以下命令,验证定义是否在服务目录中发布。

Get-AzManagedApplicationDefinition -ResourceGroupName bicepDefinitionGroup

Get-AzManagedApplicationDefinition 列出了指定资源组中的所有可用定义,例如 sampleBicepManagedApplication

请确保用户可以访问你的定义

你可以访问托管应用程序定义,但你希望确保组织中的其他用户可以访问它。 至少授予他们对定义的读者角色。 他们可能已从订阅或资源组继承了此级别的访问权限。 若要检查谁有权访问定义并添加用户或组,请转到使用 Azure 门户分配 Azure 角色

清理资源

如果要部署定义,请继续阅读链接到使用 Bicep 部署定义的文章的后续步骤部分。

如果已完成托管应用程序定义,则可以删除创建的名为 packageStorageGroup 和 bicepDefinitionGroup 的资源组

命令会提示你确认删除资源组。

Remove-AzResourceGroup -Name packageStorageGroup

Remove-AzResourceGroup -Name bicepDefinitionGroup

后续步骤

已发布托管应用程序定义。 下一步是了解如何部署该定义的实例。