How to implement CI/CD for IaC in practice? — part 3: Prepare Bicep templates for basic use cases.

How to implement CI/CD for IaC in practice? — part 3: Prepare Bicep templates for basic use cases.

Introduction

In two previous articles, we have created our organization and project. We have also set up a connection between Azure DevOps and Azure subscription. Today we will prepare some basic Bicep code for our test infrastructure. Our aim is not to architect sophisticated solutions but to have easy to setup sandbox environment for future testing. Our basic infrastructure will be combined from App service, app service plan needed for hosting, Log analytics workspace, the storage account for storing the logs, and application insights. We aim to have multistage deployments, so we intend to distinguish between test and production environments.  

App service setup

This code block defines two resources in Azure using Bicep: an App Service Plan and an App Service App. The App Service Plan resource specifies a set of resources and capacity dedicated to running the App. The App Service App resource is the web application that runs on the App Service Plan. It specifies the Application Insights component used to monitor the performance and usage of the application. The App Service Plan and the App Service App are linked by the serverFarmId property, which is set to the id of the App Service Plan. The App Service App also has an appSettings property that contains the connection strings and instrumentation key for the Application Insights component.

resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: 'newTestAppServicePlan'
  location: 'westus'
  sku: 'S1'
}

resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
  name: 'newTestAppServiceApp'
  location: 'westus'
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsights.properties.InstrumentationKey
        }
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: applicationInsights.properties.ConnectionString
        }
      ]
    }
  }
}

But the above solution is a little bit clumsy. We need to have a way of deploying target solutions for multiple environments, and having every parameter of resource hard coded is also not such a good idea. Let us add some basic parametrization to our solution.

// Location will be inherited from the resource group used for infrastructure deployment.
@description('The location into which your Azure resources should be deployed.')
param location string = resourceGroup().location

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

param storageAccountNameParam string = uniqueString(resourceGroup().id)

// Define the names for resources.
var appServiceAppName = 'newTest-${resourceNameSuffix}'
var appServicePlanName = 'newTest'

So as we have added some basic parametrization, we still lack the possibility of not repeating ourselves with multistage deployments. We can address this by introducing a configuration map that will store different configurations deployed separately depending on the environment. It is not such a good idea for larger environments where there should be a strict separation between them, but it would do the job in our simple case.

// Define the SKUs based on the environment type.
var environmentConfigurationMap = {
  Production: {
    appServicePlan: {
      sku: {
        name: 'S1'
        capacity: 1
      }
    }
  }
  Test: {
    appServicePlan: {
      sku: {
        name: 'F1'
      }
    }
  }
}

In general, our solution should look like this:

//  Location will be inherited from the resource group used for infrastructure deployment.
@description('The location into which your Azure resources should be deployed.')
param location string = resourceGroup().location

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

// Define the names for resources.
var appServiceAppName = 'test-app-${resourceNameSuffix}'
var appServicePlanName = 'test-app'

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

// Define the SKUs based on the environment type.
var environmentConfigurationMap = {
  Production: {
    appServicePlan: {
      sku: {
        name: 'S1'
        capacity: 1
      }
    }
  }
  Test: {
    appServicePlan: {
      sku: {
        name: 'F1'
      }
    }
  }
}

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

resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
  name: appServiceAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsights.properties.InstrumentationKey
        }
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: applicationInsights.properties.ConnectionString
        }
      ]
    }
  }
}

Log analytics workspace setup

In the next step, we need to add resource definitions for Log analytics workspace, storage account, and application sights. Let's not forget about proper parametrization and take into consideration multi-environment deployment. The resource definition should look like this:

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
  name: logAnalyticsWorkspaceName
  location: location
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Request_Source: 'rest'
    Flow_Type: 'Bluefield'
    WorkspaceResourceId: logAnalyticsWorkspace.id
  }
}

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

It is already incorporating parametrization and configuration map, so let us update the rest, which will result in a complete solution like this:

//  Location will be inherited from the resource group used for infrastructure deployment.
@description('The location into which your Azure resources should be deployed.')
param location string = resourceGroup().location

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

param storageAccountNameParam string = uniqueString(resourceGroup().id)

// Define the names for resources.
var appServiceAppName = 'test-app-${resourceNameSuffix}'
var appServicePlanName = 'test-app'
var applicationInsightsName = 'testapp'
var logAnalyticsWorkspaceName = 'workspace-${resourceNameSuffix}'
var storageAccountName = 'mystorageresourceNameSuffix'

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

// Define the SKUs based on the environment type.
var environmentConfigurationMap = {
  Production: {
    appServicePlan: {
      sku: {
        name: 'S1'
        capacity: 1
      }
    }
    storageAccount: {
      sku: {
        name: 'Standard_LRS'
      }
    }
  }
  Test: {
    appServicePlan: {
      sku: {
        name: 'F1'
      }
    }
    storageAccount: {
      sku: {
        name: 'Standard_GRS'
      }
    }
  }
}

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

resource appServiceApp 'Microsoft.Web/sites@2022-03-01' = {
  name: appServiceAppName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsights.properties.InstrumentationKey
        }
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: applicationInsights.properties.ConnectionString
        }
      ]
    }
  }
}

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
  name: logAnalyticsWorkspaceName
  location: location
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Request_Source: 'rest'
    Flow_Type: 'Bluefield'
    WorkspaceResourceId: logAnalyticsWorkspace.id
  }
}

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

output appServiceAppHostName string = appServiceApp.properties.defaultHostName

Summary

This blog explains how to set up a basic infrastructure for testing in Azure using Bicep. The infrastructure includes an App Service, an App Service Plan, Log Analytics Workspace, a Storage Account, and Application Insights. The post also covers adding parametrization and a configuration map to the solution to enable multi-environment deployments. In next part of the series will take care of creating a YAML based pipeline that will be used for testing and deployments.  

Subscribe to EngEX

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe