练习 - 重构 Bicep 文件

已完成

与同事一起查看模板后,你决定重构文件,使其更易于使用。 在本练习中,将应用前面单元中所学的最佳做法。

你的任务

查看之前保存的 Bicep 模板。 思考已了解到的模板构造方法建议。 尝试更新模板,以便于同事理解。

下面的部分中有一些指向模板特定部分的指针,以及有关你可能想要更改的内容的提示。 我们将提供推荐的解决方案,但你的模板内容可能会有所不同,这是完全正常的!

提示

完成重构过程时,请确保 Bicep 文件有效且未意外引入任何错误。 适用于 Visual Studio Code 的 Bicep 扩展可提供帮助。 注意代码下面的红色或黄色锯齿线,因为它们表示错误或警告。 你也可选择“查看”>“问题”来查看文件中问题的列表。

更新参数

  1. 模板中的某些参数不明确。 例如,请考虑以下参数:

    @allowed([
      'F1'
      'D1'
      'B1'
      'B2'
      'B3'
      'S1'
      'S2'
      'S3'
      'P1'
      'P2'
      'P3'
      'P4'
    ])
    param skuName string = 'F1'
    
    @minValue(1)
    param skuCapacity int = 1
    

    它们的用途是什么?

    提示

    如果你有一个试图理解的参数,Visual Studio Code 可提供帮助。 选择并按住(或右键单击)文件中任意位置的参数名,然后选择“查找所有引用”

    模板是否需要为 skuName 参数指定允许值的列表? 为这些参数选择不同的值后会影响哪些资源? 是否可以为参数提供更好的名称?

    提示

    重命名标识符时,请务必在模板的所有部分保持一致。 这对于在整个模板中引用的参数、变量和资源尤其重要。

    Visual Studio Code 提供了一种简便的符号重命名方法:选择想要重命名的标识符,选择 F2,输入新名称,然后选择 Enter

    来自 Visual Studio Code 的屏幕截图,其中显示如何重命名符号。

    这些步骤会重命名标识符并自动更新对它的所有引用。

  2. managedIdentityName 参数没有默认值。 能否修复此问题,或者最好在模板中自动创建名称?

  3. 查看 roleDefinitionId 参数定义:

    param roleDefinitionId string = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
    

    为何这里有个默认值 b24988ac-6180-42a0-ab88-20f7382dd24c? 这个长标识符是什么意思? 其他人怎么知道是使用默认值还是重写默认值? 可以如何改进标识符? 将其用作参数有意义吗?

    提示

    该标识符是 Azure 的“参与者”角色定义 ID。 如何使用该信息来改进模板?

  4. 人们部署模板时,怎样知道每个参数的用途是什么? 能否添加一些说明来为模板用户提供帮助?

添加配置集

  1. 你与同事沟通后,决定根据所部署的环境对每项资源使用特定的 SKU。 你决定为每项资源使用以下 SKU:

    资源 用于生产环境的 SKU 用于非生产环境的 SKU
    应用服务计划 S1,两个实例 F1,一个实例
    存储帐户 GRS LRS
    SQL 数据库 S1 基本
  2. 可以使用配置集来简化参数定义吗?

更新符号名称

请查看模板中资源的符号名称。 你可以执行哪些操作来改进它们?

  1. Bicep 模板包含具有各种符号名称大写样式的资源,例如:

    • storageAccountwebSite,它们使用驼峰命名法大写样式。
    • roleassignmentsqlserver,它们使用扁平命名法 (flat case) 大写样式。
    • sqlserverName_databaseNameAppInsights_webSiteName,它们使用蛇形命名法大写样式。

    是否可以纠正这些问题,而使用一种统一的样式?

  2. 查看此角色分配资源:

    resource roleassignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
      name: guid(roleDefinitionId, resourceGroup().id)
    
      properties: {
        principalType: 'ServicePrincipal'
        roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
        principalId: msi.properties.principalId
      }
    }
    

    符号名称的说明是否足以帮助其他人使用此模板?

    提示

    标识需要角色分配的原因是 Web 应用使用其托管标识连接到数据库服务器。 这是否有助于在模板中阐明这一点?

  3. 几项资源的符号名称未反映出 Azure 资源的当前名称:

    resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
      // ...
    }
    resource webSite 'Microsoft.Web/sites@2023-12-01' = {
      // ...
    }
    resource msi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = {
      // ...
    }
    

    托管标识曾被称为 MSI,应用服务计划曾被称为托管计划,应用服务应用曾被称为网站。

    是否可以将这些名称更新为最新的名称,以避免将来出现混淆?

简化 blob 容器定义

  1. 查看 blob 容器的定义方式:

    resource container1 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
      parent: storageAccount::blobServices
      name: container1Name
    }
    
    resource productmanuals 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
      name: '${storageAccount.name}/default/${productmanualsName}'
    }
    

    其中一个使用 parent 属性,另一个则没有。 能否修复此问题,让它们保持一致?

  2. Blob 容器的名称在各环境之间不会发生改变。 你认为需要使用参数来指定名称吗?

  3. 这里有两个 blob 容器。 能否使用循环来部署它们?

更新资源名称

  1. 有一些参数可显式设置资源名称:

    param managedIdentityName string
    param roleDefinitionId string = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
    param webSiteName string = 'webSite${uniqueString(resourceGroup().id)}'
    param container1Name string = 'productspecs'
    param productmanualsName string = 'productmanuals'
    

    还有其他方法可以执行此操作吗?

    注意

    请记住,资源一旦部署便无法再重命名。 修改已在使用的模板时,请谨慎更改模板创建资源名称的方式。 如果重新部署了模板且资源具有新名称,Azure 将创建另一项资源。 如果你在“完全”模式下部署旧资源,它甚至可能会被删除。

    但不必担心,因为这只是个示例。

  2. 虽然 SQL 逻辑服务器需要全局唯一名称,但它的资源名称是使用变量设置的:

    var sqlserverName = 'toywebsite${uniqueString(resourceGroup().id)}'
    

    可如何进行改进?

更新依赖项和子资源

  1. 下面是你的其中一项资源,它包含 dependsOn 属性。 真的需要它吗?

    resource sqlserverName_AllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
      name: '${sqlserver.name}/AllowAllAzureIPs'
      properties: {
        endIpAddress: '0.0.0.0'
        startIpAddress: '0.0.0.0'
      }
      dependsOn: [
        sqlserver
      ]
    }
    
  2. 注意如何在模板中声明以下子资源:

    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    
    resource sqlserverName_AllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
      name: '${sqlserver.name}/AllowAllAzureIPs'
      properties: {
        endIpAddress: '0.0.0.0'
        startIpAddress: '0.0.0.0'
      }
      dependsOn: [
        sqlserver
      ]
    }
    

    如何修改这些资源的声明方式? 模板中还有其他需要更新的资源吗?

更新属性值

  1. 请查看 SQL 数据库资源的属性:

    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    

    对 SKU 的 name 属性值进行硬编码有意义吗? collationmaxSizeBytes 属性的那些奇怪的值是什么?

    提示

    collationmaxSizeBytes 属性均设置为默认值。 如果未自行指定这些值,将使用默认值。 这能否帮助你确定如何处理它们?

  2. 能否更改存储连接字符串的设置方式,以免根据资源定义复杂表达式?

    resource webSite 'Microsoft.Web/sites@2023-12-01' = {
      name: webSiteName
      location: location
      properties: {
        serverFarmId: hostingPlan.id
        siteConfig: {
          appSettings: [
            {
              name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
              value: AppInsights_webSiteName.properties.InstrumentationKey
            }
            {
              name: 'StorageAccountConnectionString'
              value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}'
            }
          ]
        }
      }
      identity: {
        type: 'UserAssigned'
        userAssignedIdentities: {
          '${msi.id}': {}
        }
      }
    }
    

元素的顺序

  1. 你是否喜欢文件中的元素顺序? 如何通过移动元素提高文件的可读性?

  2. 请查看 databaseName 变量。 它应该位于当前的位置吗?

    var databaseName = 'ToyCompanyWebsite'
    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    
  3. 注意到注释掉的资源 webSiteConnectionStrings 了吗? 你认为它需要在文件中吗?

添加注释、标记和其他元数据

思考模板中可能不明显或需要额外解释的内容。 能否添加一些注释,让以后可能打开该文件的其他人员更容易明白?

  1. 请查看 webSite 资源的 identity 变量:

    resource webSite 'Microsoft.Web/sites@2023-12-01' = {
      name: webSiteName
      location: location
      properties: {
        serverFarmId: hostingPlan.id
        siteConfig: {
          appSettings: [
            {
              name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
              value: AppInsights_webSiteName.properties.InstrumentationKey
            }
            {
              name: 'StorageAccountConnectionString'
              value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}'
            }
          ]
        }
      }
      identity: {
        type: 'UserAssigned'
        userAssignedIdentities: {
          '${msi.id}': {}
        }
      }
    }
    

    这种句法很奇怪,不是吗? 你认为需要添加注释来帮助解释它吗?

  2. 请查看以下角色分配资源:

    resource roleassignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
      name: guid(roleDefinitionId, resourceGroup().id)
    
      properties: {
        principalType: 'ServicePrincipal'
        roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
        principalId: msi.properties.principalId
      }
    }
    

    资源的名称使用 guid() 函数。 它有助于解释原因吗?

  3. 能否向角色分配添加说明?

  4. 能否为每项资源添加一组标记?

建议的解决方案

下面是如何重构模板的示例。 你的模板可能与其不完全相似,因为样式可能不同。

@description('The location into which your Azure resources should be deployed.')
param location string = resourceGroup().location

@description('Select the type of environment you want to provision. Allowed values are Production and Test.')
@allowed([
  'Production'
  'Test'
])
param environmentType string

@description('A unique suffix to add to resource names that need to be globally unique.')
@maxLength(13)
param resourceNameSuffix string = uniqueString(resourceGroup().id)

@description('The administrator login username for the SQL server.')
param sqlServerAdministratorLogin string

@secure()
@description('The administrator login password for the SQL server.')
param sqlServerAdministratorLoginPassword string

@description('The tags to apply to each resource.')
param tags object = {
  CostCenter: 'Marketing'
  DataClassification: 'Public'
  Owner: 'WebsiteTeam'
  Environment: 'Production'
}

// Define the names for resources.
var appServiceAppName = 'webSite${resourceNameSuffix}'
var appServicePlanName = 'AppServicePLan'
var sqlServerName = 'sqlserver${resourceNameSuffix}'
var sqlDatabaseName = 'ToyCompanyWebsite'
var managedIdentityName = 'WebSite'
var applicationInsightsName = 'AppInsights'
var storageAccountName = 'toywebsite${resourceNameSuffix}'
var blobContainerNames = [
  'productspecs'
  'productmanuals'
]

@description('Define the SKUs for each component based on the environment type.')
var environmentConfigurationMap = {
  Production: {
    appServicePlan: {
      sku: {
        name: 'S1'
        capacity: 2
      }
    }
    storageAccount: {
      sku: {
        name: 'Standard_GRS'
      }
    }
    sqlDatabase: {
      sku: {
        name: 'S1'
        tier: 'Standard'
      }
    }
  }
  Test: {
    appServicePlan: {
      sku: {
        name: 'F1'
        capacity: 1
      }
    }
    storageAccount: {
      sku: {
        name: 'Standard_LRS'
      }
    }
    sqlDatabase: {
      sku: {
        name: 'Basic'
      }
    }
  }
}

@description('The role definition ID of the built-in Azure \'Contributor\' role.')
var contributorRoleDefinitionId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
var storageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}'

resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = {
  name: sqlServerName
  location: location
  tags: tags
  properties: {
    administratorLogin: sqlServerAdministratorLogin
    administratorLoginPassword: sqlServerAdministratorLoginPassword
    version: '12.0'
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
  parent: sqlServer
  name: sqlDatabaseName
  location: location
  sku: environmentConfigurationMap[environmentType].sqlDatabase.sku
  tags: tags
}

resource sqlFirewallRuleAllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
  parent: sqlServer
  name: 'AllowAllAzureIPs'
  properties: {
    endIpAddress: '0.0.0.0'
    startIpAddress: '0.0.0.0'
  }
}

resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: appServicePlanName
  location: location
  sku: environmentConfigurationMap[environmentType].appServicePlan.sku
  tags: tags
}

resource appServiceApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appServiceAppName
  location: location
  tags: tags
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsights.properties.InstrumentationKey
        }
        {
          name: 'StorageAccountConnectionString'
          value: storageAccountConnectionString
        }
      ]
    }
  }
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${managedIdentity.id}': {} // This format is required when working with user-assigned managed identities.
    }
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageAccountName
  location: location
  sku: environmentConfigurationMap[environmentType].storageAccount.sku
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
  }

  resource blobServices 'blobServices' existing = {
    name: 'default'

    resource containers 'containers' = [for blobContainerName in blobContainerNames: {
      name: blobContainerName
    }]
  }
}

@description('A user-assigned managed identity that is used by the App Service app to communicate with a storage account.')
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview'= {
  name: managedIdentityName
  location: location
  tags: tags
}

@description('Grant the \'Contributor\' role to the user-assigned managed identity, at the scope of the resource group.')
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(contributorRoleDefinitionId, resourceGroup().id) // Create a GUID based on the role definition ID and scope (resource group ID). This will return the same GUID every time the template is deployed to the same resource group.
  properties: {
    principalType: 'ServicePrincipal'
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', contributorRoleDefinitionId)
    principalId: managedIdentity.properties.principalId
    description: 'Grant the "Contributor" role to the user-assigned managed identity so it can access the storage account.'
  }
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  tags: tags
  properties: {
    Application_Type: 'web'
  }
}

提示

如果你在使用 GitHub 或 Azure Repos 与同事协作,此时将非常适合提交拉取请求,将更改集成到主分支。 建议在执行一部分重构工作后提交拉取请求。