ARM Templates, Bicep, and the Azure Developer CLI
This section bridges the full course into deployable outputs. You will learn Azure Resource Manager and Bicep as the infrastructure foundation, the Azure Developer CLI (azd) as the packaging layer that combines infrastructure, code, and CI/CD into a single deployable unit, and then deploy and explore real security tools using these patterns. By the end of the lab you will have deployed a working security testing framework using a single azd up command.
azure.yaml, /infra, /srcazd up commandLearning journey: package once, deploy repeatedly through the five-stage pipeline.
Azure Resource Manager (ARM) is the management and deployment layer for Azure resources. Azure portal, Azure CLI, Az PowerShell, Bicep, and ARM templates all rely on it for control-plane operations against https://management.azure.com/.
Understanding ARM explains:
https://management.azure.com/ are needed for infrastructure automationMicrosoft.Automation, Microsoft.Logic, Microsoft.Web, Microsoft.KeyVaultEvery resource you create in the portal is a PUT request to an ARM REST API endpoint. The endpoint format is:
PUT https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProvider}/{resourceType}/{resourceName}?api-version={version}
When something fails during deployment, the ARM REST API returns an error code and message. Understanding the ARM layer helps you interpret error messages from az deployment group create and from the Azure portal's deployment history.
ARM is the shared control plane for Azure management and deployment tooling.
Resource providers and types: every Azure resource lives under a provider/type hierarchy.
ARM templates are JSON documents that declare the desired state of Azure resources. They are the original Infrastructure as Code format for Azure.
Four core authoring sections:
{
"parameters": {
"automationAccountName": {
"type": "string",
"metadata": { "description": "Name of the Automation Account" }
}
},
"variables": {
"location": "[resourceGroup().location]"
},
"resources": [
{
"type": "Microsoft.Automation/automationAccounts",
"apiVersion": "2023-11-01",
"name": "[parameters('automationAccountName')]",
"location": "[variables('location')]",
"identity": { "type": "SystemAssigned" },
"properties": { "sku": { "name": "Basic" } }
}
],
"outputs": {
"principalId": {
"type": "string",
"value": "[reference(parameters('automationAccountName'), '2023-11-01', 'full').identity.principalId]"
}
}
}
ARM JSON is verbose: a simple Automation Account with a managed identity and a Reader role assignment takes approximately 80 lines with nested resourceId() functions and dependsOn arrays. Understanding its structure is useful because Bicep compiles to it, and ARM deployment errors reference JSON structure.
ARM JSON declares desired state; Bicep compiles to the same engine.
ARM JSON is verbose by design: explicit dependencies, nested references, concat() everywhere.
Idempotent deployment: same template, same outcome - regardless of current state.
What-if preview: every change flagged before deployment runs.
Bicep is a Domain Specific Language that compiles to ARM JSON. It provides a much more readable and maintainable authoring experience.
Bicep authors cleaner; the same ARM engine deploys.
The same Automation Account in Bicep:
param automationAccountName string
param location string = resourceGroup().location
resource automationAccount 'Microsoft.Automation/automationAccounts@2023-11-01' = {
name: automationAccountName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
sku: {
name: 'Basic'
}
}
}
output principalId string = automationAccount.identity.principalId
Bicep advantages over ARM JSON:
.bicep filesresourceId() functionsconcat()Bicep modules allow you to compose large deployments from reusable files:
// main.bicep
module automationModule 'modules/automation.bicep' = {
name: 'automationDeploy'
params: {
accountName: 'my-automation-account'
location: location
}
}
A common Bicep pattern is to separate concerns into modules such as compute, storage, and RBAC. In azd-maester, the exact /infra layout varies by solution folder, so inspect the selected solution instead of assuming every template uses the same module breakdown.
Bicep module pattern: parent calls child modules with params, collects outputs.
Bicep VS Code IntelliSense: live schema, valid API versions, required properties surfaced inline.
Running the same Bicep template twice produces the same result without errors. If the resource already exists, ARM updates it to match the desired state. If it does not exist, ARM creates it. This makes Bicep templates safe to run repeatedly and ideal for CI/CD pipelines.
Before running a deployment, preview what it will create, modify, or delete:
az deployment group what-if \
--resource-group myRG \
--template-file main.bicep \
--parameters automationAccountName=my-aa
Output shows:
- Green: new resources to create
- Yellow: existing resources to modify
- Red: resources to delete
Run what-if before every first deployment of a new template.
The Azure Developer CLI bundles infrastructure-as-code (Bicep), application code, and CI/CD pipeline configuration into a single deployable, shareable template.
azd up: one command replaces three error-prone manual phases.
Three-part azd template structure: azure.yaml manifest, /infra for Bicep, /src for code.
Install:
winget install microsoft.azd # Windows
brew tap azure/azd && brew install azd # macOS
| Command | Purpose |
|---|---|
azd init -t <template> |
Clone a template from GitHub |
azd auth login |
Authenticate with Azure |
azd provision |
Deploy infrastructure only (Bicep) |
azd deploy |
Deploy application code only |
azd up |
Provision infrastructure and deploy code in one step |
azd down |
Tear down all provisioned resources |
azd pipeline config |
Configure GitHub Actions or Azure DevOps WIF pipeline |
azd env list |
List environments (dev, staging, prod) |
In azd-maester, the repository root is a template chooser. Each solution folder has its own deployable azure.yaml. The recommended automation-account solution looks like this:
name: maester-automation-account
metadata:
template: maester-automation-account@0.0.1
infra:
provider: bicep
path: infra
workflows:
up:
- azd: provision
hooks:
preup:
shell: pwsh
run: ./scripts/Run-AzdPreUp.ps1
interactive: true
preprovision:
shell: pwsh
run: ./scripts/Run-AzdPreProvision.ps1
interactive: true
postprovision:
shell: pwsh
run: ./scripts/Run-AzdPostProvision.ps1
interactive: true
predown:
shell: pwsh
run: ./scripts/Run-AzdPreDown.ps1
interactive: true
The repository-root azure.yaml is intentionally not deployable: it runs a guard script that tells you to cd into automation-account, container-app-job, function-app, or azure-devops before running azd up.
azure.yaml: name, infra, and hooks describe the deployable solution.
azd deployment phases: init, auth, provision, deploy, plus pipeline config and teardown shortcuts.
Core azd command set: azd up combines provision and deploy, the rest support the lifecycle.
Running azd pipeline config automatically:
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID)..github/workflows/.This is the same WIF pattern from Section 4, automated for the azd deployment context. The generated subject claim is scoped to the specific repository.
azd pipeline config automates WIF setup: federated credential replaces client secrets entirely.
Three implementations ensure break-glass accounts remain excluded from all Conditional Access policies:
emergency-access-exclusion.json): runs on a schedule to check and remediate CA policy exclusions. Best for: regular remediation with minimal operational complexity.emergency-access-exclusion-sentinel.json): triggers only when a CA policy change event fires in Sentinel. More efficient and near-real-time.emergency-access-exclusion.ps1): runs in an Automation Account, schedulable or webhook-triggered.All implementations use managed identity with scoped Graph API permissions to read and modify CA policies.
Source: https://github.com/nathanmcnulty/nathanmcnulty/tree/main/Entra/emergency-access
Automation patterns for Entra Entitlement Management:
Demonstrates: Graph Identity Governance API, access package creation and assignment, attribute-driven access control without manual IT intervention.
Source: https://github.com/PatriotConsultingTech/Community/tree/main/Webinars/2025/AccessManagement
Intune Remediations (detection + remediation PowerShell script pairs) function as a lightweight endpoint telemetry pipeline. A detection script collects Defender for Endpoint performance metrics from each managed endpoint and uploads to Log Analytics or Azure Blob Storage via REST API - no additional agent required.
Two upload patterns:
- Log Analytics Data Collector API with workspace shared key
- Azure Monitor Logs Ingestion API (DCR-based) for schema-validated ingestion
This demonstrates that endpoints are API clients like any cloud automation workload - the token and REST API patterns from this course apply at the endpoint layer.
Source: https://github.com/nathanmcnulty/nathanmcnulty/tree/main/DefenderForEndpoint/Performance
This advanced pattern enables delegated-user-style automation with phishing-resistant credentials for APIs that require user context rather than app-only access.
Two components:
Key Vault-backed FIDO2 passkeys: instead of a hardware security key, the ECDSA private key material lives in Azure Key Vault. An Automation Account with managed identity access retrieves the key material and performs FIDO2/WebAuthn authentication headlessly.
- Initialize-PasskeyKeyVault.ps1: configures the vault with the required EC key type
- Register-KeyVaultPasskeyViaTAP.ps1: registers the passkey using a Temporary Access Pass
- PasskeyLogin.ps1: performs headless authentication using the vault-stored key
XDRInternals PowerShell module: wraps Defender XDR portal internal APIs for automated advanced hunting, incident management, and security operations tasks not yet exposed through the official Graph Security API.
Key Vault holding the passkey private key carries credential-store obligations.
The Key Vault containing the passkey private key is functionally equivalent to the user's credentials. Anyone with access to that Key Vault can authenticate as that user. Apply strict RBAC, audit logging, and Privileged Identity Management to both the Key Vault and the automation account that accesses it.
Source: https://github.com/nathanmcnulty/nathanmcnulty/tree/main/Entra/passkeys/keyvault
Source: https://github.com/MSCloudInternals/XDRInternals
Maester is a PowerShell security testing framework built on Pester with 286+ automated configuration tests for Microsoft 365 and Entra, mapped to MITRE ATT&CK. The azd-maester project packages Maester into four ready-to-deploy azd templates:
All templates use managed identities - no secrets. After you cd into one of the four solution folders, one azd up command deploys that solution's infrastructure, permissions, and scheduling.
azd-maester: four deployment options, one command, same managed-identity foundation.
What azd-maester deploys: Automation Account, MI, Graph permissions, schedule - one command.
Source: https://github.com/nathanmcnulty/azd-maester
Automation that sends real-time notifications to users and security teams when:
Demonstrates: Entra audit log monitoring via Log Analytics or Event Hubs, Logic App or Function App triggered on audit events, Graph API enrichment with device and user details, Teams adaptive card delivery.
Automation to ensure UAL settings are correct across the tenant:
Demonstrates: Security and Compliance PowerShell, Exchange Online PowerShell, and scheduled Automation Account runbooks for compliance state validation.
Have Azure Developer CLI, Azure CLI, Git, PowerShell 7, and Contributor access to the target subscription or lab resource group available. This lab follows the upstream function-app quickstart: initialize the template, stay inside function-app, let the interactive azd up wizard create the environment, then inspect the deployed Function App and cleanup hooks.
Initialize azd-maester with azd init -t nathanmcnulty/azd-maester, move into the function-app solution, deploy it with azd up, re-run azd up with additional options to show that the deployment is idempotent, inspect the Bicep files and azd hook outputs that drove the deployment, and then cleanly remove the environment with azd down.
az --version)azd version)azd auth login in the training tenantAzure Developer CLI:
Initialize from the template, move into the solution folder, and sign in
azd init -t nathanmcnulty/azd-maester Set-Location .\azd-maester\function-app azd env new lab-maester-func azd auth login
If azd init prompts for an environment name at the repository root, enter any disposable placeholder such as lab-maester-root. The deployable function-app folder keeps its own local azd environment, so azd env new lab-maester-func is the environment that the rest of this lab uses.
The repository root is intentionally not deployable. If you run azd up from the root, the guard script stops the run and tells you to choose a solution folder. Stay inside function-app for the rest of the lab.
Expected outcomes:
- The template is cloned locally into .\azd-maester
- The deployable function-app folder now has a selected local azd environment named lab-maester-func
- You are authenticated to Azure Developer CLI for the current tenant
- Your shell is now scoped to the deployable function-app solution
Azure Developer CLI:
Run the interactive quickstart deployment
azd up # Review the environment values and generated artifacts after deployment finishes. azd env get-values Get-ChildItem .\outputs
NoNoNoNoExpected outcomes:
- azd up runs the solution's pre-up, pre-provision, provision, post-provision, and deploy workflow from the function-app folder
- azd env get-values shows the environment-scoped resource names and identifiers
- The outputs folder contains the setup summary produced by the solution hooks
This task demonstrates idempotent deployment behavior. Re-run azd up in the same environment, keep the existing Function App resources, and add only the optional Web App and Azure RBAC pieces.
Azure Developer CLI and Azure CLI:
Prepare the rerun inputs and execute the second deployment
$resourceGroupName = (azd env get-values | Select-String '^AZURE_RESOURCE_GROUP=').Line.Split('=')[1].Trim('"') $resourceGroupId = az group show --name $resourceGroupName --query id -o tsv $securityGroupObjectId = "<lab-security-group-object-id>" azd env set AZURE_RBAC_SCOPES $resourceGroupId azd up
Yes$securityGroupObjectIdNoNoYes$resourceGroupId if promptedExpected outcomes:
- The second azd up is idempotent: unchanged Function App resources stay in place while the optional Web App and Azure RBAC settings are added
- The environment now carries AZURE_RBAC_SCOPES for future reruns and pipeline use
- The rerun proves that azd updates the declared environment instead of creating a second parallel stack
This solution is additive across reruns. Turning an include from No to Yes adds the new resources and assignments without breaking the existing deployment. Turning an include back to No later does not revoke previously granted access; cleanup happens during azd down.
Native PowerShell:
Inspect the Bicep files and timer trigger that drove the deployment
Get-ChildItem .\infra -Recurse -Filter *.bicep | Select-Object FullName Get-Content .\infra\main.bicep Get-Content .\src\MaesterTimerTrigger\function.json
AZURE_RESOURCE_GROUP.Functions, Identity, and the deployed app settings.Expected outcomes:
- The resource group contents line up with the Bicep file structure
- The Function App uses managed identity rather than a stored secret or certificate
- The timer trigger schedule in function.json explains when Maester runs after deployment
Native PowerShell:
Inspect the scripts and generated files that azd used around the deployment
Get-ChildItem .\scripts | Select-Object Name Get-ChildItem .\outputs | Select-Object Name Get-Content .\scripts\Invoke-FunctionValidation.ps1 -TotalCount 80
Expected outcomes:
- You can identify the hook and deployment script files that azd calls before and after provisioning
- The validation script shows how the project checks the Function App after deployment
- The generated setup summary in outputs\<env>-setup-summary.md gives you a readable record of what the solution configured
Azure Developer CLI:
Remove the Azure resources and the local azd environment state
$environmentName = "lab-maester-func" azd down -e $environmentName --force --purge azd env remove $environmentName --force
Expected outcomes:
- The resource group and Function App resources are deleted cleanly
- The solution's best-effort cleanup hooks run before resource deletion
- The local azd environment state is removed after azd env remove
function-app or the guard script will stop you.azd auth login is skipped or expires, azd up cannot finish the wizard or deployment.No. Turning them on requires extra values or additional privileges.azd up with different include flags is additive. It does not automatically revoke earlier permissions or delete optional resources you already created.azd down removes Azure resources, but azd env remove deletes the local environment record separately.centralus, which was the validated classroom region.Course key takeaways: the five principles to apply directly in production.
azd up do in a single command?azd pipeline config configure automatically?azd up is one command for a complete solution: infrastructure, code, and configuration.what-if before every first deployment of a new template.azd down cleanly removes everything - use it to avoid orphaned resources from labs and pilots.Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:
All links below were reviewed on 2026-03-10. Microsoft Learn and Microsoft documentation are the primary sources. Community and project references are included where they add operational context or point to actively maintained tooling.
Section 6 - Packaging Tools and azd - Reference Guide