Bicep Tips and Tricks | #7 | From Static to Dynamic Config
Overview
One of my core goals when writing IaC templates is ensuring reusability of common components, resources, and in this case, configuration. More often than not, I see configuration that is broadly common between resources (except for one or two properties) being duplicated throughout templates. This duplication means that changing a single property value requires updates across the entire codebase—a change that’s not trivial to manage unless you’re well-versed with the codebase and understand all areas where the configuration is implemented.
This pattern becomes particularly problematic as your infrastructure grows in complexity. Consider scenarios where you need to update a common tag value, modify a naming convention, or adjust a configuration parameter across dozens of resources. Without proper abstraction, these seemingly simple changes can become error-prone maintenance nightmares.
In this post, I’ll demonstrate two powerful Bicep techniques to transform static, duplicated configuration into dynamic, reusable patterns that will make your templates more maintainable and less prone to configuration drift.
Common Configuration Scenarios
Before diving into solutions, let’s examine two common scenarios where static configuration creates maintenance headaches:
Scenario 1: Resource Tagging
Every Azure resource should be properly tagged for governance, cost tracking, and management. However, I frequently see templates where tags are copy-pasted across resources, creating maintenance debt:
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = {
// ... other properties
tags: {
BusinessUnit: 'BicepTipsAndTricks'
Version: deployment().properties.template.contentVersion
Environment: environment
Region: region
ResourceName: 'Storage Account'
'hidden-title': 'Bicep Tips and Tricks Storage'
}
// ... other properties
}
This approach works for a single resource, but imagine maintaining this across 50+ resources. When the business unit name changes or you need to add a new compliance tag, you’re looking at a significant refactoring effort.
Scenario 2: Complex Configuration Structures
Complex JSON configurations, such as Consumption Logic App workflow definitions, often contain embedded values that need to vary between environments or deployments:
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Condition_-_Morning_or_Afternoon": {
"actions": {
"Terminate_-_Afternoon": {
"inputs": {
"runStatus": "Succeeded"
},
"runAfter": {},
"type": "Terminate"
}
},
"else": {
"actions": {
"Terminate_-_Morning": {
"inputs": {
"runStatus": "Succeeded"
},
"runAfter": {},
"type": "Terminate"
}
}
},
"expression": {
"and": [
{
"greaterOrEquals": [
"@utcNow('H:mm:ss')",
"12:00:00" // ← Currently hard-coded static configuration
]
}
]
},
"runAfter": {},
"type": "If"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {},
"triggers": {
"Recurrence_-_Start": {
"evaluatedRecurrence": {
"frequency": "Day",
"interval": 1, // ← Currently hard-coded static configuration
"schedule": {
"hours": [
"11"
]
}
},
"recurrence": {
"frequency": "Hour",
"interval": "2" // ← Currently hard-coded static configuration
},
"type": "Recurrence"
}
}
},
"parameters": {}
}
In this Consumption Logic App workflow, the recurrence interval, evaluatedRecurrence interval, and condition time values are hard-coded. If you need different intervals or values for different environments (perhaps more frequent polling in production), you’d need to maintain separate configuration files or manually edit values during deployment.
My Recommended Approaches
Approach 1: Union Function for Object Composition
The union()
function is perfect for combining base configuration with resource-specific overrides. This approach works exceptionally well for scenarios like tagging, where you have a core set of common properties and some resource-specific additions.
The Problem: Without union, you end up repeating common tags across every resource:
// Bad: Repeated configuration
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = {
tags: {
BusinessUnit: 'BicepTipsAndTricks'
Version: deployment().properties.template.contentVersion
Environment: environment
Region: region
ResourceName: 'Storage Account'
'hidden-title': 'Bicep Tips and Tricks Storage'
}
}
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
tags: {
BusinessUnit: 'BicepTipsAndTricks' // Duplicated!
Version: deployment().properties.template.contentVersion // Duplicated!
Environment: environment // Duplicated!
Region: region // Duplicated!
ResourceName: 'Key Vault'
'hidden-title': 'Bicep Tips and Tricks Key Vault'
}
}
The Solution: Use union to combine base and specific configurations:
// Good: Centralized common configuration
// ** Parameters **
@description('The environment to deploy to')
param environment string = 'dev'
@description('The Azure region to deploy to')
param region string = 'UkSouth'
// ** Variables **
var coreTags = {
BusinessUnit: 'BicepTipsAndTricks'
Version: deployment().properties.template.contentVersion
Environment: environment
Region: region
}
// Create resource-specific tag combinations
var storageAccountTags = union(coreTags, {
ResourceName: 'Storage Account'
'hidden-title': 'Bicep Tips and Tricks Storage'
})
var keyVaultTags = union(coreTags, {
ResourceName: 'Key Vault'
'hidden-title': 'Bicep Tips and Tricks Key Vault'
DataClassification: 'Confidential' // Additional tag specific to Key Vault
})
// ** Resources **
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = {
name: 'mystorageaccount'
location: region
tags: storageAccountTags
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: 'mykeyvault'
location: region
tags: keyVaultTags
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
}
}
Key Benefits:
- Single source of truth: Common tags are defined once in
coreTags
- Easy maintenance: Update business unit name? Change it in one place
- Flexible: Each resource can still have unique tags
Approach 2: Token Replacement for Complex Configurations
For complex JSON configurations that need dynamic values, token replacement provides an elegant solution. This approach is particularly powerful when working with imported JSON files that contain configuration.
The Problem: Static values embedded in complex configurations:
The Solution: Use token placeholders and replacement functions.
First, modify your JSON configuration file to use tokens:
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Condition_-_Morning_or_Afternoon": {
"actions": {
"Terminate_-_Afternoon": {
"inputs": {
"runStatus": "Succeeded"
},
"runAfter": {},
"type": "Terminate"
}
},
"else": {
"actions": {
"Terminate_-_Morning": {
"inputs": {
"runStatus": "Succeeded"
},
"runAfter": {},
"type": "Terminate"
}
}
},
"expression": {
"and": [
{
"greaterOrEquals": [
"@utcNow('H:mm:ss')",
"__schedule_time__" // ← Token for schedule time
]
}
]
},
"runAfter": {},
"type": "If"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {},
"triggers": {
"Recurrence_-_Start": {
"evaluatedRecurrence": {
"frequency": "Day",
"interval": 1,
"schedule": {
"hours": [
"__schedule_hour__" // ← Token for schedule hour
]
}
},
"recurrence": {
"frequency": "Hour",
"interval": "__interval__" // ← Token for interval
},
"type": "Recurrence"
}
}
},
"parameters": {}
}
Then, create a robust token replacement system in your Bicep template:
// ** Imported Types and Functions **
@description('Token Replacement Definition')
@sealed()
@export()
type TokenReplacement = {
@description('Token to be replaced')
token: string
@description('Replacement value')
replacement: string
}
@description('String Tokens Replacement Function')
@export()
func stringTokensReplacement(stringValue string, tokenReplacements TokenReplacement[]) string =>
reduce(tokenReplacements, stringValue, (current, next) => replace(string(current), next.token, next.replacement))
// ** Parameters **
@description('The interval for the Logic App trigger')
param logicAppInterval int = 2
@description('The schedule hour for the Logic App trigger')
param scheduleHour int = 11
@description('The schedule time for condition comparison')
param scheduleTime string = '12:00:00'
@description('The environment for deployment')
param environment string = 'dev'
// ** Variables **
var logicAppConsumptionWorkflow = loadJsonContent('./logicAppConsumptionWorkflow.json')
// Define all token replacements in one place
var tokenReplacements = [
{
token: '__interval__'
replacement: string(logicAppInterval)
}
{
token: '__schedule_hour__'
replacement: string(scheduleHour)
}
{
token: '__schedule_time__'
replacement: scheduleTime
}
]
// Apply all token replacements
var processedWorkflowDefinition = json(stringTokensReplacement(
string(logicAppConsumptionWorkflow),
tokenReplacements
))
// ** Resources **
resource logicApp 'Microsoft.Logic/workflows@2023-12-01' = {
name: 'my-dynamic-logic-app-${environment}'
location: resourceGroup().location
properties: {
definition: processedWorkflowDefinition.definition
parameters: processedWorkflowDefinition.parameters
}
}
Key Benefits:
- Environment-specific values: Different intervals for dev/test/prod
- Centralized configuration: All replacements defined in one place
- Scalable: Easy to add new tokens as requirements grow
- Version control friendly: JSON templates remain clean and readable
Summary
Transforming static configuration into dynamic, reusable patterns is essential for maintainable Infrastructure as Code. Use the union()
function when combining common configuration with resource-specific properties (like tags), and token replacement for complex JSON configurations that need dynamic values injected. Both approaches centralize configuration management, reduce duplication, and make your Bicep templates more maintainable across environments. The next time you find yourself copy-pasting configuration, choose the right pattern and eliminate that technical debt!
For the original version of this post see Andrew Wilson's personal blog at Bicep Tips and Tricks | #7 | From Static to Dynamic Config