🌐 Building Multi-Region Azure Infrastructure with Bicep — Lessons from the Deep End

🌐 Building Multi-Region Azure Infrastructure with Bicep — Lessons from the Deep End


For the past few weeks, I’ve been digging into Bicep, Azure’s domain-specific language for infrastructure-as-code. I wanted to go beyond basic single-region deployments and challenge myself to build something more real-world: a reusable, modular, and multi-region infrastructure setup that could scale and adapt across environments.

This post is a breakdown of what I built, the decisions I made, and some of the issues I ran into along the way. Hopefully future-me — or anyone else doing something similar — finds it useful.




🧱 What I Built

At a high level, this project deploys:

  • App Services in three Azure regions
  • Azure SQL logical servers + databases, one per region
  • Virtual Networks with frontend/backend subnets, per region
  • Conditional resources based on environment (dev vs prod)
  • Outputs that summarize everything once deployed

The structure is fully modular — each major component (App, DB, VNet) lives in its own Bicep module, and the main file orchestrates deployments across regions using loops.




🧠 Key Decisions & Learnings



1. Start Modular

I knew early on I didn’t want to write one giant .bicep file, so I broke everything into separate modules:

  • app.bicep: App Service + App Plan
  • database.bicep: SQL Server + SQL DB (+ auditing in prod)
  • vnet.bicep: Region-specific VNets with subnet definitions

This made debugging easier, but also gave me flexibility — I could iterate on one module at a time and test it in isolation.




2. Dynamic, Secure Parameters

No hardcoded secrets. I marked my SQL credentials as @secure() parameters. I didn’t integrate Key Vault (yet), but the setup is ready for it.

Names like sqlServerName and appName use uniqueString(resourceGroup().id) to ensure uniqueness across environments and reruns. That little pattern saved me a ton of headaches.




3. Multi-Region with Bicep Loops

Once I had a list of regions:

param locations array = [
  'westus'
  'eastus2'
  'westus2'
]
Enter fullscreen mode

Exit fullscreen mode

…I used Bicep’s for loop pattern to deploy everything in each one:

module appModule 'modules/app.bicep' = [for (location, i) in locations: {
  name: 'app-${location}'
  ...
}]
Enter fullscreen mode

Exit fullscreen mode

This was one of the coolest parts. Each iteration gets a unique name, and I use the index (i) to generate IP ranges and subnet prefixes per region.




4. Environment-Based Logic

I wanted different behaviors in dev vs prod:

  • prod uses a higher App Service SKU (P1v2)
  • prod turns on SQL auditing and deploys a Storage Account
  • dev skips that extra auditing config

This was done with simple conditional flags:

var auditingEnabled = environment == 'prod'
Enter fullscreen mode

Exit fullscreen mode

It’s a simple trick, but makes the modules way more flexible.




5. Location Consistency

One of the earliest bugs I ran into: SQL Servers and Databases have to be in the same region, or the deployment fails. I made sure every module uses the same location parameter passed from the main file — defaulting to the resource group’s location unless explicitly overridden.




📦 Outputs That Help You Test Fast

Each module returns outputs, and the root template summarizes them:

output appSummary array = [for (location, i) in locations: {
  region: location
  url: appModule[i].outputs.appServiceUrl
}]
Enter fullscreen mode

Exit fullscreen mode

I even added https:// directly in the App Service output so I could just click the link right after deploying and test immediately. Small thing, big productivity boost.




⚠️ Things I Messed Up (and Fixed)

  • Hardcoded resource names → broke everything on the second run. Fixed by using uniqueString.
  • Forgot to loop VNets → initially deployed just one VNet, then added a loop to create one per region with indexed IP spaces.
  • Inconsistent locations → SQL resources failed until I ensured all related resources shared the same location.
  • Tried to over-optimize too early → once I relaxed and just focused on clean patterns, everything clicked into place faster.



🛠️ What I Want to Add Next

  • [ ] Private endpoints for SQL
  • [ ] Key Vault integration
  • [ ] GitHub Actions workflow for CI/CD
  • [ ] CDN module for static asset distribution
  • [ ] Cost estimation output (maybe?)



🧵 Closing Thoughts

This project gave me a much deeper appreciation of Bicep. It feels like the right balance of abstraction and control. It’s declarative, but you can still think programmatically when you need to — like looping, conditional logic, and dynamic outputs.

More than anything, it taught me how real-world infra needs to be flexible, modular, and repeatable — and how a little structure upfront saves a lot of time later.

If you’re experimenting with Bicep or looking to level up your Azure IaC skills, I’d definitely recommend trying a multi-region setup like this. It forces you to think about naming, location, outputs, and reusability in a really practical way.

If this was helpful or if you’ve got Bicep ideas to share, come say hey on LinkedIn. Let’s learn together! 🚀



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *