Exercise - Refactor your Bicep file

Completed

After you've reviewed your template with your colleagues, you decide to refactor the file to make it easier for them to work with. In this exercise, you apply the best practices you learned in the preceding units.

Your task

Review the Bicep template that you saved earlier. Think about the advice you've read about how to structure your templates. Try to update your template to make it easier for your colleagues to understand.

In the next sections, there are some pointers to specific parts of the template and some hints about things you might want to change. We provide a suggested solution, but your template might look different, which is perfectly OK!

Tip

As you work through the refactoring process, it's good to ensure that your Bicep file is valid and that you haven't accidentally introduced any errors. The Bicep extension for Visual Studio Code helps with this. Watch out for any red or yellow squiggly lines below your code, because they indicate an error or a warning. You can also view a list of the problems in your file by selecting View > Problems.

Update the parameters

  1. Some parameters in your template aren't clear. For example, consider these parameters:

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

    What are they used for?

    Tip

    If you have a parameter that you're trying to understand, Visual Studio Code can help. Select and hold (or right-click) a parameter name anywhere in your file and select Find All References.

    Does the template need to specify the list of allowed values for the skuName parameter? What resources are affected by choosing different values for these parameters? Are there better names that you can give the parameters?

    Tip

    When you rename identifiers, be sure to rename them consistently in all parts of your template. This is especially important for parameters, variables, and resources that you refer to throughout your template.

    Visual Studio Code offers a convenient way to rename symbols: select the identifier that you want to rename, select F2, enter a new name, and then select Enter:

    Screenshot from Visual Studio Code that shows how to rename a symbol.

    These steps rename the identifier and automatically update all references to it.

  2. The managedIdentityName parameter doesn't have a default value. Could you fix that or, better yet, create the name automatically within the template?

  3. Look at the roleDefinitionId parameter definition:

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

    Why is there a default value of b24988ac-6180-42a0-ab88-20f7382dd24c? What does that long identifier mean? How would someone else know whether to use the default value or override it? What could you do to improve the identifier? Does it even make sense to have this as a parameter?

    Tip

    That identifier is the Contributor role definition ID for Azure. How can you use that information to improve the template?

  4. When someone deploys the template, how will they know what each parameter is for? Can you add some descriptions to help your template's users?

Add a configuration set

  1. You speak to your colleagues and decide to use specific SKUs for each resource, depending on the environment being deployed. You decide on these SKUs for each of your resources:

    Resource SKU for production SKU for non-production
    App Service plan S1, two instances F1, one instance
    Storage account GRS LRS
    SQL database S1 Basic
  2. Can you use a configuration set to simplify the parameter definitions?

Update the symbolic names

Take a look at the symbolic names for the resources in the template. What could you do to improve them?

  1. Your Bicep template contains resources with a variety of capitalization styles for their symbolic names, such as:

    • storageAccount and webSite, which use camelCase capitalization.
    • roleassignment and sqlserver, which use flat case capitalization.
    • sqlserverName_databaseName and AppInsights_webSiteName, which use snake case capitalization.

    Can you fix these to use one style consistently?

  2. Look at this role assignment resource:

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

    Is the symbolic name descriptive enough to help someone else work with this template?

    Tip

    The reason the identity needs a role assignment is that the web app uses its managed identity to connect to the database server. Does that help you to clarify this in the template?

  3. A few resources have symbolic names that don't reflect the current names of Azure resources:

    resource hostingPlan 'Microsoft.Web/serverfarms@2020-06-01' = {
      // ...
    }
    resource webSite 'Microsoft.Web/sites@2020-06-01' = {
      // ...
    }
    resource msi 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
      // ...
    }
    

    Managed identities used to be called MSIs, App Service plans used to be called hosting plans, and App Service apps used to be called websites.

    Can you update these to the latest names to avoid confusion in the future?

Simplify the blob container definitions

  1. Look at how the blob containers are defined:

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

    One of them uses the parent property, and the other doesn't. Can you fix these to be consistent?

  2. The blob container names won't change between environments. Do you think the names need to be specified by using parameters?

  3. There are two blob containers. Could they be deployed by using a loop?

Update the resource names

  1. There are some parameters that explicitly set resource names:

    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'
    

    Is there another way you could do this?

    Caution

    Remember that resources can't be renamed once they're deployed. When you modify templates that are already in use, be careful when you change the way the template creates resource names. If the template is redeployed and the resource has a new name, Azure will create another resource. It might even delete the old resource if you deploy it in Complete mode.

    You don't need to worry about this here, because it's only an example.

  2. Your SQL logical server's resource name is set using a variable, even though it needs a globally unique name:

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

    How could you improve this?

Update dependencies and child resources

  1. Here's one of your resources, which includes a dependsOn property. Does it really need it?

    resource sqlserverName_AllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2014-04-01' = {
      name: '${sqlserver.name}/AllowAllAzureIPs'
      properties: {
        endIpAddress: '0.0.0.0'
        startIpAddress: '0.0.0.0'
      }
      dependsOn: [
        sqlserver
      ]
    }
    
  2. Notice how these child resources are declared in your template:

    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2020-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@2014-04-01' = {
      name: '${sqlserver.name}/AllowAllAzureIPs'
      properties: {
        endIpAddress: '0.0.0.0'
        startIpAddress: '0.0.0.0'
      }
      dependsOn: [
        sqlserver
      ]
    }
    

    How could you modify how these resources are declared? Are there any other resources in the template that should be updated too?

Update property values

  1. Take a look at the SQL database resource properties:

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

    Does it make sense to hard-code the SKU's name property value? And what are those weird-looking values for the collation and maxSizeBytes properties?

    Tip

    The collation and maxSizeBytes properties are set to the default values. If you don't specify the values yourself, the default values will be used. Does that help you to decide what to do with them?

  2. Can you change the way the storage connection string is set so that the complex expression isn't defined inline with the resource?

    resource webSite 'Microsoft.Web/sites@2020-06-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}': {}
        }
      }
    }
    

Order of elements

  1. Are you happy with the order of the elements in the file? How could you improve the file's readability by moving the elements around?

  2. Take a look at the databaseName variable. Does it belong where it is now?

    var databaseName = 'ToyCompanyWebsite'
    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2020-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    
  3. Did you notice the commented-out resource, webSiteConnectionStrings? Do you think that needs to be in the file?

Add comments, tags, and other metadata

Think about anything in the template that might not be obvious, or that needs additional explanation. Can you add comments to make it clearer for others who might open the file in the future?

  1. Take a look at the webSite resource's identity property:

    resource webSite 'Microsoft.Web/sites@2020-06-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}': {}
        }
      }
    }
    

    That syntax is strange, isn't it? Do you think this needs a comment to help explain it?

  2. Look at the role assignment resource:

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

    The resource name uses the guid() function. Would it help to explain why?

  3. Can you add a description to the role assignment?

  4. Can you add a set of tags to each resource?

Suggested solution

Here's an example of how you might refactor the template. Your template might not look exactly like this, because your style might be different.

@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@2019-06-01-preview' = {
  name: sqlServerName
  location: location
  tags: tags
  properties: {
    administratorLogin: sqlServerAdministratorLogin
    administratorLoginPassword: sqlServerAdministratorLoginPassword
    version: '12.0'
  }
}

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

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

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

resource appServiceApp 'Microsoft.Web/sites@2020-06-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@2019-06-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@2018-11-30' = {
  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@2020-04-01-preview' = {
  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@2018-05-01-preview' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  tags: tags
  properties: {
    Application_Type: 'web'
  }
}

Tip

If you're working with your colleagues using GitHub or Azure Repos, this would be a great time to submit a pull request to integrate your changes into the main branch. It's a good idea to submit pull requests after you do a piece of refactoring work.