> **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. # Code Deployment — Container Apps After IaC deployment creates the Container App with a placeholder image (`mcr.microsoft.com/azuredocs/containerapps-helloworld:latest`), deploy the actual application code. This is **Phase 2** of the two-phase Container Apps deployment pattern. > ⛔ **`--subscription {subscriptionId}` on EVERY `az` command** (from `context.json.azure.subscriptionId`). Without it, the CLI uses whatever subscription is currently active — which may have changed since the prepare phase. This applies to ALL commands below. > ⛔ **Phase 2 is NOT optional.** If IaC deployed a Container App with a placeholder image, you MUST execute Phase 2 before health checks. Do NOT leave a placeholder image and tell the user to deploy code manually. **Determine the code deploy path:** | Condition | Path | Steps | |-----------|------|-------| | ACR exists in IaC (`services[]` has Container Registry) | ACR build + image update | Steps 1–4 below | | No ACR in IaC, simple app (single Dockerfile or source) | Add ACR to IaC + redeploy, then ACR build | Step 0 + Steps 1–4 | | No ACR in IaC, no Dockerfile, Oryx-compatible (Node/Python/Go/.NET) | `az containerapp update --source .` | Step 5 (Oryx shortcut) | ## Step 0 — Add ACR to IaC (if not already present) Add `container-registry.bicep` module (ACR Basic, `adminUserEnabled: false`) + AcrPull role assignment for CA's managed identity (role GUID: `7f951dda-4ed3-4680-a7ca-43fe172d538d`). Redeploy IaC, wait ~60s for AcrPull role propagation. Log as healing attempt. ⛔ Update `prepare-plan.json` (add ACR to `services[]`), `scaffold-manifest.json.files[]`, and `costEstimate`. ## Steps 1–4 — ACR Build + Image Update (IaC-compliant) | Step | Command | Notes | |------|---------|-------| | 1. Build image | `az acr build --subscription {subscriptionId} -r {acrName} -t {appName}:latest . --no-logs` | Builds from workspace root. If `buildRequirements.hasBuildKitSyntax`, use `-f Dockerfile.azure` (see BuildKit handling below). ⛔ **Build-time env vars:** If Dockerfile has `ARG NEXT_PUBLIC_*` or `ARG VITE_*`, pass `--build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn}` to inject the deployed API URL. These vars are baked into the JS bundle at build time — runtime env vars have no effect on client-side code. Get the API FQDN from Phase 1 output: `az containerapp show -g {rg} -n {apiApp} --query properties.configuration.ingress.fqdn -o tsv`. | | 2. Update Bicep image | Edit `infra/modules/{containerapp}.bicep`: replace placeholder with `image: '{acrLoginServer}/{appName}:latest'`. Add ACR registry config: `registries: [{ server: acrLoginServer, identity: 'system' }]` | IaC-only — no imperative `az containerapp update` | | 3. Redeploy | `az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --parameters containerImage='{acrLoginServer}/{appName}:latest' --name app-onboard-code-deploy-{timestamp} --query properties.provisioningState -o tsv` | Emit new portal link. ⛔ No DB password param — databases are managed-identity only (see [Parameter Pass-Through](#parameter-pass-through-bicep-redeploy-safety)). | | 4. Verify revision | `az containerapp show --subscription {subscriptionId} -g {rg} -n {ca} --query "{revision:properties.latestReadyRevisionName, image:properties.template.containers[0].image}" -o json` | Confirm image is not the placeholder | > ⛔ **`az acr build` failures count toward the `deploy-result.json.healingAttempts[]` counter.** After 3 failed builds (even with different root causes), pause and present a diagnosis to the user: "Web image build failed 3 times: [root causes]. Continue healing? (Yes / Cancel)." Each build fix attempt = 1 healing entry. Cross-reference deploy instructions.md healing loop rule. > ⛔ **Windows: `az acr build` may fail with `UnicodeEncodeError`.** Azure CLI log streaming crashes on non-ASCII characters (✓, ✗) using Windows `charmap` codec. Append `--no-logs` to the build command. Build success/failure is still reported via exit code. ## Step 5 — Oryx Shortcut (no ACR needed, no Dockerfile) ```powershell az containerapp update --subscription {subscriptionId} -g {rg} -n {ca} --source . --set-env-vars NODE_ENV=production ``` > ⛔ **`az containerapp update --source` is allowed for code deploy ONLY.** This is different from `az containerapp up --source` (which is ⛔ BLOCKED because it creates resources imperatively). `update --source` updates an EXISTING Container App — no state drift. ## App-Internal Secrets (native CA secrets — No Key Vault, no seeding step) App-internal secrets are set as **native Container Apps secrets** directly in the Bicep, with the value supplied as an `@secure()` param on the deployment command. ⛔ **No Key Vault, no `az keyvault secret set` step.** Generate each secret ONCE and pass it on every `az deployment sub create`: ```powershell $secretKey = (openssl rand -base64 32) -replace '[/+=]','' # generate ONCE, persist to deploy-secrets.env az deployment sub create ... --parameters secretKey=$secretKey --query properties.provisioningState -o tsv ``` > ⛔ Reload the SAME value from `deploy-secrets.env` on every retry/redeploy (shell variables don't persist between tool calls). Pass it to EVERY deployment so the desired-state apply keeps the CA secret stable. ## After Code Deploy (all paths) - Wait 30–60s for the new revision to become ready - Proceed to Step 7 (Health-Check Endpoints) - If health check returns placeholder content → image update didn't take effect. Check `az containerapp revision list` > ⛔ **`az containerapp revision restart` does NOT pick up a changed secret.** Container Apps bind secret values at revision *creation* time. To pick up an updated native secret, create a NEW revision by redeploying Bicep (preferred) or `az containerapp update --revision-suffix rev{timestamp}`. > ⛔ **ACR registries require the CA managed identity + AcrPull to exist first.** Phase 1 of the two-phase deploy creates the Container App with a placeholder image and `registries: []` (native secrets MAY be set in Phase 1 — they have no RBAC dependency). ACR `registries` with `identity: 'system'` fails in Phase 1 because the CA's managed identity doesn't exist yet (no principalId → AcrPull fails → "Operation expired"). After Phase 1 completes and RBAC propagates (~60s), Phase 2 redeploys with ACR registries + real image + correct `targetPort`. ## Config-File Apps (Go/Viper, Spring Boot, etc.) Creating Azure-specific config (e.g., `config-azure.yml`) is fine for non-secret values. > ⛔ **NEVER bake secrets into config files COPY'd into Docker images.** **Secret injection:** Config uses placeholders → `az containerapp secret set` → `az containerapp update --set-env-vars KEY=secretref:name` → framework env var override. > **Go/Viper:** `AutomaticEnv()` maps `POSTGRES_PASSWORD` → `postgres_password` (underscores), NOT `postgres.password`. Without `SetEnvKeyReplacer(strings.NewReplacer(".", "_"))`, env vars can't override nested keys. ## BuildKit Dockerfile Handling ACR's `az acr build` uses the classic Docker builder — it does NOT support BuildKit. When `buildRequirements.hasBuildKitSyntax == true`: build using the ACR-compatible copy instead: `az acr build -f Dockerfile.azure .`. The user's original Dockerfile stays untouched. If `Dockerfile.azure` doesn't exist yet, create it by stripping BuildKit syntax from the original Dockerfile per [`dockerfile-generation.md § ACR Build Compatibility`](../../scaffold/references/dockerfile-generation.md). ## Parameter Pass-Through (Bicep Redeploy Safety) ⛔ **On every redeploy, pass the SAME values you originally used** — a full desired-state apply, not a patch, so an omitted param reverts to its default. The one param that bites: - **`containerImage`** — omitting it reverts the Container App to the placeholder image. > ⛔ **No `administratorLoginPassword` (or any DB password/key) param exists** — databases are Entra/managed-identity only. There is nothing to re-pass and no `deploy-secrets.env` DB password. App-internal secrets (e.g. `SECRET_KEY`) live in Key Vault and are read via the app's MI — they are not passed as deploy params. ```powershell az deployment sub create ... ` --parameters containerImage='{acrLoginServer}/{appName}:latest' ` --query properties.provisioningState -o tsv ``` ## Database Post-Deploy Verification > ⛔ **You MUST read [`database-post-deploy.md`](database-post-deploy.md)** for migration discovery, execution via Container Apps exec, error handling, and PostgreSQL-specific checks.