Why IaC Matters Architecturally
Infrastructure as Code isn't just automation — it's an architectural control plane. Without it, you have:
- Configuration drift — prod diverges from what you think it is
- No review process — changes bypass pull requests and peer review
- No blast radius control — one click can delete everything
- No repeatability — recreating an environment means tribal knowledge
IaC as Architecture Governance
When infrastructure is code, you get: version control, pull request reviews, automated testing, deployment gates, and audit trails. This makes your infrastructure as governable as your application code.
ARM vs Bicep vs Terraform — Decision Framework
| Criterion | ARM JSON | Bicep | Terraform |
|---|---|---|---|
| Syntax | Verbose JSON | Clean, concise DSL | HCL (declarative) |
| Learning curve | High (JSON nesting) | Low (transpiles to ARM) | Medium |
| State management | None (stateless) | None (stateless) | Required (state file) |
| Multi-cloud | Azure only | Azure only | Any cloud |
| Day-0 support | Immediate | Immediate (same engine) | Delayed (provider updates) |
| Modularity | Linked templates | Modules + registries | Modules + registry |
| Ecosystem | Microsoft docs | Growing, Microsoft-backed | Massive (HashiCorp) |
| Best for | Legacy, auto-generated | Azure-only shops | Multi-cloud or existing Terraform orgs |
AZ-305 Exam Tip
The exam tests Bicep/ARM concepts (deployment scopes, what-if, template specs) — not Terraform. For exam prep, focus on Bicep. In real life, the choice depends on your org's multi-cloud strategy and existing tooling.
Architect's Rule of Thumb
- Azure-only + greenfield → Bicep (first-class Azure support, no state to manage)
- Multi-cloud or existing Terraform → Terraform (unified tooling across providers)
- Never write raw ARM JSON — Bicep transpiles to it; use Bicep as your authoring format
Bicep Deep Dive
Core Syntax
// Parameters with decorators
@description('The Azure region for deployment')
@allowed(['eastus', 'westeurope', 'southeastasia'])
param location string = resourceGroup().location
@minValue(1)
@maxValue(10)
param instanceCount int = 2
// Variables
var baseName = 'app-${uniqueString(resourceGroup().id)}'
// Resource with conditions
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: '${baseName}st'
location: location
kind: 'StorageV2'
sku: { name: 'Standard_ZRS' }
properties: {
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
// Loops
resource nsgRules 'Microsoft.Network/networkSecurityGroups/securityRules@2023-04-01' = [for (rule, i) in securityRules: {
name: rule.name
properties: rule.properties
}]
// Outputs
output storageId string = storageAccount.id
output storageName string = storageAccount.name
Key Language Features for Architects
- Decorators (
@allowed,@minValue,@secure) — enforce guardrails at template level - Conditions (
if) — deploy resources conditionally based on environment - Loops (
for) — create multiple resources from arrays - Existing resources (
existing) — reference resources you don't own without recreating - User-defined types — define complex parameter shapes for module interfaces
Deployment Scopes
Bicep/ARM deployments target a scope. The scope determines what you can deploy. This is a critical architecture concept — you deploy different things at different levels.
| Scope | Target | What You Deploy | CLI Flag |
|---|---|---|---|
| Resource Group | Existing RG | Resources (VMs, storage, VNets) | az deployment group create |
| Subscription | Existing subscription | Resource groups, policies, RBAC, budgets | az deployment sub create |
| Management Group | Existing MG | Policies, RBAC, sub-management-groups | az deployment mg create |
| Tenant | Entra ID tenant | Management groups, tenant-level policies | az deployment tenant create |
Platform Team vs App Team Scopes
- Platform team deploys at tenant/MG/subscription scope: governance, networking, shared services
- App teams deploy at resource group scope: their application resources
- This separation enforces the principle of least privilege in IaC
Modules, Registries & Versioning
Module Composition
// main.bicep — composes modules for a 3-tier app
module network './modules/network.bicep' = {
name: 'network-deploy'
params: {
vnetAddressSpace: '10.1.0.0/16'
location: location
}
}
module compute './modules/compute.bicep' = {
name: 'compute-deploy'
params: {
subnetId: network.outputs.appSubnetId
vmSize: 'Standard_D4s_v5'
instanceCount: instanceCount
}
}
module data './modules/database.bicep' = {
name: 'data-deploy'
params: {
subnetId: network.outputs.dataSubnetId
skuName: 'GP_Gen5_4'
}
}
Module Registry (Azure Container Registry)
// Consuming a module from a private registry
module network 'br:myregistry.azurecr.io/bicep/modules/network:v1.2.0' = {
name: 'network-deploy'
params: { ... }
}
// Publishing a module
// az bicep publish --file ./modules/network.bicep \
// --target br:myregistry.azurecr.io/bicep/modules/network:v1.2.0
Versioning Strategy
- Use semantic versioning: major.minor.patch
- Major = breaking changes (removed parameters, renamed outputs)
- Minor = new features (new optional parameters)
- Patch = bug fixes (no interface changes)
- App teams pin to major version; platform team owns the registry
Template Specs & Deployment Stacks
| Feature | Template Spec | Deployment Stack |
|---|---|---|
| What it is | Versioned ARM/Bicep template stored in Azure | Azure resource that tracks deployed resources |
| Purpose | Share curated templates with RBAC | Lifecycle management + drift protection |
| Drift detection | No | Yes — can deny out-of-band changes |
| Resource cleanup | Manual | Automatic — deletes resources removed from template |
| Deny settings | No | Yes — denyWriteAndDelete, denyDelete |
| Best for | Self-service catalog ("deploy this approved pattern") | Platform team managing landing zones with drift protection |
Deployment Stacks Are New (GA 2024)
Deployment Stacks solve the "I deleted a resource from my template but it still exists in Azure" problem. They track what was deployed and clean up orphaned resources. Expect this to appear on updated AZ-305 exams.
What-If & Rollback
What-If Deployments
# Preview changes before deploying
az deployment group what-if \
--resource-group rg-app-prod \
--template-file main.bicep \
--parameters @params.prod.json
What-if shows: Create, Delete, Modify, NoChange, Ignore. Use it in CI/CD as a mandatory gate before production deployments.
Rollback Strategies
- Redeploy previous version — IaC is idempotent; just redeploy the last-known-good commit
- Deployment Stacks — automatic cleanup when you roll back template changes
- Feature flags in parameters — toggle features without changing templates
- Blue-green at infra level — deploy new resource group, switch traffic, delete old
Multi-Environment Patterns
Parameter Files per Environment
├── main.bicep # Shared template
├── modules/ # Reusable modules
├── params.dev.json # Dev: small SKUs, single instance
├── params.staging.json # Staging: prod-like but fewer replicas
└── params.prod.json # Prod: full scale, HA, geo-redundancy
CI/CD Pipeline Pattern
Real-World: Platform Team IaC Governance at CloudScale Inc.
Scenario: Platform team serving 20 application teams. They need: consistent infrastructure patterns, security guardrails, and team autonomy for app-specific resources.
Architecture
- Module registry (ACR): Platform team publishes approved modules (vnet, aks-cluster, sql-server, storage-secure) with semantic versioning
- App teams compose their
main.bicepfrom registry modules — can't use raw resource definitions for governed resources - Deployment Stacks at subscription scope: Platform team manages landing zone resources with
denyDelete— app teams can't accidentally remove shared infra - CI/CD gates: All PRs run
bicep lint+what-if; prod deployments require platform team approval if the diff touches networking or identity - Template specs for self-service: App teams can "Deploy a new microservice" from a catalog that provisions RG + AKS namespace + Key Vault + monitoring
Result
Time-to-provision for a new service dropped from 2 weeks to 45 minutes. Zero drift incidents in production (Deployment Stacks + deny settings). App teams have autonomy within guardrails.
Knowledge Check
1. Your organization is Azure-only and wants the simplest IaC approach with no state file to manage. Which tool?
2. You need to deploy Azure Policy assignments across all subscriptions in a management group. What deployment scope do you use?
3. A platform team wants to prevent app teams from deleting shared networking resources deployed via IaC. What should they use?
4. What is the primary advantage of a Bicep module registry over local module files?
5. In a CI/CD pipeline, what should run BEFORE the approval gate for a production deployment?
Key Takeaways
- IaC is governance infrastructure — it enables review, audit, and blast radius control
- Bicep for Azure-only; Terraform for multi-cloud. Never write raw ARM JSON.
- Deploy at the right scope: platform team owns MG/sub, app teams own RG
- Module registries + semantic versioning = enterprise IaC at scale
- Deployment Stacks solve drift and orphan cleanup — the future of Azure IaC