Exercise - Refactor your Bicep file
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
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:
These steps rename the identifier and automatically update all references to it.
The
managedIdentityName
parameter doesn't have a default value. Could you fix that or, better yet, create the name automatically within the template?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?
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
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 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?
Your Bicep template contains resources with a variety of capitalization styles for their symbolic names, such as:
storageAccount
andwebSite
, which use camelCase capitalization.roleassignment
andsqlserver
, which use flat case capitalization.sqlserverName_databaseName
andAppInsights_webSiteName
, which use snake case capitalization.
Can you fix these to use one style consistently?
Look at this role assignment resource:
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 } }
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?
A few resources have symbolic names that don't reflect the current names of Azure resources:
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' = { // ... }
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
Look at how the blob containers are defined:
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}' }
One of them uses the
parent
property, and the other doesn't. Can you fix these to be consistent?The blob container names won't change between environments. Do you think the names need to be specified by using parameters?
There are two blob containers. Could they be deployed by using a loop?
Update the resource names
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.
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
Here's one of your resources, which includes a
dependsOn
property. Does it really need it?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 ] }
Notice how these child resources are declared in your template:
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 ] }
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
Take a look at the SQL database resource properties:
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 } }
Does it make sense to hard-code the SKU's
name
property value? And what are those weird-looking values for thecollation
andmaxSizeBytes
properties?Tip
The
collation
andmaxSizeBytes
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?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@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}': {} } } }
Order of elements
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?
Take a look at the
databaseName
variable. Does it belong where it is now?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 } }
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?
Take a look at the
webSite
resource'sidentity
property: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}': {} } } }
That syntax is strange, isn't it? Do you think this needs a comment to help explain it?
Look at the role assignment resource:
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 } }
The resource name uses the
guid()
function. Would it help to explain why?Can you add a description to the role assignment?
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@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'
}
}
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.