Status: Reviewed
Stack: PowerShell → SQL / stored procs → Blazor
Depends on: SQL-CLI-01/03/04 for query/deploy into Azure SQL after infra is ready
Goal: Use az sql (and a few adjacent Azure CLI commands) from Windows/PowerShell to provision, firewall, Entra admin, and inspect Azure SQL in ways that feed Rick’s pipelines — without turning this into a full Azure encyclopedia.
az sql vs Portal vs ARM/Bicep vs Az.SqlRick’s loop: AD/Windows/PowerShell → SQL stored procs → Blazor. az sql is the control-plane CLI for Azure SQL infra (logical server, DB, firewall, Entra admin). It does not replace sqlcmd, Invoke-Sqlcmd, or sqlpackage.
| Need | az sql (CLI) |
Portal | ARM / Bicep | Az.Sql (PowerShell) |
|---|---|---|---|---|
| One-off lab: create server + DB + firewall | ✅ Fast, scriptable | ✅ Click-ops | Overkill | ✅ Equivalent |
| Repeatable env bootstrap from jump box | ✅ Prefer | ❌ Drift | ✅ Best for IaC repos | ✅ If PS-native shop |
| CI/CD desired-state infra | Possible; fragile | ❌ | ✅ Source of truth | Possible |
| Firewall for this client IP before sqlcmd | ✅ Recipe-friendly | Manual | Static ranges only | ✅ |
| Set / swap Microsoft Entra admin | ✅ | ✅ | ✅ | ✅ |
| Scale SKU / serverless pause knobs | ✅ az sql db update |
✅ | ✅ | ✅ |
| Query data / run procs | ❌ → SQL-CLI-01 / 03 | Query editor | ❌ | ❌ |
| Schema publish / BACPAC | ❌ → SQL-CLI-04 | Limited | ❌ | Limited |
| Full Azure encyclopedia (VNet, PE, MI farm) | ❌ Stay lean here | OK | ✅ | OK |
Rule of thumb
az sql (or Bicep if the env is checked into Git).| Method | Command / note |
|---|---|
| winget (preferred) | winget install --exact --id Microsoft.AzureCLI then reopen the terminal |
| MSI / ZIP | Install Azure CLI on Windows |
| Cloud Shell | Preinstalled; fine for poke, not for Rick’s local Agent jobs |
az version
az upgrade # keep current; sql surface changes with CLI releases
az is a Python CLI that works from PowerShell and cmd. Prefer calling it from PowerShell so you can pipe -o json | ConvertFrom-Json.
| Scenario | Command | Notes |
|---|---|---|
| Interactive (laptop / RDP) | az login |
Browser or device code; MFA required for user identities (enforced for Azure CLI automation paths since Sep 2025) |
| Device code (locked-down jump box) | az login --use-device-code |
No local browser |
| Service principal (CI / Agent) | az login --service-principal -u <appId> -p <secret\|cert> --tenant <tenantId> |
Prefer cert over secret; store secret in Key Vault / pipeline secret store — never in the .ps1 |
| Managed identity (Azure VM jump box) | az login --identity |
System-assigned |
| User-assigned MI | az login --identity --client-id <id> |
Or --object-id / --resource-id |
az account show -o table
az account set -s "<subscriptionIdOrName>"
az configure --defaults group=rg-sql-lab location=eastus sql-server=sql-lab-01 # optional shortcuts
RBAC reminder: creating servers/DBs needs Contributor (or SQL-specific roles) on the RG. Setting Entra admin / Entra-only auth needs rights that can manage SQL + read directory (often SQL Security Manager + Directory Reader patterns — confirm with your tenant’s role model).
Prioritize logical server + single database — the common Blazor app shape. MI is mentioned only so you do not confuse command groups.
| Area | Command group | What it is | Rick priority |
|---|---|---|---|
| Logical server | az sql server |
PaaS “server” hostname *.database.windows.net; holds admin, firewall, Entra settings |
✅ Core |
| Database | az sql db |
Single DB (or pool member); SKU, TDE, audit, connection-string templates | ✅ Core |
| Firewall (server IP rules) | az sql server firewall-rule |
Client IP allow list; 0.0.0.0–0.0.0.0 = Allow Azure services |
✅ Core |
| Entra admin | az sql server ad-admin |
Required before Entra users/groups can be created in the DB | ✅ Core |
| Entra-only auth | az sql server ad-only-auth |
Disables SQL auth logins at server | Use when policy demands |
| Auditing | az sql db audit-policy / az sql server audit-policy |
Ship audit logs to storage / Log Analytics / Event Hub | When compliance asks |
| VNet rules | az sql server vnet-rule |
Allow specific subnets (service endpoints) | When leaving public IP model |
| Managed Instance | az sql mi |
Near-full SQL Server in VNet; different networking story | Mention only — out of scope for most Blazor single-DB apps |
| Elastic pools | az sql elastic-pool |
Shared eDTU/vCore across DBs | Only if multi-tenant cost packing |
Mental model
Subscription → Resource group → Logical server (firewall + Entra admin)
└─ Database(s) ← Blazor connection string points here
Hostname for tools: <server>.database.windows.net — but az sql flags take the short server name, not the FQDN.
Variables used below (PowerShell):
$rg = "rg-sql-lab"
$loc = "eastus"
$server = "sql-lab-01" # globally unique, lowercase
$db = "AppDb"
$admin = "sqladmin" # SQL auth admin (skip if Entra-only from day one)
# Create logical server (SQL auth admin — store password in secret store, not transcript)
az sql server create -g $rg -n $server -l $loc `
--admin-user $admin --admin-password $dnv:SQL_ADMIN_PASSWORD `
--enable-public-network true --minimal-tls-version 1.2
az sql server list -g $rg -o table
az sql server show -g $rg -n $server -o jsonc
az sql server delete -g $rg -n $server --yes # destroys server + DBs
Useful create flags: --enable-ad-only-auth, --assign-identity, --enable-public-network false (forces private-path designs).
# DTU-style
az sql db create -g $rg -s $server -n $db --service-objective S0
# vCore General Purpose Gen5, 2 vCores
az sql db create -g $rg -s $server -n $db `
--edition GeneralPurpose --family Gen5 --capacity 2
# Serverless (common for labs / low Blazor traffic)
az sql db create -g $rg -s $server -n $db `
-e GeneralPurpose -f Gen5 -c 2 --compute-model Serverless --auto-pause-delay 60
# Named objective shorthand
az sql db create -g $rg -s $server -n $db --service-objective GP_S_Gen5_2
az sql db list -g $rg -s $server -o table
az sql db show -g $rg -s $server -n $db -o jsonc
az sql db list-editions -l $loc -a -o table # see what your sub can buy
# Scale / retier
az sql db update -g $rg -s $server -n $db --edition GeneralPurpose --family Gen5 --capacity 4
az sql db update -g $rg -s $server -n $db --service-objective S2 --max-size 250GB
az sql server firewall-rule create -g $rg -s $server -n AllowClient `
--start-ip-address 203.0.113.10 --end-ip-address 203.0.113.10
az sql server firewall-rule list -g $rg -s $server -o table
az sql server firewall-rule show -g $rg -s $server -n AllowClient
az sql server firewall-rule delete -g $rg -s $server -n AllowClient --yes
# Allow Azure services (portal toggle) — start=end=0.0.0.0
az sql server firewall-rule create -g $rg -s $server -n AllowAllWindowsAzureIps `
--start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0
# Display name + object id of user OR group (group preferred for ops team)
az sql server ad-admin create -g $rg -s $server `
--display-name "SQL-Admins" --object-id "<entra-object-guid>"
az sql server ad-admin list -g $rg -s $server -o table
az sql server ad-admin update -g $rg -s $server `
--display-name "SQL-Admins" --object-id "<new-guid>"
az sql server ad-admin delete -g $rg -s $server
# Entra-only (kills SQL auth) — set admin first
az sql server ad-only-auth enable -g $rg -n $server
az sql server ad-only-auth get -g $rg -n $server
az sql server ad-only-auth disable -g $rg -n $server
# Prints a template with <username>/<password> tokens — not live secrets from Azure
az sql db show-connection-string -s $server -n $db -c ado.net
az sql db show-connection-string -s $server -n $db -c ado.net --auth-type ADIntegrated
az sql db show-connection-string -s $server -n $db -c sqlcmd
Clients: ado.net, jdbc, odbc, php, php_pdo, sqlcmd. Auth types: SqlPassword (default), ADPassword, ADIntegrated.
Do not commit the output. Prefer Key Vault references / User Secrets for Blazor ConnectionStrings.
az sql db audit-policy show -g $rg -s $server -n $db
# update needs storage / workspace targets — wire when compliance requires; skip for lab bootstrap
$ErrorActionPreference = "Stop"
$serverObj = az sql server show -g $rg -n $server -o json | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) { throw "az sql server show failed ($LASTEXITCODE)" }
$fqdn = $serverObj.fullyQualifiedDomainName # sql-lab-01.database.windows.net
$state = $serverObj.state
$dbs = az sql db list -g $rg -s $server -o json | ConvertFrom-Json
$dbs | Where-Object name -ne "master" | Select-Object name, status, earliestRestoreDate, currentSku
JMESPath keeps payloads small:
az sql db list -g $rg -s $server --query "[?name!='master'].{name:name,sku:currentSku.name,status:status}" -o table
az sql * create fails if the resource exists. Pattern o:
function Ensure-SqlFirewallRule {
param($Rg, $Server, $Name, $Ip)
$existing = az sql server firewall-rule show -g $Rg -s $Server -n $Name -o json 2>$null
if ($LASTEXITCODE -eq 0 -and $existing) {
az sql server firewall-rule update -g $Rg -s $Server -n $Name `
--start-ip-address $Ip --end-ip-address $Ip | Out-Null
} else {
az sql server firewall-rule create -g $Rg -s $Server -n $Name `
--start-ip-address $Ip --end-ip-address $Ip | Out-Null
}
if ($LASTEXITCODE -ne 0) { throw "firewall ensure failed" }
}
function Test-AzSqlDbExists {
param($Rg, $Server, $Name)
az sql db show -g $Rg -s $Server -n $Name -o none 2>$null
return ($LASTEXITCODE -eq 0)
}
if (-not (Test-AzSqlDbExists $rg $server $db)) {
az sql db create -g $rg -s $server -n $db --service-objective GP_S_Gen5_2
if ($LASTEXITCODE -ne 0) { throw "db create failed" }
}
Once firewall + Entra admin (or SQL admin) are in place:
| Next step | Doc | Typical call |
|---|---|---|
| Smoke query / procs | SQL-CLI-01 | sqlcmd -S $fqdn -d $db -G (go-sqlcmd Entra) |
| Token from PS | SQL-CLI-03 | Invoke-Sqlcmd -ServerInstance $fqdn -Database $db -AccessToken $token |
| Schema publish | SQL-CLI-04 | sqlpackage /Action:nlish /TargetServerName:$fqdn /TargetDatabaseName:$db ... |
az sql stops at the door; the other three walk inside.
| Gotcha | Symptom | Fix |
|---|---|---|
| No Entra admin set | Cannot CREATE USER [user@tenant] FROM EXTERNAL PROVIDER |
az sql server ad-admin create with user or group object id |
| Entra-only enabled | SQL auth / Uid=sqladmin fails |
Expected; use Entra token paths (SQL-CLI-01 -G / SQL-CLI-03 -AccessToken) or disable only if policy allows |
| Firewall missing client IP | Error 40615 / cannot open server | Add rule for egress IP (VPN/NAT may differ from whatip.com on the box) |
Allow Azure services (0.0.0.0) |
App Service / Functions connect without PE | Convenient; broad — any Azure tenant’s resource can attempt auth. Prefer VNet/PE for prod |
| Private endpoint only | Public FQDN times out from internet | Disable public network (--enable-public-network false) + PE/DNS; jump box must be on the VNet/VPN |
| Server name vs FQDN | az 404 / weird errors |
-s / -n = short name; sqlcmd -S = FQDN |
| AAD admin ≠ DB users | Admin works; app identity fails | Still need T-SQL CREATE USER ... FROM EXTERNAL PROVIDER + roles inside the DB (sqlcmd) |
MFA on user az login in automation |
Scripts break | Use SP or managed identity — not interactive user login |
show-connection-string looks “real” |
Accidental commit | It is a template; still treat as sensitive once filled |
Assume $rg, $server, $db are set. Run from elevated-enough PowerShell where az is on PATH.
az login
# or: az login --use-device-code
# or: az login --identity
az account set -s "<subscriptionIdOrName>"
az account show -o table
$ip = (Invoke-RestMethod -Uri "https://api.ipify.org")
Ensure-SqlFirewallRule -Rg $rg -Server $server -Name "Client-$env:COMPUTERNAME" -Ip $ip
# or inline:
az sql server firewall-rule create -g $rg -s $server -n "Client-$env:COMPUTERNAME" `
--start-ip-address $ip --end-ip-address $ip
az sql server firewall-rule list -g $rg -s $server -o table
az group create -n $rg -l $loc
az sql server create -g $rg -n $server -l $loc `
--admin-user $admin --admin-password $env:SQL_ADMIN_PASSWORD `
--minimal-tls-version 1.2
az sql db create -g $rg -s $server -n $db `
--edition GeneralPurpose --family Gen5 --capacity 2 --compute-model Serverless --auto-pause-delay 60
$groupName = "SQL-Admins"
$oid = az ad group show --group $groupName --query id -o tsv
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($oid)) { throw "group not found / Graph rights?" }
az sql server ad-admin create -g $rg -s $server --display-name $groupName --object-id $oid
az sql server ad-admin list -g $rg -s $server -o table
$info = az sql server show -g $rg -n $server -o json | ConvertFrom-Json
$dbInfo = az sql db show -g $rg -s $server -n $db -o json | ConvertFrom-Json
[pscustomobject]@{
Fqdn = $info.fullyQualifiedDomainName
Db = $dbInfo.name
Status = $dbInfo.status
Sku = $dbInfo.currentSku.name
Collation= $dbInfo.collation
EarliestRestore = $dbInfo.earliestRestoreDate
}
az sql db show-connection-string -s $server -n $db -c ado.net --auth-type ADIntegrated
$servers = az sql server list -g $rg -o json | ConvertFrom-Json
foreach ($s in $servers) {
Write-Host "`n=== $($s.name) ($($s.fullyQualifiedDomainName)) ===" -ForegroundColor Cyan
az sql db list -g $rg -s $s.name --query "[].{name:name,status:status,sku:currentSku.name}" -o table
if ($LASTEXITCODE -ne 0) { Write-Warning "list failed for $($s.name)" }
}
-G (Entra)# 1) Firewall (R2) 2) Entra admin (R4) 3) then:
$fqdn = az sql server show -g $rg -n $server --query fullyQualifiedDomainName -o tsv
# Interactive Entra (go-sqlcmd) — see SQL-CLI-01 for auth-method details
sqlcmd -S $fqdn -d $db -G -Q "SELECT SUSER_SNAME() AS me, DB_NAME() AS db;"
After first Entra admin login, create the Blazor app identity inside the DB (still T-SQL via sqlcmd — not az sql):
CREATE USER [app-registration-or-mi-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [app-registration-or-mi-name];
-- add db_datawriter / exec on procs as needed
az sql db update -g $rg -s $server -n $db --service-objective GP_Gen5_2
if ($LASTEXITCODE -ne 0) { throw "scale failed" }
$fqdn = az sql server show -g $rg -n $server --query fullyQualifiedDomainName -o tsv
# Schema deploy — SQL-CLI-04
# sqlpackage /Action:Publish /SourceFile:.\AppDb.dacpac /TargetServerName:$fqdn /TargetDatabaseName:$db ...
az sql| Temptation | Why not | Use instead |
|---|---|---|
| Load CSV / inventory into tables | Control plane only; no bulk insert path | bcp → staging → stored proc |
Run CREATE PROC / migrations as strings via az |
No T-SQL execution surface on az sql db |
sqlcmd / go-sqlcmd |
“Backup” with az sql db export as DR |
BACPAC export ≠ PITR / native backup story | PITR / LTR via Azure SQL backup; verify with SQL-CLI-03 where applicable |
| Schema drift deploy | Wrong tool | sqlpackage Publish / Script |
| Store SQL admin password in the script or transcript | Credential leak | $env: from Key Vault / pipeline secret; prefer Entra-only |
Treat firewall 0.0.0.0 as “secure enough for prod” |
Allows Azure-wide network reach (auth still required) | Private endpoint / VNet rules + tight IP rules |
Encode full IaC sprawl in ad-hoc az history |
Unreviewable | Bicep/ARM for shared envs; keep az for jump-box / break-glass |
Pipeline shape that matches this stack
az sql → server, db, firewall, Entra admin
sqlpackage → schema (DACPAC) [SQL-CLI-04]
sqlcmd / -G → CREATE USER FROM EXTERNAL PROVIDER, smoke procs [SQL-CLI-01]
Invoke-Sqlcmd → token-based PS automation / tests [SQL-CLI-03]
Blazor → connection string / MI to AppDb
Cross-links: SQL-CLI-01 · SQL-CLI-03 · SQL-CLI-04