All skills
microsoft avatar

/azure-deploy

@b3c238e
by microsoftmicrosoft/skills3.1k stars
351

Execute Azure deployments for ALREADY-PREPARED applications that have existing .azure/deployment-plan.md and infrastructure files. DO NOT use this skill when the user asks to CREATE a new application — use azure-prepare instead. This skill runs azd up, azd deploy, terraform apply, and az deployment commands with built-in error recovery. Requires .azure/deployment-plan.md from azure-prepare and validated status from azure-validate. WHEN: "run azd up", "run azd deploy", "execute deployment", "push to production", "push to cloud", "go live", "ship it", "bicep deploy", "terraform apply", "publish to Azure", "launch on Azure". DO NOT USE WHEN: "create and deploy", "build and deploy", "create a new app", "set up infrastructure", "create and deploy to Azure using Terraform" — use azure-prepare for these.

Use this Skill: https://skilld.dev/gh/microsoft/skills/azure-deploy

This session only. Nothing lands on disk.

referencespre-deploy-checklist.md

≈4.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Pre-Deployment Checklist

CRITICAL: Before running ANY provisioning commands, you MUST complete this checklist IN ORDER.

⛔ DO NOT run azd up until ALL steps are complete. Trial-and-error wastes time and creates orphan resources.

Step 1: Check Current Subscription

Use the Azure MCP tool to get current subscription:

mcp_azure_mcp_subscription_list

CLI fallback:

az account show --query "{name:name, id:id}" -o json

Step 2: Prompt User for Subscription

You MUST use ask_user to confirm the subscription. Find the default subscription (marked isDefault: true) from Step 1 results and present it as the recommended choice.

✅ Correct — show actual name and ID as a choice:

ask_user(
  question: "Which Azure subscription would you like to deploy to?",
  choices: [
    "Use current: <subscription-name> (<subscription-id>) (Recommended)",
    "Let me specify a different subscription"
  ]
)

❌ Wrong — never use freeform input for subscription:

ask_user(
  question: "Which Azure subscription should I deploy to? I'll need the subscription name or ID."
)

Step 3: Create AZD Environment FIRST

⚠️ MANDATORY — Create the environment BEFORE setting any variables or running azd up.

⛔ DO NOT manually create .azure/ folder with mkdir or New-Item. Let azd create it.

For new projects (no azure.yaml):

azd init -e <environment-name> --no-prompt

For existing projects (azure.yaml exists):

azd env new <environment-name> --no-prompt

Both commands create:

  • .azure/<env-name>/ folder with config files
  • Set the environment as default

The environment name becomes part of the resource group name (rg-<env-name>).

Step 4: Check if Resource Group Already Exists

⛔ CRITICAL — Skip this and you'll hit "Invalid resource group location" errors.

Use the Azure MCP tool to list resource groups:

mcp_azure_mcp_group_list
  subscription: <subscription-id>

Then check if rg-<env-name> exists in the results.

CLI fallback:

az group show --name rg-<env-name> --query "{location:location}" -o json 2>&1

If RG exists:

  • Use ask_user to offer choices:
    1. Use existing RG location (show the location)
    2. Choose a different environment name
    3. Delete the existing RG and start fresh

If RG doesn't exist: Proceed to location selection.

Step 5: Check for Tag Conflicts (AZD only)

⚠️ AZD uses azd-service-name tags to find deployment targets within the target resource group. Multiple resources with the same tag in the same RG cause failures. Tags in other RGs are fine.

az resource list --resource-group rg-<env-name> --tag azd-service-name=<service-name> --query "[].name" -o table

Check for each service in azure.yaml. If duplicates exist in the target RG:

  1. Preferred — Fresh environment: Run azd env new <new-name> --no-prompt and restart from Step 4. Non-destructive, no user confirmation needed, avoids orphan risks.
  2. Alternative — Delete conflicts: Use ask_user to confirm deletion of old resources (required by global rules).

Step 5a: Check for Existing Container Apps Environments (Container Apps only)

⛔ MANDATORY for Container Apps deployments — Skip this and azd up may silently create a new Container Apps environment with an unexpected name (e.g. "deployment-prod"), causing a much longer deployment and environment drift.

Only run this step if the resource group rg-<env-name> already exists (confirmed in Step 4). If the resource group does not exist yet, skip to Step 6.

If azure.yaml includes a Container Apps service and the resource group exists, check for existing Container Apps environments before running azd up:

az containerapp env list \
  --resource-group rg-<env-name> \
  --query "[].{name:name, location:location, provisioningState:properties.provisioningState}" \
  -o table

PowerShell:

az containerapp env list `
  --resource-group rg-<env-name> `
  --query "[].{name:name, location:location, provisioningState:properties.provisioningState}" `
  -o table

If no existing environments are found: No action needed — proceed to Step 6.

If existing environments are found: Check the provisioningState column in the output. Environments with a state of Failed or Deleting are not usable — treat them the same as no conflict (proceed to Step 6), or use option 3 below to delete the stuck environment first.

For environments with a provisioningState of Succeeded, use ask_user to present the conflict and offer choices:

ask_user(
  question: "I found existing Container Apps environment(s) in rg-<env-name>:
    <environment-list>
  Proceeding without resolving this conflict may cause azd to create an additional environment.
  How would you like to proceed?",
  choices: [
    "Use the existing environment — select the matching AZD environment (Recommended)",
    "Choose a different AZD environment name to deploy to a new resource group",
    "Delete the existing Container Apps environment and start fresh (DESTRUCTIVE)"
  ]
)

Resolution per choice:

  1. Use existing environment — First check if the matching AZD environment exists locally:

    azd env list
    • If the environment exists locally, select it:
      azd env select <matching-env-name>
    • If the environment does NOT exist locally (e.g., it was provisioned on a different machine or has been cleaned up), create it and configure it to target the existing resource group:
      azd env new <matching-env-name> --no-prompt
      azd env set AZURE_SUBSCRIPTION_ID <subscription-id>
      azd env set AZURE_LOCATION <location-of-existing-rg>
  2. Choose a different name — Create a new AZD environment:

    azd env new <new-unique-env-name> --no-prompt
    azd env set AZURE_SUBSCRIPTION_ID <subscription-id>
    # Then restart from Step 4 with the new environment name
  3. Delete and start fresh — Delete the conflicting environment (requires ask_user confirmation per global-rules):

    az containerapp env delete \
      --name <environment-name> \
      --resource-group rg-<env-name> \
      --yes

    PowerShell:

    az containerapp env delete `
      --name <environment-name> `
      --resource-group rg-<env-name> `
      --yes

Step 6: Prompt User for Location

You MUST use ask_user with regions that support ALL services in the architecture.

See Region Availability for service-specific limitations.

Step 7: Set Environment Variables

⚠️ Set ALL variables BEFORE running azd up — not during error recovery.

Environment should already be configured during azure-validate. Run azd env get-values to confirm.

Verify settings:

azd env get-values

Step 8: Only NOW Run Deployment

azd up --no-prompt

Step 9: Verify Terraform Variable Resolution (AZD+Terraform Only)

⚠️ MANDATORY for azd+Terraform projects. Skip this step for Bicep or pure Terraform deployments.

Before running azd up, verify no Go-style template variables exist in Terraform files:

# Fail if Go-style template variables found in Terraform files
if grep -rn '{{ *\.Env\.' infra/ --include='*.tf' --include='*.tfvars.json'; then
  echo "ERROR: Unresolved Go-style template variables found"
  exit 1
fi

# Check main.tfvars.json uses correct ${VAR} syntax (not Go-style templates)
if test -f infra/main.tfvars.json; then
  if grep -q '{{ *\.Env\.' infra/main.tfvars.json; then
    echo "ERROR: main.tfvars.json uses Go-style templates. Use \${VAR} syntax instead."
    exit 1
  fi
fi

If either check fails:

  1. Fix main.tfvars.json syntax: replace {{ .Env.VAR }} with ${VAR} (e.g., ${AZURE_ENV_NAME})
  2. For variables not in main.tfvars.json, use TF_VAR_* environment variables
  3. Re-run azure-validate before proceeding

Quick Reference: Correct AZD Sequence

# 1. Create environment FIRST
azd env new myapp-dev --no-prompt

# 2. Set subscription
azd env set AZURE_SUBSCRIPTION_ID 25fd0362-...

# 3. Set location (after checking RG doesn't conflict)
azd env set AZURE_LOCATION westus2

# 4. Verify
azd env get-values

# 5. Deploy
azd up --no-prompt

Common Mistakes to Avoid

❌ Wrong ✅ Correct
azd up --location eastus2 azd env set AZURE_LOCATION eastus2 then azd up
Running azd up without environment azd env new <name> --no-prompt first
Assuming location without checking RG Check az group show before choosing
Ignoring tag conflicts in target RG Check az resource list --resource-group rg-<env-name> before deploy
Skipping Container Apps environment check Run az containerapp env list --resource-group rg-<env-name> before deploy (Step 5a)

Service-Specific Checks

Container Apps + ACR — Pre-Deploy RBAC Health Check

⛔ MANDATORY: If the plan includes Container Apps that pull images from ACR using a managed identity, you MUST use a two-phase flow with an AcrPull RBAC propagation gate between provisioning and image deployment. Skipping the gate causes the Container App revision to time out (~900 seconds) waiting for image pull permission — a known Azure RBAC propagation delay.

The exact phase-1/phase-2 commands depend on whether the infra is Bicep (managed by azd) or Terraform (managed by terraform). Pick the matching path below.

This check is required when ALL of the following are true:

  • The plan includes a Container App service that pulls images from ACR
  • The infra (Bicep or Terraform) assigns an AcrPull role for the Container App's managed identity on ACR using the two-phase deployment pattern
  • Phase 1 has just completed and the real application image has not yet been pushed/deployed

📦 Placeholder image: Both paths use mcr.microsoft.com/azuredocs/containerapps-helloworld:latest as the public phase-1 placeholder so the Container App can be provisioned before the real image exists in ACR.

Path A — Bicep (AZD)

💡 Two-phase Bicep pattern: azd provision succeeds immediately because the Container App is provisioned with a public placeholder image (not an ACR image). The AcrPull role assignment is deployed in a separate module with no circular dependency. azd deploy then configures the registry/identity link (the equivalent CLI step is az containerapp registry set --name <app-name> --resource-group rg-<env-name> --server <acr-login-server> --identity system) and pushes the real image via the Azure API — but the AcrPull role still needs time to propagate before this succeeds.

⛔ Do not use azd up for this scenario. azd up combines provisioning and deployment and skips the propagation gate.

Required flow:

  1. Run azd provision
  2. Complete the RBAC health check (Steps A–C below)
  3. Run azd deploy --no-prompt
Path B — Terraform (CLI)

💡 Two-phase Terraform pattern: terraform apply succeeds immediately because the Container App is provisioned with a public placeholder image (not an ACR image) and no registry block. The AcrPull role assignment is a separate azurerm_role_assignment resource that depends on the Container App's system-assigned identity, so there is no circular dependency. The post-apply CLI step then builds and pushes the real image with az acr build, configures the registry/identity link with az containerapp registry set --name <app-name> --resource-group rg-<env-name> --server <acr-login-server> --identity system, and switches the revision to the real image with az containerapp update --image <acr-login-server>/<image>:<tag> — but the AcrPull role still needs time to propagate before these succeed.

Required flow:

  1. Run terraform apply (provisions ACR, Container App with placeholder image, and AcrPull role assignment)

  2. Complete the RBAC health check (Steps A–C below)

  3. Build, push, and switch to the real image:

    az acr build --registry <acr-name> --image <image>:<tag> ./src/<service>
    az containerapp registry set \
      --name <app-name> \
      --resource-group rg-<env-name> \
      --server <acr-login-server> \
      --identity system
    az containerapp update \
      --name <app-name> \
      --resource-group rg-<env-name> \
      --image <acr-login-server>/<image>:<tag>

    PowerShell:

    az acr build --registry <acr-name> --image <image>:<tag> ./src/<service>
    az containerapp registry set `
      --name <app-name> `
      --resource-group rg-<env-name> `
      --server <acr-login-server> `
      --identity system
    az containerapp update `
      --name <app-name> `
      --resource-group rg-<env-name> `
      --image "<acr-login-server>/<image>:<tag>"
RBAC Health Check (Both Paths)

The following Steps A–C are identical for Bicep (AZD) and Terraform.

Step A — Get the Container App's managed identity principal ID:

PRINCIPAL_ID=$(az containerapp identity show \
  --name <app-name> \
  --resource-group rg-<env-name> \
  --query principalId -o tsv)

PowerShell:

$PrincipalId = az containerapp identity show `
  --name <app-name> `
  --resource-group rg-<env-name> `
  --query principalId -o tsv

Step B — Get the ACR resource ID:

ACR_ID=$(az acr show \
  --name <acr-name> \
  --resource-group rg-<env-name> \
  --query id -o tsv)

PowerShell:

$AcrId = az acr show `
  --name <acr-name> `
  --resource-group rg-<env-name> `
  --query id -o tsv

Step C — Poll until the AcrPull role is visible (up to 5 minutes):

for attempt in 1 2 3 4 5; do
  ROLE=$(az role assignment list \
    --scope "$ACR_ID" \
    --assignee-object-id "$PRINCIPAL_ID" \
    --query "[?roleDefinitionName=='AcrPull'].roleDefinitionName" \
    -o tsv 2>/dev/null)

  if printf '%s\n' "$ROLE" | grep -qx 'AcrPull'; then
    echo "AcrPull role confirmed. Proceeding with azd deploy."
    break
  fi

  if [ "$attempt" -eq 5 ]; then
    echo "AcrPull role not found after 5 minutes. Assign it manually before retrying."
    exit 1
  fi

  echo "Waiting for AcrPull RBAC propagation (attempt $attempt/5, waiting 60s)..."
  sleep 60
done

PowerShell:

for ($attempt = 1; $attempt -le 5; $attempt++) {
    $Role = az role assignment list `
      --scope $AcrId `
      --assignee-object-id $PrincipalId `
      --query "[?roleDefinitionName=='AcrPull'].roleDefinitionName" `
      -o tsv 2>$null

    if ($Role -eq "AcrPull") {
        Write-Output "AcrPull role confirmed. Proceeding with azd deploy."
        break
    }

    if ($attempt -eq 5) {
        Write-Output "AcrPull role not found after 5 minutes. Assign it manually before retrying."
        exit 1
    }

    Write-Output "Waiting for AcrPull RBAC propagation (attempt $attempt/5, waiting 60s)..."
    Start-Sleep -Seconds 60
}

Only after this check confirms AcrPull has propagated should you run Path A azd deploy --no-prompt or Path B az acr build / az containerapp registry set / az containerapp update.

💡 Tip: If AcrPull is missing entirely, assign it manually using the steps in Container App Revision Timeout, then re-run the poll loop above.


AZD/Bicep — Non-User Principal RBAC Fix

⛔ MANDATORY: If the deploying identity is NOT an interactive user (e.g., service principal in CI/CD), you MUST check and patch Bicep templates that hardcode user-type RBAC role assignments. Skipping this causes PrincipalType mismatch errors during provisioning, leading to costly retry loops and potential test timeouts.

This check is required when ALL of the following are true:

  • The deployment uses AZD with Bicep infrastructure
  • The deploying identity is not an interactive user (service principal, managed identity, etc.)
  • The Bicep templates contain allowUserIdentityPrincipal set to true or role assignments with principalType: 'User'

⚠️ Known affected templates: functions-quickstart-python-http-azd and other AZD quickstart templates that include optional user-identity RBAC. See Principal Type Mismatch for details.

Step A — Detect the deploying identity type:

AUTH_TYPE=$(az account show --query user.type -o tsv)
echo "Auth type: $AUTH_TYPE"

PowerShell:

$AuthType = az account show --query user.type -o tsv
Write-Output "Auth type: $AuthType"

If AUTH_TYPE is user, skip to the next section — no fix needed.

Step B — Search for user-type RBAC in Bicep templates:

grep -rn "allowUserIdentityPrincipal" infra/ --include='*.bicep'

PowerShell:

Select-String -Path infra\*.bicep -Pattern 'allowUserIdentityPrincipal' -Recurse

If no matches are found, skip to the next section.

Step C — Patch allowUserIdentityPrincipal to false:

For each match where allowUserIdentityPrincipal is set to true, change it to false:

// Before (fails with service principal deployments):
allowUserIdentityPrincipal: true

// After (CI/CD compatible):
allowUserIdentityPrincipal: false

⚠️ This is a CI/CD compatibility patch. Setting this to false disables optional interactive-user RBAC role assignments. The application's managed identity roles are unaffected. If the user later needs local interactive access, they can re-enable this flag or assign roles manually.

⚠️ Do NOT attempt to fix this by clearing AZURE_PRINCIPAL_ID. The azd CLI repopulates this value from the current auth context on every run.


Durable Functions — Verify DTS Backend

⛔ MANDATORY: If the plan includes Durable Functions, verify infrastructure uses Durable Task Scheduler (DTS), NOT Azure Storage.

Check that infra/ Bicep files contain:

  • Microsoft.DurableTask/schedulers resource
  • Microsoft.DurableTask/schedulers/taskHubs child resource
  • Durable Task Data Contributor RBAC role assignment
  • DURABLE_TASK_SCHEDULER_CONNECTION_STRING app setting

If any are missing, STOP and invoke azure-prepare to regenerate with the durable recipe.


Non-AZD Deployments

For Azure CLI / Bicep:

az account set --subscription <subscription-id-or-name>
# Pass location as parameter: --location <location>

For Terraform:

az account set --subscription <subscription-id-or-name>
# Set in terraform.tfvars or -var="location=<location>"

Source: SKILL.md on GitHub

2 warnings15d4 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    This skill provides a robust framework for executing Azure deployments using various infrastructure-as-code tools. It includes several security considerations such as remote script installation and command execution, which are essential for its deployment functionality and are managed through strict prerequisite checks and user confirmation rules.

  • Socket15d

    1 alert: gptAnomaly

  • Snyk15d

    Risk: LOW · No issues

  • Runlayer7mo

    30/31 files flagged

Signed by skilld at b3c238e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 19 hours ago.

Activeupdated 2 months ago
metadata
{
  "author": "Microsoft",
  "version": "1.2.1"
}

README badge

README badge for microsoft/skills/azure-deploy