Translations: "Dutch" |
Linking Azure Deployment Stacks: Overcoming ARM Size Limits and Managing Dependencies
The Problem: Hitting ARM Template Size Limits
Two years ago, we built a Bicep deployment system that automatically creates alerts based on discovered resources in our Azure tenant. The architecture was clean: a single main.bicep
file orchestrated multiple modules, deployed fast, and worked great for smaller clients.
But as we scaled to larger clients, we hit a hard wall: the 4MB ARM template size limit. Despite using modules to keep our Bicep code organized, the compiled ARM templates grew massive due to the overhead that Bicep modules create in the final ARM JSON.
The Complete Mode Dilemma
Our deployment strategy relies on complete
mode deployments to ensure resource groups stay exactly in sync with our code. This creates a catch-22 when you need to split large deployments:
- Single large deployment: Hits the 4MB limit
- Multiple separate deployments: Last deployment wins in complete mode, removing resources from earlier deployments
- Incremental mode: Loses the guarantee that your resource group matches your code
We needed a way to split our deployment into multiple files while maintaining complete mode behavior across the entire resource group.
Enter Azure Deployment Stacks
Azure Deployment Stacks solve this exact problem. They let you:
- Deploy multiple templates to the same resource group while maintaining complete mode semantics
- Share outputs between stacks without complex parameter passing
- Manage dependencies between different parts of your infrastructure
Implementation: Main Stack with Shared Resources
Our main stack deploys shared infrastructure like action groups that other stacks need to reference:
1// Main Stack - Action Group
2@description('Environment name')
3param environment string = 'test'
4
5@description('Location for resources')
6param location string = resourceGroup().location
7
8@description('Action group name')
9param actionGroupName string = 'ag-${environment}-alerts'
10
11@description('Email address for notifications')
12param emailAddress string
13
14resource actionGroup 'Microsoft.Insights/actionGroups@2023-01-01' = {
15 name: actionGroupName
16 location: 'Global'
17 properties: {
18 groupShortName: 'alerts'
19 enabled: true
20 emailReceivers: [
21 {
22 name: 'emailReceiver'
23 emailAddress: emailAddress
24 useCommonAlertSchema: true
25 }
26 ]
27 }
28}
29
30@description('Action Group Resource ID')
31output actionGroupId string = actionGroup.id
32
33@description('Action Group Name')
34output actionGroupName string = actionGroup.name
Referencing Outputs in Sub-Stacks
The real power comes from referencing outputs directly from other deployment stacks:
1// Reference the main deployment stack to get outputs
2resource mainStack 'Microsoft.Resources/deploymentStacks@2024-03-01' existing = {
3 name: mainStackName
4}
5
6// Get outputs from main stack
7var mainStackOutput = mainStack.properties.outputs
8var actionGroupId = mainStackOutput.actionGroupId.value
9
10// Reference existing Action Group using output from main stack
11resource existingActionGroup 'Microsoft.Insights/actionGroups@2023-01-01' existing = {
12 name: last(split(actionGroupId, '/'))
13 scope: resourceGroup(split(actionGroupId, '/')[2], split(actionGroupId, '/')[4])
14}
Key Benefits
Size Limit Solution: Split large deployments across multiple stacks while staying under the 4MB limit per stack.
Complete Mode Preserved: Each stack can use complete mode, but they work together to manage the entire resource group.
Direct Output Reference: No need to pass outputs through parameters or external storage - reference them directly from other stacks.
Dependency Management: Deploy stacks in sequence, with later stacks automatically getting updated outputs from earlier ones.
Lessons Learned
Deployment stacks fundamentally change how you architect large Azure deployments. Instead of fighting ARM template size limits, you can design your infrastructure in logical chunks that work together seamlessly.
The ability to reference outputs directly between stacks eliminates the complexity of parameter chains and makes dependencies explicit and manageable.
Next post: I'll cover how we use Bicep User Defined Types (UDTs) to make this output-to-input mapping even cleaner and provide better IDE support.