Bicep

Purpose

The idea is to gain more exposure to authoring and deploying Bicep configurations. Configurations will start small and then grow larger and more complex as I progress. It’s also a handy spot to keep links, code examples, etc.

Process

Old habits have me going to PowerShell first, deploying, and then if the project is large enough, repeatable, etc. doing an ARM > Bicep deployment. The three step process is incredibly inefficient for scenarios that call for these types of deployments.

  1. The Azure portal and scripting is used when creating one-off items or resources that are pretty static.
    • Creating a one-off VM and attaching it to a pre-existing vnet, creating a core storage account, etc.
  2. Bicep is very structured and will sort out most of your dependencies which lends itself to in-depth, repeatable deployments
    • The yaml structure and modular files lends itself well to larger or more complex deployments.
    • The modules can be re-used for various other projects (e.g. a standard Network module so anything/everything deployed has standard rules)
    • Defaults and required resource properties are still hit and miss so this is great exposure for those items
  3. Add content to the Bicep Notes page to develop a set of standard naming conventions, tags, loops, etc.

Comparing Deployment Processes

Using the fist lab as an example we’re going to create the resource group, then deploy the Bicep file. It’s interesting seeing the difference in the converted ARM > Bicep file vs my authored one. There are a lot of items that Azure will simply handle on the back end for you - but watch out for default settings that are either unsecure or costly.

  1. az group create --name rg-az104bi-westus2 --location westus2
  2. az deployment group create --template-file .\main.bicep

With the main.bicep file contents being:

param location string = resourceGroup().location
param diskname string = 'disk-az104-001'
param storagename string = 'storageaz104bi'
param tags object = {
  environemnt: 'Learning'
  module: 'AZ-104'
  method: 'Bicep'
}

resource az104storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
  name: storagename
  location: location
  kind: 'StorageV2'
  tags: tags
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
  }
}

resource disk 'Microsoft.Compute/disks@2023-04-02' = {
  name: diskname
  location: location
  tags: tags
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    diskSizeGB: 64
    creationData: {
      createOption: 'Empty'
    }
  }
}

Versus the ARM > Bicep conversion which is 5x the length.

param disks_disk_az104_00_name string = 'disk-az104-00'
param storageAccounts_storageaz104westus3_name string = 'storageaz104westus3'

resource disks_disk_az104_00_name_resource 'Microsoft.Compute/disks@2023-01-02' = {
  name: disks_disk_az104_00_name
  location: 'westus3'
  sku: {
    name: 'Standard_LRS'
    tier: 'Standard'
  }
  properties: {
    creationData: {
      createOption: 'Empty'
    }
    diskSizeGB: 64
    diskIOPSReadWrite: 500
    diskMBpsReadWrite: 60
    encryption: {
      type: 'EncryptionAtRestWithPlatformKey'
    }
    networkAccessPolicy: 'AllowAll'
    publicNetworkAccess: 'Enabled'
    diskState: 'Unattached'
  }
}

resource storageAccounts_storageaz104westus3_name_resource 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccounts_storageaz104westus3_name
  location: 'westus3'
  sku: {
    name: 'Standard_LRS'
    tier: 'Standard'
  }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_0'
    allowBlobPublicAccess: false
    networkAcls: {
      bypass: 'AzureServices'
      virtualNetworkRules: []
      ipRules: []
      defaultAction: 'Allow'
    }
    supportsHttpsTrafficOnly: true
    encryption: {
      services: {
        file: {
          keyType: 'Account'
          enabled: true
        }
        blob: {
          keyType: 'Account'
          enabled: true
        }
      }
      keySource: 'Microsoft.Storage'
    }
    accessTier: 'Hot'
  }
}

resource storageAccounts_storageaz104westus3_name_default 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  parent: storageAccounts_storageaz104westus3_name_resource
  name: 'default'
  sku: {
    name: 'Standard_LRS'
    tier: 'Standard'
  }
  properties: {
    cors: {
      corsRules: []
    }
    deleteRetentionPolicy: {
      allowPermanentDelete: false
      enabled: false
    }
  }
}

resource Microsoft_Storage_storageAccounts_fileServices_storageAccounts_storageaz104westus3_name_default 'Microsoft.Storage/storageAccounts/fileServices@2023-01-01' = {
  parent: storageAccounts_storageaz104westus3_name_resource
  name: 'default'
  sku: {
    name: 'Standard_LRS'
    tier: 'Standard'
  }
  properties: {
    protocolSettings: {
      smb: {}
    }
    cors: {
      corsRules: []
    }
    shareDeleteRetentionPolicy: {
      enabled: true
      days: 7
    }
  }
}

resource Microsoft_Storage_storageAccounts_queueServices_storageAccounts_storageaz104westus3_name_default 'Microsoft.Storage/storageAccounts/queueServices@2023-01-01' = {
  parent: storageAccounts_storageaz104westus3_name_resource
  name: 'default'
  properties: {
    cors: {
      corsRules: []
    }
  }
}

resource Microsoft_Storage_storageAccounts_tableServices_storageAccounts_storageaz104westus3_name_default 'Microsoft.Storage/storageAccounts/tableServices@2023-01-01' = {
  parent: storageAccounts_storageaz104westus3_name_resource
  name: 'default'
  properties: {
    cors: {
      corsRules: []
    }
  }
}

The ARM template defines the resources as they exist, live and deployed, which is why the converted template is so much larger than the custom one. There are more items that make up a resource then we typically are aware of and account for. This is an example of where I think the ARM > Bicep process can be useful. There are assumed parameters that are difficult to keep in mind, such as minimum TLS version, that you don’t see, know about, etc. until you look at the output ARM template and realize, “That’s not what I want”.

A lot of the pain points around missing parameters, valid name formats, unknown resource types (vscode did NOT want to give me an option for a disk resource until the API was specified), etc. will all come with more experience.

Azure Storage Security Lab

The AZ-104 learn module around managing storage had a decent environment that needed to be setup. As this is the first refresher on module deployment, I figured I would add it here as well. There are items that, theoretically, should have been added if this were a production setup. This is a learning environment though and I want to track my development as I go through these. Not having something perfect on the first run isn’t a failure - it’s just not perfect yet.

Basically, I’m not making it perfect the first time around - I’m meeting the immediate need and will keep adding things as I progress. This definitely goes against the grain of my personality. I’ve heard, “Don’t let perfect be the enemy of good” as constructive criticism a few times.

  1. Admin password should probably integrate with/use Azure Key Vault vs prompting the user to enter the password.
  2. There are no conditional parameters around the environment and deployment types
    1. e.g. Dev vs Production and manipulating the SKU
  3. Standards are already shifting
    1. This is a good example of why modules and parameter files are great - set your standard and move forward. When you need to modify or update, update the standard module (VNETs, VMs, Storage, etc.) or the parameters file to progress everything together.
  4. The VM and Vnets should be separated out to their own separate modules
    1. I’m kind of saving this as a progress marker - passing outputs from one module to another is one trick on reducing the amounts of ‘dependsOn’ statements and I want to see how that goes when I have more time.
  5. The power of automation is also the terrifying part - one-click deployment is also one-click destruction. Adding logic around resource locks with the appropriate lock types will be another item to add to the automation.

There was some amount of Portal -> ARM Template -> Bicep conversion due to the unknown parameters and values for the Microsoft.Storage service endpoint and associating it with the VNET and Storage account. Overall though I’m pretty happy with the progress around creating -> tuning -> deploying at the moment.

The file structure is the standard layout with the following files:

/ main.bicep
|
└───modules
        storage104.bicep
        vm_nets.bicep

The main.bicep holds the majority of the parameter values and passes them into the storage and VM/Vnet modules.

/* General items*/
@description('Setting the location to the resource  group location')
param location string = resourceGroup().location

@description('Standard set of test tags')
param tags object = {
  Environment: 'Learning'
  Method: 'Bicep'
  Module: 'AZ-104'
}

param deployVM bool

/*The required parameters for the storage account resources/module */
@description('prepend storage account name with this string')
param storagePrefix string = 'stg'

@description('Specify the public IP that should be allowed to access resources')
param publicIP string

@description('Build the storage account name based on prefix and resource group ID')
var storageAcctName = '${toLower(storagePrefix)}${uniqueString(resourceGroup().id)}'

/* The required parameters for the VMs and Vnet module*/
@description('Virtual machine size')
param vmSize string = 'Standard_B2ms'

@description('Admin username')
param adminUsername string

@description('Admin password - Must be deployed from shell to provide the password.')
@secure()
param adminPassword string

/* The VM and network variables that are passed to the modules*/
var vmName = 'vm-az104-01'
var nicName = 'nic-${vmName}'
var virtualNetworkName = 'vnet-az104-01'
var subnetName = 'subnet0'
var vnetIpPrefix = '10.90.0.0/24'
var subnetIpPrefix = '10.90.0.0/25'
var publicIPAddressName = 'pIP-${vmName}'
var nsgName = 'nsg-${vmName}'
var subnetRef = resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, subnetName)

/* Start defining resources by pointing them to the modules*/
module storage 'modules/storage104.bicep' = {
  name: '104stgaccount'
  params: {
    publicIP: publicIP
    location: location
    storageAcctName: storageAcctName
    tags: tags
    subnetID: subnetRef
  }
}

module virtualmachines 'modules/vm_nets.bicep' = if (deployVM) {
  name: 'vmresources'
  params: {
    adminPassword: adminPassword
    adminUsername: adminUsername
    vmSize: vmSize
    location: location
    tags: tags
    vmName: vmName
    publicIP: publicIP
    nicProps: {
      nicName: nicName
      virtualNetworkName: virtualNetworkName
      publicIPAddressName: publicIPAddressName
      nsgName: nsgName
      vnetIpPrefix: vnetIpPrefix
      subnetIpPrefix: subnetIpPrefix
      subnetName: subnetName
      subnetRef: subnetRef
    }
  }
}

I need some more scenarios around the VMs, VNETs, nic properties, etc.. I kind of brute forced the nicProps parameters and the corresponding resource parameters in the vmresources resource/module. It does work though and I believe I’ll get a good refresher on those as

@description('Virtual machine size')
param vmSize string
param location string = resourceGroup().location

@description('Admin username')
param adminUsername string

@description('Admin password')
@secure()
param adminPassword string

param tags object
param publicIP string

param vmName string
param nicProps object

var nicName = nicProps.nicName
var virtualNetworkName = nicProps.virtualNetworkName
var publicIPAddressName = nicProps.publicIPAddressName
var nsgName = nicProps.nsgName
var vnetIpPrefix = nicProps.vnetIpPrefix
var subnetIpPrefix = nicProps.subnetIpPrefix
var subnetName = nicProps.subnetName
var subnetRef = nicProps.subnetRef


resource vm01 'Microsoft.Compute/virtualMachines@2023-07-01' = {
  name: vmName
  location: location
  tags: tags
  properties: {
    osProfile: {
      computerName: vmName
      adminUsername: adminUsername
      adminPassword: adminPassword
      windowsConfiguration: {
        provisionVMAgent: true
      }
    }
    hardwareProfile: {
      vmSize: vmSize
    }
    storageProfile: {
      imageReference: {
        publisher: 'MicrosoftWindowsServer'
        offer: 'WindowsServer'
        sku: '2019-Datacenter'
        version: 'latest'
      }
      osDisk: {
        createOption: 'fromImage'
      }
      dataDisks: []
    }
    networkProfile: {
      networkInterfaces: [
        {
          properties: {
            primary: true
          }
          id: nic.id
        }
      ]
    }
  }
}

resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = {
  name: virtualNetworkName
  location: location
  tags: tags
  properties: {
    addressSpace: {
      addressPrefixes: [
        vnetIpPrefix
      ]
    }
    subnets: [
      {
        name: subnetName
        properties: {
          addressPrefix: subnetIpPrefix
          serviceEndpoints: [
            {
              service: 'Microsoft.Storage'
              locations: [
                location
              ]
            }
          ]
          delegations: [
          ]
          privateEndpointNetworkPolicies: 'Disabled'
          privateLinkServiceNetworkPolicies: 'Enabled'
        }
      }
    ]
  }
}

resource nic 'Microsoft.Network/networkInterfaces@2023-05-01' = {
  name: nicName
  location: location
  tags: tags
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          subnet: {
            id: subnetRef
          }
          privateIPAllocationMethod: 'Dynamic'
          publicIPAddress: {
            id: publicIpAddress.id
          }
        }
      }
    ]
    networkSecurityGroup: {
      id: nsg.id
    }
  }
  dependsOn: [
    virtualNetwork
  ]
}

resource publicIpAddress 'Microsoft.Network/publicIPAddresses@2023-05-01' = {
  name: publicIPAddressName
  tags: tags
  location: location
  properties: {
    publicIPAllocationMethod: 'Dynamic'
  }
}

resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
  name: nsgName
  location: location
  tags: tags
  properties: {
    securityRules: [
      {
        name: 'default-allow-rdp'
        properties: {
          priority: 1000
          sourceAddressPrefix: publicIP
          protocol: 'Tcp'
          destinationPortRange: '3389'
          access: 'Allow'
          direction: 'Inbound'
          sourcePortRange: '*'
          destinationAddressPrefix: vnetIpPrefix
        }
      }
    ]
  }
}

The Access Tier, SKU, and VNET integration are areas of improvement on the storage module.

param storageAcctName string
param publicIP string
param tags object
param location string
param subnetID string
param deployVM bool = true

var vnetRulesArray = deployVM ? [
  {
    id: subnetID
    action: 'Allow'
  }
] : []

resource storageaccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
  name: storageAcctName
  location: location
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    allowSharedKeyAccess: true
    supportsHttpsTrafficOnly: true
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: [
        {
          action: 'Allow'
          value: publicIP
        }
      ]
      virtualNetworkRules: vnetRulesArray
    }
  }
  sku: {
    name: 'Standard_LRS'
  }
  tags: tags
}


resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  parent: storageaccount
  name: 'default'
  properties: {

  }
}


resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
  parent: blobService
  name: 'test01'
  properties: {
    
  }
}

Another great takeaway from this little lab was the reminder on how the code really defines the infrastructure post-deployment. Additional changes around tweaking the NSG rules, VM SKU size, etc. triggered the appropriate modifications of the pre-existing resources. There were no conflicts, teardown -> deploy processes, or anything like that. It’s literally as if someone went in via the Azure portal to make the changes.

Conditions Vs Modules

One scenario I brushed up against was the idea of deploying a Windows or Linux VM based off of a parameter prompt for osType. Depending on the osType value, the appropriate VM would then be deployed. While what I did worked I don’t think it would be a valid path forward for any meaningful standard of deployment and simply having different modules based on the type of VM to deploy is the more solid option.

Below is some of the code that made it possible as well as additional information on why I don’t think it’s a valid approach.

Resource Names And Images

VM resources have an imageReference that defines the OS. When you have a scenario where the VM resource name can repeat, but the OS version is different, it will error out.

For instance, having a Linux imageReference

var vmName 'vm-az104-01'
/*bunch of other code */
imageReference: {
      publisher: 'Canonical'
      offer: '0001-com-ubuntu-server-jammy'
      sku: '22_04-lts-gen2'
      version: 'latest'
    }

and a pre-existing vm-az104-01 Windows VM will error out, changing the vm-az104-01 VM from Windows to Linux isn’t possible.

One possible way around this (in a smaller environment) would be to ensure the VM name is unique. I approached this by combining one of the tags, the osType and a two-digit integer into the vmName variable

@description('Standard set of test tags')
param tags object = {
  Environment: 'Learning'
  Method: 'Bicep'
  Module: 'AZ-104'
}

@description('Are we going to deploy a VM? True or null')
param deployVM bool

@description('Windows Server or Ubuntu 22.04 LTS')
@allowed([
  'Windows'
  'Linux'
])
param osType string = 'Linux'

@description('Enter a two digit number for the VM name')
@minLength(2)
@maxLength(2)
param vmNumber string

var vmName = 'vm-${tags.Module}-${osType}-${vmNumber}'

Then, in the VM module/template two different arrays are created to satisfy the storageProfile for the two types of VMs. The array is then passed through to the VM resource to create the appropriate machine.

/* Example snippet */
var windowsStorageProfile = [
  {
    osDisk: {
      createOption: 'FromImage'
      managedDisk: {
        storageAccountType: osDiskStrgAcctType
      }
    }
    imageReference: {
      publisher: 'MicrosoftWindowsServer'
      offer: 'WindowsServer'
      sku: '2019-Datacenter'
      version: 'latest'
    }
  }
]

/*Set the Linux storage profile if ofType is 'Linux'*/
var linuxStorageProfile = [
  {
    osDisk: {
      createOption: 'FromImage'
      managedDisk: {
        storageAccountType: osDiskStrgAcctType
      }
    }
    imageReference: {
      publisher: 'Canonical'
      offer: '0001-com-ubuntu-server-jammy'
      sku: '22_04-lts-gen2'
      version: 'latest'
    }
  }
]

/*Conditionally use the osType value to set the selectedStorageProfile variable*/
var selectedStorageProfile = osType == 'Windows' ? windowsStorageProfile : osType == 'Linux' ? linuxStorageProfile : linuxStorageProfile

resource vm01 'Microsoft.Compute/virtualMachines@2023-07-01' = {
  name: vmName
  location: location
  tags: tags
  properties: {
    osProfile: {
      computerName: vmName
      adminUsername: adminUsername
      adminPassword: adminPassword
    }
    hardwareProfile: {
      vmSize: vmSize
    }
    storageProfile: selectedStorageProfile[0]
    networkProfile: {
      networkInterfaces: [
        {
          properties: {
            primary: true
          }
          id: nic.id
        }
      ]
    }
  }
}

And this works! There are some significant problems around OS specific options though. For instance, Linux VMs should authorize connections via SSH keys, not usernames and passwords, which would need to be addressed. Other points are different types of VM agents, NSG rules, backup strategies, RBAC rules, etc. that would all differ based on the OS type.

So while this method works and it was a good exercise, it’s not exactly viable in a production environment. My existing Bicep items will simply need to adjust to have a module for different types of VMs (probably different templates for different purposes as well), and then conditionally deploy the appropriate resource using logic in the main.bicep file.