Featured image of post Design Smarter with Bicep: Conditions, Loops, and Dependencies Explained

Design Smarter with Bicep: Conditions, Loops, and Dependencies Explained

Go beyond the basics and master advanced Bicep techniques. Learn how to build dynamic, scalable, and environment-aware templates using conditional logic, loops, and deployment dependencies—so you can move from writing templates to designing infrastructure systems.

Azure Bicep Advanced Techniques: Mastering Conditional Deployments, Loops, and Dependencies

Welcome back to the Azure Bicep journey. If you’ve followed along from the start, you’ve gone from writing basic templates to designing clean, modular infrastructure. You’ve learned to define parameters, use variables for clarity, and structure your deployments with reusable components. You’ve started thinking like an infrastructure architect—not just someone who writes code, but someone who designs systems.

Now it’s time to bring that foundation to life—with real-world fluency.

This is where we stop treating infrastructure as a static checklist—and start treating it as a living, adaptable system.
The features we explore in this article—conditions, loops, and dependencies—aren’t just technical tools. They’re how modern cloud architectures scale intelligently and adapt to their environment. And that’s exactly the kind of thinking we’ll sharpen in this next phase.

These are the tools that turn good templates into dynamic, composable cloud systems.

Here’s what you’ll learn:

  1. Deploy resources conditionally based on environment, configuration, or business logic
  2. Create multiple resources at once using for expressions to avoid repetition
  3. Manage dependencies cleanly and explicitly so resources deploy in the right order with minimal guesswork

We’ll go beyond syntax—you’ll learn why each pattern matters, when to use it, and how it fits into a broader way of thinking about cloud architecture.

If you’re part of a team, these patterns will help you understand and reason through infrastructure designed by others. If you’re leading deployments, they’ll give you the tools to evolve your own designs with clarity and purpose.

Ready to dive in? Let’s begin.

Conditional Deployments — Teaching Templates to Make Decisions

In production, you might need a premium SKU. In dev, the basic tier will do. In staging, you might spin up extra diagnostics—but skip them in live environments.

These variations are common. But managing them cleanly in a single template?
That’s where conditional deployments shine.

With just a single line of logic, you can instruct your Bicep templates to include or exclude resources based on parameters, environment, or any condition you define. Your template doesn’t just run—it responds to context.

Think of conditional deployments as a “choose your own adventure” path in your infrastructure.

The diagram below shows how your template can include or skip resources based on logic you define:

Conditional Deployment diagram

Here, the dotted line represents a conditional path between main.bicep and a resource. It means:
“Only deploy this if a certain condition is met.”

This isn’t just about skipping code—it’s about giving your infrastructure logic that matches your intent.

How It Works

In Bicep, you make a resource deployment conditional by adding an if clause directly to the declaration:

1
2
3
resource myResource 'Microsoft.Storage/storageAccounts@2021-06-01' = if (shouldDeploy) {
  // resource properties
}

This if (...) clause might look simple, but it’s powerful.
It tells Bicep to skip the entire resource unless the condition is true.
No dummy values, no feature flags—just clean logic and smart behaviour.

Real-World Example: Environment-Specific Logic

Let’s say you’re deploying a web application. You want a staging slot to exist in development and test—but never in production.

Here’s how you’d write that:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
param environment string = 'dev'

resource webApp 'Microsoft.Web/sites@2021-02-01' = {
  name: 'myWebApp'
  // other properties...
}

resource stagingSlot 'Microsoft.Web/sites/slots@2021-02-01' = if (environment != 'prod') {
  name: '${webApp.name}/staging'
  // slot-specific properties...
}

This template always deploys the main web app.
But the staging slot only gets created when environment is not 'prod'.

One parameter value. One logic check.
And suddenly, this template adapts cleanly to dev, test, and prod.

🧠 Architectural Insight

This is one of the clearest signs of an architectural mindset:
Separating configuration from behaviour.

Rather than hardcoding logic, drive decisions through parameters like environment.
This keeps your templates flexible, reusable, and easy to understand—without touching the file for every change.

✅ Best Practices for Conditional Deployments

  • Use clear and simple conditions. Avoid nesting or overly clever logic. Prioritise readability.
  • Drive logic with parameters. Use parameter-driven logic like if (environment == 'prod')—not hardcoded strings or magic values..
  • Document your intent. A quick comment like // Only deploy in non-production goes a long way in team settings.
  • Watch for dependent resources. Skipping a resource can break others if they expect it to exist. (We’ll look at how to manage this with dependencies soon.)
  • Yes—you can use conditions with modules too.
    Just like with resources, you can make a module deployment conditional by applying an if clause. This makes it easy to include or skip entire parts of your infrastructure—like diagnostics, monitoring, or networking—based on environment or feature flags. We’ll explore this in more detail later in the article.

Thinking Ahead

Conditional deployments help your templates adapt—without branching, duplication, or separate files.

And once your templates can think, the next step is helping them scale.
Let’s explore how to deploy multiple resources at once, cleanly and dynamically—with loops.

Loops — Deploying Multiple Resources, Cleanly and Dynamically

Ever duplicated the same resource block three times with tiny changes—just to deploy to dev, test, and prod?

It works… but it’s tedious, error-prone, and hard to maintain.
That’s not a personal failing—it’s a sign that your template is ready for something more powerful: loops.

Loops let you define once, deploy many.

You write a single resource block and let Bicep repeat it with variations—based on an array of values, a numeric range, or even a dynamic input from a parameter file.

The diagram below shows how your main.bicep file can define a repeatable pattern—and how Bicep turns that pattern into multiple concrete resources:
Think of it like stamping out consistent infrastructure, one variation at a time:

Loops diagram

How It Works

In Bicep, you can loop over values using a for expression inside your resource declaration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
param names array = [
  'dev'
  'test'
  'prod'
]

resource storageAccounts 'Microsoft.Storage/storageAccounts@2021-06-01' = [for name in names: {
  name: '${name}${uniqueString(resourceGroup().id)}'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}]

This loop creates three separate storage accounts, one for each value in the names array.

This replaces three nearly identical resource blocks—and makes it easier to:

  • Add new environments
  • Maintain consistency
  • Avoid human error

You’ve gone from copy-paste to repeatable, parameter-driven infrastructure.

🧠 Architectural Insight

If you’ve ever found yourself copying a resource block, tweaking a name, and pasting it again…
You’ve already discovered a pattern.
Loops are how you elevate that pattern into clean, declarative structure—one that grows without getting messy.

This mindset shift—recognising repetition as an opportunity—is what makes your infrastructure scalable by design.

Loop Types in Bicep

Bicep supports two primary loop types. Use them depending on your scenario:

  • Array Loops – iterate over specific values
    Example: ['dev', 'test', 'prod']

  • Range Loops – repeat a block N times
    Example: range(0, 3)

Here’s what a range loop looks like:

1
2
3
4
5
resource virtualMachines 'Microsoft.Compute/virtualMachines@2021-03-01' = [for i in range(0, 3): {
  name: 'vm${i}'
  location: resourceGroup().location
  // other properties...
}]

This creates three VMs—vm0, vm1, and vm2.
Great for test environments, burst workloads, or any repeated infrastructure that just needs a unique name.

✅ Best Practices for Loops

  • Use meaningful loop variables. Avoid cryptic names like i unless you’re doing simple indexing. Use env, name, or region instead.
  • Drive loop values from parameters. Don’t hardcode arrays unless you’re testing. Let users or environments control behaviour.
  • Keep logic readable. You can use if conditions inside loops—but if your logic gets messy, it’s a sign to split the pattern.
  • Don’t assume sequential deployment.
    Looped resources may deploy in parallel. If something depends on another, use dependsOn explicitly.

💡 We’ll dive deeper into dependency management in the very next section.

Thinking Ahead

Loops help your templates scale with intent. They let you say,
“Do this… again, and again—with variation.”

But scale isn’t just about quantity—it’s about coordination.
Some resources must come first. Others must wait.

Let’s look at how to manage the flow and timing of your deployments—with clarity, precision, and confidence.

Dependencies — Orchestrating the Flow of Your Deployments

So far, you’ve taught your templates to adapt (with conditionals) and scale (with loops).
Now comes the final piece of the puzzle: orchestrating the flow of deployment.

Some resources can be deployed in parallel.
Others must wait for something else to finish.

You can’t create a database inside a server until the server exists.
You can’t assign a private endpoint to a storage account until that account has been deployed.

This isn’t just about sequencing—this is about designing infrastructure that knows what depends on what, and behaves accordingly.

That’s where dependencies come in.

Visualising a Dependency

Think of dependencies as the connections between pieces of your infrastructure—lines of trust, timing, and sequence.

The diagram below shows one resource depending on another:

A diagram showing a dependency between Resource A and Resource B

Here, Resource A depends on Resource B.
That means A won’t even begin deploying until B is ready and completed successfully.

Bicep handles this sequencing behind the scenes when it can—but sometimes, you need to step in and define it yourself.

Let’s explore both approaches.

Implicit Dependencies — Let Bicep Do the Work

In most cases, you don’t need to write anything extra.
If one resource references another—by its name, ID, or property—Bicep automatically figures out the order and applies the correct sequencing.

Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
resource storage 'Microsoft.Storage/storageAccounts@2021-06-01' = {
  name: 'mystorage${uniqueString(resourceGroup().id)}'
  location: resourceGroup().location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
}

resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-06-01' = {
  name: '${storage.name}/default/logs'
  properties: {
    publicAccess: 'None'
  }
}

In this case, the blobContainer references storage.name.
That’s all Bicep needs to know the correct order—no dependsOn, no extra configuration.

💡 This is one of Bicep’s quiet superpowers.
It reduces the noise and lets your logic express itself naturally.
You describe relationships by referencing them—and Bicep handles the rest.

Explicit Dependencies — When You Need to Be Direct

Sometimes, the relationship between resources isn’t visible through a property.
Maybe you’re using conditionals, modules, or orchestrating components that don’t directly reference one another.

That’s when you step in and say, clearly:

“This resource must wait.”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
resource config 'Microsoft.Insights/components@2020-02-02' = {
  name: 'myAppInsights'
  location: resourceGroup().location
  kind: 'web'
  properties: {
    Application_Type: 'web'
  }
}

resource dashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = {
  name: 'monitoring-dashboard'
  location: resourceGroup().location
  dependsOn: [
    config
  ]
  // other properties...
}

Think of implicit dependencies as inferred trust.
Bicep sees the connection and handles it.
But sometimes, you need to write a manual instruction—like saying:
“This dashboard depends on telemetry being ready—even if it’s not obvious from the code.”

🧠 Architectural Insight

Overusing dependsOn is like shifting gears in a car that already has a great automatic transmission.
It works—but it’s extra effort you don’t always need.

A clean Bicep template leans on implicit logic wherever possible.
But when the relationship isn’t clear—or when using modules or conditionals—being explicit is not only okay, it’s essential.

Let the system do the heavy lifting, and intervene only when clarity or correctness calls for it.

✅ Best Practices for Managing Dependencies

  • Trust implicit logic. If Bicep sees a reference, it will enforce the correct order automatically.
  • Be explicit when necessary. Especially in complex templates with conditionals or loosely coupled resources.
  • Avoid circular references. If A depends on B, and B depends on A—your deployment will fail.
  • Group related resources logically. If several things depend on a shared component, like a VNet, consider placing them together.
  • Document your intent. If you’re using dependsOn, include a comment explaining why.

🔍 Ask yourself: Would this still work if I removed this dependsOn?
If yes—you probably don’t need it.

Thinking Ahead

With conditions, your templates can make decisions.
With loops, they can scale patterns.
And with dependencies, they orchestrate complex systems with clarity and precision.

You’re not just listing resources anymore.
You’re designing an infrastructure that adapts, scales, and coordinates—like a well-conducted symphony.

Now, let’s put it all together.
In the next section, we’ll walk through a real-world scenario that uses conditions, loops, and dependencies in a unified, practical template.

Putting It All Together — A Real-World Scenario

You’ve learned how to adapt your templates with conditionals, scale them with loops, and control flow with dependencies.
Now it’s time to think like a system designer.

This isn’t just an example. It’s your first step toward building infrastructure that’s reusable, modular, and architected with intent.

The Scenario: A Multi-App, Multi-Environment Deployment

You’re working on a small platform team managing shared infrastructure for internal projects.

Each project requires:

  • A shared App Service Plan
  • Multiple Web Apps for different stages (dev, test, prod)
  • A single, shared SQL Server and Database
  • Optional Application Insights—but only in production, where monitoring really matters

Your goal is to build a single Bicep template that:

  • Scales across app environments using a loop
  • Deploys App Insights only when needed
  • Coordinates shared resources with dependencies
  • Outputs key values for integration into downstream systems

In short: you’re building a real-world system, not just deploying resources.

How the Techniques Interact

Technique Purpose
Loop Deploy multiple apps cleanly
Condition Control App Insights based on context
Dependency Ensure plan, DB, and telemetry are ready when needed

Step 1: Looping Over Web Apps

Let’s start with a loop to deploy web apps for dev, test, and prod—each using the same shared plan.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
param appNames array = [
  'webapp-dev'
  'webapp-test'
  'webapp-prod'
]

resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: 'sharedAppPlan'
  location: resourceGroup().location
  sku: {
    name: 'B1'
    tier: 'Basic'
  }
}

resource webApps 'Microsoft.Web/sites@2021-02-01' = [for name in appNames: {
  name: name
  location: resourceGroup().location
  properties: {
    serverFarmId: appServicePlan.id
  }
}]

This defines a reusable pattern that adapts based on input, while anchoring every app to a shared plan.

Step 2: Conditionally Deploying Application Insights

Application Insights is crucial for monitoring in production, but we might skip it in dev/test to reduce noise and cost.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
param isProd bool = true

resource appInsights 'Microsoft.Insights/components@2020-02-02' = if (isProd) {
  name: 'shared-insights'
  location: resourceGroup().location
  kind: 'web'
  properties: {
    Application_Type: 'web'
  }
}

This condition ensures telemetry is added only when it adds value—and keeps the rest of the template untouched.

Step 3: Shared SQL Server and Database

Let’s define a shared SQL Server and a database that all apps can use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
resource sqlServer 'Microsoft.Sql/servers@2021-05-01-preview' = {
  name: 'shared-sql-server'
  location: resourceGroup().location
  properties: {
    administratorLogin: 'adminuser'
    administratorLoginPassword: 'P@ssw0rd123!' // Hardcoded for demo—use Key Vault in real deployments!
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2021-05-01-preview' = {
  name: 'shared-db'
  parent: sqlServer
  location: resourceGroup().location
}

⚠️ The admin password is hardcoded here for simplicity—but in production, you’d parameterise this or pull it securely from Azure Key Vault.

Step 4: Outputs and Orchestration

Finally, let’s return the important pieces that other systems or pipelines might need.

1
2
output hostnames array = [for app in webApps: app.properties.defaultHostName]
output connectionString string = 'Server=tcp:${sqlServer.name}.database.windows.net;Database=${sqlDatabase.name}'

Bicep handles the dependencies automatically here:

  • Web Apps depend on the App Service Plan → ✅
  • SQL DB depends on the SQL Server → ✅
  • App Insights is conditional → deployed only in production

No need for dependsOn unless you’re managing complex or decoupled scenarios. Let Bicep do the sequencing for you.

🧠 Design Reflections

This isn’t just a demo—it’s a platform pattern.

  • Loops give you scale without duplication
  • Conditions make your infrastructure context-aware
  • Dependencies ensure things happen in the right order
  • Outputs connect your templates to the rest of your deployment lifecycle

You’re not building a file—you’re designing modular infrastructure that adapts and evolves.

Want to Make It Yours?

Try one of these next steps to take this design from solid to scalable:

  • Turn the looped app block into a reusable module
  • Secure the SQL credentials via Key Vault integration
  • Move the shared infrastructure to a base layer and use nested deployments
  • Add dynamic tags per environment using for and object literals

Each one deepens your fluency and gives you more confidence with real-world Bicep design.

Onward

With just a few techniques, you’ve gone from defining infrastructure to orchestrating systems.
You’re not just writing Bicep—you’re designing infrastructure that adapts, scales, and speaks for itself.

You’ve got everything you need. Let’s bring it all home in the conclusion.

Conclusion & What’s Next

Think back to when you first began this journey.
Back then, Bicep was just syntax, and infrastructure felt like merely a list of resources.

Now, look at you—you haven’t just learned Bicep; you’ve completely transformed how you think about building cloud systems.

You’ve grown from individual features—parameters, variables, modules—to a unified, orchestrated approach to infrastructure, using:

  • Conditionals to make your templates adaptive to real-world scenarios.
  • Loops to scale your deployments effortlessly.
  • Dependencies to orchestrate complex infrastructure precisely.
  • And finally, a real-world scenario that pulled all these skills together into a cohesive design.

But beyond syntax and features, you’ve practiced the mindset of an infrastructure designer—someone who writes code not just to provision, but to model systems with clarity, reuse, and intent.

What Comes Next?

If this series marks your graduation from “Bicep user” to “system designer,” here’s how to keep the momentum going:

  • Build Reusable Modules
    Start small—identify one resource you repeatedly deploy. Turn it into your first reusable module, share it with your team, and watch your impact multiply.

  • Integrate with Pipelines
    Automate a template you built with GitHub Actions or Azure DevOps. Witness firsthand how fast your infrastructure moves from code to reality.

  • Apply Governance
    Try adding tags or enforcing naming standards in an existing deployment. You’ll quickly realise governance becomes second nature when embedded directly into your templates.

  • Use Module Registries
    Publish your best modules internally via a Bicep registry or Azure Container Registry. Watch them spread through your team or organisation, lifting everyone’s productivity.

Each step will deepen your mastery, multiply your impact, and elevate your work from tactical scripting to strategic system orchestration.

Final Reflections

Infrastructure-as-Code is no longer about “writing resources.” It’s about designing systems that can evolve with your team, your workloads, and your business.

You now have the mindset of an infrastructure architect—someone who sees not just the pieces, but the patterns.

So whether you’re:

  • Reviewing a complex template and reasoning through its flow
  • Designing new systems for scale
  • Teaching others to structure infrastructure with confidence…

Remember:

🧠 Clarity beats complexity. Reusability scales. Structure is strategy.

This isn’t the end of your journey.
It’s the beginning of what you can build—with intent, with confidence, and with Bicep.

Thank you 🙌 for joining me on this journey. And happy authoring. You’ve got this. 💪