| name | aigbb-azd-compliance |
| description | Validate and deploy Azure Developer CLI (azd) projects with compliance-first patterns. Covers azure.yaml configuration, Bicep parameter sync, Container Apps image preservation (IMAGE_NAME/RESOURCE_EXISTS), service-to-resource binding, azd tags, hook scripts, shared subscription safety, and pre-flight validation. Use when setting up azd projects, writing azure.yaml, validating Bicep infrastructure for Container Apps, troubleshooting azd up failures, running compliance checks, or preparing for deployment. Triggers on azd, azure.yaml, azd compliance, azd deploy, azd provision, azd up, Container Apps deployment, IMAGE_NAME, RESOURCE_EXISTS, remoteBuild, azd tags, parameter mismatch, pre-flight check, deployment validation. |
Azure Developer CLI (azd) Compliance & Deployment
Deploy containerized applications to Azure Container Apps with full compliance validation. This skill combines deployment patterns with the checks needed to prevent azd provision and azd deploy failures.
Philosophy: Only flag issues that will cause failures or runtime problems. Skip style preferences.
Quick Start
azd auth login
azd init
azd env new <env-name>
azd up
1. azure.yaml Configuration
Minimal Configuration
name: my-app
services:
api:
project: ./src/api
language: python
host: containerapp
docker:
path: ./Dockerfile
remoteBuild: true
Full Configuration with Hooks
name: my-app
metadata:
template: my-project@1.0.0
infra:
provider: bicep
path: ./infra
services:
web:
project: ./src/web
language: ts
host: containerapp
docker:
path: ./Dockerfile
context: .
remoteBuild: true
api:
project: ./src/api
language: python
host: containerapp
docker:
path: ./Dockerfile
context: .
remoteBuild: true
hooks:
preprovision:
shell: sh
run: python infra/scripts/preprovision.py
postprovision:
shell: sh
run: python infra/scripts/postprovision.py
predeploy:
shell: sh
run: python infra/scripts/predeploy.py
postdeploy:
shell: sh
run: python infra/scripts/postdeploy.py
Critical azure.yaml Rules
| Check | Why It Matters |
|---|
name field exists | azd refuses to run without it |
infra.path points to existing directory | azd provision fails immediately |
Service project paths exist | azd deploy can't find source code |
Service host matches infrastructure | Deploys to wrong/nonexistent resource |
remoteBuild: true on all containerapp services | Local builds fail on ARM Macs, in CI/CD, and require Docker Desktop |
Service Hosts
azure.yaml host | Required Bicep Resource |
|---|
containerapp | Container App with matching name pattern |
function | Function App |
appservice | App Service |
staticwebapp | Static Web App |
Service Languages
| Language | Value | Package Manager |
|---|
| Python | python | requirements.txt or pyproject.toml |
| TypeScript | ts | package.json |
| JavaScript | js | package.json |
| C# | csharp | .csproj |
| Java | java | pom.xml or build.gradle |
| Go | go | go.mod |
2. Parameter Sync (Most Common Failure)
Every parameter in main.bicep that lacks a default MUST have a mapping in main.parameters.json.
main.bicep Parameters
// These MUST be in main.parameters.json (no default)
param environmentName string
param location string
// These are OPTIONAL in main.parameters.json (have defaults)
param principalId string = ''
param apiImageName string = ''
param apiExists bool = false
main.parameters.json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": { "value": "${AZURE_ENV_NAME}" },
"location": { "value": "${AZURE_LOCATION}" },
"principalId": { "value": "${AZURE_PRINCIPAL_ID}" },
"apiImageName": { "value": "${SERVICE_API_IMAGE_NAME=}" },
"apiExists": { "value": "${SERVICE_API_RESOURCE_EXISTS=false}" }
}
}
Parameter Injection Syntax
| Syntax | Meaning |
|---|
${AZURE_ENV_NAME} | Required — azd auto-populates |
${AZURE_LOCATION} | Required — azd auto-populates |
${MY_VAR} | Must be set via azd env set MY_VAR "value" |
${MY_VAR=default} | Uses default if not set |
${SERVICE_API_IMAGE_NAME=} | Empty string default (for first deploy) |
${SERVICE_API_RESOURCE_EXISTS=false} | Boolean default (for first deploy) |
Validation
# List parameters without defaults in main.bicep
Select-String -Path infra/main.bicep -Pattern "^param\s+\w+\s+\w+$" |
Where-Object { $_ -notmatch "=" }
# List parameters in main.parameters.json
(Get-Content infra/main.parameters.json | ConvertFrom-Json).parameters.PSObject.Properties.Name
# Compare — every param without a default must appear in parameters.json
3. Container Image Preservation (IMAGE_NAME / RESOURCE_EXISTS)
For each service with host: containerapp in azure.yaml, azd manages two variables to prevent re-provision from overwriting the deployed container image.
Reference: Deploy to Azure Container Apps using azd
Required Variables Per Service
For a service named api in azure.yaml:
| Variable | Set By | Purpose |
|---|
SERVICE_API_IMAGE_NAME | azd deploy | Currently deployed image tag |
SERVICE_API_RESOURCE_EXISTS | azd provision | Whether Container App already exists |
Pattern: SERVICE_<UPPER_CASE_SERVICE_NAME>_IMAGE_NAME and SERVICE_<UPPER_CASE_SERVICE_NAME>_RESOURCE_EXISTS
Check 1: main.parameters.json Must Map Both
{
"parameters": {
"apiImageName": {
"value": "${SERVICE_API_IMAGE_NAME=}"
},
"apiExists": {
"value": "${SERVICE_API_RESOURCE_EXISTS=false}"
}
}
}
The = after the variable name provides a default (empty string or false) for first-time deployments.
Check 2: Bicep Must Declare and Use Both
@description('Container image name for the api service')
param apiImageName string = ''
@description('Whether the api Container App already exists')
param apiExists bool = false
module api 'br/public:avm/res/app/container-app:0.18.1' = {
name: 'api'
params: {
name: '${abbrs.appContainerApps}api-${resourceToken}'
tags: union(tags, { 'azd-service-name': 'api' })
environmentResourceId: containerAppsEnvironment.outputs.resourceId
containers: [
{
name: 'main'
image: !empty(apiImageName) ? apiImageName : 'mcr.microsoft.com/k8se/quickstart:latest'
resources: { cpu: json('0.5'), memory: '1Gi' }
}
]
ingressExternal: true
ingressTargetPort: 8000
}
}
Check 3: Container App Module with Image Guard (Recommended)
Use the AVM Container App module directly with the image guard pattern:
module api 'br/public:avm/res/app/container-app:0.18.1' = {
params: {
name: '${abbrs.appContainerApps}api-${resourceToken}'
tags: union(tags, { 'azd-service-name': 'api' })
environmentResourceId: containerAppsEnvironment.outputs.resourceId
containers: [
{
name: 'main'
image: !empty(apiImageName) ? apiImageName : 'mcr.microsoft.com/k8se/quickstart:latest'
resources: { cpu: json('0.5'), memory: '1Gi' }
env: [
{ name: 'AZURE_CLIENT_ID', value: managedIdentity.outputs.clientId }
]
}
]
ingressExternal: true
ingressTargetPort: 8000
registries: [
{
server: '${containerRegistry.outputs.name}.azurecr.io'
identity: managedIdentity.outputs.resourceId
}
]
}
}
What Breaks Without This
| Scenario | Without IMAGE_NAME / RESOURCE_EXISTS | With both variables |
|---|
First azd provision | ✅ Works (uses default placeholder) | ✅ Works |
azd deploy | ✅ Pushes new image | ✅ Pushes new image |
Re-run azd provision | ❌ Overwrites deployed image with placeholder, app breaks | ✅ Preserves current image |
azd up (provision + deploy) | ⚠️ Temporary downtime between provision and deploy | ✅ Image preserved during provision phase |
Validation Command
# Verify both variables exist for each containerapp service
Select-String -Path infra/main.parameters.json -Pattern 'SERVICE_.*_IMAGE_NAME|SERVICE_.*_RESOURCE_EXISTS'
4. Service-to-Resource Binding & Tags
azd uses tags to discover deployed resources. Without them, azd deploy and azd down can't find your resources.
Required: Resource Group Tag
var tags = {
'azd-env-name': environmentName // REQUIRED — azd uses this to find resources
}
Required: Service Resource Tags
For each service in azure.yaml, the corresponding Bicep resource must include azd-service-name:
tags: union(tags, {
'azd-service-name': 'api' // MUST match service key in azure.yaml
})
Tag-to-Service Mapping
| azure.yaml | Bicep tag required |
|---|
services.api: | 'azd-service-name': 'api' |
services.web: | 'azd-service-name': 'web' |
services.frontend: | 'azd-service-name': 'frontend' |
services.backend: | 'azd-service-name': 'backend' |
Why it matters: Without these tags, azd can't find deployed resources for azd deploy or azd down.
5. Bicep Output Naming Convention
azd needs outputs to know where services deployed and to auto-populate .azure/<env>/.env.
Per-Service Outputs (Required)
// Pattern: SERVICE_<SERVICE_NAME>_<PROPERTY>
output SERVICE_API_ENDPOINT_URL string = api.outputs.fqdn
output SERVICE_API_NAME string = api.outputs.name
output SERVICE_WEB_ENDPOINT_URL string = web.outputs.fqdn
output SERVICE_WEB_NAME string = web.outputs.name
Infrastructure Outputs (Common)
output AZURE_LOCATION string = location
output AZURE_RESOURCE_GROUP string = resourceGroup().name
output AZURE_CLIENT_ID string = userAssignedIdentity.outputs.clientId
output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
All Bicep outputs automatically become azd environment variables in .azure/<env>/.env.
6. Hook Scripts
Available Hook Points
| Hook | Timing | Use Case |
|---|
prerestore | Before package restore | Pre-install setup |
postrestore | After package restore | Post-install setup |
preprovision | Before infra deployment | Save existing state, validation |
postprovision | After infra deployment | RBAC, DNS, configuration |
prepackage | Before app packaging | Build steps |
postpackage | After app packaging | Verification |
predeploy | Before app deployment | Pre-deploy checks |
postdeploy | After app deployment | Smoke tests, notifications |
predown | Before resource deletion | Backup, confirmation |
postdown | After resource deletion | Cleanup |
Hook Properties
hooks:
postprovision:
shell: sh
run: |
echo "Running post-provision..."
continueOnError: false
interactive: false
cwd: ./scripts
Critical Hook Rules
-
Non-zero exit = deployment stops — Always handle errors:
run: |
az role assignment create ... 2>/dev/null || true
-
Undeclared hooks don't run — Scripts in infra/scripts/ must be declared in azure.yaml:
# Check for undeclared hook scripts
Get-ChildItem infra/scripts/ -Filter "*.py" |
Where-Object { $_.BaseName -match "pre|post" }
-
Required tools must be available — If hooks use uv, python, node, etc., they must be installed.
Hook Environment Variables
Available in all hooks after provision:
${AZURE_ENV_NAME}
${AZURE_LOCATION}
${AZURE_SUBSCRIPTION_ID}
${AZURE_RESOURCE_GROUP}
${SERVICE_<NAME>_URI}
${SERVICE_<NAME>_IMAGE_NAME}
7. Container Apps Build Configuration
Always Use Remote Build
services:
api:
host: containerapp
docker:
path: ./Dockerfile
context: .
remoteBuild: true
Why:
- Local builds require Docker Desktop running
- Local builds fail on ARM Macs deploying to AMD64
- Local builds fail in CI/CD without proper Docker setup
- Remote builds are more reliable and portable
ACR Integration
For host: containerapp with docker.remoteBuild: true:
- Bicep must output
AZURE_CONTAINER_REGISTRY_ENDPOINT or similar
- Container App must reference the correct registry
- ACR must have the managed identity or admin access configured
8. main.bicep Organization (7 Sections)
targetScope = 'resourceGroup'
// ── 1. METADATA ──────────────────────────────────────────────
metadata description = 'Main infrastructure template for [Project Name]'
// ── 2. PARAMETERS ────────────────────────────────────────────
@description('Environment name')
@minLength(1)
@maxLength(64)
param environmentName string
@description('Primary location for all resources')
param location string
@description('Principal ID for local RBAC (empty in CI)')
param principalId string = ''
// Per-service image + exists params
@description('Container image for the api service')
param apiImageName string = ''
@description('Whether the api Container App already exists')
param apiExists bool = false
// ── 3. VARIABLES ─────────────────────────────────────────────
var abbrs = loadJsonContent('./abbreviations.json')
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
var tags = {
'azd-env-name': environmentName
}
// ── 4. SHARED INFRASTRUCTURE ─────────────────────────────────
module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
name: 'managedIdentity'
scope: rg
params: {
name: '${abbrs.managedIdentityUserAssignedIdentities}${resourceToken}'
location: location
tags: tags
}
}
module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.12.0' = { /* ... */ }
module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = { /* ... */ }
module keyVault 'br/public:avm/res/key-vault/vault:0.13.3' = { /* ... */ }
// ── 5. HOSTING INFRASTRUCTURE ────────────────────────────────
module containerRegistry 'br/public:avm/res/container-registry/registry:0.9.3' = { /* ... */ }
module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.3' = { /* ... */ }
// ── 6. APPLICATION MODULES ───────────────────────────────────
module api 'br/public:avm/res/app/container-app:0.18.1' = {
name: 'api'
scope: rg
params: {
name: '${abbrs.appContainerApps}api-${resourceToken}'
tags: union(tags, { 'azd-service-name': 'api' }) // ← REQUIRED for azd
environmentResourceId: containerAppsEnvironment.outputs.resourceId
containers: [
{
name: 'main'
image: !empty(apiImageName) ? apiImageName : 'mcr.microsoft.com/k8se/quickstart:latest'
resources: { cpu: json('0.5'), memory: '1Gi' }
}
]
ingressExternal: true
ingressTargetPort: 8000
}
}
// ── 7. OUTPUTS ───────────────────────────────────────────────
output AZURE_LOCATION string = location
output AZURE_RESOURCE_GROUP string = resourceGroup().name
output AZURE_CLIENT_ID string = managedIdentity.outputs.clientId
output SERVICE_API_ENDPOINT_URL string = api.outputs.fqdn
output SERVICE_API_NAME string = api.outputs.name
9. RBAC Assignments
Always guard with if (!empty(principalId)) to prevent failures when principalId is empty (CI/CD):
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) {
name: guid(resource.id, principalId, roleId)
scope: resource
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId)
principalId: principalId
principalType: 'ServicePrincipal'
}
}
Common Role IDs
| Role | ID |
|---|
| Cognitive Services User | a97b65f3-24c7-4388-baec-2e87135dc908 |
| Key Vault Secrets User | 4633458b-17de-408a-b874-0445c86b69e6 |
| Storage Blob Data Contributor | ba92f5b4-2d11-453d-a403-e96b0029c9fe |
| Search Service Contributor | 7ca78c08-252a-4471-8644-bb5ff32d4ba0 |
| AcrPull | 7f951dda-4ed3-4680-a7ca-43fe172d538d |
Never use Contributor or Owner when a specific role suffices.
10. Shared Subscription Considerations
Resource Naming Collisions
In shared subscriptions, globally unique names cause conflicts:
| Resource | Uniqueness | Fix |
|---|
| Key Vault | Global | Include resourceToken (uniqueString) |
| Storage Account | Global | Include resourceToken, max 24 chars |
| Container Registry | Global | Include resourceToken |
| Cognitive Services | Global | Include resourceToken |
var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
Soft-Delete Conflicts
Redeploying fails if a soft-deleted resource with same name exists:
| Resource | Retention | Mitigation |
|---|
| Key Vault | 90 days | Use unique names OR purge in preprovision hook |
| Cognitive Services | 48 hours | Use unique names OR purge in preprovision hook |
| API Management | Soft-deleted blocks new | Use unique names |
Environment Isolation
Each developer's environment must be isolated:
// Good: Environment name in resource group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: '${abbrs.resourcesResourceGroups}${environmentName}' // ✅ Includes env name
}
Quota-Friendly Defaults
Shared subscriptions often hit quota limits:
| Resource | Watch For |
|---|
| Container Apps | CPU/memory requests |
| Cognitive Services | SKU tier (use S0 not S1+ unless needed) |
| AI Search | SKU (basic vs standard) |
| VMs/VMSS | Core counts |
11. Environment Variables Flow
Three-Level Configuration
- Local
.env — For local development only
.azure/<env>/.env — azd-managed, auto-populated from Bicep outputs
main.parameters.json — Maps azd env vars to Bicep parameters
Setting Environment Variables
azd env set AZURE_OPENAI_ENDPOINT "https://my-openai.openai.azure.com"
azd env get-values
Never manually edit .azure/<env>/.env — use azd env set.
Bicep Env Var Alignment
Container Apps env vars must match the application's configuration class:
// Bicep
environmentVariables: [
{ name: 'AZURE_CLIENT_ID', value: identity.outputs.clientId }
{ name: 'AZURE_OPENAI_ENDPOINT', value: openAi.outputs.endpoint }
]
class Settings(BaseSettings):
azure_client_id: str | None = None
azure_openai_endpoint: str
12. Container App — Direct AVM Usage
Use the AVM Container App module (br/public:avm/res/app/container-app) directly in main.bicep. No wrapper module needed.
// In main.bicep — call AVM directly, no infra/core/ wrapper
module api 'br/public:avm/res/app/container-app:0.18.1' = {
name: 'api'
scope: rg
params: {
name: '${abbrs.appContainerApps}api-${resourceToken}'
location: location
tags: union(tags, { 'azd-service-name': 'api' }) // ← REQUIRED for azd
environmentResourceId: containerAppsEnvironment.outputs.resourceId
managedIdentities: {
userAssignedResourceIds: [managedIdentity.outputs.resourceId]
}
containers: [
{
name: 'main'
image: !empty(apiImageName) ? apiImageName : 'mcr.microsoft.com/k8se/quickstart:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
env: [
{ name: 'AZURE_CLIENT_ID', value: managedIdentity.outputs.clientId }
{ name: 'AZURE_OPENAI_ENDPOINT', value: azureOpenAi.outputs.endpoint }
]
}
]
ingressExternal: true
ingressTargetPort: 8000
registries: [
{
server: '${containerRegistry.outputs.name}.azurecr.io'
identity: managedIdentity.outputs.resourceId // ✅ Managed identity — NOT admin credentials
}
]
}
}
// AVM module outputs use .outputs.resourceId, .outputs.name, .outputs.fqdn
output SERVICE_API_ENDPOINT_URL string = api.outputs.fqdn
output SERVICE_API_NAME string = api.outputs.name
13. Internal Service Discovery
Container Apps in the same environment communicate via internal HTTP DNS:
// Backend reference in frontend env vars
env: [
{
name: 'BACKEND_URL'
value: 'http://ca-backend-${resourceToken}' // Internal DNS — HTTP, not HTTPS
}
]
Always use http:// (not https://) for internal service-to-service communication within a Container Apps environment.
14. Common Commands
azd env list
azd env select <name>
azd env get-values
azd env set KEY value
azd up
azd provision
azd deploy
azd deploy --service api
azd config list
azd auth login --check-status
azd show
azd up --debug
az containerapp logs show -n <app> -g <rg> --follow
15. Pre-Flight Checklist
Run these before azd up:
# 1. Verify azd can parse the project
azd config list
# 2. Check authentication
azd auth login --check-status
# 3. Verify environment is set
azd env list
# 4. Validate Bicep compiles (catches syntax errors)
az bicep build --file infra/main.bicep --stdout | Out-Null
# 5. What-if deployment (catches parameter/resource issues)
azd provision --preview
If all pass, azd up should succeed.
16. Anti-Patterns Summary
| Anti-Pattern | Impact | Fix |
|---|
Missing remoteBuild: true | Build fails on ARM Macs / CI | Add remoteBuild: true to docker config |
Missing azd-env-name tag | azd can't find resource group | Add to tags variable |
Missing azd-service-name tag | azd deploy can't find resources | Add union(tags, {'azd-service-name': '...'}) |
| Missing IMAGE_NAME/EXISTS params | Re-provision overwrites deployed image | Add both params per service |
| Parameter without default, missing from parameters.json | azd provision fails | Add mapping to parameters.json |
| Hardcoded secrets in Bicep | Security vulnerability | Use azd env set + Key Vault |
Manual .azure/.env edits | Values overwritten by azd | Use azd env set |
Missing || true in hooks | RBAC "already exists" fails deployment | Add error suppression |
| External URLs for internal services | Unnecessary network hops, TLS overhead | Use internal http:// DNS |
| ACR admin credentials | Security risk | Use managed identity |
Empty principalId in RBAC | Role assignment fails in CI | Guard with if (!empty(principalId)) |
| Undeclared hook scripts | Scripts exist but never run | Declare in azure.yaml hooks: |
No AZURE_CLIENT_ID env var | Managed identity auth fails at runtime | Output from Bicep |
Reference Files
External References