Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Azure Dev Environment Deployment

Author: Austin Dennis — LinkedIn

A PowerShell script that stands up a full Azure development environment using free or near-free resources, wired together with managed identity and Entra authentication. Designed for standard web projects — a static frontend talking to a serverless API backed by Azure SQL.

What Gets Deployed

Resource Tier Purpose
Static Web App Free Frontend hosting
Function App Flex Consumption (Node.js 24, falls back to 22 or 20 if region doesn't support 24) Backend API
SQL Server + SQL Database Free tier (useFreeLimit=true) Database (GP_S_Gen5 serverless, auto-pause 60 min)
Key Vault Standard (RBAC-enabled) Secret storage
Application Insights Workspace-backed Function App telemetry
Log Analytics Pay-per-GB App Insights backing + SQL diagnostics
Storage Account Standard_LRS Function App runtime storage + deployment container

Security posture:

  • Function App uses a system-assigned managed identity — no connection strings
  • SQL Server uses Entra-only authentication — no SQL login exists
  • Key Vault uses RBAC authorization with an explicit empty access policy list
  • Function App deployment storage authenticated via managed identity
  • Function App CORS pre-configured to allow only the Static Web App origin plus localhost:3000, 5173, and 4200 (with supportCredentials=true)

Prerequisites

  • Azure subscription with Owner or Contributor + User Access Administrator roles
  • PowerShell 5.1+ or 7+ (works in Azure Cloud Shell too)
  • Az PowerShell module (Install-Module Az -Scope CurrentUser)

Running the Script

.\Deploy-AzureDevEnvironment.ps1

No parameters. The script will prompt you for:

  1. Azure login (skipped in Cloud Shell)
  2. Subscription
  3. Resource group name
  4. Region

It registers required resource providers, creates everything in dependency order, and configures RBAC + SQL database user automatically.

Total time: ~5–8 minutes.

Using the Environment

1. Storing Secrets in Key Vault

Add secrets to your Key Vault — your Function App's managed identity already has Key Vault Secrets User role:

# Set a secret
$secretValue = ConvertTo-SecureString "my-api-key-value" -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName "<kv-name>" -Name "MyApiKey" -SecretValue $secretValue

# Or via Azure CLI
az keyvault secret set --vault-name <kv-name> --name MyApiKey --value "my-api-key-value"

You'll need Key Vault Secrets Officer role on the vault to write secrets. Grant it to yourself once:

$me = (Get-AzADUser -SignedIn).Id
$kvId = (Get-AzKeyVault -VaultName "<kv-name>").ResourceId
New-AzRoleAssignment -ObjectId $me `
    -RoleDefinitionName "Key Vault Secrets Officer" `
    -Scope $kvId

2. Referencing Secrets from the Function App

Use Key Vault references in Function App settings. Azure automatically resolves them at runtime using the managed identity — you never see or handle the secret value in code.

Format:

@Microsoft.KeyVault(VaultName=<kv-name>;SecretName=<secret-name>)

Set an app setting that references a Key Vault secret:

# Azure CLI
az functionapp config appsettings set \
  --name <function-app-name> \
  --resource-group <rg-name> \
  --settings "MY_API_KEY=@Microsoft.KeyVault(VaultName=<kv-name>;SecretName=MyApiKey)"

In your function code, just read it as a normal environment variable:

// Node.js
const apiKey = process.env.MY_API_KEY;
// C#
var apiKey = Environment.GetEnvironmentVariable("MY_API_KEY");

Verify the reference resolved correctly in the portal: Function App → Environment variables → App settings. A healthy reference shows a green checkmark. A red X means the identity can't access the secret (check RBAC) or the secret doesn't exist.

3. Database Access from the Function App

The Function App's managed identity already has db_datareader and db_datawriter on the database. Connect from your code using an Entra token — no connection string secrets:

Node.js (using mssql + @azure/identity):

const sql = require('mssql');
const { DefaultAzureCredential } = require('@azure/identity');

const credential = new DefaultAzureCredential();
const token = await credential.getToken('https://database.windows.net/.default');

const pool = await sql.connect({
  server: '<sql-server-name>.database.windows.net',
  database: '<db-name>',
  authentication: {
    type: 'azure-active-directory-access-token',
    options: { token: token.token }
  },
  options: { encrypt: true }
});

C# (using Microsoft.Data.SqlClient):

using Microsoft.Data.SqlClient;

var connectionString =
    "Server=tcp:<sql-server-name>.database.windows.net,1433;" +
    "Database=<db-name>;" +
    "Authentication=Active Directory Default;" +
    "Encrypt=True;";

using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();

DefaultAzureCredential / Active Directory Default automatically use the managed identity when running in Azure, and your developer credentials when running locally.

For local development, the SQL firewall rule ClientDeploymentIP was added for your machine, and the signed-in Entra user is the server administrator — so you can connect from SSMS, Azure Data Studio, or your dev machine directly.

Deploying Code

Static Web App

Option A — GitHub Actions (recommended):

  1. Azure Portal → your Static Web App → OverviewManage deployment token — copy the token
  2. In your GitHub repo settings, add the token as a secret named AZURE_STATIC_WEB_APPS_API_TOKEN
  3. Add a workflow at .github/workflows/azure-static-web-apps.yml:
name: Deploy Static Web App
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: upload
          app_location: "/"           # path to frontend source
          api_location: ""            # leave empty, we use separate Function App
          output_location: "dist"     # build output folder

Option B — SWA CLI (local):

npm install -g @azure/static-web-apps-cli
swa deploy ./dist --deployment-token <token>

Function App

Option A — VS Code Azure Functions extension (easiest):

  1. Install the Azure Functions extension
  2. Sign in to Azure in VS Code
  3. Right-click your function project → Deploy to Function App → select your app

Option B — Azure Functions Core Tools (func CLI):

cd /path/to/function-project
func azure functionapp publish <function-app-name>

Option C — GitHub Actions:

Use the Deployment Center in the portal (Function App → Deployment Center → GitHub) to scaffold a workflow with a pre-configured publish profile secret. Recommended for production flows.

Note on Flex Consumption: Deployment uses the deployments blob container in your storage account, authenticated via the Function App's managed identity. You don't need to manage a deployment connection string.

Expanding Permissions

Add a role to the Function App's managed identity

$func = Get-AzFunctionApp -ResourceGroupName "<rg>" -Name "<function-app-name>"
$principalId = $func.IdentityPrincipalId

# Example: grant Cosmos DB data access
New-AzRoleAssignment `
    -ObjectId $principalId `
    -RoleDefinitionName "DocumentDB Account Contributor" `
    -Scope "<cosmos-db-resource-id>"

Common built-in roles you may need:

Role Use for
Storage Blob Data Contributor Read/write blobs (already assigned)
Storage Queue Data Contributor Use queue triggers/bindings (already assigned)
Storage Table Data Contributor Use table storage
Service Bus Data Sender/Receiver Service Bus triggers/bindings
Event Hubs Data Receiver/Sender Event Hubs triggers/bindings
Cognitive Services User Call Azure AI/OpenAI services

Add additional database roles to the Function App

Connect to the database as the Entra admin (you) and run:

-- Additional permissions on specific tables
GRANT SELECT ON dbo.MyTable TO [<function-app-name>];

-- Execute stored procedures
GRANT EXECUTE ON SCHEMA::dbo TO [<function-app-name>];

-- Full access role
ALTER ROLE db_owner ADD MEMBER [<function-app-name>];  -- use sparingly

Grant another user access to the database

CREATE USER [user@domain.com] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [user@domain.com];

For SQL Server admin rights, update the Entra admin in the portal (SQL Server → Microsoft Entra ID) or via:

Set-AzSqlServerActiveDirectoryAdministrator `
    -ResourceGroupName "<rg>" `
    -ServerName "<sql-server-name>" `
    -DisplayName "<user-display-name>"

Update Function App CORS origins

The script pre-configures CORS on the Function App to allow the Static Web App plus localhost:3000, 5173, and 4200. To add another origin:

# Azure CLI - add a single origin
az functionapp cors add `
    --name <function-app-name> `
    --resource-group <rg> `
    --allowed-origins https://my-other-domain.com

To replace the full list:

# Overwrite CORS origins via REST
$cors = @{
    properties = @{
        cors = @{
            allowedOrigins     = @('https://<swa>.azurestaticapps.net', 'https://my-other-domain.com')
            supportCredentials = $true
        }
    }
} | ConvertTo-Json -Depth 5

Invoke-AzRestMethod `
    -Path "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Web/sites/<function-app>/config/web?api-version=2023-12-01" `
    -Method PATCH `
    -Payload $cors

Allow another IP through the SQL firewall

New-AzSqlServerFirewallRule `
    -ResourceGroupName "<rg>" `
    -ServerName "<sql-server-name>" `
    -FirewallRuleName "MyHomeIP" `
    -StartIpAddress "1.2.3.4" `
    -EndIpAddress "1.2.3.4"

Monitoring

  • Function App logs: Function App → MonitorLogs (queries Log Analytics via App Insights)
  • Live metrics: Function App → Application InsightsLive metrics
  • SQL performance: SQL Database → Intelligent PerformanceQuery Performance Insight
  • All logs (KQL): Log Analytics Workspace → Logs — query across everything:
// Recent function executions with errors
requests
| where timestamp > ago(1h)
| where success == false
| project timestamp, name, resultCode, duration, operation_Id

Cost Notes

Free tier allowances:

  • Static Web App — Free tier is genuinely free
  • SQL Database — 100,000 vCore-seconds + 32 GB storage per month (one free-limit database per subscription)
  • Function App — 1M executions/month free grant (shared with the Consumption plan)
  • App Insights + Log Analytics — 5 GB/month ingestion free (workspace-based App Insights shares one allowance; ingestion is deducted from Log Analytics)

You will pay for:

  • Storage Account — small ongoing cost (typically pennies/month for dev)
  • Key Vault — $0.03 per 10k operations
  • Data egress — if you exceed free thresholds
  • SQL Database — if you exceed the free limits, behavior is controlled by freeLimitExhaustionBehavior. The script sets this to AutoPause, which pauses the database instead of billing you. Change to BillOverUsage if you want continued availability.

Teardown: Delete the entire resource group to remove everything:

Remove-AzResourceGroup -Name "<rg-name>" -Force

Troubleshooting

"Cannot open server 'xxx' requested by the login" Your client IP isn't in the SQL firewall. Add it:

New-AzSqlServerFirewallRule -ResourceGroupName "<rg>" -ServerName "<sql-server>" `
    -FirewallRuleName "MyIP" -StartIpAddress "<your-ip>" -EndIpAddress "<your-ip>"

Key Vault reference shows red X in Function App

  • Managed identity doesn't have Key Vault Secrets User role (should already be assigned by the script)
  • Secret name in the reference doesn't match the actual secret
  • Secret was deleted or soft-deleted
  • Check Function App → Environment variables → App settings and click the reference for the specific error

Function App deployment fails with storage errors Verify the managed identity has Storage Blob Data Owner on the storage account (assigned by the script). Flex Consumption needs Owner, not just Contributor, because it manages blobs in the deployments container.

SQL database is paused / slow first query Free-tier database is serverless with autoPauseDelay=60. First query after idle wakes it up (can take ~30 seconds). This is expected behavior.

"Authentication failed against tenant" warnings during login You have access to an Entra tenant that requires MFA but the script wasn't given a -TenantId. Harmless — the script lists subscriptions from the tenants it could authenticate to. If you need that specific tenant, add -TenantId <id> to Connect-AzAccount.

Login failed for user '<token-identified principal>' during SQL user creation This is an AAD admin propagation race — SQL Server's authentication backend hasn't finished replicating the admin configuration yet. The script retries 5 times with 30-second backoff, which usually clears it. If it still fails after that, wait ~2 minutes and manually run the three CREATE USER / ALTER ROLE statements shown in the script output from Cloud Shell or any SQL client where you're authenticated as the Entra admin:

CREATE USER [<function-app-name>] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [<function-app-name>];
ALTER ROLE db_datawriter ADD MEMBER [<function-app-name>];

Re-running the Script

The script is not idempotent. To redeploy, delete the resource group first:

Remove-AzResourceGroup -Name "<rg-name>" -Force

Then re-run. The generated resource names include a random 6-digit suffix so you can also deploy multiple environments into separate resource groups.

About

A PowerShell script that stands up a full Azure development environment using free or near-free resources, wired together with managed identity and Entra authentication. Designed for standard web projects: a static frontend talking to a serverless API backed by Azure SQL.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages