Convert ARM Template to Bicep and Deploy with Azure DevOps - Part 3 - Refactor

2022, Mar 08

This is the third part of the series where an ARM Template is converted to Bicep. In this article, we should continue on Phase 3 - Refactor.

Phased Migration

As a reference to Part 2, Microsoft suggests a migration in 5 phases, to convert the ARM Template into Bicep Template.

Five-Phases

The comprehensive details provided by Microsoft Docs are available as a reference1.

Phase 3 - Refactor

On this refactor, we should end up with a version that is streamlined, modular, ready to go for Testing and then for Production.

At the end of the previous migration, the bicep template created was in a good shape, as we had fixed and removed warnings.

On this refactor our goal is to perform:

  • Review of resource API versions

Considering the bicep template resource API reference. e.g.: Microsoft.Web/sites@2020-12-01

Is that enough for you to build your template? Do you need to add a property that is not available on VStudio Code?

These are questions you should ask yourself to check whether the resource API you have referenced is enough or not. In this example, the resource API seems to do the job.

  • Review of linter warnings and errors

As we are using the Bicep extension for Visual Studio, we have linting of suggestions, warnings and errors as the Bicep template is loaded. Make the appropriate changes to remove those annoying warnings.

  • Review of parameters, variables and any symbolic names

When we decompiled the JSON onto bicep, parameters, variables and symbolic names were created automatically. I like professional solutions by leveraging naming conventions. Best practices2 indicate the use of camelCase.

  • Review to simplify expressions

What sort of expressions do we need to simplify? In general, this is aligned with the review of parameters and variables, because this will be created automatically during the decompilation. In this example I use interpolation.e.g.: var webSiteName = 'sample-web-${environment}' rather than using functions like concat().

  • Review of parent/child references

When creating resources on Azure, some components become parents, and others, because of dependencies become children. This is because of how resources are created, and the sequence they need to be provisioned. In this refactor some of these references are used (available in the code below), consider a resource where you would have string concatenation for a resource like '${parent}/${child}', e.g.: ${webSiteName}/web.

  • Review of modular scripts

This is a great way of making templates from Bicep scripts, and enabling reuse of components, with the use of Bicep modules. This will make a huge difference in how productive you/your team will become.

  • Review of comments and descriptions

Check the existing comments and descriptions, and at the same time, feel free to complement templates with extra details/information.

  • Review of best practices

Microsoft provides some best practices2 for parameters, variables, names and definitions, that are important to be double-checked.

Modules

After these considerations, let's break the bicep file azuredeploy.bicep into modules.

Looking at the resources we have: App Plan, Web App and Application Insights. So let's split them in different bicep files. Use this folder/file structure:

azuredeploy.bicep
|--modules
|--|--appPlan.bicep
|--|--apps.bicep
|--|--insights.bicep

File appPlan.bicep:

@minLength(1)
@maxLength(3)
param environment string

@minLength(3)
@maxLength(40)
param appPlanName string

@description('Describes plan\'s pricing tier and capacity. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/')
@allowed([
  'F1'
  'D1'
  'B1'
  'B2'
  'B3'
  'S1'
  'S2'
  'S3'
  'P1'
  'P2'
  'P3'
  'P4'
])
param appPlanSku string = 'B1'

param location string = resourceGroup().location

@description('Resource: App Plan')
resource appPlan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: appPlanName
  location: location
  sku: {
    name: appPlanSku
  }
  kind: 'app'
  tags: {
    displayName: 'HostingPlan'
  environment: environment
  }
}

output appPlanId string = appPlan.id

File insights.bicep:

@minLength(1)
@maxLength(3)
param environment string

@minLength(3)
@maxLength(60)
param webSiteName string

param location string = resourceGroup().location

@description('Resource: Application Insights')
resource aiWeb 'Microsoft.Insights/components@2020-02-02' = {
  name: webSiteName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
  }
  tags: {
    displayName: 'AppInsightsComponent'
  environment: environment
  }
}

output aiWebInstrumentationKey string = aiWeb.properties.InstrumentationKey

File apps.bicep:

@minLength(1)
@maxLength(3)
param environment string

@minLength(3)
@maxLength(60)
param webSiteName string

param appPlanId string

param webAppInsights string

param location string = resourceGroup().location

var aspnetEnvironment = {
  dev: 'Development'
  tst: 'Test'
  uat: 'UAT'
  prd: 'Production'
}

@description('Resource: Apps')
resource appWeb 'Microsoft.Web/sites@2020-12-01' = {
  name: webSiteName
  location: location
  kind: 'app'
  properties:{
    serverFarmId: appPlanId
  }
  tags: {
    displayName: 'Website'
  environment: environment
  }
}

@description('Resource: App Config')
resource appWebConfig 'Microsoft.Web/sites/config@2020-12-01' = {
  name: '${webSiteName}/web'
  dependsOn: [
    appWeb
  ]
  properties: {
    appSettings: [
      {
        name: 'ASPNETCORE_ENVIRONMENT'
        value: aspnetEnvironment[environment]
      }
      {
        name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
        value: webAppInsights
      }
    ]
  }
}

File azuredeploy.bicep:

@minLength(1)
@maxLength(3)
param environment string

@minLength(3)
param appPlanName string

@description('Describes plan\'s pricing tier and capacity. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/')
@allowed([
  'F1'
  'D1'
  'B1'
  'B2'
  'B3'
  'S1'
  'S2'
  'S3'
  'P1'
  'P2'
  'P3'
  'P4'
])
param appPlanSku string = 'F1'

param location string = resourceGroup().location

var resourceGroups = {
  dev: 'sample-rg-nonprd'
  tst: 'sample-rg-nonprd'
  uat: 'sample-rg-uat'
  prd: 'sample-rg-prd'
}
var webSiteName = 'sample-web-${environment}'
var resourceGroupName = resourceGroups[environment]

@description('Module: App Plan')
module appPlan './modules/appPlan.bicep' = {
  name: 'appPlanModule'
  params: {
    appPlanName: appPlanName
    appPlanSku: appPlanSku
    location: location
    environment: environment
  }
  scope: resourceGroup(resourceGroupName)
}

@description('Module: Application Insights')
module ai './modules/insights.bicep' = {
  name: 'appInsightsModule'
  params: {
    webSiteName: webSiteName
    location: location
    environment: environment
  }
}

@description('Module: Apps')
module app './modules/apps.bicep' = {
  name: 'appModule'
  params: {
    appPlanId: appPlan.outputs.appPlanId
    webSiteName: webSiteName
    webAppInsights: ai.outputs.aiWebInstrumentationKey
    location: location
    environment: environment
  }
}

The files were split. Note that the dependency between webApp and app Insights were modified, and some ASPNET environment variables were added to the apps.bicep.

Observe the use of output variables as well, introduced because the modules split into different files.

Because of the different resource groups we are going to use, the scope was introduced on the appPlan.

Compiling

To make sure that we can compile the bicep changes, run the following command:

bicep build azuredeploy.bicep

No suggestions, errors or warnings are expected. After compilation, the file azuredeploy.json should be generated, and with that, we have achieved our goal of reviewing the different parts of this Phase 3, making this template modular and compliant with Bicep best practices.

In Phase 4 we'll adjust the variables for Testing and get to Test the new Bicep template.

Full Series

Phase 1 - Convert

Phase 2 - Migration

Phase 3 - Refactor

Phase 4 - Test

Phase 5 - Deploy


  1. References: Migrate to Bicep
  2. Best practices: Best practices