This is Part 3 of a four-part series for Azure Spring Clean “Policy-as-Code Made Simple: Modern Azure Governance with Bicep”.
In Part 2 I introduced multi-line interpolated strings as one of the Bicep features that simplifies policy authoring. I want to spend more time on this feature specifically, because the syntax has a few details that are easy to get wrong, and the design choice behind how Bicep and ARM expressions interact inside these blocks is worth understanding properly.
Bicep 0.40.2 graduated multi-line interpolated strings to general availability. The feature itself is not complicated, but it sits at the intersection of two different expression engines (Bicep and ARM), and that boundary is exactly where policy authoring gets interesting.
Bicep has always supported single-line string interpolation:
var env = 'prd'
var region = 'uks'
var name = 'app-payments-${env}-${region}-001'
// Result: 'app-payments-prd-uks-001'
Before interpolation, Bicep also supports plain multi-line string literals using ''' (three apostrophes, no leading $). These are useful anywhere you need a long string without needing to embed Bicep expressions — @description() decorators are a common example, where the alternative is an awkward single-line string that wraps badly in an editor:
@description('''
Enforces a minimum TLS version on all Storage Accounts in scope.
Assign with the effect set to Audit in lower environments and Deny in production.
Does not apply to accounts excluded via the exemption mechanism.
''')
param minimumTlsVersion string = 'TLS1_2'
Multi-line interpolated strings extend this across multiple lines using $''' to open and ''' to close:
var env = 'prd'
var region = 'uks'
var owner = 'platform-team'
var configBlock = $'''
Environment : ${env}
Region : ${region}
Owner : ${owner}
'''
A few syntax rules worth internalising:
$''' — a dollar sign immediately followed by three apostrophes''' on its own line${...}, identical to single-line interpolation''' sets the baseline — Bicep strips that many leading spaces from every content line automaticallyThat last rule is the one I have tripped over most. If the closing ''' is indented by two spaces, Bicep strips two spaces from the start of every line. It means code formatted with normal indentation produces clean output without manual padding adjustments.
The capability that matters most for policy-as-code is the ability to inline a JSON policy rule and inject Bicep values into it at compile time. The example in Part 2 was deliberately minimal. Here is a more complete scenario.
This policy enforces a minimum TLS version on storage accounts. The minimum TLS version is locked in at deployment time via a Bicep parameter, so different initiatives can deploy different variants of the definition without duplicating the JSON. The policy effect is left as an ARM runtime parameter so it can be configured differently per assignment:
targetScope = 'subscription'
@description('Minimum TLS version to enforce. Baked in at deployment time.')
@allowed(['TLS1_2', 'TLS1_3'])
param minimumTlsVersion string = 'TLS1_2'
@description('Policy category for portal grouping.')
param policyCategory string = 'Storage'
// The policy rule mixes two kinds of expression:
// ${minimumTlsVersion} — resolved by Bicep at deployment time
// [[parameters('effect')] — passed through to ARM as a runtime reference
var policyRule = json($'''
{
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Storage/storageAccounts"
},
{
"anyOf": [
{
"field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"notEquals": "true"
},
{
"field": "Microsoft.Storage/storageAccounts/minimumTlsVersion",
"less": "${minimumTlsVersion}"
}
]
}
]
},
"then": {
"effect": "[[parameters('effect')]"
}
}
''')
resource storageTlsPolicy 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {
name: 'enforce-storage-tls-${toLower(minimumTlsVersion)}'
properties: {
displayName: 'Storage accounts must use HTTPS and ${minimumTlsVersion} or higher'
policyType: 'Custom'
mode: 'Indexed'
metadata: {
version: '1.0.0'
category: policyCategory
}
parameters: {
effect: {
type: 'String'
metadata: {
displayName: 'Effect'
description: 'The policy effect applied to non-compliant storage accounts'
}
allowedValues: [
'Audit'
'Deny'
'Disabled'
]
defaultValue: 'Deny'
}
}
policyRule: policyRule
}
}
There are two things I want to highlight here.
First, ${minimumTlsVersion} inside the $''' block is resolved by Bicep when the template is compiled. The resulting ARM JSON contains the literal value, for example "less": "TLS1_2". Deploy from prod.bicepparam with minimumTlsVersion = 'TLS1_3' and the policy definition will enforce TLS 1.3. Two different definitions, one template.
Second, [[parameters('effect')] is not a Bicep expression. The double opening bracket [[ tells Bicep to output a single literal [, so the compiled ARM JSON contains [parameters('effect')]. The ARM engine evaluates this expression at assignment time, not at deployment time. This is what allows the policy effect to be configured per assignment rather than being hardcoded when the definition is deployed.
The json() function wrapping the whole block converts the string to an object, which the policyRule property expects. Without it, Bicep would reject the assignment because string is not compatible with object.
$$''' Escape for ScriptsThere is a third delimiter variant that is less commonly needed but important for a specific scenario: embedding shell scripts or other $-heavy content inside a Bicep template.
In a standard $''' block, every ${...} is treated as a Bicep interpolation expression. If you are generating a bash script that itself uses shell variable syntax (${VAR}), those would be consumed by Bicep at compile time, which is not what you want.
The $$''' prefix changes the interpolation marker: Bicep expressions inside must use $${...}, and a single ${...} is treated as a literal string. This is particularly useful when building remediation scripts as part of a policy deployment:
param appName string // Bicep parameter — WILL be interpolated
param region string // Bicep parameter — WILL be interpolated
var deployScript = $$'''
#!/bin/bash
# Bicep params are interpolated with $${} syntax
APP_NAME="$${appName}"
REGION="$${region}"
# Shell variables use normal ${} — treated as LITERAL by Bicep
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE=${APP_NAME}_${TIMESTAMP}.log # <-- shell syntax, not Bicep
echo "Deploying ${APP_NAME} to ${REGION}"
echo "Log: ${LOG_FILE}"
'''
// Inside a $$''' block:
// $${appName} → becomes the VALUE of the Bicep param appName
// ${LOG_FILE} → stays as the literal string "${LOG_FILE}" (shell variable)
The things worth holding onto from this post:
''' for plain multi-line strings, $''' for multi-line interpolated strings, and $$''' when the content itself contains ${} syntax you need to preserve literally$''' block, ${expression} is resolved by Bicep at compile time; [[parameters('name')] is passed through to ARM and resolved at assignment time, they are evaluated by different engines entirely''' strings are useful well beyond policy authoring @description() decorators are a natural fit anywhere a long description would otherwise wrap badly as a single-line stringIn Part 4, I will move up a level and look at the module patterns that make policy-as-code reusable across teams and projects, covering initiative definitions that group related policy assignments.