Skip to content

Commit 2fc85ab

Browse files
add Azure Key Vault integration and connection string management
1 parent dd9652f commit 2fc85ab

4 files changed

Lines changed: 167 additions & 0 deletions

File tree

samples/web-app-sql-database/python/bicep/main.bicep

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,8 @@ param username string = 'paolo'
322322
var sqlServerName = '${prefix}-sqlserver-${suffix}'
323323
var webAppName = '${prefix}-webapp-${suffix}'
324324
var appServicePlanName = '${prefix}-app-service-plan-${suffix}'
325+
var keyVaultName = '${prefix}-kv-${suffix}'
326+
var sqlConnectionStringSecretName = 'sql-connection-string'
325327
var identity = {
326328
type: 'SystemAssigned'
327329
}
@@ -429,6 +431,42 @@ resource webApp 'Microsoft.Web/sites@2024-11-01' = {
429431
}
430432
}
431433

434+
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
435+
name: keyVaultName
436+
location: location
437+
tags: tags
438+
properties: {
439+
tenantId: subscription().tenantId
440+
sku: {
441+
family: 'A'
442+
name: 'standard'
443+
}
444+
accessPolicies: [
445+
{
446+
tenantId: subscription().tenantId
447+
objectId: webApp.identity.principalId
448+
permissions: {
449+
secrets: [
450+
'get'
451+
'list'
452+
]
453+
}
454+
}
455+
]
456+
enableRbacAuthorization: false
457+
enableSoftDelete: true
458+
softDeleteRetentionInDays: 7
459+
}
460+
}
461+
462+
resource sqlConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2024-11-01' = {
463+
parent: keyVault
464+
name: sqlConnectionStringSecretName
465+
properties: {
466+
value: 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${sqlDatabaseName};User ID=${sqlDatabaseUsername};Password=${sqlDatabasePassword};Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;'
467+
}
468+
}
469+
432470
resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = {
433471
parent: webApp
434472
name: 'appsettings'
@@ -439,6 +477,7 @@ resource configAppSettings 'Microsoft.Web/sites/config@2024-11-01' = {
439477
SQL_DATABASE: sqlDatabaseName
440478
SQL_USERNAME: sqlDatabaseUsername
441479
SQL_PASSWORD: sqlDatabasePassword
480+
SQL_CONNECTION_STRING: '@Microsoft.KeyVault(SecretUri=${sqlConnectionStringSecret.properties.secretUri})'
442481
LOGIN_NAME: username
443482
}
444483
}
@@ -459,3 +498,6 @@ output webAppUrl string = webApp.properties.defaultHostName
459498
output sqlServerName string = sqlServer.name
460499
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName
461500
output sqlDatabaseName string = sqlDatabase.name
501+
output keyVaultName string = keyVault.name
502+
output keyVaultUrl string = keyVault.properties.vaultUri
503+
output sqlConnectionStringSecretUri string = sqlConnectionStringSecret.properties.secretUri

samples/web-app-sql-database/python/scripts/deploy.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ RUNTIME="python"
2222
RUNTIME_VERSION="3.13"
2323
DEPLOY_APP=1
2424
ENVIRONMENT=$(az account show --query environmentName --output tsv)
25+
KEYVAULT_NAME="${PREFIX}-kv-${SUFFIX}"
26+
SECRET_NAME="SqlConnectionString"
2527

2628
# Change the current directory to the script's directory
2729
cd "$CURRENT_DIR" || exit
@@ -298,6 +300,7 @@ $AZ webapp create \
298300
--plan "$APP_SERVICE_PLAN_NAME" \
299301
--name "$WEB_APP_NAME" \
300302
--runtime "$RUNTIME:$RUNTIME_VERSION" \
303+
--assign-identity \
301304
--only-show-errors 1>/dev/null
302305

303306
if [ $? -eq 0 ]; then
@@ -307,6 +310,72 @@ else
307310
exit 1
308311
fi
309312

313+
# Get Web App principal ID
314+
PRINCIPAL_ID=$($AZ webapp identity show \
315+
--name "$WEB_APP_NAME" \
316+
--resource-group "$RESOURCE_GROUP_NAME" \
317+
--query "principalId" \
318+
--output tsv)
319+
320+
# Create Key Vault
321+
echo "Creating Key Vault [$KEY_VAULT_NAME]..."
322+
$AZ keyvault create \
323+
--name "$KEY_VAULT_NAME" \
324+
--resource-group "$RESOURCE_GROUP_NAME" \
325+
--location "$LOCATION" \
326+
--enable-soft-delete true \
327+
--retention-days 7 \
328+
--only-show-errors 1>/dev/null
329+
330+
if [ $? -eq 0 ]; then
331+
echo "Key Vault [$KEY_VAULT_NAME] created successfully."
332+
else
333+
echo "Failed to create Key Vault [$KEY_VAULT_NAME]."
334+
exit 1
335+
fi
336+
337+
# Assign access policy to Web App managed identity
338+
echo "Assigning Key Vault access policy to Web App..."
339+
$AZ keyvault set-policy \
340+
--name "$KEY_VAULT_NAME" \
341+
--object-id "$PRINCIPAL_ID" \
342+
--secret-permissions get \
343+
--only-show-errors 1>/dev/null
344+
345+
if [ $? -eq 0 ]; then
346+
echo "Key Vault access policy assigned successfully."
347+
else
348+
echo "Failed to assign Key Vault access policy."
349+
exit 1
350+
fi
351+
352+
# Build connection string
353+
SQL_CONNECTION_STRING="Server=tcp:${SQL_SERVER_FQDN},1433;Database=${SQL_DATABASE_NAME};User ID=${DATABASE_USER_NAME};Password=${DATABASE_USER_PASSWORD};Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=30;"
354+
355+
# Create secret
356+
echo "Creating secret [$SECRET_NAME] in Key Vault..."
357+
$AZ keyvault secret set \
358+
--vault-name "$KEY_VAULT_NAME" \
359+
--name "$SECRET_NAME" \
360+
--value "$SQL_CONNECTION_STRING" \
361+
--only-show-errors 1>/dev/null
362+
363+
if [ $? -eq 0 ]; then
364+
echo "Secret [$SECRET_NAME] created successfully."
365+
else
366+
echo "Failed to create secret [$SECRET_NAME]."
367+
exit 1
368+
fi
369+
370+
# Get Secret URI
371+
SECRET_URI=$($AZ keyvault secret show \
372+
--vault-name "$KEY_VAULT_NAME" \
373+
--name "$SECRET_NAME" \
374+
--query "id" \
375+
--output tsv)
376+
377+
echo "Secret URI: $SECRET_URI"
378+
310379
# Set web app settings
311380
echo "Setting web app settings for [$WEB_APP_NAME]..."
312381
$AZ webapp config appsettings set \

samples/web-app-sql-database/python/scripts/validate.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,17 @@ $AZ sql db show \
3939
--name PlannerDB \
4040
--server local-sqlserver-test \
4141
--resource-group local-rg \
42+
--output table
43+
44+
# Check Azure Key Vault
45+
$AZ keyvault show \
46+
--name local-kv-test \
47+
--resource-group local-rg \
48+
--output table
49+
50+
# Check Key Vault secret
51+
$AZ keyvault secret show \
52+
--vault-name local-kv-test \
53+
--name sql-connection-string \
54+
--query "{name:name, enabled:attributes.enabled, created:attributes.created}" \
4255
--output table

samples/web-app-sql-database/python/src/database.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Supports both traditional ODBC connections and passwordless Azure AD authentication
44
"""
55

6+
from multiprocessing import connection
67
import os
78
import struct
89
import pyodbc
@@ -73,6 +74,10 @@ def from_env(cls) -> 'SqlHelper':
7374
- SQL_USERNAME
7475
- SQL_PASSWORD
7576
"""
77+
connection_string = os.environ.get("SQL_CONNECTION_STRING")
78+
if connection_string:
79+
return cls.from_connection_string(connection_string)
80+
7681
client_id = os.environ.get("AZURE_CLIENT_ID")
7782
client_secret = os.environ.get("AZURE_CLIENT_SECRET")
7883
tenant_id = os.environ.get("AZURE_TENANT_ID")
@@ -94,6 +99,44 @@ def from_env(cls) -> 'SqlHelper':
9499
use_azure_credential=all([client_id, client_secret, tenant_id])
95100
)
96101

102+
@classmethod
103+
def from_connection_string(cls, connection_string: str) -> 'SqlHelper':
104+
"""
105+
Create a SqlHelper instance from a connection string.
106+
107+
This is useful when the connection string is stored in an environment variable
108+
(e.g., resolved by Azure App Service from Key Vault via @Microsoft.KeyVault(SecretUri=...)).
109+
110+
"""
111+
parts = {}
112+
for part in connection_string.split(';'):
113+
if '=' in part:
114+
key, value = part.split('=', 1)
115+
parts[key.strip()] = value.strip()
116+
117+
server = parts.get('Server', '').replace('tcp:', '').replace(',1433', '')
118+
database = parts.get('Database')
119+
username = parts.get('User ID')
120+
password = parts.get('Password')
121+
122+
if not all([server, database, username, password]):
123+
raise ValueError(
124+
f"Could not parse all required parameters from connection string. "
125+
f"Found - Server: {bool(server)}, Database: {bool(database)}, "
126+
f"Username: {bool(username)}, Password: {bool(password)}"
127+
)
128+
129+
logger.info("Connection string parsed successfully")
130+
logger.debug(f"Server: {server}, Database: {database}, Username: {username}")
131+
132+
return cls(
133+
server=server,
134+
database=database,
135+
username=username,
136+
password=password,
137+
use_azure_credential=False
138+
)
139+
97140
def _build_connection_string(self) -> str:
98141
"""Build the ODBC connection string."""
99142
conn_str = (

0 commit comments

Comments
 (0)