Files
The-transit-Project/.github/agents/azure-deploy/scaffold/references/bicep-patterns.md
T
Sven laptop 635a130f0b
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
──────
### Summary of Changes
  1. Documentation Consolidation:
      • Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
      DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
      SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
      • Updated document links in README.md.
  2. AI Agent Project Map:
      • Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
      schemas, environment variables, and developer workflows.
      • Added PROJECT_MAP.md to .gitignore.
  3. Monitoring UI & Infrastructure Integration:
      • Added monitoring_ui service to docker-compose.yml (port 5175).
      • Configured Gitea workflow docker-build.yml for monitoring_ui builds.
      • Added CORS middleware headers in index.js.
      • Added nginx.conf copy step in Dockerfile.

  ──────
  ### Recommended Commit Message

    docs: consolidate project documentation into monitoring_ui and add agent project map

    - Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
    - Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
    - Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
    - Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
    - Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
    - Configure CORS handling in employee_api for monitoring dashboard access
    ──────
  ### Alternative Short Commit Message

    refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
2026-09-20 23:33:55 +02:00

12 KiB

Important: This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.

Bicep Patterns

Bicep default-path patterns for AppOnboard scaffold. Used as the primary IaC format. For the alternative Terraform path (existing .tf files or user override), scaffold uses mcp_azure_mcp_azureterraformbestpractices output patterns.

Source: Adapted from Azure Bicep best practices. See Bicep best practices for updates.

File Structure

⛔ Always use targetScope = 'subscription'. Subscription-scope Bicep creates the resource group in IaC with all 5 AppOnboard tags (including created-at). Resource-group scope requires az group create via CLI, which consistently misses created-at because CLI-created resource groups don't receive IaC-managed tags. There are zero benefits to resource-group scope for AppOnboard.

infra/
├── main.bicep              # Entry point (subscription scope)
├── main.parameters.json    # ARM JSON parameter values (NOT .bicepparam)
└── modules/
    ├── container-app.bicep
    ├── app-service.bicep
    ├── sql-database.bicep
    ├── key-vault.bicep
    ├── log-analytics.bicep
    └── ...

Each service gets its own module. main.bicep orchestrates resource group creation + module calls.

main.bicep Skeleton

targetScope = 'subscription'

@minLength(1)
@maxLength(64)
param environmentName string

@minLength(1)
param location string

param sessionId string

param deployedBy string   // resolved via: az ad signed-in-user show --query displayName -o tsv

// ⛔ createdAt: passed in parameters.json, NOT utcNow() default (crashes Portal blade)
param createdAt string

var tags = {
  'app-onboard-skill': 'true'
  'app-onboard-session-id': sessionId
  'created-at': createdAt
  environment: environmentName
  'deployed-by': deployedBy
}

resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: 'rg-${environmentName}'
  location: location
  tags: tags
}

// ⛔ scope: rg (symbolic) — creates implicit dependsOn. Do NOT use resourceGroup(name) — it races against RG creation.
module resources './modules/resources.bicep' = {
  name: 'resources'
  scope: rg
  params: {
    location: location
    environmentName: environmentName
    tags: tags
  }
}

main.parameters.json

⛔ ARM JSON only. Do NOT use .bicepparam syntax (using, param, readEnvironmentVariable()). AppOnboard deploys via az deployment sub create (subscription-scope default) — not azd — and .bicepparam requires azd or newer tooling. If the user lacks subscription-level permissions, the deploy phase falls back to az deployment group create automatically.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environmentName": { "value": "{project}-{env}" },
    "location": { "value": "{region}" },
    "sessionId": { "value": "{context.json.sessionId}" },
    "deployedBy": { "value": "{context.json.azure.userDisplayName}" }
  }
}

Naming Convention (Bicep)

The prepare phase generates a logical resource prefix in prepare-plan.json.naming.resourcePrefix (e.g., myapp-dev). Scaffold MUST add a globally unique suffix using Bicep's uniqueString() function to prevent cross-deployment name collisions on globally unique Azure resources (App Service, Storage Account, ACR).

// main.bicep — derive unique suffix from resource group
var nameSuffix = uniqueString(resourceGroup().id)

// Pass unique names to modules
param appName string = 'app-${resourcePrefix}-${take(nameSuffix, 4)}'
param storName string = 'st${replace(resourcePrefix, '-', '')}${take(nameSuffix, 4)}'
param acrName string = 'cr${replace(resourcePrefix, '-', '')}${take(nameSuffix, 4)}'

⛔ Do NOT use uniqueString() for secrets — it is deterministic and predictable. See bicep-patterns-security.md § Secrets for correct secret patterns.

If prepare-plan.json.naming.resources[] provides pre-computed names with suffixes, prefer those — but ALWAYS ensure globally unique resources include a uniqueString() or equivalent hash in main.bicep as a safety net.

Log Analytics Module Output

⛔ Output the resource ID (.id), NOT .properties.customerId. Container Apps Environment requires workspaceResourceId (the full ARM resource ID). .properties.customerId is the GUID used for queries — passing it as workspaceResourceId causes an ARM deploy failure (BadRequest). Separate the two outputs:

output workspaceId string = logAnalyticsWorkspace.id                          // ARM resource ID — for CAE, App Insights
output workspaceCustomerId string = logAnalyticsWorkspace.properties.customerId // GUID — for Log Analytics queries only

Compute-Target Patterns

Read the file(s) matching the service mapping — load only what's needed:

  • Functions on Flex Consumption (the default Functions floor): bicep-functions-flex.md — functionAppConfig, MI-based deployment storage, forbidden App Service settings. ⛔ Do NOT use the App Service module for a Flex function app.
  • App Service (and Functions on Premium/EP1): bicep-app-service.md — module template, SCM/FTP auth, native module deploy strategy
  • Container Apps: bicep-container-apps.md — two-phase ACR wiring, ingress, secretRef, image parameter, multi-container DNS
  • Static Web Apps: bicep-swa.md — module template, detached deploy rule

Load multiple only if the plan includes multiple compute targets.

⛔ F1/D1 SKU: do NOT generate a Dockerfile. If prepare-plan.json specifies F1 or D1 (free/shared tier), use the platform's built-in runtime stack (e.g., NODE|20-lts for Node.js, PYTHON|3.12 for Python). Dockerfiles are for B1+ or Container Apps only.

⛔ Native module deploy strategy. If prepare-plan.json.deployStrategy exists, read bicep-app-service.md § Native Module Deploy Strategy and apply the startup command + app settings. deployStrategy.startupCommand → appCommandLine, deployStrategy.requiredAppSettings → appSettings[]. When no deployStrategy exists, do NOT set appCommandLine.

Cross-Origin (CORS) Between Frontend and Backend

⛔ If the frontend is served from a different hostname than the API, the API MUST declare the frontend origin. A Static Web App calling a Function App / App Service is always cross-origin — different hostname means different origin, so the browser sends a preflight OPTIONS and blocks the response unless the API returns Access-Control-Allow-Origin. A backend deployed with no cors block fails every browser call with: No 'Access-Control-Allow-Origin' header is present on the requested resource. The API works fine from curl and from the portal test console, so this surfaces only in the browser.

⛔ Local success proves nothing here. local.settings.json has "Host": { "CORS": "*" }, but that file configures the local Functions emulator only — it is git-ignored and never deployed. F5 working is not evidence that the deployed API allows the deployed frontend.

Emit cors on the backend's siteConfig, alongside the frontend resource in the same module:

resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
  properties: {
    siteConfig: {
      cors: {
        // staticSites → `defaultHostname`; the value has no scheme, so add https://
        allowedOrigins: [ 'https://${staticWebApp.properties.defaultHostname}' ]
        supportCredentials: false
      }
    }
  }
}

When the frontend and backend are in different modules, pass the origin instead of referencing across. Guard with empty() so the same module still compiles for an API with no browser frontend:

// api module
param allowedOrigin string = ''
...
    siteConfig: {
      cors: {
        allowedOrigins: empty(allowedOrigin) ? [] : [ allowedOrigin ]
        supportCredentials: false
      }
    }

// frontend module
output defaultHostname string = staticWebApp.properties.defaultHostname

// main.bicep
module api './modules/api.bicep' = {
  params: { allowedOrigin: 'https://${frontend.outputs.defaultHostname}' }
}

⛔ A symbolic reference only compiles inside the file that declares it. staticWebApp.properties... fails bicep build with BCP057: The name "staticWebApp" does not exist in the current context whenever the SWA lives in another module — or when there is no frontend at all. Use the direct reference only in the file that declares both; otherwise use the allowedOrigin param form above. For an API with no browser frontend, leave allowedOrigin empty (or omit cors entirely).

⛔ Property casing differs by resource type. Microsoft.Web/staticSites exposes properties.defaultHostname (lowercase n); Microsoft.Web/sites exposes properties.defaultHostName (capital N). Using the wrong one fails at bicep build — verify against the resource you are actually referencing when the frontend is an App Service rather than a SWA.

⛔ allowedOrigins: ['*'] cannot be combined with supportCredentials: true — Azure rejects the pair. Always emit the explicit origin; it is required anyway the moment cookie or bearer auth is added, and it avoids re-work. Include any custom domain as an additional entry when one is configured.

No dependency cycle. The frontend receives the API URL at build time (VITE_API_BASE_URL / NEXT_PUBLIC_API_URL, baked into the bundle from deploy-result.json.endpoints[] — see deploy-checklist-template.md), so the frontend resource never references the backend in Bicep. The backend may therefore reference the frontend hostname freely. Do not add the API URL to the SWA resource to "pair" them — that is what creates a cycle.

Alternative that removes CORS entirely: linking an existing Function App as an SWA backend makes the frontend call /api/* on its own origin, so no preflight occurs. This is allowed only when context.json.overrides[] records explicit user approval for linkFunctionsToStaticWebApp. The Function App remains a separate resource and uses the Functions deployment channel. Never move or copy its source into an SWA API directory, emit SWA buildProperties.apiLocation, or change the backend's runtime or authentication provider to azureStaticWebApps.

Self-review check: If the plan has a browser frontend AND a separate HTTP API resource, verify the API's siteConfig.cors.allowedOrigins contains the frontend origin. FLAGGED if absent — the app will deploy successfully and then fail on first use.

Service Tagging

⛔ You MUST read iac-generation-rules.md § Session Tags. All resources MUST include the 5 AppOnboard session tags. Pass tags object from main.bicep into every module.

API Version Policy

Use the latest stable API version for each resource type. Never use preview APIs unless required for a feature with no GA alternative. Validate via bicep build — stale API versions produce warnings.

App-Internal Secret Syntax (No Key Vault)

⛔ No Key Vault is created. App-internal secrets (e.g. SECRET_KEY) are stored on the compute resource from an @secure() param at deploy time. App Service → siteConfig.appSettings; Container Apps → native secrets: [{ name, value }] + secretRef (see bicep-container-apps.md). Never use @Microsoft.KeyVault(...) / keyVaultUrl. See bicep-patterns-security.md § Secrets. For Terraform, see terraform-patterns.md.