Securing Applications and Automation - Training Series
Section 6 of 6
Reference Guide

Packaging Tools and azd

ARM Templates, Bicep, and the Azure Developer CLI


Section
Section 6 - Packaging Tools and azd
Document Type
Reference Guide
Format
Instructor-led with guided lab
Audience
Infrastructure and security administrators

Section purpose

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.

Learning Objectives

Deployment Pipeline 1 Write .psm1, .psd1, and Pester tests 2 Package module manifest and NuGet package 3 Sign Authenticode certificate and hash check 4 Publish private feed or PowerShell Gallery 5 Deploy Install-Module or CI/CD pipeline rollout package once, deploy repeatedly

Learning journey: package once, deploy repeatedly through the five-stage pipeline.

Azure Resource Manager

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:

ARM REST API

Every 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.

Portal CLI / PS SDK / API Azure Resource Manager management.azure.com RBAC · Authentication · Throttling Subscription / Resource Group Resource providers register here Token scope: management.azure.com Graph tokens (graph.microsoft.com) are separate Azure RBAC governs ARM · OAuth consent governs Graph

ARM is the shared control plane for Azure management and deployment tooling.

Azure Subscription Microsoft.Automation automationAccounts aa-secops-prod runbooks Microsoft.KeyVault vaults kv-secops-prod secrets keys Microsoft.Web sites (Function Apps) func-secops-prod serverfarms (App Service Plans) Provider not registered? → ARM deployment fails before creating anything ARM errors reference: provider / type / apiVersion / property path

Resource providers and types: every Azure resource lives under a provider/type hierarchy.

ARM Templates (JSON)

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 "resources": [{ "type": "Microsoft.Automation/... "apiVersion": "2023-11-01", "dependsOn": [ "[resourceId(... "principalId": "[reference(res... "roleDefinitionId": "[concat... /subscriptions/[subscription... /providers/Microsoft.Auth... ~80 lines • nested resourceId() calls Bicep resource aa 'Microsoft.Automation/ automationAccounts@2023-11-01' = { identity: { type: 'SystemAssigned' } } resource role 'Microsoft.Auth/ roleAssignments@2022-04-01' = { principalId: aa.identity.principalId } ~25 lines • symbolic refs • IntelliSense

ARM JSON declares desired state; Bicep compiles to the same engine.

arm-template.json (~80 lines) "type": "Microsoft.Automation/..." "apiVersion": "2023-11-01", "dependsOn": [ "[resourceId('Microsoft.KeyVault/...')]" ], explicit deps "principalId": "[reference(...).identity" .principalId]", nested ref() "roleDefinitionId": "[concat( ... )]", "/subscriptions/" + subscription().id concat() about 80 lines of JSON for one Automation Account and RBAC setup

ARM JSON is verbose by design: explicit dependencies, nested references, concat() everywhere.

Deploy ARM Template ARM Template Resource exists? No Created Yes Matches config? Yes Unchanged No Updated Same desired state - always

Idempotent deployment: same template, same outcome - regardless of current state.

PS> New-AzResourceGroupDeployment -WhatIf -TemplateFile main.bicep What-if output + Microsoft.Automation/automationAccounts will be created ~ Microsoft.KeyVault/vaults/myVault sku: Standard -> Premium - Microsoft.Storage/storageAccounts/old will be deleted = 3 resource(s) unchanged Always run what-if on new or modified templates No resources change during a what-if run. Deletions and modifications are flagged before deployment.

What-if preview: every change flagged before deployment runs.

Bicep

Bicep is a Domain Specific Language that compiles to ARM JSON. It provides a much more readable and maintainable authoring experience.

ARM JSON "resources": [{ "type": "Microsoft.Automation/... "apiVersion": "2023-11-01", "dependsOn": [ "[resourceId(... "principalId": "[reference(res... "roleDefinitionId": "[concat... /subscriptions/[subscription... /providers/Microsoft.Auth... ~80 lines • nested resourceId() calls Bicep resource aa 'Microsoft.Automation/ automationAccounts@2023-11-01' = { identity: { type: 'SystemAssigned' } } resource role 'Microsoft.Auth/ roleAssignments@2022-04-01' = { principalId: aa.identity.principalId } ~25 lines • symbolic refs • IntelliSense

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 modules

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.

main.bicep parent template compute.bicep Automation Account storage.bicep Storage + reports rbac.bicep Role assignments module outputs flow back to parent · chain to next module Separation of concerns Compute · Storage · Identity · RBAC Reusable & testable Pass params · get outputs Common pattern; inspect the chosen solution's /infra for exact layout

Bicep module pattern: parent calls child modules with params, collects outputs.

main.bicep - VS Code + Bicep extension 1 resource aa 'Microsoft.Automation/ 2 automationAccounts@ 2023-11-01 (latest) 2022-08-08 2021-06-22 3 name: aaName 4 location: location 5 identity: { 6 type: 'SystemAssigned' required property surfaced inline by schema 7 } ARM API schema pulled live resource types, API versions, and required properties ARM JSON (manual) little guidance, easy to miss details Bicep + IntelliSense schema-guided authoring in the editor

Bicep VS Code IntelliSense: live schema, valid API versions, required properties surfaced inline.

Idempotent deployments

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.

What-if deployments

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.

Azure Developer CLI (azd)

The Azure Developer CLI bundles infrastructure-as-code (Bicep), application code, and CI/CD pipeline configuration into a single deployable, shareable template.

single command azd up Phase 1 Provision - Bicep deploys Azure resources Phase 2 Deploy - push application code Phase 3 Pipeline - configure CI/CD (WIF) Before azd az deployment group create + deploy code separately + configure GitHub manually 3 steps · error-prone With azd azd up All three phases in sequence Repeatable · shareable

azd up: one command replaces three error-prone manual phases.

my-solution/ (azd root) azure.yaml (service map) /infra (Bicep templates) main.bicep, modules/ /src (app code) runbooks/, functions/ Maps services to infra and codeazd reads this to know what to deploy Infrastructure as Code (Bicep)Defines resources, RBAC, identities Application codeRunbooks, Functions, scripts azd up = provision + deployOne command deploys everything ✓

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

Core command set

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)

azure.yaml structure

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: maester-automation-account infra: provider: bicep path: infra hooks: preup: shell: pwsh run: ./scripts/Run-AzdPreUp.ps1 postprovision: run: ./scripts/Run-AzdPostProvision.ps1 deploy from a solution folder, not the repo root preup preprovision predown Hook lifecycle preprovision -> provision -> postprovision predeploy -> deploy -> postdeploy

azure.yaml: name, infra, and hooks describe the deployable solution.

azd init-t templateClone template azd authloginAuthenticate azd provision(Bicep deploy)Create resources azd deploy(push code)Deploy app code ✓ Runningin AzureFull solution live azd up = provision + deploy ↑ azd down = teardown azd pipeline config = WIF setup

azd deployment phases: init, auth, provision, deploy, plus pipeline config and teardown shortcuts.

azd init -t template Clone template azd auth login Authenticate azd provision Bicep → ARM Create resources azd deploy Push code Deploy app code ✓ Running in Azure Full solution live azd up = provision + deploy Additional Commands azd down Tear down all provisioned resources azd pipeline config Configure WIF for GitHub/DevOps CI/CD azd monitor Open Application Insights dashboard azd init → azd auth login → azd up → done Three commands to go from zero to a running solution - shareable with anyone

Core azd command set: azd up combines provision and deploy, the rest support the lifecycle.

azd pipeline config

Running azd pipeline config automatically:

  1. Creates an app registration or uses an existing one.
  2. Configures a federated credential with a scoped subject claim for your GitHub repository.
  3. Sets repository variables (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID).
  4. Generates a workflow YAML file in .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.

runs once azd pipeline config App Registration Entra ID identity for the pipeline Federated Credential scoped to repo · no client secret Repository Variables set AZURE_CLIENT_ID · AZURE_TENANT_ID · AZURE_SUBSCRIPTION_ID Workflow YAML generated azure/login with WIF · zero secrets in repo No client secrets - anywhere GitHub Actions authenticates via OIDC token exchange subject claim scoped to the specific repository

azd pipeline config automates WIF setup: federated credential replaces client secrets entirely.

Example solutions

Emergency Access CA Exclusion Management

Three implementations ensure break-glass accounts remain excluded from all Conditional Access policies:

  1. Scheduled Logic App (emergency-access-exclusion.json): runs on a schedule to check and remediate CA policy exclusions. Best for: regular remediation with minimal operational complexity.
  2. Sentinel-triggered Logic App (emergency-access-exclusion-sentinel.json): triggers only when a CA policy change event fires in Sentinel. More efficient and near-real-time.
  3. PowerShell runbook (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

Identity Governance and Entitlement Management

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 and Log Analytics data pipeline

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

Key Vault-backed passkeys and XDRInternals

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:

  1. 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

  2. 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 contains the passkey private key Treat it as a credential store access controls, audit logs, and purge protection all apply Key Vault RBAC with least-privilege access soft-delete and purge protection enabled diagnostic logs sent to Log Analytics alerts on unexpected key or secret access private key in Key Vault = credential handling obligations

Key Vault holding the passkey private key carries credential-store obligations.

⚠️Risk - Key Vault as Credential Equivalent

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

azd + Maester

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 deployment options Automation Account Recommended System-Assigned MI No secrets · PS 7.x runtime Container App Job Runs on schedule Managed identity Function App HTTP / timer trigger Managed identity Azure DevOps Pipeline agent Service connection 286+ security tests MITRE ATT&CK mapped findings actionable posture report one command cd solution → azd up → infra + perms + schedule Microsoft 365 · Entra ID · Intune · Teams continuous security posture monitoring

azd-maester: four deployment options, one command, same managed-identity foundation.

command azd up Resource Group Automation Account PowerShell 7.x runtime Hosts Maester runbook System-Assigned MI No secrets, no certificates Created with AA Graph API Permissions Assigned via post-provision hook Read-only: Directory, Policy, etc. Scheduled Trigger Maester Security Report 286+ tests / MITRE ATT&CK mapped Defined across azure.yaml, infra/main.bicep, and postprovision hooks azure.yaml infra/main.bicep scripts/Run-AzdPostProvision.ps1

What azd-maester deploys: Automation Account, MI, Graph permissions, schedule - one command.

Source: https://github.com/nathanmcnulty/azd-maester

New Device and MFA Notifications

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.

Unified Audit Log monitoring

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.

Lab readiness notes

🧪Lab Readiness

Have Azure Developer CLI, Git, Azure CLI, a subscription/resource group, and the prepared azd-maester checkpoint available. If live deployment is slow, inspect the provided output and screenshots, then focus on Bicep structure, managed identity permissions, pipeline federation, and cleanup with azd down.

Guided lab

Lab goal

Deploy the automation-account solution from the azd-maester repository with azd up, inspect the Bicep files that created the resources, configure GitHub Actions federation with azd pipeline config, and then cleanly remove the environment with azd down.

Prerequisites

Task 1: Clone and initialize the azd-maester template

GitHub CLI and Azure Developer CLI:

Fork the repo, move into the deployable solution, and verify auth
gh repo fork <upstream-owner>/azd-maester --clone
Set-Location .\azd-maester\automation-account

# Confirm azd is already authenticated before starting the environment work.
azd auth login --check-status
🧭Multi-solution repo note

The repository root is intentionally not deployable. If you run azd up from the root, the root guard stops the deployment and instructs you to choose a solution folder. Stay inside automation-account for the rest of the lab.

Azure Developer CLI:

Create the environment and pin the validated region
$environmentName = "lab-maester-aa"

azd env new $environmentName
azd env set AZURE_LOCATION centralus

# Review the local azd state for this solution.
Get-ChildItem .azure -Force
Get-Content ".azure\$environmentName\.env"

Expected outcomes:
- The environment is created under automation-account\.azure\<env-name>
- The region is pinned to centralus, which avoided Automation Account quota issues during validation
- The local .env file shows the azd environment metadata

Task 2: Deploy with azd up

Azure Developer CLI:

Deploy the solution from the solution folder
azd up

# Review the deployment outputs after provision and deploy complete.
azd env get-values

Expected outcomes:
- azd up provisions the resource group and the Automation Account stack from the Bicep files in this solution
- The deployment uploads the application content after infrastructure is ready
- azd env get-values shows the environment-scoped resource names and identifiers

Task 3: Walk through the Bicep /infra files

Native PowerShell:

Inspect the Bicep files that drove the deployment
Get-ChildItem .\infra -Recurse -Filter *.bicep | Select-Object FullName
Get-Content .\infra\main.bicep

Azure Portal:

Match the deployed resources to the Bicep definitions
  1. Open the deployed resource group.
  2. Open the Automation Account and check Identity and Azure role assignments.
  3. Compare the deployed resources and RBAC assignments with the Bicep resources and outputs.

Expected outcomes:
- The resource group contents line up with the Bicep file structure
- The Automation Account uses managed identity rather than a stored secret or certificate
- The Bicep files explain the Azure resources you just deployed

Task 4: Run azd pipeline config

Native PowerShell and Azure Developer CLI:

Prepare the solution for pipeline generation
# Stay in .\azd-maester\automation-account

# azd pipeline config expects workflow folders to exist in this repo shape.
New-Item -ItemType Directory -Path ..\.github\workflows -Force | Out-Null
New-Item -ItemType Directory -Path .\.github\workflows -Force | Out-Null

$resourceGroupName = (azd env get-values | Select-String '^AZURE_RESOURCE_GROUP=').Line.Split('=')[1].Trim('"')
$resourceGroupId = az group show --name $resourceGroupName --query id -o tsv

# Populate the scope explicitly. The generated workflow depends on this value.
azd env set AZURE_RBAC_SCOPES $resourceGroupId

azd pipeline config

GitHub Actions:

Patch the generated workflow for this multi-solution repository
on:
  workflow_dispatch:
  push:
   branches:
    - main

permissions:
  id-token: write
  contents: read

jobs:
  build:
   runs-on: ubuntu-latest
   env:
    AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
    AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
    AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
      AZURE_LOCATION: centralus
    AZURE_RBAC_SCOPES: ${{ vars.AZURE_RBAC_SCOPES }}
    AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}
   steps:
    - uses: actions/checkout@v4
    - uses: Azure/setup-azd@v2
    - uses: azure/login@v2
      with:
       client-id: ${{ vars.AZURE_CLIENT_ID }}
       tenant-id: ${{ vars.AZURE_TENANT_ID }}
       subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
    - name: Log in with azd
      working-directory: automation-account
      shell: pwsh
      run: |
       azd auth login `
        --client-id "$Env:AZURE_CLIENT_ID" `
        --federated-credential-provider "github" `
        --tenant-id "$Env:AZURE_TENANT_ID"
    - name: Provision Infrastructure
      working-directory: automation-account
      run: azd provision --no-prompt
    - name: Deploy Application
      working-directory: automation-account
      run: azd deploy --no-prompt

Az CLI:

Grant the pipeline app the Graph permissions this solution's hooks expect
$pipelineClientId = "<AZURE_CLIENT_ID from GitHub variables>"
$pipelineServicePrincipalId = az ad sp list --filter "appId eq '$pipelineClientId'" --query "[0].id" -o tsv

$graphServicePrincipal = az rest `
  --method get `
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'&`$select=id,appRoles" | ConvertFrom-Json

$graphServicePrincipalId = $graphServicePrincipal.value[0].id
$existingAssignments = az rest `
  --method get `
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$pipelineServicePrincipalId/appRoleAssignments" | ConvertFrom-Json

foreach ($roleValue in @("Directory.Read.All", "Application.Read.All", "AppRoleAssignment.ReadWrite.All")) {
  $roleId = ($graphServicePrincipal.value[0].appRoles | Where-Object {
    $_.value -eq $roleValue -and $_.allowedMemberTypes -contains "Application"
  } | Select-Object -First 1 -ExpandProperty id)

  if (-not ($existingAssignments.value | Where-Object {
    $_.resourceId -eq $graphServicePrincipalId -and $_.appRoleId -eq $roleId
  })) {
    $bodyPath = Join-Path $env:TEMP ("section6-{0}.json" -f ($roleValue -replace '[^A-Za-z0-9]', '-'))
    (@{
        principalId = $pipelineServicePrincipalId
        resourceId  = $graphServicePrincipalId
        appRoleId   = $roleId
    } | ConvertTo-Json -Compress) | Set-Content -Path $bodyPath -Encoding ascii

    az rest `
      --method post `
      --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$pipelineServicePrincipalId/appRoleAssignments" `
      --headers "Content-Type=application/json" `
      --body "@$bodyPath" | Out-Null
  }
}

GitHub and Entra validation:

Confirm the generated trust objects
  1. In GitHub, open Settings > Secrets and variables > Actions and confirm the Azure variables are present.
  2. In Entra, open the app registration created by azd pipeline config and confirm the federated credential is scoped to your repository.

Expected outcomes:
- azd pipeline config creates the Entra app registration and GitHub repository variables
- The workflow uses OIDC rather than a client secret
- In this multi-solution repo, the workflow must run from automation-account, include both Azure CLI login and azd auth login, carry the Graph application permissions the pre-provision and post-provision hooks require, and pin the region that actually has Automation quota available

Task 5: Tear down with azd down

Azure Developer CLI:

Remove the environment and purge the Azure resources
azd down --environment lab-maester-aa --force --purge --no-prompt

Expected outcomes:
- The resource group is deleted cleanly
- The environment is removed without interactive prompts
- The teardown removes resource locks before deleting the Automation stack

Common pitfalls

Admin takeaways

Key Principles 1. No client secrets for Azure workloads managed identity and WIF replace static credentials 2. ARM is the foundation layer portal, CLI, PowerShell, and SDKs all call management.azure.com 3. Bicep beats raw ARM JSON for authoring same engine, better readability, and IntelliSense guidance 4. azd up makes solutions portable repeatable provisioning, code deployment, and CI/CD setup 5. Key Vault can be a credential store treat private keys, tokens, and secrets with the same controls Apply these patterns directly to production environments

Course key takeaways: the five principles to apply directly in production.

Quick recap questions

  1. What is Azure Resource Manager and why does understanding it matter for automation?
  2. What does Bicep compile to and what are three advantages Bicep has over raw ARM JSON?
  3. What does azd up do in a single command?
  4. What is the structure of an azd template? Name the three main components.
  5. Why must the Key Vault in the Key Vault-backed passkey pattern be treated as a credential store?
  6. What does azd pipeline config configure automatically?

Key reminders

Current source notes

Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:

References

References Appendix

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.

Microsoft documentation: Azure Resource Manager

Microsoft documentation: ARM Templates (JSON)

Microsoft documentation: Bicep

Microsoft documentation: Azure Developer CLI (azd)

Microsoft documentation: Azure Automation and managed identity

Microsoft documentation: Key Vault

Microsoft documentation: app-only tokens and management.azure.com

Community and project references: Maester

Community and project references: azd-maester

Community and project references: Key Vault-backed passkeys and XDRInternals

Section 6 - Packaging Tools and azd - Reference Guide