AZ-104 · Skill Domain

Infrastructure as Code

Modular Bicep templates, bootstrap patterns, container deployments, secret management via Key Vault, and a production-grade portfolio site — all built to CAF naming conventions with zero stored credentials.

4 Projects
8 Bicep Modules
0 Stored Credentials
P11–P15 Phase 4
01

Projects

11
Bicep · IaC
Modular Bicep Infrastructure
Bootstrap pattern · 8 modules · policy-enforced tags · secret injection via .bicepparam

Replaced ad-hoc CLI scripts with a modular Bicep stack orchestrated by main.bicep. A bootstrap shell script handles pre-flight work that Bicep cannot — resource group creation, Key Vault secret seeding, and subnet delegation — then hands off to Bicep for all infrastructure resources. Every deploy is idempotent; --what-if runs before every apply.

networking.bicep
VNet, subnets, NSGs
vm-linux.bicep
Ubuntu VM + NIC
vm-win.bicep
Windows VM + NIC
storage.bicep
Storage account
acr.bicep
Container Registry
appgw.bicep
Application Gateway
bastion.bicep
Bastion + condition flag
ca.bicep / cae.bicep
Container Apps
main.bicep — orchestration entry point az104-bootstrap.sh — pre-flight script
dev.bicepparam — secret injection
using 'main.bicep'

param adminPassword = az.getSecret(
  '343a8a7e-...',
  'rg-az104-dev-wus3-01',
  'kv-az104-dev-wus3-01',
  'vm-admin-password'
)

param tags = {
  env: 'dev'
  owner: 'geoste'
  managed-by: 'bicep'
}
  • Bootstrap + Bicep split — Bootstrap handles what Bicep cannot: RG creation (Bicep can't create its own target RG), KV secret seeding, subnet delegation flags. Bicep owns all infrastructure resources.
  • Secrets via getSecret(), never in templates.bicepparam resolves secrets from Key Vault at deploy time. Credentials never appear in template files, parameter files, or CLI args.
  • Policy-enforced tag inheritancerequire-tag-env and require-tag-owner deny assignments on the RG. inherit-tag-* policies push tags from RG to all child resources automatically. Manual tagging is inconsistent and breaks.
  • Bastion condition flagdeployBastionHost bool parameter controls whether the host deploys. Standard SKU costs ~$140/mo; delete between lab phases.
  • guid() with stable inputs — Role assignment names use guid() seeded with static param values, not module outputs. Module outputs aren't known at deployment start.
⚠ Gotcha — Windows computerName limit

Azure resource names can be any valid length but Windows OS hostnames are hard-limited to 15 characters. vm-win-dev-wus3-01 is 18 chars and fails at provisioning. Pass computerName as a separate explicit parameter — never rely on take(vmName, 15) for multi-VM deployments as it produces duplicate hostnames.

⚠ Gotcha — .bicepparam no interpolation

Cannot reference other parameters within the same .bicepparam file. param tags = { env: env } fails — use literals only.

Bicep Azure CLI Key Vault Azure Policy Managed Identity Bash PowerShell
12
Containers · ACR · ACA
Container Apps Deployment
ACR · Container Apps Environment · blue/green traffic split · scale to zero

Built a complete container delivery pipeline: images built and pushed to Azure Container Registry, deployed via Bicep to an internal Container Apps Environment on a delegated /23 subnet. Two image versions deployed simultaneously with an 80/20 traffic split — demonstrating blue/green rollout patterns without downtime. Scale-to-zero configured with minReplicas: 0.

Traffic split — CLI post-deploy
az containerapp ingress traffic set \
  --name ca-dev-wus3-01 \
  --resource-group rg-az104-dev-wus3-01 \
  --revision-weight \
    latest=80 \
    ca-dev-wus3-01--v1=20

Container Apps Environment sits on snet-capp-dev-wus3-01 (10.0.8.0/23, delegated to Microsoft.App/environments). Internal ingress only — no public endpoint. ACR admin access disabled; all pulls use AcrPull role on the managed identity.

View architecture diagram — project12-architecture.svg
  • activeRevisionsMode: Multiple — required for traffic splitting between revisions. Single mode replaces the active revision on every deploy — no split possible.
  • minReplicas: 0 — scale to zero when idle. First request cold-starts in seconds. Eliminates compute cost during inactive periods.
  • Traffic weights via CLI, not Bicep — revision names are auto-generated at deploy time and unknown to Bicep. Set weights post-deploy via CLI; attempting to set them in Bicep requires hardcoding revision names that don't exist yet.
  • User-assigned MI preferred over system-assigned — system-assigned identity only exists after the Container App is created, creating a role-assignment timing window. User-assigned MI is created as infrastructure ahead of time with AcrPull pre-assigned.
⚠ Gotcha — Subnet delegation required

snet-capp-dev-wus3-01 must be delegated to Microsoft.App/environments before the Container Apps Environment can deploy. Minimum subnet size /23 (512 addresses). Delegation cannot be added after subnet creation if resources are attached.

Azure Container Registry Container Apps Bicep Docker Managed Identity RBAC
14
Key Vault · Managed Identity
Zero-Credential Secret Management
RBAC model · system-assigned MI · KV references in App Service · audit via KQL

Deployed Key Vault with RBAC authorization model, assigned system-managed identities to both VM and App Service, and wired App Service configuration to pull secrets via KV reference strings — no credentials stored anywhere in code or config. Diagnostic logs routed to Log Analytics; KQL queries used to audit every secret access event.

KV reference string in App Service config
@Microsoft.KeyVault(
  VaultName=kv-az104-dev-wus3-01;
  SecretName=vm-admin-password
)
KQL — secret access audit
AzureDiagnostics
| where ResourceType == "VAULTS"
| where OperationName == "SecretGet"
| project TimeGenerated,
          CallerIPAddress,
          OperationName,
          ResultType
| order by TimeGenerated desc
View architecture diagram — project14-architecture.svg
  • RBAC model, never Access Policies — KV created with --enable-rbac-authorization true. Access Policies are deprecated for new vaults. RBAC provides per-secret granularity with full audit trail in Entra ID.
  • Two separate access grants required — (1) creator needs Key Vault Secrets Officer to write secrets; (2) ARM needs --enabled-for-template-deployment true to resolve getSecret() in Bicep. Neither is implied by the other.
  • System-assigned MI at creation time--assign-identity "[system]" passed during resource creation. Post-creation assignment introduces a timing window where RBAC assignment may fail before the identity GUID propagates in Entra ID.
  • KV reference strings — use Cloud Shell — The @ and parentheses in KV reference values cause PowerShell parsing failures even inside quotes. Always use Cloud Shell Bash for az webapp config appsettings set with KV references.
⚠ Gotcha — Soft delete blocks name reuse

Deleted secrets are retained for 7 days by default. Attempting to reuse a secret name during the retention window fails. Purge explicitly: az keyvault secret purge --vault-name kv-az104-dev-wus3-01 --name secret-name

Key Vault Managed Identity RBAC App Service Log Analytics KQL Bicep
15
Capstone · Production
Portfolio Site Infrastructure
Front Door · WAF · Static Web App · 5-module Bicep · GitHub Actions CI/CD · ostebovik.net

The capstone project is this site. A production-grade Azure stack deployed entirely via Bicep: Front Door with WAF policy, Static Web App with GitHub Actions CI/CD, Key Vault, storage account for static assets, and Log Analytics with Application Insights. Custom domain ostebovik.net with managed TLS certificate — all provisioned from a single az deployment group create command.

monitoring.bicep
LAW + App Insights
keyvault.bicep
RBAC · soft-delete · purge protection
storage.bicep
Static assets
staticwebapp.bicep
SWA + GitHub CI/CD
frontdoor.bicep
CDN · WAF · custom domain · TLS
main.bicep — orchestration entry point frontdoor.bicep — CDN, WAF, custom domain, TLS keyvault.bicep — RBAC model, soft-delete, purge protection monitoring.bicep — Log Analytics + App Insights staticwebapp.bicep — SWA + GitHub Actions CI/CD storage.bicep — static asset storage
Deploy from bootstrap.sh
az deployment group create \
  --resource-group rg-geoste-prod-wus3-01 \
  --template-file main.bicep \
  --parameters prod.bicepparam \
  --what-if
  • Front Door as perimeter, not App Gateway — Front Door operates at the edge (global CDN + WAF), appropriate for a static site with no backend compute. App Gateway sits inside a VNet and is designed for internal load balancing — wrong tool at this scale.
  • SWA serves from GitHub, not storage — Static Web App requires content in a GitHub repo; Storage Account holds referenced assets (diagrams, screenshots). Push to main → GitHub Action → deployed in under 60 seconds.
  • GitHub Actions workflow created manuallyskipGithubActionWorkflowGeneration: false requires Azure write access to the repo at deploy time. If not granted, the workflow file is never auto-generated. Added manually to .github/workflows/ with deployment token in repo secrets.
  • Managed TLS certificatecertificateType: 'ManagedCertificate' on the Front Door custom domain. Free, auto-renewed — no certificate management overhead.
⚠ Gotcha — Storage account global uniqueness

stprodwus301 and stgeostwus301 were both taken across all Azure tenants globally. Always include an owner-specific string. Final working name: stgeostewus301.

Azure Front Door WAF Static Web App Bicep GitHub Actions Key Vault Application Insights Log Analytics
02

Lessons Learned

Bicep is idempotent — use it
Re-running after a partial failure is safe. Bicep skips resources that already exist and creates only what's missing. Always run --what-if first to see exactly what will change before committing.
CRLF kills shell scripts
VS Code on Windows saves files with \r\n. Cloud Shell expects \n only. Symptom: $'\r': command not found on every line. Fix: sed -i 's/\r//' script.sh. Prevention: set "files.eol": "\n" globally in VS Code.
Policy remediation needs a wait
Remediation tasks fail immediately after policy assignment because the managed identity needs 30–60 seconds to propagate through Entra ID. Wait 60 seconds, use unique names for retry attempts. Fallback: az resource tag --is-incremental.
Local Bash corrupts KV reference strings
The @ symbol and parentheses in Key Vault reference values cause parsing failures in local PowerShell and Git Bash. Always use Azure Cloud Shell for any command passing KV reference strings as flag values.
dependsOn has legitimate uses
When Bicep can't infer a dependency because the dependent resource doesn't reference the dependency in its properties — Bastion host depending on NSG rules, Front Door route depending on origin — explicit dependsOn is correct, not a code smell.
Inline NSG rules solve the Bastion compliance check
NSG rules defined inside securityRules: [...] on the NSG resource deploy atomically with it. Separate child rule resources deploy in parallel and may not be complete when the compliance check runs. The deployBastionHost bool condition flag exists for cost control (~$140/mo Standard SKU) — not to work around a deployment ordering problem.
Container App role assignment timing
System-assigned MI only exists after the Container App is created, so an AcrPull role assignment in the same deployment pass fails. The condition flag pattern gates it to a second pass. The cleaner fix: user-assigned MI created ahead of time with the role pre-assigned — no timing dependency.