Translations: "Dutch" |

Linking Azure Deployment Stacks: Overcoming ARM Size Limits and Managing Dependencies

Share on:

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:

  1. Deploy multiple templates to the same resource group while maintaining complete mode semantics
  2. Share outputs between stacks without complex parameter passing
  3. 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.


Reactions

comments powered by Disqus (not working in Firefox)

Receive the monthly update

* Indicates required

Related Posts