It's time for a work-out. Today, I want to show you how to use Bicep with an Azure Container Registry.
Why
Infrastructure as Code is a great way to make sure your infrastructure can be redeployed at a moment's notice. The whole idea of the DevOps movement was to tear down the silos between developers and ops, so that developers had the freedom to deploy at will. IaC is an integral part of the whole team being more aware of how we go from lines of code to getting our production application alive and working. ARM templates are a great way to facilitate that. At least until you have to author one by hand or modify an existing one. In 2020, Bicep was first released, and since then it has become nearly feature parity with ARM templates. Bicep introduces a much easier to read and understand file structure and a language service (and thus intellisense) to the unruly JSON ARM templates. Bicep modules offer an easy way to share code across multiple teams or even companies to save time and effort.
How
For a quick Bicep introduction, Bicep is a declarative language for deploying Azure resources. It's a bit like Typescript and JavaScript. TS needs to be compiled down to JS, but it's much easier to read, use and reuse. Bicep does that for ARM templates. You can decompile ARM templates to Bicep and also build Bicep back to ARM templates, using the CLI.
Let's take a look at a simple Azure App Service and App Service Plan. We can export this Resource Group as a template and then download the resulting ARM template. This simple example is over 150 lines long, and at the end we'll have a 17 line Bicep file instead. Due to the length of the ARM template, I'm just going to link to it on GitHub .
We can decompile from ARM to Bicep with bicep decompile site-example.json
, which will give us site-example.bicep
. We can also build Bicep back down to ARM with bicep build site-example.bicep
which will output a JSON file with the same name.
Let's look at the converted Bicep template.
param location string = resourceGroup().location
@description('Name of the Azure App Service Plan.')
@minLength(5)
@maxLength(40)
param appServicePlanName string = 'asp-${uniqueString(resourceGroup().id)}'
@description('Provide a tier of your Azure App Service.')
param sku string = 'F1'
@description('Name of the Azure Website.')
@minLength(5)
@maxLength(40)
param webSiteName string = 'wapp-${uniqueString(resourceGroup().id)}'
resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = {
name: appServicePlanName
location: location
sku: {
name: sku
}
properties: {
reserved: true
}
kind: 'linux'
}
resource webSite 'Microsoft.Web/sites@2021-03-01' = {
name: webSiteName
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'DOTNETCORE|6.0'
}
}
identity: {
type: 'SystemAssigned'
}
}
The first 14 lines are dedicated to parameters. The only thing I want to call your attention to here is the unique string. The neat thing about these functions is that if you don't provide a value, which you should, it will generate a consistent (meaning that each time you deploy it, you will get the same name) but unique value based on the resource group.
After that, we have our two resources, the service plan and the website. You will notice that on line 32 we are referencing the service plan's ID, which makes the site dependent on the plan.
I find the Bicep is much easier to read, figure out what's going on a make changes to existing templates thanks to the intellisense you get. And that's the 100 level Bicep.
Modules
Of course, there's more. Bicep supports modules. If you've every used ARM's linked templates, then this will be a similar concept.
App Service
I've taken our site example from before and split it out into modules. The first module is for the app service.
@minLength(5)
@maxLength(40)
@description('Name of the Azure App Service Plan.')
param appServicePlanName string = 'asp-${uniqueString(resourceGroup().id)}'
@description('Provide a location.')
param location string = resourceGroup().location
@description('Provide a tier of your Azure App Service.')
param sku string = 'F1'
resource appServicePlanResource 'Microsoft.Web/serverfarms@2021-03-01' = {
name: appServicePlanName
location: location
sku: {
name: sku
}
properties: {
reserved: true
}
kind: 'linux'
}
@description('Output the farm id')
output farmId string = appServicePlanResource.id
Nothing much is different here, except the code for the site is obviously missing. Additionally, the resource ID is now an output, so we can use it in other modules.
We can deploy this with the AZ CLI:
az deployment group create --resource-group bicep-modules-rg
--template-file appservice-linux.bicep
--parameters appServicePlanName='$(var)' sku='B1'
Web App
The module for the web app is much the same, except that here the farm ID is a parameter. It deploys in the same manner as the previous module, if you want to do things piece by piece. The obvious difference is that you need to change the file and parameters in use.
@minLength(5)
@maxLength(40)
@description('Name of the Azure Website.')
param webSiteName string = 'wapp-${uniqueString(resourceGroup().id)}'
@description('Provide a location.')
param location string = resourceGroup().location
@description('Provide a Server Farm Id.')
param farmId string
resource appService_resource 'Microsoft.Web/sites@2021-03-01' = {
name: webSiteName
location: location
properties: {
serverFarmId: farmId
siteConfig: {
linuxFxVersion: 'DOTNETCORE|6.0'
}
}
identity: {
type:'SystemAssigned'
}
}
Local Module Usage
Now if we look at the site example we can see how to bring it all together.
param location string = resourceGroup().location
module farm './1-appservice-linux.bicep' = {
name: 'farmDeployment'
params: {
appServicePlanName: 'asp-mytestfarm'
location: location
}
}
module app './2-webapp-linux.bicep' = {
name: 'appDeployment'
params: {
farmId: farm.outputs.farmId
location: location
}
}
We can use the app service module to create our plan and then use the output of that module as the parameter for the web app module.
This bicep file can now be deployed either from our local CLI or using an ARM template deploy step in our pipeline (assuming the modules are also part of the artifacts).
Using Azure Container Registry
Using the module in our local folder structure is useful, but is less useful across multiple teams. I did spend a little bit of time trying to get a pipeline to consume the modules across a different repo, but I didn't have time to fully vet that option.
What we can do, though, is publish these modules to an Azure Container Registry. Let's take a look at a simple ACR bicep file. The ACR name is an important parameter here, as it has similar restrictions like Storage Accounts (for some reason).
@minLength(5)
@maxLength(50)
@description('Provide a globally unique name of your Azure Container Registry. Alphanumeric only')
param acrName string = 'acr${uniqueString(resourceGroup().id)}'
@description('Provide a location for the registry.')
param location string = resourceGroup().location
@description('Provide a tier of your Azure Container Registry.')
param acrSku string = 'Basic'
resource acrResource 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = {
name: acrName
location: location
sku: {
name: acrSku
}
properties: {
adminUserEnabled: false
}
}
@description('Output the login server property for later use')
output loginServer string = acrResource.properties.loginServer
We have an output of the login so that we can use that in other places. This will be the name of the ACR which choose dot azurecr.io.
Let's run this command to deploy the ACR to a resource group.
az deployment group create --resource-group bicep-demo-rg
--template-file acr.bicep
--parameters acrName=myACRbicepModuleTest
Once that's done, we need to go back to the modules we saw previous and publish those to a container.
I'll publish each one pointing to our module file and the ACR. The name of the file and the artifact can be different and must include the "br" prefix, as this is the bicep registry schema name. A version tag is also required, and I'll get in to some caveats with that a bit later.
az bicep publish --file appservice-linux.bicep
--target br:myACRbicepModuleTest.azurecr.io/bicep/modules/appservice-linux:v1
az bicep publish --file webapp-linux.bicep
--target br:myACRbicepModuleTest.azurecr.io/bicep/modules/webapp-linux:v1
With the modules published, we can then use those modules from the ACR. This example is just like the local modules, except the path to the modules is now in the same pattern we used to publish them.
param location string = resourceGroup().location
module farm 'br:myACRbicepModuleTest.azurecr.io/bicep/modules/appservice-linux:v1' = {
name: 'farmDeployment'
params: {
appServicePlanName: 'asp-mytestfarmPub'
location: location
}
}
module app 'br:myACRbicepModuleTest.azurecr.io/bicep/modules/webapp-linux:v1' = {
name: 'appDeployment'
params: {
farmId: farm.outputs.farmId
location: location
}
}
The site example can be deployed just like we would an ARM template. Visual Studio Code will attempt to automatically download intellisense about your modules to provide you with help and descriptions about parameters and so forth.
A couple of caveats with Bicep to be aware of. There's currently a bug when using the ARM deployment step in Azure DevOps that it doesn't support spaces in the path to the bicep file.
The version numbers also require planning. Bicep doesn't support partial SemVer tags. In other words, you can't publish a 1.1.3 and ask for a module published 1.*. This seems like a limitation to me. I work a lot with NPM and there's a good bit of flexibility here. Bicep, however, is all or nothing. You either want 1.0 or you want 1.5 or 2.0. This becomes important depending on the number of projects that you shared these modules with. If you have 40 projects that reference a base template, for instance, publishing a change or fix to an existing tag might be good or bad. Publishing a new tag requires any project that needs the change to also update their IaC to use the new tag.
What
And there you have it, from ARM templates to Bicep modules in a private container registry.
I hope you learned something today, and I would love it if you shared and commented.
Comments
You can also comment directly on GitHub.