Azure Bicep is a revolution in Azure DevOps

Have you been writing Azure ARM templates for years? Stop it…right now and begin writing in Bicep.

This blog is popular, among other things, for its sample ARM templates in JSON. But I’m likely to never post another Azure ARM JSON file now that I have begun working in Bicep. There’s so much to like about Bicep and even though I’ve only been using it for a week or so, I’d never go back to OG ARM templates.

My task recently was to develop a reference architecture template for a client that deploys a simple Azure CDN profile and endpoint. While working on that, it occurred to me that deploying just the CDN resources might not be all that useful for the DevOps engineers tasked with deploying it. If an engineer didn’t have a website handy to test the CDN deployment with, it would take extra time to set one up and link it as the CDN’s origin.

So, for completeness and the engineers’ convenience, I decided to optionally deploy a bare bones Azure static web app. That meant learning about Bicep’s conditional deployment capabilities and its logical operators. This post shows the Bicep template snippets I coded to do this and, for grins, compares a portion of the Bicep file to the generated JSON template output. The full templates are below.

First, I needed a switch parameter to indicate whether or not this deployment should deploy the static web app. Here’s how you can default a switch parameter to false in Bicep. Notice the @description decorator and comment capabilities:

@description('Should this Bicep file deploy a bare-bones static web app and connect that to the CDN?')
param createStaticWebAppSwitch bool = false // If deploying using pwsh New-AzResourceGroupDeployment and the bare-bones static web test app is desired, add '-createStaticWebAppSwitch $true' to the cmdlet's parameters

Next, I needed a variable that would either contain the hostname from the conditionally deployed static web app or the value of a parameter from the main Bicep template. Check out the power of the ternary if logical operator in Bicep. The code below can be read as, “If the switch parameter is true, then set value of originFqdn to an output string from the conditionally deployed Bicep template; otherwise use the default FQDN defined elsewhere in this Bicep template.”

// If createStaticWebAppSwitch is true, use the host name from the module staticWebApp
var originFqdn = createStaticWebAppSwitch ? createStaticWebApp.outputs.staticWebAppFqdn : defaultOriginFqdn 

Finally, I needed to tell Bicep to conditionally deploy a template named createStaticWebApp. This statement tests the same switch parameter and if true, deploys the module named deployStaticWebApp.bicep which is located in the modules directory relative to the directory of the main Bicep file.

module createStaticWebApp 'modules/deployStaticWebApp.bicep' = if (createStaticWebAppSwitch) {
  name: 'deployStaticWebApp'
}

If you had tried to do all this in an ARM template in JSON, you’d have to use a Microsoft.Resources/deployments type to deploy a nested template — one of the nastiest and most convoluted of all ARM template types. And the if operator in native ARM is hard to code and harder to read.

For grins, take a look at what you’d have to code in standard ARM JSON to specify the hostname property of the CDN endpoint to make it dynamically equal either a parameter specified in the main Bicep file or the conditionally deployed Bicep file. It’s not nearly as concise as the Bicep ternary if. (Q: How’d I get this JSON version? A: bicep build.)

...
"properties": {
              "hostName": "[if(parameters('createStaticWebAppSwitch'), reference(resourceId('Microsoft.Resources/deployments', 'deployStaticWebApp'), '2020-06-01').outputs.staticWebAppFqdn.value, parameters('defaultOriginFqdn'))]"
            }
...

This simple example combines Bicep’s comparison operators and conditional deployment in ways one would be reluctant to try if one was coding in traditional ARM JSON templates. Bicep combines what ARM template coders need in an easy-to-learn domain specific language, compiles it to native JSON ARM templates and offers (in VS Code) some pretty neat tooling.

Here is the complete main.bicep and the module Bicep file it conditionally deploys. It’s more comments than code — and a heckuva lot fewer braces and brackets. 🙂

main.bicep

/*
This template defines a CDN profile and endpoint for an Azure web app using ONLY the Microsoft Standard CDN offering. Verizon CDN or Akamai CDNs, either standard or premium are not configured.
The template will optionally create a bare-bones static web app and use its default URL as the endpoint for this CDN if the parameter createStaticWebAppSwitch is set to true. This can be useful in initial deployments to show how a static web app fits into the CDN defintions. If createStaticWebAppSwitch is false (the default), you MUST supply the origin host name in defaultOriginFqdn.
Be CERTAIN that defaultOriginFqdn is all lower case and contains only alphabetic characters (no numbers or special characters).
Alex Neihaus 2021-11-23
(c) 2021 Air11 Technology LLC -- licensed under the Apache OpenSource 2.0 license, https://opensource.org/licenses/Apache-2.0
	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at
	http://www.apache.org/licenses/LICENSE-2.0
	
	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
	
	Author's blog: https://www.yobyot.com
*/
/* BEGIN PARAMETERS */
@description('Should this Bicep file deploy a bare-bones static web app and connect that to the CDN?')
param createStaticWebAppSwitch bool = false // If deploying using pwsh New-AzResourceGroupDeployment and the bare-bones static web test app is desired, add '-createStaticWebAppSwitch $true' to the cmdlet's parameters
@description('The default origin FQDN for this endpoint')
param defaultOriginFqdn string = 'yourcdnazstdwebapptest.azurewebsites.net'
/* This is just an example URL. Be sure to change it as this example site does not exist. Note this template does NOT create custom domains. It simply associates a preexisting default domain web app with this CDN endpoint.
   This parameter is NOT USED if createStaticWebAppSwitch is true. See the variable originFqdn */
@description('Set this parameter to be the name of the endpoint which will provide access to the application')
@maxLength(63)
param cdnEndpointHostName string = toLower('yourAzureCdnEndpointName') 
/* Change this to the host name of the endpoint for the application or specify as dynamic parameter. Must be lowercase and contain only alphabetic characters.
    If you are NOT creating a bare-bone static web app in this deployment, this parameter becomes the hostname in domain azurewebsites.net. IOW, using the defaults, this parameter will create an endpoint FQDN of yourazurecdnendpointname.azurewebsites.net.
    Note that THIS MUST BE IN LOWER CASE. It's shown here with the toLower() function to make that point. Also, DNS host names cannot be longer than 63 characters.
*/
@description('Pricing tier of the CDN Profile.')
@allowed([
  'Standard_Microsoft'
])
param cdnSku string = 'Standard_Microsoft'
// IgnoreQueryString is the default for the standard tier. See https://docs.microsoft.com/en-us/azure/cdn/cdn-query-string
@description('Query string caching behavior.')
@allowed([
  'IgnoreQueryString'
  'BypassCaching'
  'UseQueryString'
])
param queryStringCachingBehavior string = 'IgnoreQueryString'
@description('Content type that is compressed.')
param contentTypesToCompress array = [
  'application/eot'
  'application/font'
  'application/font-sfnt'
  'application/javascript'
  'application/json'
  'application/opentype'
  'application/otf'
  'application/pkcs7-mime'
  'application/truetype'
  'application/ttf'
  'application/vnd.ms-fontobject'
  'application/xhtml+xml'
  'application/xml'
  'application/xml+rss'
  'application/x-font-opentype'
  'application/x-font-truetype'
  'application/x-font-ttf'
  'application/x-httpd-cgi'
  'application/x-javascript'
  'application/x-mpegurl'
  'application/x-opentype'
  'application/x-otf'
  'application/x-perl'
  'application/x-ttf'
  'font/eot'
  'font/ttf'
  'font/otf'
  'font/opentype'
  'image/svg+xml'
  'text/css'
  'text/csv'
  'text/html'
  'text/javascript'
  'text/js'
  'text/plain'
  'text/richtext'
  'text/tab-separated-values'
  'text/xml'
  'text/x-script'
  'text/x-component'
  'text/x-java-source'
]
@allowed([
  true
])
@description('Compression should always be set to true')
param isCompressionEnabled bool = true
@description('For Azure global, the location is always "global"')
@allowed([
  'global'
])
param location string = 'global'
/* END PARAMETERS */
/* BEGIN VARIABLES */
// If createStaticWebAppSwitch is true, use the host name from the module staticWebApp
var originFqdn = createStaticWebAppSwitch ? createStaticWebApp.outputs.staticWebAppFqdn : defaultOriginFqdn 
var originName = 'cdnOrigin${uniqueString(resourceGroup().id)}'
var cdnProfileName = 'cdnProfile${uniqueString(resourceGroup().id)}'
/* END VARIABLES */
/* BEGIN MODULES */
module createStaticWebApp 'modules/deployStaticWebApp.bicep' = if (createStaticWebAppSwitch) {
  name: 'deployStaticWebApp'
}
/* END MODULES*/
/* BEGIN RESOURCES */
resource profile 'Microsoft.Cdn/profiles@2020-09-01' = {
  name: cdnProfileName
  location: location
  properties: {}
  sku: {
    name: cdnSku
  }
}
resource endpoint 'Microsoft.Cdn/profiles/endpoints@2020-09-01' = {
  parent: profile
  location: location
  name: cdnEndpointHostName
  properties: {
    originHostHeader: originFqdn
    isHttpAllowed: false
    isHttpsAllowed: true
    queryStringCachingBehavior: queryStringCachingBehavior
    contentTypesToCompress: contentTypesToCompress
    isCompressionEnabled: isCompressionEnabled
    origins: [
      {
        name: originName
        properties: {
          hostName: originFqdn
        }
      }
    ]
  }
}
/* END RESOURCES */
/* BEGIN OUTPUTS */
output originFqdn string = originFqdn
output cdnProfileName string = cdnProfileName
output cdnEndpointHostName string = cdnEndpointHostName
output cdnSku string = cdnSku
/* END OUTPUTS */
/* END TEMPLATE */

deployStaticWebApp.bicep

/* This template produces a bare-bones static web app. The web app has no deployment, uses the free tier and should not be used in production.
Alex Neihaus 2021-11-23
(c) 2021 Air11 Technology LLC -- licensed under the Apache OpenSource 2.0 license, https://opensource.org/licenses/Apache-2.0
	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at
	http://www.apache.org/licenses/LICENSE-2.0
	
	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
	
	Author's blog: https://www.yobyot.com
*/
@description('The name of the static webapp')
param staticWebAppName string = 'yourStaticWebAppName'
@description('The location in which to deploy the static web app')
param staticWebAppLocation string = 'eastus2'
resource createStaticWebApp 'Microsoft.Web/staticSites@2021-01-15' = {
  name: staticWebAppName
  location: staticWebAppLocation
  sku: {
    tier: 'Free'
    name: 'Free'
  }
  properties: {}
}
// Output the hostname of the bare bones static web app so it can be retrieved by the calling main.bicep template
output staticWebAppFqdn string = createStaticWebApp.properties.defaultHostname

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *