--- name: deploy-app-to-azure description: >- Takes a working local application and its Azure SQL Database to Azure with the Azure Developer CLI, reading its infrastructure rather than inheriting it. The one first-party template pairing a web app with Azure SQL Database uses a database password and grants db_owner, and the firewall rule it ships under an Azure services name spans the whole public address space. Use when a user says "deploy my app to Azure", "azd up", "which azd template should I start from", "get this into Azure without a password", or when a deployment reported success and the app then fails its first database call with a login failure. Covers what each template does about identity, what init, provision, deploy and up each do, and what teardown leaves behind. github-actions-for-sql owns pipelines, provision-azure-sql-db creating the server and database, entra-id-auth the database user and the grant. --- # Deploy an application and its Azure SQL Database to Azure **This owns the deployment sequence, and the infrastructure handed to you at the start of it.** It does not create the database, it does not own the identity, and it is not the pipeline. Verified 2026-09-03 against the Azure Developer CLI 1.32.0, Azure CLI 2.90.0, sqlcmd 1.10.0 and .NET 8.0.421, by listing the template gallery and reading the shipped Bicep of every Microsoft-published template it returns for Azure SQL Database. ## The correction: a first-party template's infrastructure is not a reviewed baseline An agent treats a Microsoft sample's `infra` directory as settled, spends its attention on the application code, and asked to reach Azure SQL Database without a password recommends the first-party template as the way to get there. **That inference is wrong in two separate ways at once**, and every claim below is settled by a command rather than by reading. ### Ask the gallery what exists, do not extrapolate from the family ```bash azd version azd template list --output json | grep -c '"Azure-Samples/todo-' ``` Measured 2026-09-18: 303 templates, **zero** `todo-` entries. The family was archived 2026-09-04 and left the gallery 2026-09-08, yet `azd init -t todo-csharp-sql` still initializes it and Microsoft Learn still lists it, so users still arrive with it. `todo-csharp-cosmos-sql` is Cosmos DB for NoSQL. **`todo-nodejs-sql` and `todo-python-sql` never existed.** ### The archived web application blueprint for Azure SQL Database uses a password Read its infrastructure without cloning it: ```bash curl -s https://raw.githubusercontent.com/Azure-Samples/todo-csharp-sql/main/infra/app/db-avm.bicep \ | grep -n -i "administratorLogin\|create user\|db_owner\|external provider" ``` Measured 2026-09-03: `administratorLogin`, `create user ${APPUSERNAME} with password`, `alter role db_owner add member`, and zero hits for `external provider`. | It does | Not | |---|---| | Provisions the logical server with an `administratorLogin` and a password | Microsoft Entra-only authentication | | Creates the application's database user with a password, from a deployment script | `CREATE USER ... FROM EXTERNAL PROVIDER` | | Adds that user to `db_owner` | Any narrower role | | Stores the connection string, user name and password included, in a vault | A passwordless connection string | **The template does create a managed identity, and that is what makes the misreading survive a glance.** It is system-assigned to the API and it reads the vault. What the vault holds is a connection string with a password in it. "It uses managed identity" is true of the vault and false of the database. Converting the template is an infrastructure change, not an application one: the server's authentication mode, the deployment script, the role and the connection string all move together. ### The firewall rule that survives choosing correctly ```bash for r in todo-csharp-sql:app/db-avm.bicep \ functions-quickstart-dotnet-azd-sql:app/db.bicep \ functions-quickstart-python-azd-sql:app/db.bicep \ functions-quickstart-typescript-azd-sql:app/db.bicep; do printf '%-42s ' "${r%%:*}" curl -s "https://raw.githubusercontent.com/Azure-Samples/${r%%:*}/main/infra/${r##*:}" \ | grep -c "startIpAddress: '0.0.0.1'" done ``` **Four of four**, measured 2026-09-03: `1` on every line. All four build the logical server from the Azure Verified Module and all four ship a rule named `Azure Services` running `0.0.0.1` to `255.255.255.254`, **every address a client can present**. Three are Entra-only with a managed identity and are otherwise the ones to copy, so picking the better template fixes the password and leaves the firewall open. **The name points at a different rule.** The Azure CLI's own help for `--start-ip-address` says to use `0.0.0.0` to represent all Azure-internal IP addresses, and Microsoft Learn records the Allow Azure services special case as a rule whose start and end address are both `0.0.0.0`. A rule from `0.0.0.1` to `255.255.255.254` is not a narrower version of it. **Say this to the user, then let them decide.** These are Microsoft-published samples and are not ours to edit on someone's behalf. Name the rule out loud and offer to narrow it: ```bash az sql server firewall-rule list -g -s -o table az sql server firewall-rule delete -g -s -n "Azure Services" # the documented Allow Azure services case, if Azure-internal traffic is what was meant az sql server firewall-rule create -g -s -n AllowAzureServices \ --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0 # and the developer machine, if it connects directly az sql server firewall-rule create -g -s -n dev-box \ --start-ip-address --end-ip-address ``` **Narrow it after the first successful deployment, not before.** The post-provision hook runs from a machine that then needs a rule of its own, and without one it fails as a timeout rather than as a permission error. The three Functions quickstarts ship `infra/scripts/addclientip.ps1` for exactly this. `provision-azure-sql-db` owns authoring firewall rules; this skill owns catching an inherited one. ### On any other template, run the same two greps before running anything ```bash grep -ri "external provider" ./infra | wc -l # 0 means the database user is password-based grep -rn "IpAddress" ./infra # read the range, not the rule's name azd provision --preview # what would be created, before it is ``` Open [references/azd-and-azure-sql.md](references/azd-and-azure-sql.md) **before** choosing a template or writing the hook: it carries the per-template evidence, which templates are Microsoft-published rather than community ones, and the hook wiring end to end. **What to copy instead.** The three serverless quickstarts are the pattern worth taking: an Entra-only server, the deploying user as administrator, a user-assigned managed identity for the application, and the grant run from a `postprovision` hook against the user database. Their roles are `db_datareader`, `db_datawriter` and `db_ddladmin`, still wider than most applications need; replace them with a custom role containing only the permissions the application uses. ## The seam in the middle, which the good templates exist to close **The deployment tool provisions Azure resources and deploys code. A database user is neither.** The application's identity means nothing to the database until somebody runs `CREATE USER ... FROM EXTERNAL PROVIDER` and grants it roles, inside the database, from a connection authenticated with Microsoft Entra ID. Until then the run goes green and the application fails at its first query with a login failure that reads as a bad credential. `entra-id-auth` owns that statement, its clauses and its error codes. **Open it when you are about to write the grant.** This skill owns where the grant goes in the sequence and how it gets run. ## The four commands, and what each one actually does | Command | What it does | What it does not do | |---|---|---| | `init` | Sets up `azure.yaml`, an `infra` directory, an environment under `.azure`, from a template or from existing code | Create anything in Azure | | `provision` | Creates the Azure resources and writes the template's outputs into the environment | Deploy code. Create anything inside the database | | `deploy` | Pushes built application code to resources that already exist | Create resources. A deploy before a provision has nothing to deploy to | | `up` | Runs packaging, provisioning and deployment in one command | Anything the individual steps do not do. It is a convenience, not extra behaviour | Keeping the two halves apart is how you debug them. Build the artifact yourself and hand it over: ```bash dotnet publish ./src/api -c Release -o ./artifacts/api azd deploy api --from-package ./artifacts/api ``` **Do not depend on the internal ordering of `up`.** `azd up --help` calls it package, provision and deploy; the hooks reference on Microsoft Learn calls it restore, provision and deploy. What is stable is that provisioning happens before deployment and that `postprovision` runs after resources are created. Pin the order explicitly if the project needs it, in the shape the command's own help gives: ```yaml # azure.yaml workflows: up: - azd: provision - azd: package --all - azd: deploy --all ``` ## Step 1: know where the infrastructure is coming from `azure.yaml` at the project root is the only required file, mapping each service to a host and a source directory: ```yaml name: my-project services: api: project: ./src/api language: csharp host: appservice ``` Provisioning reads Bicep from `./infra` with `main.bicep` as the entry point, both overridable under an `infra` key, `path` and `module`. **That Bicep has to come from somewhere, and there are only two real sources**: a template, read first on the terms above, or hand-written from Azure Verified Modules. There is no third option, and this is where an agent invents one: > **The compose feature cannot create an Azure SQL Database.** Its database resource types are > Azure Cosmos DB, Azure Cosmos DB for MongoDB, Azure Cosmos DB for PostgreSQL, Azure Cache for > Redis and Azure Database for MySQL. There is no Azure SQL Database entry, so `azd add` will not > produce one, and `azd infra generate` only writes out what compose already knows. Creating the server, the database and the firewall rule with the Azure CLI instead belongs to `provision-azure-sql-db`. ## Step 2: put the grant between provisioning and deployment The database user has to exist after the identity and the database do, and before the application serves its first request. There is exactly one place that is true, and it is the `postprovision` hook, registered at the project root rather than inside a service: ```yaml # azure.yaml hooks: postprovision: posix: shell: sh run: ./infra/scripts/configure-database.sh continueOnError: false interactive: false windows: shell: pwsh run: ./infra/scripts/configure-database.ps1 continueOnError: false interactive: false ``` Inside the script, read what provisioning wrote and authenticate as the identity that provisioned: ```bash eval "$(azd env get-values | sed 's/^/export /')" sqlcmd -S "$AZURE_SQL_SERVER_NAME" -d "$AZURE_SQL_DATABASE_NAME" \ --authentication-method ActiveDirectoryAzureDeveloperCli \ -i ./infra/scripts/grant-app-identity.sql ``` Develop that hook on its own, without reprovisioning: ```bash azd hooks run postprovision ``` Four things make this work, and each fails quietly if it is missing. 1. **The infrastructure outputs the identity's name and principal id**, because a hook is a shell script and can only see what provisioning wrote into the environment. 2. **The hook reads those outputs** rather than guessing resource names. 3. **`continueOnError` stays false.** Setting it true turns the one step that silently matters into a step that silently does not happen. 4. **Whoever runs the hook is administrator of the logical server, authenticated with Microsoft Entra ID.** `-G` alone falls back to `ActiveDirectoryDefault`, which can pick a different signed-in identity than the one that just provisioned; naming the method is what pins it. ## Step 3: get the connection string to the application **An environment value is not an application setting.** The value really is set and really does show up locally, which is why this reads as success. | | Where it lives | What reads it | |---|---|---| | Environment value | `.azure//.env` in the project | Provisioning inputs, hooks, and the local command session | | Application setting | The deployed application's configuration in Azure | The running application | The infrastructure has to write it as an application setting, which in Bicep means passing it into the hosting module: ```bicep appSettings: { AZURE_SQL_CONNECTION_STRING: 'Server=${sqlServer.outputs.fullyQualifiedDomainName}; Authentication=Active Directory Default; Database=${databaseName}; User Id=${apiIdentity.outputs.clientId}' } ``` That is the shape Microsoft Learn gives for a function app reaching Azure SQL Database with a managed identity. There is no password and no user name, which is the whole point. For a user-assigned identity the **client** id has to be named, because the default credential otherwise cannot choose between the identities attached to the host; for a system-assigned one, `User Id` is omitted instead. The client id is not the principal id the grant uses. **Open `connect-to-azure-sql` when the driver is not `Microsoft.Data.SqlClient`**: the keyword spellings differ per driver and are not guessable. ## Step 4: know what teardown does and does not remove ```bash azd down # prompts for confirmation azd down --purge # also permanently deletes resources that are soft-deleted by default azd down --force --purge # no confirmation at all ``` It deletes the Azure resources for the environment, not local project files and not the environment's stored values, so `.azure//.env` survives holding outputs that now name resources that no longer exist. **The database goes with everything else** and its data is not backed up by this command. **A database the project did not create is not deleted**, and the database user created for the application identity survives inside it, pointing at a principal that no longer exists. ## Check it worked A green run is not the check. Four things, in this order, and the last one is the only one that settles it. ```bash azd env get-values | grep -i sql # provisioning wrote the server and database names azd show # the resources azd believes it owns az sql server ad-admin list -g -s -o table az sql server firewall-rule list -g -s -o table az webapp config appsettings list -g -n \ --query "[?name=='AZURE_SQL_CONNECTION_STRING'].name" -o tsv ``` Expect a server and database name in the environment, a named Entra administrator, no rule wider than you chose, and the setting present on the deployed application. An empty result from the last command is the Step 3 failure: the value exists locally and never reached Azure. Then confirm the database user exists, connected to the **user database** and not `master`: ```sql SELECT p.name, p.authentication_type_desc, r.name AS role_name FROM sys.database_principals AS p LEFT JOIN sys.database_role_members AS m ON m.member_principal_id = p.principal_id LEFT JOIN sys.database_principals AS r ON r.principal_id = m.role_principal_id WHERE p.name = ''; ``` Expect at least one row, `authentication_type_desc` reading `EXTERNAL`, and named roles that are not `db_owner`. **Zero rows is the seam**: the grant did not run, or it ran against `master`. Finish by calling an application endpoint that reads the database, because only the application proves its own identity works. ## Under an agent, prompts are failures `azd` documents `--no-prompt` on every command as automatically enabled when it detects a CI/CD or AI-agent environment, failing if any required value cannot be resolved (`AZD_NON_INTERACTIVE=false` opts out). So under an agent, any value that would have been prompted for is a hard failure. Set them before running anything: ```bash azd env new azd env set AZURE_SUBSCRIPTION_ID azd env set AZURE_LOCATION ``` Hooks are the other half, and the two Learn pages disagree: the hooks reference says hooks run in interactive mode by default, the schema reference says `interactive` defaults to false. Set it explicitly on any hook that runs unattended rather than relying on either. ## Where this stops | The request | The owner | |---|---| | Create the server, database and firewall rule with the Azure CLI | `provision-azure-sql-db` | | The database user, the grant, the driver keywords, the error codes | `entra-id-auth` | | Retry, pooling and encryption doctrine | `connect-to-azure-sql` | | A pipeline that deploys on push or on merge | `github-actions-for-sql` | | Getting the schema into the database | `schema-migrations-safely` | | What roles to leave the application identity holding | `entra-id-auth`, followed by a custom least-privilege role | | Sequencing a whole new project, before any of this | Plan the application, data-access, schema, identity, and deployment choices together | **The boundary with the pipeline is a real one, not a filing decision.** Deploying from a laptop makes the developer administrator of the logical server, so the developer can run the grant. Deploying from a pipeline makes the pipeline's identity administrator instead, and a developer then cannot. `azd pipeline config` is the handoff point. ## Do not - Do not treat a first-party template's `infra` directory as reviewed, and do not call one passwordless because a managed identity appears in it. Check what that identity authenticates to. - Do not accept a firewall rule because its name says Azure services. Read the range, and expect the wide one even in the templates that get identity right. - Do not extrapolate template names from the family, and do not report a deployment as working because the tool reported success. - Do not put the grant in a hook that runs after deployment, and do not set `continueOnError` true on it. The application can serve a request before it runs. - Do not set a connection string as an environment value and expect the deployed application to read it, and do not leave `TrustServerCertificate` on in one copied out of a sample. - Do not assume a database, server or resource name in a hook. Read it from the environment values that provisioning wrote. - Do not restate the `CREATE USER` statement, the role grants or the driver keyword spellings here. `entra-id-auth` owns them, and two skills stating the same syntax is how they start disagreeing. ## References - [references/azd-and-azure-sql.md](references/azd-and-azure-sql.md): open it before choosing a template, before writing the post-provision hook, or when a claim above disagrees with what you are seeing. It carries the per-template evidence, which templates are Microsoft-published, the naming traps, and the hook wiring from infrastructure output to grant. - `azd template list` and the community gallery at `azure.github.io/awesome-azd` are the only current statement of what templates exist. List them rather than recalling them. - [Hooks reference](https://learn.microsoft.com/azure/developer/azure-developer-cli/azd-extensibility) and [azure.yaml schema](https://learn.microsoft.com/azure/developer/azure-developer-cli/azd-schema), when a hook name or option is in question; [environment variables](https://learn.microsoft.com/azure/developer/azure-developer-cli/manage-environment-variables) when a value is not arriving where it was expected; [network access controls](https://learn.microsoft.com/azure/azure-sql/database/network-access-controls-overview) when a firewall rule needs justifying.