Click-ops monitoring dies the moment someone asks "can we reproduce this in another environment?" Bicep is the answer.

I've inherited monitoring setups built by hand. You know the type — someone clicked through the Azure Portal, created alert rules one by one, configured action groups by memory, and documented the process as "I know how it works."

Then that person left. And the monitoring became a black box that nobody dared to touch.

This is why I wrote the entire monitoring infrastructure as Bicep modules. Every DCR. Every alert rule. Every action group. Every custom log table. Every workspace. All of it — code.

Why Bicep, Not Terraform

I love Terraform. Use it daily. But for Azure-native monitoring resources, Bicep has an unfair advantage: it speaks Azure's language natively.

Terraform's Azure provider is a translation layer. It takes your HCL, converts it to ARM API calls, and hopes the mapping is correct. Usually it is. But with monitoring resources — Data Collection Rules, scheduled query rules, diagnostic settings — there are edge cases where Terraform's abstraction leaks.

Bicep compiles directly to ARM templates. No translation layer. No edge cases. What you write is what Azure gets. And when Microsoft releases a new monitoring feature, Bicep supports it on day one. Terraform might take months.

For this project — 31 alert rules, 5 DCR types, 8 action groups, custom log tables — Bicep was the right tool.

The 7 Modules

I built 7 composable Bicep modules that together deploy the complete monitoring stack:

1. Resource Group — simple, but important. Environment-aware naming. Tags. Location. The foundation.

2. Log Analytics Workspace — where all the data lands. Retention policies. Access control. The single pane of glass that every other module depends on.

3. DCR - VM Insights — performance metrics and dependency mapping for all VMs. CPU, memory, disk, network — the basics that should be non-negotiable in any monitoring setup.

4. DCR - Logs — the split strategy. Separate DCR definitions for Windows generic, Windows DC-specific, Linux, SQL, and application-specific logs. Each scoped to exactly the servers that need it.

5. Custom Log Tables — for SQL Server error logs, application logs, and anything that doesn't fit the standard schema. Defined as code, versioned, reproducible.

6. Alert Rules — 31 scheduled query rules with KQL queries, severity levels, evaluation frequencies, and auto-mitigation. Every alert is parameterized: environment prefix, thresholds, action group assignments.

7. Action Groups — 8 team-specific notification channels. Email routing, escalation paths, all defined as structured data in parameter files.

The Two-Stage Deployment

Here's something most Bicep tutorials skip: dependency ordering matters.

You can't create alert rules that query a Log Analytics Workspace that doesn't exist yet. You can't assign DCRs to VMs if the DCR deployment hasn't completed. You can't create custom log tables before the workspace is ready.

So the deployment is two-stage:

Stage 1: Infrastructure. Resource group, workspace, DCRs, custom tables. The foundation. This runs first and must complete before anything else.

Stage 2: Alerting. Alert rules and action groups. These reference the workspace and tables from Stage 1. They deploy only after infrastructure is confirmed.

Could I have used dependsOn in a single deployment? Sure. But two explicit stages give you a clear checkpoint. Stage 1 fails? You know it's infrastructure. Stage 2 fails? You know it's alerting logic. No guessing.

Parameter Files Are the Real Configuration

The Bicep modules are generic. They don't know about DEV or PROD. They don't know about specific servers or thresholds. That's what parameter files are for.

Each environment gets its own parameter file:

params/monitoring-infrastructure-dev.parameters.json
params/monitoring-infrastructure-prod.parameters.json
params/alert-rules-dev.parameters.json
params/alert-rules-prod.parameters.json

Same modules. Different parameters. DEV gets relaxed thresholds and test email addresses. PROD gets tight thresholds and real team distribution lists.

This is the key insight: modules are logic, parameters are configuration. Separate them, and you can deploy the same monitoring stack to any environment without changing a single line of Bicep code.

The KQL Inside

Each alert rule contains a KQL query. And KQL queries embedded in Bicep parameters are... not pretty. They're strings. Long, escaped, sometimes multi-line strings that make your parameter files hard to read.

But they're versioned. They're reviewed. They're deployed consistently. And when someone asks "what does alert X actually check?" — you read the parameter file. Not the Azure Portal. Not someone's Slack message from three months ago. The code.

A typical pattern:

Event
| where TimeGenerated > ago(5m)
| where EventLog == 'System'
| where Source == 'Service Control Manager'
| where EventID == 7036
| where RenderedDescription contains 'ServiceName'
  and RenderedDescription contains 'stopped'
| summarize Count = count() by Computer

Simple. Specific. Testable. And when you need to change it — you change it in the parameter file, deploy, done. No Portal clicking.

The Reproducibility Test

The ultimate test of IaC: can you tear it down and rebuild it from scratch?

Yes. Stage 1, Stage 2, done. Same monitoring stack. Same alert rules. Same action groups. Same custom tables. Identical to what was running before, because it's the same code.

Try that with click-ops monitoring. I dare you.

What I'd Do Differently

If I started over:

  1. Use Bicep modules from the Azure Verified Modules registry for the standard resources. I wrote some modules from scratch that already existed in better form.

  2. Template the KQL queries instead of embedding them as strings. A separate query library with unit tests would catch syntax errors before deployment.

  3. Add deployment validation as a third stage. Not just "did it deploy?" but "does alert X actually fire when condition Y is met?" Automated monitoring-of-monitoring.

But the core approach — modular Bicep, parameterized environments, two-stage deployment — that's solid. It's been running in production for months. Zero drift. Zero "someone changed it in the Portal."

7 modules. Complete monitoring stack. One az deployment command.

That's Infrastructure as Code done right.