python-appservice-deploy
Deploy Python (Flask/Django/FastAPI) code to Azure App Service Linux. WHEN: "Flask App Service", "Django App Service", "FastAPI App Service", "deploy Python to App Service". DO NOT USE FOR: Container Apps, Functions, non-Python, Terraform/Bicep/IaC, full infra — use azure-prepare
Install
npx skills add https://github.com/microsoft/skills/tree/main/.github/plugins/azure-skills/skills/python-appservice-deploy
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install microsoft-skills@llmmart
git clone https://github.com/microsoft/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole microsoft/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Python on Azure App Service — Code Deploy
Deploys Python (Flask, Django, FastAPI, generic) code to Azure App Service Linux (P0v3, Python 3.14). Creates RG + Plan + Web App if missing. Hand off to azure-prepare for VNet, Key Vault, databases, or IaC.
MCP tools used: mcp_azure_mcp_subscription_list, mcp_azure_mcp_group_list, mcp_azure_mcp_appservice, mcp_azure_mcp_azd (when azure.yaml is present).
Workflow
- Resolve context — smart defaults, minimal prompts. Only the app name is interactive; RG (
<app>-rg), Plan (<app>-plan), region (currentazdefault oreastus2), subscription are derived. create-app.md §1. - Detect framework (advisory, never blocks). detect.md.
- Choose path —
azure.yamlhost: appservice → deploy-azd.md; else deploy-azcli.md. - Ensure RG → Plan (
P0v3 --is-linux) → Web App (--runtime "PYTHON:3.14") exist. On transient ARM errors, follow transient-retry.md. create-app.md. - Set startup — Flask/Django: none (Oryx auto-detects). FastAPI: always
python -m uvicorn main:app --host 0.0.0.0. Other: warn. startup-commands.md. - Set
SCM_DO_BUILD_DURING_DEPLOYMENT=true. - Deploy —
azd deployoraz webapp deploy --type zip --track-status false. - STOP. Print the post-deploy message (post-deploy-message.md) and end the turn.
Hard rules
- ⛔ NO POST-DEPLOY VERIFICATION — after deploy returns, do not run
az webapp log tail,curl,Invoke-WebRequest, or any health probe. App Service needs 2–3 min to warm; a quiet log or early 5xx is not failure. - ⛔ SHELL SAFETY — for
--runtimealways use"PYTHON:3.14"(colon). Never"PYTHON|3.14"(pipe is a shell operator). - ⛔ NEVER
az webapp up— deprecated. Use Step 7 commands. - ✅ URL FORMAT — present endpoints as
https://...URLs.
Error Handling
See errors.md for the full symptom → cause → fix matrix. Quick triage: missing plan/app → re-run Step 4; container ping timeout on 8000 → fix startup (Step 5); ModuleNotFoundError after deploy → ensure Step 6 ran, redeploy.
Files (skills)
-
references
-
create-app.md 6.1 KB
# Create RG + App Service Plan + Web App (Linux, P0v3) Creates resources only when missing — every step is `show || create` (idempotent against existing user-supplied names). > 💡 **Shell note**: Bash blocks below use `\` line continuation, `||`, `2>/dev/null`, `$(…)`. PowerShell equivalents are shown alongside where the bash form doesn't round-trip — substitute `` ` `` for `\`, `2>$null` for `2>/dev/null`, and `$LASTEXITCODE` checks for `||`. ## 1. Resolve Azure context — minimize prompts **Ask the user at most ONE question (the app name).** Derive everything else. If the user's request already names an RG / Plan / region / subscription (e.g. *"deploy to my-team-rg in westus3"*), use those values verbatim and skip the corresponding derivation — the `show || create` flow below works against existing resources. ### 1a. Subscription ```bash az account show --query id -o tsv ``` If unset, prompt the user to `az login`. Only call `ask_user` if multiple subscriptions are configured and no default is set. ### 1b. App name Ask the user **once**: > "What name would you like for your App Service? (Press Enter to auto-generate one.)" If empty / "any" / "you choose", call the generator script — it implements the slug rules (lowercase, hyphen-collapse, ≤ 40 chars, `^[a-z][a-z0-9-]{1,38}[a-z0-9]$`) and an 8-hex-char GUID suffix: - Bash / zsh: [`scripts/generate-app-name.sh`](../scripts/generate-app-name.sh) → `APP_NAME=$(./scripts/generate-app-name.sh [folder])` - PowerShell: [`scripts/generate-app-name.ps1`](../scripts/generate-app-name.ps1) → `$appName = & .\scripts\generate-app-name.ps1 [-FolderName <folder>]` Example: folder `my-flask-app/` → `my-flask-app-a3f9c1d2`. ### 1c. Derived names (use only when user did not specify) | Resource | Default | |---|---| | Resource group | `<app-name>-rg` | | App Service Plan | `<app-name>-plan` | ### 1d. Region 1. If user specified a region, use it. 2. Else read the CLI default. Suppress the "Configuration is not set" stderr line **only** when paired with an exit-code check — never blindly drop stderr, since it would also swallow auth/transport errors: ```bash REGION=$(az config get defaults.location -o tsv 2>/dev/null) || REGION="" ``` ```powershell $region = az config get defaults.location -o tsv 2>$null; if ($LASTEXITCODE -ne 0) { $region = "" } ``` 3. Else default to `eastus2`. 4. Only call `ask_user` if `az group create` later fails with a region/quota/availability error. ### 1e. Show the defaults summary BEFORE creating Print one concise block so the user can interrupt to override. **Example** (illustrative — substitute the actual derived values, do not print verbatim): ``` Using these defaults for your Python App Service deployment: • App name : flask-app-demo-27may • Resource group : flask-app-demo-27may-rg (auto-derived) • App Service Plan: flask-app-demo-27may-plan (auto-derived) • Region : eastus2 (CLI default) • Plan SKU : P0v3 Linux • Runtime : PYTHON:3.14 Proceeding with create. Reply "stop" within the next message to change any value. ``` Do **not** call `ask_user` for confirmation here — just print and proceed. ### 1f. Transient error handling On connection-level or 429/5xx errors from any `az ... create` in §§2–4, see [transient-retry.md](transient-retry.md). Configuration errors (`AuthorizationFailed`, `SkuNotAvailable`, `QuotaExceeded`, etc.) must **not** be retried — surface them. ## 2. Resource Group ```bash az group show -n <rg> --only-show-errors 2>/dev/null || \ az group create -n <rg> -l <region> ``` ```powershell az group show -n <rg> --only-show-errors 2>$null if ($LASTEXITCODE -ne 0) { az group create -n <rg> -l <region> } ``` ## 3. App Service Plan — **Linux, P0v3 by default** > ⚠️ **MANDATORY**: Use `--is-linux` and `--sku P0v3`. Do not change OS or SKU unless the user explicitly requests it. ```bash az appservice plan show -n <plan> -g <rg> --only-show-errors 2>/dev/null || \ az appservice plan create -n <plan> -g <rg> --is-linux --sku P0v3 -l <region> ``` ```powershell az appservice plan show -n <plan> -g <rg> --only-show-errors 2>$null if ($LASTEXITCODE -ne 0) { az appservice plan create -n <plan> -g <rg> --is-linux --sku P0v3 -l <region> } ``` ## 4. Web App — Python 3.14 runtime (Linux) > ⚠️ **Shell safety**: Always use the **colon** form `PYTHON:3.14` — never the pipe form `PYTHON|3.14`. The pipe character is a shell operator in PowerShell, Bash, and cmd, and breaks the command even when quoted in some contexts. The colon form is fully supported by `az webapp create --runtime` and is shell-safe everywhere. ```bash az webapp show -n <app> -g <rg> --only-show-errors 2>/dev/null || \ az webapp create -n <app> -g <rg> -p <plan> --runtime "PYTHON:3.14" ``` ```powershell az webapp show -n <app> -g <rg> --only-show-errors 2>$null if ($LASTEXITCODE -ne 0) { az webapp create -n <app> -g <rg> -p <plan> --runtime "PYTHON:3.14" } ``` The 8-hex-char GUID suffix from §1b is sufficient for global hostname uniqueness; the optional `--domain-name-scope TenantReuse` flag (Azure CLI ≥ 2.76, July 2025) is intentionally omitted to stay compatible with older CLIs. ### Discover available runtimes If `PYTHON:3.14` is unavailable in the region: ```bash az webapp list-runtimes --os linux --query "[?contains(@, 'PYTHON')]" -o tsv ``` The output uses the pipe form (e.g., `PYTHON|3.14`) — **convert to colon form** before passing to `--runtime`. Prefer 3.14; fall back to 3.13, then 3.12. ## 5. Verify ```bash az webapp show -n <app> -g <rg> --query "{name:name, state:state, host:defaultHostName, linuxFx:siteConfig.linuxFxVersion}" -o table ``` Expected: `state: Running`, `linuxFx: PYTHON|3.14` (Azure stores it in pipe form internally — normal), `host: <app>.azurewebsites.net`. ## Notes - ⛔ Never use `az webapp up` — deprecated. See [deploy-azcli.md](deploy-azcli.md). - If the user requests a different SKU (e.g. `B1` for dev/test), respect it but warn that **P0v3** is the documented default. - If a Windows plan is requested, hand off to `azure-prepare`. -
deploy-azcli.md 4.7 KB
# Deploy via `az` CLI Use this path when there is **no** `azure.yaml` or it doesn't target `appservice`. > ⛔ **NEVER USE `az webapp up`** — this command is deprecated. Use the explicit create + deploy commands below. ## Prerequisites - `az login` complete - Subscription, resource group, region, and app name decided (see [create-app.md](create-app.md)) - App Service Plan (Linux, P0v3) and Web App (Python runtime) exist (see [create-app.md](create-app.md)) ## 1. Enable server-side build ```bash az webapp config appsettings set \ -n <app> -g <rg> \ --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true ``` ```powershell az webapp config appsettings set ` -n <app> -g <rg> ` --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true ``` This tells Oryx to run `pip install -r requirements.txt` during deploy. ## 2. Startup command — skip for Flask/Django, always set for FastAPI Azure App Service (Oryx) auto-detects **Flask** and **Django** — **do not set a startup command** for these. Skip this step entirely. For **FastAPI**, always set the uvicorn startup command, regardless of the Python runtime version. This skill does **not** rely on Oryx FastAPI auto-detection, so the behavior is identical on every supported runtime (3.12, 3.13, 3.14, …): ```bash az webapp config set -n <app> -g <rg> \ --startup-file "python -m uvicorn main:app --host 0.0.0.0" ``` ```powershell az webapp config set -n <app> -g <rg> ` --startup-file "python -m uvicorn main:app --host 0.0.0.0" ``` (Replace `main:app` if the FastAPI entry point differs — e.g., `app.main:app`.) For other frameworks (generic WSGI / ASGI / unknown), **skip this step** and emit the manual-startup warning. See [startup-commands.md](startup-commands.md). ## 3. Package the code Zip the project (excluding venv, caches, git, node_modules): ```bash # bash zip -r app.zip . \ -x ".git/*" -x ".venv/*" -x "venv/*" -x "__pycache__/*" \ -x "*.pyc" -x ".env" -x "node_modules/*" ``` ```powershell # PowerShell $exclude = @('.git','.venv','venv','__pycache__','node_modules') $items = Get-ChildItem -Force | Where-Object { $exclude -notcontains $_.Name } Compress-Archive -Path $items -DestinationPath app.zip -Force ``` ## 4. Deploy the zip ```bash az webapp deploy \ -n <app> -g <rg> \ --src-path app.zip \ --type zip \ --track-status false ``` ```powershell az webapp deploy ` -n <app> -g <rg> ` --src-path app.zip ` --type zip ` --track-status false ``` > 💡 `--track-status false` returns once the ZIP is **accepted by the SCM endpoint** — this is **not** the same as "Oryx build succeeded". The server-side `pip install` / startup-command rendering happens asynchronously after the CLI returns. A zero exit code only confirms the upload + a deployment record. If the site never starts, inspect the build outcome via `az webapp log deployment list/show` — that is the only authoritative confirmation that the build itself succeeded. ## 5. Stop. Report the endpoint to the user. After `az webapp deploy` returns, the skill is done. > ℹ️ `az webapp deploy` does **not** initiate a cold start by pinging the site. With `--track-status false`, it returns as soon as the SCM endpoint accepts the ZIP; the Oryx build and container restart happen asynchronously on the SCM side. The container only warms up when an inbound HTTP request actually hits `https://<app>.azurewebsites.net` — which is why the post-deploy message tells the user to expect a 2–3 minute wait on their first visit. > ⛔ **Do NOT run** `az webapp log tail`, `curl`, `Invoke-WebRequest`, `wget`, or any other "verify startup" command. App Service routinely needs **2–3 minutes** to warm the container; a quiet log stream or a 5xx in the first couple of minutes is **not** a failure signal, and running these probes here will mislead the user. Resolve the host name without hitting the site: ```bash HOST=$(az webapp show -n <app> -g <rg> --query defaultHostName -o tsv) echo "https://$HOST" ``` ```powershell $host_ = az webapp show -n <app> -g <rg> --query defaultHostName -o tsv "https://$host_" ``` Then print the post-deploy message from [post-deploy-message.md](post-deploy-message.md) and end the turn. The user will run `az webapp log tail -n <app> -g <rg>` themselves if they want to watch logs. ## Common pitfalls | Pitfall | Fix | |---------|-----| | Deployed code missing dependencies | `SCM_DO_BUILD_DURING_DEPLOYMENT=true` not set — re-run step 1 then redeploy | | Container ping timeout on port 8000 | Wrong startup command — see [startup-commands.md](startup-commands.md) | | Zip too large (>500 MB) | Exclude `.venv`, caches; consider `.deployment` `.gitignore`-style file | | `webapp up` examples in older docs | Replace with `az webapp create` + `az webapp deploy` (this file) | -
deploy-azd.md 2.3 KB
# Deploy via `azd` Use this path when the workspace already has an `azure.yaml` whose service host is `appservice`. ## When to use | Condition | Use azd? | |---|---| | `azure.yaml` exists AND `services.<name>.host: appservice` | ✅ Yes | | `azure.yaml` exists but targets Container Apps / Functions / etc. | ❌ Hand off to `azure-prepare` | | No `azure.yaml` | ❌ Use [deploy-azcli.md](deploy-azcli.md) | ## Confirm host target ```bash # Look for `host: appservice` under services in azure.yaml grep -E "host:\s*appservice" azure.yaml ``` ```powershell # Look for `host: appservice` under services in azure.yaml Select-String -Path azure.yaml -Pattern 'host:\s*appservice' ``` If no match → use the az CLI path. ## Authenticate ```bash azd auth login --check-status || azd auth login ``` ```powershell azd auth login --check-status if ($LASTEXITCODE -ne 0) { azd auth login } ``` ## Provision (first time only) If no `azd` environment exists in this folder: ```bash azd env new <env-name> azd env set AZURE_LOCATION <region> # Optional: override default SKU via the template's parameters azd env set APP_SERVICE_SKU P0v3 ``` Then provision + deploy in one call: ```bash azd up ``` ## Deploy code only (subsequent deploys) ```bash azd deploy ``` After `azd deploy` returns, **stop**. Do not run `azd monitor`, `az webapp log tail`, or any HTTP probe. ## Report endpoint to the user > ⛔ **Do NOT probe the endpoint** (no `curl`, no `Invoke-WebRequest`) and **do NOT tail logs** as a verification step. App Service can take 2–3 minutes after deploy before the site responds. Read the endpoint from azd without hitting the site: ```bash # bash azd env get-values | grep -E '^SERVICE_.*_URI=' ``` ```powershell # PowerShell azd env get-values | Select-String "SERVICE_.*_URI" ``` Present the URL with the `https://` prefix and the post-deploy message from [post-deploy-message.md](post-deploy-message.md), then end the turn. ## Notes - ⛔ Do **not** run `azd init -t <template>` in an existing workspace — it can destroy user code. Only `azd init` (no template) is safe inside an existing project. - If `azd up` provisions infra that doesn't match P0v3 Linux, the underlying template owns that decision — don't fight it. Note this to the user. - If `azd deploy` fails because no environment exists, fall back to `azd env new` then re-run. -
detect.md 3.6 KB
# Framework Detection (Advisory) Framework detection is **advisory only**. The deployment never blocks because of an unknown framework. ## What to check 1. **Confirm Python project** — at least one of: - `requirements.txt` - `pyproject.toml` - `*.py` files in workspace root 2. **Scan dependencies** in `requirements.txt` or `pyproject.toml`: | Token (case-insensitive) | Detected framework | |---|---| | `flask`, `Flask` | **flask** | | `django`, `Django` | **django** | | `fastapi` | **fastapi** | | `gunicorn` (alone, no flask) | **wsgi-generic** | | `uvicorn` (alone, no fastapi) | **asgi-generic** | | None of the above | **unknown** | 3. **Locate the WSGI / ASGI entry point** (best-effort): - Flask common: `app.py` exporting `app`, `application.py`, `wsgi.py` - Django common: `<project>/wsgi.py` - FastAPI common: `main.py` exporting `app` 4. **Record findings** in your working memory so Step 5 (startup) can use them. ## Outcomes | Detection | Step 5 behavior | |---|---| | `flask` | Skip startup auto-config. Oryx auto-detects Flask and starts it. | | `django` | Skip startup auto-config. Oryx auto-detects Django via `wsgi.py` and starts it. | | `fastapi` (any Python version) | **Always auto-set** startup: `python -m uvicorn main:app --host 0.0.0.0` (replace `main:app` with the discovered entry point if different — e.g., `app.main:app`). The skill does not rely on Oryx FastAPI auto-detection. | | `wsgi-generic`, `asgi-generic`, `unknown` | Skip startup auto-config. Emit warning: *"Could not auto-detect a supported framework (only Flask, Django, and FastAPI are auto-configured today). The app will deploy, but you may need to set the startup command manually: `az webapp config set --startup-file '<your-command>'`"* | > Priority rule when both `flask` and `fastapi` appear in `requirements.txt` (or `pyproject.toml`): **always treat as FastAPI** — set the explicit uvicorn startup command. This is deterministic and avoids relying on import-order or token-order heuristics. Rationale: Flask is happily auto-detected by Oryx with no startup command, but FastAPI requires the explicit uvicorn command to run reliably; if the project actually uses Flask as the served app, the user can override the startup command later, but if it uses FastAPI and we silently picked Flask, the container ping fails. ## Important rules - ⛔ **Do not** abort deployment when framework is unknown. - ⛔ **Do not** try to install Flask, Django, or any framework into the user's project. - ✅ Always report what was detected so the user has full context. - ✅ When detection is `wsgi-generic`, `asgi-generic`, or `unknown`, **remember this fact for Step 8** — the post-deploy message must use the **unknown-framework template** in [post-deploy-message.md](post-deploy-message.md), which adds an explicit "set a startup command" instruction. The Step 5 warning alone is not enough; the user needs the reminder at the end of the run too. ## Example output to surface to the user ``` Detected: Flask (Python 3.14) Entry point: app.py Startup command will be auto-detected by Oryx — no startup command needed. ``` or ``` Detected: Django (Python 3.14) Entry point: <project>/wsgi.py Startup command will be auto-detected by Oryx — no startup command needed. ``` or ``` Detected: FastAPI (Python 3.14) Entry point: main.py → app Setting startup command (always set for FastAPI): python -m uvicorn main:app --host 0.0.0.0 ``` or ``` Detected: Python project, framework unknown. The app will deploy, but App Service may not start it correctly until you set a startup command: az webapp config set -n <app> -g <rg> --startup-file '<command>' See startup-commands.md for examples. ``` -
errors.md 3.8 KB
# Troubleshooting ## Quick diagnosis flow ``` Deploy failed? ├─ Check `az webapp log tail` first ├─ Then `az webapp log deployment list` for build-time errors ├─ Then `az webapp config show` to verify runtime + startup └─ For `Connection reset` / `429` / `502-504` on create commands, see [transient-retry.md](transient-retry.md) ``` ## Symptom → Cause → Fix | Symptom | Likely cause | Fix | |---|---|---| | `Container ... didn't respond to HTTP pings on port: 8000` | Wrong/missing startup command, or app bound to 127.0.0.1 | Set startup to bind `0.0.0.0:8000` ([startup-commands.md](startup-commands.md)) | | `ModuleNotFoundError: No module named 'flask'` (or any dep) | Build skipped, deps not installed | `az webapp config appsettings set --settings SCM_DO_BUILD_DURING_DEPLOYMENT=true` then redeploy | | `gunicorn: command not found` | `gunicorn` missing from `requirements.txt` | Add `gunicorn>=21` to requirements, redeploy | | Deploy returns 202 but site still shows default page | Async deploy not finished | Wait, or re-run with `--track-status true` to make the CLI block until completion; check `az webapp log deployment list` | | `OperationNotAllowed: ... requires the plan to be Linux` | Plan was created as Windows | Recreate plan with `--is-linux` ([create-app.md](create-app.md)) | | `Resource group '<rg>' could not be found` | RG missing | `az group create -n <rg> -l <region>` | | `An app with name '<app>' already exists` | Global name collision | Pick a different name (App Service host names are globally unique) | | `az webapp up` shown in user's notes | Deprecated command | Replace with `az webapp create` + `az webapp deploy --type zip` | | `azd up` provisions Container Apps instead of App Service | azure.yaml host isn't `appservice` | Either fix the template's `host:` or fall back to az CLI path | | Deployment hangs in "Building..." | Oryx pip install failing on native deps | Run `az webapp log deployment list -n <app> -g <rg>` to find the deployment ID, then `az webapp log deployment show -n <app> -g <rg> --deployment-id <id>`; pin known-good versions in requirements | | 401 / unauthorized on deploy | `az login` expired or wrong subscription | `az login` + `az account set -s <sub>` | | `LinuxFxVersion` shows `DOCKER\|...` | Web app was created as custom container | Recreate with `--runtime "PYTHON:3.14"` (colon form is shell-safe) | ## Where logs live > ⚠️ **Prereq for `az webapp log tail` on a fresh app**: filesystem logging must be enabled, otherwise the stream stays empty. Run **once** after the app is created: > > ```bash > az webapp log config -n <app> -g <rg> \ > --application-logging filesystem \ > --web-server-logging filesystem \ > --level information > ``` > ```powershell > az webapp log config -n <app> -g <rg> ` > --application-logging filesystem ` > --web-server-logging filesystem ` > --level information > ``` > > `az webapp log deployment list/show` and `az webapp log download` do **not** require this — they read from a different store. | Log | Command | |---|---| | Live stream | `az webapp log tail -n <app> -g <rg>` | | Deployment history | `az webapp log deployment list -n <app> -g <rg>` | | Deployment details | `az webapp log deployment show -n <app> -g <rg> --deployment-id <id>` | | Full log download | `az webapp log download -n <app> -g <rg>` | ## When to hand off to `azure-prepare` Hand off (and stop) if the user needs: - VNet integration, private endpoints, or Front Door - Key Vault references for app settings - A database (Cosmos DB, PostgreSQL, MySQL, SQL) - Multiple environments with Bicep/Terraform-managed infra - A non-App-Service target (Container Apps, Functions, AKS) Tell the user clearly: *"This deployment goes beyond a code push — `azure-prepare` will build the full infrastructure plan."* -
post-deploy-message.md 4.4 KB
# Post-deploy message to the user After `az webapp deploy` / `azd deploy` returns successfully, **the skill is done**. Do not run any further verification commands. ## Hard rules - ⛔ Never run `az webapp log tail` as a "confirm startup" step — logs are often quiet for 1–2 min during build/warm-up; silence is not a failure signal. - ⛔ Never run `curl`, `Invoke-WebRequest`, `wget`, or any HTTP request against the deployed URL — first request often 502s or times out on a healthy deploy. - ⛔ Never present an early 5xx or quiet log stream as a deploy failure — the deploy succeeded; the platform just isn't warm yet. - ✅ Always give the user the URL, the wait expectation, and the log command, then **stop**. ## Message template — standard (Flask / Django / FastAPI) Use this when Step 2 detected a known framework (`flask`, `django`, or `fastapi`). Print exactly this (substituting the real values) as the final output of the skill, then end the turn: ``` ✅ Deployment complete. 🌐 App URL: https://<app>.azurewebsites.net It can take 2–3 minutes for the site to be reachable while App Service finishes warming up the container. Open it in your browser after a short wait. 📜 If you want to watch live logs: az webapp log config -n <app> -g <rg> --application-logging filesystem --level information az webapp log tail -n <app> -g <rg> (The first command is a one-time prereq on a fresh app — without it the stream stays empty.) ``` ## Message template — unknown framework Use this when Step 2 detected `wsgi-generic`, `asgi-generic`, or `unknown` (i.e. **not** Flask, Django, or FastAPI). The code is already deployed and `SCM_DO_BUILD_DURING_DEPLOYMENT=true` is set, but Oryx will not know how to start the app until the user sets a startup command. Print this instead: ``` ✅ Code deployed — but framework not detected. 🌐 App URL: https://<app>.azurewebsites.net It can take 2–3 minutes for App Service to finish building and start the container. The site will likely return an error page until you set a startup command (next step). ⚠️ We could not detect Flask, Django, or FastAPI in your project, so no startup command was set automatically. Set one with: az webapp config set -n <app> -g <rg> \ --startup-file "<your-startup-command>" Examples: • Generic WSGI (gunicorn): gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable> • Generic ASGI (uvicorn): python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000 See references/startup-commands.md for more guidance. 📜 If you want to watch live logs: az webapp log config -n <app> -g <rg> --application-logging filesystem --level information az webapp log tail -n <app> -g <rg> (The first command is a one-time prereq on a fresh app — without it the stream stays empty.) ``` Replace `<module>:<callable>` with the user's actual entry point (e.g. `app:app`, `main:application`, `myapp.wsgi:application`). If you can identify a likely entry point from the source code, **suggest a concrete command** instead of leaving placeholders. ## Logging tips (mention only if the user asks for them) > ⚠️ **Prereq for `az webapp log tail` on a fresh app**: filesystem logging must be enabled or the live stream stays empty. Run once: > > ```bash > az webapp log config -n <app> -g <rg> \ > --application-logging filesystem --web-server-logging filesystem --level information > ``` > ```powershell > az webapp log config -n <app> -g <rg> ` > --application-logging filesystem --web-server-logging filesystem --level information > ``` > > Deployment-build logs do **not** require this — read them with `az webapp log deployment list/show`. | Log | Command | |---|---| | Deployment history | `az webapp log deployment list -n <app> -g <rg>` | | Deployment details | `az webapp log deployment show -n <app> -g <rg> --deployment-id <id>` | | Full log download | `az webapp log download -n <app> -g <rg>` | ## Picking which template to use | Detected framework (Step 2) | Template | |---|---| | `flask`, `django`, `fastapi` | **standard** | | `wsgi-generic`, `asgi-generic`, `unknown` | **unknown framework** | ## What "success" means here Either of these is enough to print the success message — do **not** gate on log output or an HTTP probe: - `az webapp deploy` returned without an error, **or** - `azd deploy` printed `SUCCESS: Your application was deployed to Azure` -
startup-commands.md 4.3 KB
# Startup Commands by Framework Linux App Service (Oryx) auto-detects **Flask** and **Django** — no startup command is needed for these. **FastAPI does NOT rely on Oryx auto-detection** in this skill: we always set an explicit uvicorn startup command so the behavior is identical on every supported Python runtime (3.12, 3.13, 3.14, …). This skill also sets a startup command (or emits a manual hint) for non-Flask/Django/FastAPI frameworks. ## Flask — no startup command needed Azure App Service (Oryx) auto-detects Flask and starts it correctly. **Do not run `az webapp config set --startup-file` for Flask apps.** ## Django — no startup command needed Azure App Service (Oryx) auto-detects Django by finding `wsgi.py` and starts gunicorn against `<project>.wsgi:application` automatically. **Do not run `az webapp config set --startup-file` for Django apps.** Make sure the project has: - `requirements.txt` containing `django` (and ideally `gunicorn`; Oryx provides one if missing) - `<project>/wsgi.py` at a path Oryx can discover (the default Django layout works out of the box) - `ALLOWED_HOSTS` includes `<app>.azurewebsites.net` (or `*` for first-deploy validation — tighten later) ## FastAPI — always set the uvicorn startup command The skill sets the startup command unconditionally for FastAPI, regardless of the Python runtime version. Oryx FastAPI auto-detection is **not** relied on — an explicit startup command always works and avoids version-dependent surprises: ```bash az webapp config set -n <app> -g <rg> \ --startup-file "python -m uvicorn main:app --host 0.0.0.0" ``` ```powershell az webapp config set -n <app> -g <rg> ` --startup-file "python -m uvicorn main:app --host 0.0.0.0" ``` Replace `main:app` with the discovered entry point if different (e.g., `app.main:app`, `src.api:app`). The `--host 0.0.0.0` flag is mandatory — uvicorn defaults to 127.0.0.1, which causes App Service container-ping timeouts. Make sure `requirements.txt` contains both `fastapi` and `uvicorn` (or `uvicorn[standard]`). ## Not auto-configured — warn & deploy anyway When the detected framework is `wsgi-generic`, `asgi-generic`, or `unknown`, **deploy the code without setting a startup command** and surface this message to the user: > ⚠️ This skill auto-configures Flask, Django, and FastAPI only. The app has been deployed, but App Service may not start it until you set a startup command. Choose the matching example below and run it: ### Generic WSGI ```bash az webapp config set -n <app> -g <rg> \ --startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>" ``` ```powershell az webapp config set -n <app> -g <rg> ` --startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <module>:<callable>" ``` ### Generic ASGI ```bash az webapp config set -n <app> -g <rg> \ --startup-file "python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000" ``` ```powershell az webapp config set -n <app> -g <rg> ` --startup-file "python -m uvicorn <module>:<callable> --host 0.0.0.0 --port 8000" ``` ### Django override (only if auto-detection fails) Oryx auto-detection covers the standard Django layout. Set this manually **only** if the project uses a non-standard layout and the auto-detected startup doesn't find your WSGI app: ```bash az webapp config set -n <app> -g <rg> \ --startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <project>.wsgi" ``` ```powershell az webapp config set -n <app> -g <rg> ` --startup-file "gunicorn --bind=0.0.0.0 --timeout 600 <project>.wsgi" ``` ## Diagnosing a wrong startup command ```bash # Current value az webapp config show -n <app> -g <rg> --query linuxFxVersion -o tsv az webapp config show -n <app> -g <rg> --query appCommandLine -o tsv # Live logs az webapp log tail -n <app> -g <rg> ``` Look for: - `Container <app>_<...> didn't respond to HTTP pings on port: 8000` → app didn't bind to 0.0.0.0 (or 0.0.0.0:8000) - `ModuleNotFoundError` → wrong `<module>:<callable>` in startup command, or build didn't install deps - `gunicorn: command not found` / `uvicorn: command not found` → add `gunicorn` / `uvicorn` to `requirements.txt` ## Why `--host 0.0.0.0` matters App Service forwards traffic to the container on the port the runtime listens on (8000 by default). The startup command must bind to `0.0.0.0` so the platform's ping reaches it — binding to `127.0.0.1` causes ping timeouts. -
transient-retry.md 2.3 KB
# Transient ARM error handling for `az ... create` Load this file only when an `az group create`, `az appservice plan create`, or `az webapp create` command fails. Healthy runs never need it. ## Which errors are transient ARM `PUT` operations sometimes fail with **transient** errors: - `Connection reset`, `Connection aborted`, `ConnectionError` - `Read timed out`, `Max retries exceeded` - `BadGatewayConnection`, `ServiceUnavailable` - `TooManyRequests` / HTTP `429` - HTTP `502` / `503` / `504` These are ARM frontend / network blips, not configuration problems — they almost always succeed on retry. **Do NOT retry on** `AuthorizationFailed`, `SubscriptionNotFound`, `ResourceGroupNotFound`, `InvalidTemplateDeployment`, `SkuNotAvailable`, `QuotaExceeded`, or any non-429 4xx. Those are real config errors — surface them to the user. ## Rules - Apply to every `az ... create` in [create-app.md](create-app.md) §§2–4. - Retry **silently** — do not narrate "let me retry". - Up to **2 retries** (3 attempts total); wait **5s** before retry #1 and **15s** before retry #2. If the error carries a `Retry-After` header, honour that instead. - **Wrap the full idempotent pair** `(az ... show ...) || (az ... create ...)` — never the bare `create`. The `show` short-circuits any partially-succeeded prior attempt and avoids false `Conflict` / `NameAlreadyExists`. - After 3 failed attempts, surface the original error with one line of context (e.g., `"ARM frontend is returning transient errors — please retry in a few minutes"`). ## Use the wrapper scripts Both shells have a ready-to-call wrapper that implements the loop, the transient-error filter, and the backoff. Prefer these over inlining the loop: - Bash / zsh (Linux, macOS): [`scripts/retry-az-create.sh`](../scripts/retry-az-create.sh) ```bash ./scripts/retry-az-create.sh \ "az group show -n my-rg --only-show-errors" \ "az group create -n my-rg -l eastus2" ``` - PowerShell (Windows): [`scripts/retry-az-create.ps1`](../scripts/retry-az-create.ps1) ```powershell .\scripts\retry-az-create.ps1 ` -ShowCommand "az group show -n my-rg --only-show-errors" ` -CreateCommand "az group create -n my-rg -l eastus2" ``` Both scripts run the `show` command first, fall through to `create` on failure, and retry up to 2 times on transient errors only.
-
-
scripts
-
generate-app-name.ps1 1.7 KB · in bundle
-
generate-app-name.sh 1.8 KB
#!/usr/bin/env bash # generate-app-name.sh # Generates a valid Azure App Service name from a folder name + 8 hex chars # of randomness, suitable for `az webapp create -n <name>`. # # Rules enforced (matches Azure App Service naming requirements): # - lowercase a-z, 0-9, and hyphens only # - starts with a letter, ends with letter or digit # - total length <= 40 chars (slug truncated as needed) # - regex: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$ # # Usage: # ./generate-app-name.sh [folder-name] # # If folder-name is omitted, the current working directory's basename is used. # # Examples: # ./generate-app-name.sh # uses current folder # ./generate-app-name.sh my-flask-app # → my-flask-app-a3f9c1d2 set -euo pipefail INPUT="${1:-$(basename "$PWD")}" # Lowercase, replace non-[a-z0-9] with '-', collapse repeats, trim hyphens. SLUG=$(echo "$INPUT" \ | tr '[:upper:]' '[:lower:]' \ | sed -E 's/[^a-z0-9]+/-/g; s/-+/-/g; s/^-//; s/-$//') # Fallback if the slug ended up empty (e.g. folder was only symbols). if [ -z "$SLUG" ]; then SLUG="app" fi # 8 hex chars from a fresh GUID (segment before the first '-'). if command -v uuidgen >/dev/null 2>&1; then SUFFIX=$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d- -f1 | cut -c1-8) else # Fallback: /dev/urandom + xxd / od. SUFFIX=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n' | cut -c1-8) fi # Reserve 9 chars for "-XXXXXXXX" so the total stays <= 40. MAX_SLUG_LEN=31 if [ ${#SLUG} -gt $MAX_SLUG_LEN ]; then SLUG=$(echo "$SLUG" | cut -c1-$MAX_SLUG_LEN | sed -E 's/-$//') fi NAME="${SLUG}-${SUFFIX}" # Ensure the name starts with a letter (Azure requirement). Prefix 'a' if not. case "$NAME" in [a-z]*) ;; *) NAME="a${NAME}";; esac # Final length guard. NAME=$(echo "$NAME" | cut -c1-40 | sed -E 's/-$//') echo "$NAME" -
retry-az-create.ps1 2.1 KB · in bundle
-
retry-az-create.sh 1.9 KB
#!/usr/bin/env bash # retry-az-create.sh # Wraps the idempotent `(az ... show ...) || (az ... create ...)` pair with # silent retries against transient ARM frontend errors (connection resets, # 429/502/503/504, timeouts). # # Usage: # ./retry-az-create.sh "<show-command>" "<create-command>" # # Both commands are passed as single shell-quoted strings. The script: # - runs `show` first (short-circuits any partially-succeeded prior attempt # and avoids false NameAlreadyExists / Conflict errors); # - if `show` fails, runs `create`; # - on transient failure, retries up to 2 times (3 attempts total) with # 5s then 15s backoff; # - on non-transient failure, surfaces the original error and exits 1. # # Examples: # ./retry-az-create.sh \ # "az group show -n my-rg --only-show-errors" \ # "az group create -n my-rg -l eastus2" # # ./retry-az-create.sh \ # "az appservice plan show -n my-plan -g my-rg --only-show-errors" \ # "az appservice plan create -n my-plan -g my-rg --is-linux --sku P0v3 -l eastus2" set -uo pipefail SHOW_CMD="${1:?Usage: $0 \"<show-command>\" \"<create-command>\"}" CREATE_CMD="${2:?Usage: $0 \"<show-command>\" \"<create-command>\"}" TRANSIENT='Connection reset|Connection aborted|ConnectionError|Read timed out|BadGatewayConnection|ServiceUnavailable|Max retries exceeded|TooManyRequests|\b429\b|\b50[234]\b' for attempt in 1 2 3; do # Run show silently; on failure, run create and capture stderr. if err=$({ eval "$SHOW_CMD -o none 2>/dev/null" || eval "$CREATE_CMD -o none"; } 2>&1); then exit 0 fi if ! echo "$err" | grep -qE "$TRANSIENT"; then echo "$err" >&2 exit 1 fi if [ "$attempt" -eq 3 ]; then echo "$err" >&2 echo "ARM frontend is returning transient errors — please retry in a few minutes." >&2 exit 1 fi sleep "$([ "$attempt" -eq 1 ] && echo 5 || echo 15)" done
-
-
SKILL.md 2.7 KB
--- name: python-appservice-deploy description: "Deploy Python (Flask/Django/FastAPI) code to Azure App Service Linux. WHEN: \"Flask App Service\", \"Django App Service\", \"FastAPI App Service\", \"deploy Python to App Service\". DO NOT USE FOR: Container Apps, Functions, non-Python, Terraform/Bicep/IaC, full infra — use azure-prepare." license: MIT metadata: author: Microsoft version: "1.1.1" --- # Python on Azure App Service — Code Deploy Deploys Python (Flask, Django, FastAPI, generic) code to Azure App Service Linux (P0v3, Python 3.14). Creates RG + Plan + Web App if missing. Hand off to `azure-prepare` for VNet, Key Vault, databases, or IaC. **MCP tools used**: `mcp_azure_mcp_subscription_list`, `mcp_azure_mcp_group_list`, `mcp_azure_mcp_appservice`, `mcp_azure_mcp_azd` (when `azure.yaml` is present). ## Workflow 1. **Resolve context — smart defaults, minimal prompts.** Only the app name is interactive; RG (`<app>-rg`), Plan (`<app>-plan`), region (current `az` default or `eastus2`), subscription are derived. [create-app.md](references/create-app.md) §1. 2. **Detect framework** (advisory, never blocks). [detect.md](references/detect.md). 3. **Choose path** — `azure.yaml` host: appservice → [deploy-azd.md](references/deploy-azd.md); else [deploy-azcli.md](references/deploy-azcli.md). 4. **Ensure RG → Plan (`P0v3 --is-linux`) → Web App (`--runtime "PYTHON:3.14"`)** exist. On transient ARM errors, follow [transient-retry.md](references/transient-retry.md). [create-app.md](references/create-app.md). 5. **Set startup** — Flask/Django: none (Oryx auto-detects). FastAPI: always `python -m uvicorn main:app --host 0.0.0.0`. Other: warn. [startup-commands.md](references/startup-commands.md). 6. **Set `SCM_DO_BUILD_DURING_DEPLOYMENT=true`**. 7. **Deploy** — `azd deploy` or `az webapp deploy --type zip --track-status false`. 8. **STOP. Print the post-deploy message** ([post-deploy-message.md](references/post-deploy-message.md)) and end the turn. ### Hard rules - ⛔ **NO POST-DEPLOY VERIFICATION** — after deploy returns, do not run `az webapp log tail`, `curl`, `Invoke-WebRequest`, or any health probe. App Service needs 2–3 min to warm; a quiet log or early 5xx is not failure. - ⛔ **SHELL SAFETY** — for `--runtime` always use `"PYTHON:3.14"` (colon). Never `"PYTHON|3.14"` (pipe is a shell operator). - ⛔ **NEVER `az webapp up`** — deprecated. Use Step 7 commands. - ✅ **URL FORMAT** — present endpoints as `https://...` URLs. ## Error Handling See [errors.md](references/errors.md) for the full symptom → cause → fix matrix. Quick triage: missing plan/app → re-run Step 4; container ping timeout on 8000 → fix startup (Step 5); `ModuleNotFoundError` after deploy → ensure Step 6 ran, redeploy.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.