SuperAPI AI Directory

deploy-app-to-azure

Deployment & operations

View repository

Updated 10-16 00:31

About this skill

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

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:

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

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:

az sql server firewall-rule list -g <group> -s <server> -o table
az sql server firewall-rule delete -g <group> -s <server> -n "Azure Services"

# the documented Allow Azure services case, if Azure-internal traffic is what was meant
az sql server firewall-rule create -g <group> -s <server> -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 <group> -s <server> -n dev-box \
  --start-ip-address <your-ip> --end-ip-address <your-ip>

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

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 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:

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:

# 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:

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:

# 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:

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:

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/<environment>/.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:

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

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/<environment>/.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.

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 <group> -s <server> -o table
az sql server firewall-rule list -g <group> -s <server> -o table
az webapp config appsettings list -g <group> -n <app-name> \
  --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:

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 = '<identity-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:

azd env new <environment-name>
azd env set AZURE_SUBSCRIPTION_ID <subscription-id>
azd env set AZURE_LOCATION <region>

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: 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 and azure.yaml schema, when a hook name or option is in question; environment variables when a value is not arriving where it was expected; network access controls when a firewall rule needs justifying.