⏱️ Estimated Time: 20-30 minutes | 💰 Estimated Cost: ~$15-25/month | ⭐ Complexity: Intermediate
This complete, working example demonstrates how to use the Azure Developer CLI (azd) to deploy a Python Flask web application with a Microsoft SQL Database to Azure. All code is included and tested—no external dependencies required.
By completing this example, you will:
- Deploy a multi-tier application (web app + database) using infrastructure-as-code
- Configure secure database connections without hardcoding secrets
- Monitor application health with Application Insights
- Manage Azure resources efficiently with AZD CLI
- Follow Azure best practices for security, cost optimization, and observability
- Web App: Python Flask REST API with database connectivity
- Database: Azure SQL Database with sample data
- Infrastructure: Provisioned using Bicep (modular, reusable templates)
- Deployment: Fully automated with
azdcommands - Monitoring: Application Insights for logs and telemetry
Before starting, verify you have these tools installed:
-
Azure CLI (version 2.50.0 or higher)
az --version # Expected output: azure-cli 2.50.0 or higher -
Azure Developer CLI (azd) (version 1.0.0 or higher)
azd version # Expected output: azd version 1.0.0 or higher -
Python 3.8+ (for local development)
python --version # Expected output: Python 3.8 or higher -
Docker (optional, for local containerized development)
docker --version # Expected output: Docker version 20.10 or higher
- An active Azure subscription (create a free account)
- Permissions to create resources in your subscription
- Owner or Contributor role on the subscription or resource group
This is an intermediate-level example. You should be familiar with:
- Basic command-line operations
- Fundamental cloud concepts (resources, resource groups)
- Basic understanding of web applications and databases
New to AZD? Start with the Getting Started guide first.
This example deploys a two-tier architecture with a web application and SQL database:
graph TD
Browser[User Browser] <--> WebApp[Azure Web App<br/>Flask API<br/>/health<br/>/products]
WebApp -- Secure Connection<br/>Encrypted --> SQL[Azure SQL Database<br/>Products table<br/>Sample data]
Resource Deployment:
- Resource Group: Container for all resources
- App Service Plan: Linux-based hosting (B1 tier for cost efficiency)
- Web App: Python 3.11 runtime with Flask application
- SQL Server: Managed database server with TLS 1.2 minimum
- SQL Database: Basic tier (2GB, suitable for development/testing)
- Application Insights: Monitoring and logging
- Log Analytics Workspace: Centralized log storage
Analogy: Think of this like a restaurant (web app) with a walk-in freezer (database). Customers order from the menu (API endpoints), and the kitchen (Flask app) retrieves ingredients (data) from the freezer. The restaurant manager (Application Insights) tracks everything that happens.
All files are included in this example—no external dependencies required:
examples/database-app/
│
├── README.md # This file
├── azure.yaml # AZD configuration file
├── .env.sample # Sample environment variables
├── .gitignore # Git ignore patterns
│
├── infra/ # Infrastructure as Code (Bicep)
│ ├── main.bicep # Main orchestration template
│ ├── abbreviations.json # Azure naming conventions
│ └── resources/ # Modular resource templates
│ ├── sql-server.bicep # SQL Server configuration
│ ├── sql-database.bicep # Database configuration
│ ├── app-service-plan.bicep # Hosting plan
│ ├── app-insights.bicep # Monitoring setup
│ └── web-app.bicep # Web application
│
└── src/
└── web/ # Application source code
├── app.py # Flask REST API
├── requirements.txt # Python dependencies
└── Dockerfile # Container definition
What Each File Does:
- azure.yaml: Tells AZD what to deploy and where
- infra/main.bicep: Orchestrates all Azure resources
- infra/resources/*.bicep: Individual resource definitions (modular for reuse)
- src/web/app.py: Flask application with database logic
- requirements.txt: Python package dependencies
- Dockerfile: Containerization instructions for deployment
git clone https://github.com/microsoft/AZD-for-beginners.git
cd AZD-for-beginners/examples/database-app✓ Success Check: Verify you see azure.yaml and infra/ folder:
ls
# Expected: README.md, azure.yaml, infra/, src/azd auth loginThis opens your browser for Azure authentication. Sign in with your Azure credentials.
✓ Success Check: You should see:
Logged in to Azure.
azd initWhat happens: AZD creates a local configuration for your deployment.
Prompts you'll see:
- Environment name: Enter a short name (e.g.,
dev,myapp) - Azure subscription: Select your subscription from the list
- Azure location: Choose a region (e.g.,
eastus,westeurope)
✓ Success Check: You should see:
SUCCESS: New project initialized!
azd provisionWhat happens: AZD deploys all infrastructure (takes 5-8 minutes):
- Creates resource group
- Creates SQL Server and Database
- Creates App Service Plan
- Creates Web App
- Creates Application Insights
- Configures networking and security
You'll be prompted for:
- SQL admin username: Enter a username (e.g.,
sqladmin) - SQL admin password: Enter a strong password (save this!)
✓ Success Check: You should see:
SUCCESS: Your application was provisioned in Azure in X minutes Y seconds.
You can view the resources created under the resource group rg-<env-name> in Azure Portal:
https://portal.azure.com/#@/resource/subscriptions/.../resourceGroups/rg-<env-name>
⏱️ Time: 5-8 minutes
azd deployWhat happens: AZD builds and deploys your Flask application:
- Packages the Python application
- Builds the Docker container
- Pushes to Azure Web App
- Initializes the database with sample data
- Starts the application
✓ Success Check: You should see:
SUCCESS: Your application was deployed to Azure in X minutes Y seconds.
You can view the resources created under the resource group rg-<env-name> in Azure Portal:
https://portal.azure.com/#@/resource/subscriptions/.../resourceGroups/rg-<env-name>
⏱️ Time: 3-5 minutes
azd browseThis opens your deployed web app in the browser at https://app-<unique-id>.azurewebsites.net
✓ Success Check: You should see JSON output:
{
"message": "Welcome to the Database App API",
"endpoints": {
"/": "This help message",
"/health": "Health check endpoint",
"/products": "List all products",
"/products/<id>": "Get product by ID"
}
}Health Check (verify database connection):
curl https://app-<your-id>.azurewebsites.net/healthExpected Response:
{
"status": "healthy",
"database": "connected"
}List Products (sample data):
curl https://app-<your-id>.azurewebsites.net/productsExpected Response:
[
{
"id": 1,
"name": "Laptop",
"description": "High-performance laptop",
"price": 1299.99,
"created_at": "2025-11-19T10:30:00"
},
...
]Get Single Product:
curl https://app-<your-id>.azurewebsites.net/products/1✓ Success Check: All endpoints return JSON data without errors.
🎉 Congratulations! You've successfully deployed a web application with a database to Azure using AZD.
Secrets are managed securely via Azure App Service configuration—never hardcoded in source code.
Configured Automatically by AZD:
SQL_CONNECTION_STRING: Database connection with encrypted credentialsAPPLICATIONINSIGHTS_CONNECTION_STRING: Monitoring telemetry endpointSCM_DO_BUILD_DURING_DEPLOYMENT: Enables automatic dependency installation
Where Secrets Are Stored:
- During
azd provision, you provide SQL credentials via secure prompts - AZD stores these in your local
.azure/<env-name>/.envfile (git-ignored) - AZD injects them into Azure App Service configuration (encrypted at rest)
- Application reads them via
os.getenv()at runtime
For local testing, create a .env file from the sample:
cp .env.sample .env
# Edit .env with your local database connectionLocal Development Workflow:
# Install dependencies
cd src/web
pip install -r requirements.txt
# Set environment variables
export SQL_CONNECTION_STRING="your-local-connection-string"
# Run the application
python app.pyTest locally:
curl http://localhost:8000/health
# Expected: {"status": "healthy", "database": "connected"}All Azure resources are defined in Bicep templates (infra/ folder):
- Modular Design: Each resource type has its own file for reusability
- Parameterized: Customize SKUs, regions, naming conventions
- Best Practices: Follows Azure naming standards and security defaults
- Version Controlled: Infrastructure changes are tracked in Git
Customization Example:
To change the database tier, edit infra/resources/sql-database.bicep:
sku: {
name: 'Standard' // Changed from 'Basic'
tier: 'Standard'
capacity: 10
}This example follows Azure security best practices:
- ✅ Credentials stored in Azure App Service configuration (encrypted)
- ✅
.envfiles excluded from Git via.gitignore - ✅ Secrets passed via secure parameters during provisioning
- ✅ TLS 1.2 minimum for SQL Server
- ✅ HTTPS-only enforced for Web App
- ✅ Database connections use encrypted channels
- ✅ SQL Server firewall configured to allow Azure services only
- ✅ Public network access restricted (can be further locked down with Private Endpoints)
- ✅ FTPS disabled on Web App
⚠️ Current: SQL authentication (username/password)- ✅ Production Recommendation: Use Azure Managed Identity for passwordless authentication
To Upgrade to Managed Identity (for production):
- Enable managed identity on Web App
- Grant identity SQL permissions
- Update connection string to use managed identity
- Remove password-based authentication
- ✅ Application Insights logs all requests and errors
- ✅ SQL Database auditing enabled (can be configured for compliance)
- ✅ All resources tagged for governance
Security Checklist Before Production:
- Enable Azure Defender for SQL
- Configure Private Endpoints for SQL Database
- Enable Web Application Firewall (WAF)
- Implement Azure Key Vault for secret rotation
- Configure Azure AD authentication
- Enable diagnostic logging for all resources
Estimated Monthly Costs (as of November 2025):
| Resource | SKU/Tier | Estimated Cost |
|---|---|---|
| App Service Plan | B1 (Basic) | ~$13/month |
| SQL Database | Basic (2GB) | ~$5/month |
| Application Insights | Pay-as-you-go | ~$2/month (low traffic) |
| Total | ~$20/month |
💡 Cost-Saving Tips:
-
Use Free Tier for Learning:
- App Service: F1 tier (free, limited hours)
- SQL Database: Use Azure SQL Database serverless
- Application Insights: 5GB/month free ingestion
-
Stop Resources When Not in Use:
# Stop the web app (database still charges) az webapp stop --name <app-name> --resource-group <rg-name> # Restart when needed az webapp start --name <app-name> --resource-group <rg-name>
-
Delete Everything After Testing:
azd down
This removes ALL resources and stops charges.
-
Development vs. Production SKUs:
- Development: Basic tier (used in this example)
- Production: Standard/Premium tier with redundancy
Cost Monitoring:
- View costs in Azure Cost Management
- Set up cost alerts to avoid surprises
- Tag all resources with
azd-env-namefor tracking
Free Tier Alternative:
For learning purposes, you can modify infra/resources/app-service-plan.bicep:
sku: {
name: 'F1' // Free tier
tier: 'Free'
}Note: Free tier has limitations (60 min/day CPU, no always-on).
This example includes Application Insights for comprehensive monitoring:
What's Monitored:
- ✅ HTTP requests (latency, status codes, endpoints)
- ✅ Application errors and exceptions
- ✅ Custom logging from Flask app
- ✅ Database connection health
- ✅ Performance metrics (CPU, memory)
Access Application Insights:
- Open Azure Portal
- Navigate to your resource group (
rg-<env-name>) - Click on Application Insights resource (
appi-<unique-id>)
Useful Queries (Application Insights → Logs):
View All Requests:
requests
| where timestamp > ago(1h)
| order by timestamp desc
| project timestamp, name, url, resultCode, durationFind Errors:
exceptions
| where timestamp > ago(24h)
| order by timestamp desc
| project timestamp, type, outerMessage, operation_NameCheck Health Endpoint:
requests
| where name contains "health"
| summarize count() by resultCode, bin(timestamp, 1h)SQL Database auditing is enabled to track:
- Database access patterns
- Failed login attempts
- Schema changes
- Data access (for compliance)
Access Audit Logs:
- Azure Portal → SQL Database → Auditing
- View logs in Log Analytics workspace
View Live Metrics:
- Application Insights → Live Metrics
- See requests, failures, and performance in real-time
Set Up Alerts: Create alerts for critical events:
- HTTP 500 errors > 5 in 5 minutes
- Database connection failures
- High response times (>2 seconds)
Example Alert Creation:
az monitor metrics alert create \
--name "High-Response-Time" \
--resource-group <rg-name> \
--scopes <app-insights-resource-id> \
--condition "avg requests/duration > 2000" \
--description "Alert when response time exceeds 2 seconds"Symptom:
Error: The subscription is not registered for the resource type 'components' in the location 'centralus'.
Solution: Choose a different Azure region or register the resource provider:
az provider register --namespace Microsoft.InsightsSymptom:
pyodbc.OperationalError: ('08001', '[08001] [Microsoft][ODBC Driver 18 for SQL Server]TCP Provider...')
Solution:
- Verify SQL Server firewall allows Azure services (configured automatically)
- Check the SQL admin password was entered correctly during
azd provision - Ensure SQL Server is fully provisioned (can take 2-3 minutes)
Verify Connection:
# From Azure Portal, go to SQL Database → Query editor
# Try to connect with your credentialsSymptom: Browser shows generic error page.
Solution: Check application logs:
# View recent logs
az webapp log tail --name <app-name> --resource-group <rg-name>Common causes:
- Missing environment variables (check App Service → Configuration)
- Python package installation failed (check deployment logs)
- Database initialization error (check SQL connectivity)
Symptom:
Error: Failed to build project
Solution:
- Ensure
requirements.txthas no syntax errors - Check that Python 3.11 is specified in
infra/resources/web-app.bicep - Verify Dockerfile has correct base image
Debug locally:
cd src/web
docker build -t test-app .
docker run -p 8000:8000 test-appSymptom:
ERROR: (Unauthorized) The client '<id>' with object id '<id>' does not have authorization
Solution: Re-authenticate with Azure:
# Required for AZD workflows
azd auth login
# Optional if you are also using Azure CLI commands directly
az loginVerify you have the correct permissions (Contributor role) on the subscription.
Symptom: Unexpected Azure bill.
Solution:
- Check if you forgot to run
azd downafter testing - Verify SQL Database is using Basic tier (not Premium)
- Review costs in Azure Cost Management
- Set up cost alerts
View All AZD Environment Variables:
azd env get-valuesCheck Deployment Status:
az webapp show --name <app-name> --resource-group <rg-name> --query stateAccess Application Logs:
az webapp log download --name <app-name> --resource-group <rg-name> --log-file app-logs.zipNeed More Help?
Goal: Confirm all resources are deployed and the application is working.
Steps:
-
List all resources in your resource group:
az resource list --resource-group rg-<env-name> --output table
Expected: 6-7 resources (Web App, SQL Server, SQL Database, App Service Plan, Application Insights, Log Analytics)
-
Test all API endpoints:
curl https://app-<your-id>.azurewebsites.net/ curl https://app-<your-id>.azurewebsites.net/health curl https://app-<your-id>.azurewebsites.net/products curl https://app-<your-id>.azurewebsites.net/products/1
Expected: All return valid JSON without errors
-
Check Application Insights:
- Navigate to Application Insights in Azure Portal
- Go to "Live Metrics"
- Refresh your browser on the web app Expected: See requests appearing in real-time
Success Criteria: All 6-7 resources exist, all endpoints return data, Live Metrics shows activity.
Goal: Extend the Flask application with a new endpoint.
Starter Code: Current endpoints in src/web/app.py
Steps:
-
Edit
src/web/app.pyand add a new endpoint after theget_product()function:@app.route('/products/search/<keyword>') def search_products(keyword): """Search products by name or description.""" try: conn = get_db_connection() cursor = conn.cursor() cursor.execute( "SELECT id, name, description, price, created_at FROM products WHERE name LIKE ? OR description LIKE ?", (f'%{keyword}%', f'%{keyword}%') ) products = [] for row in cursor.fetchall(): products.append({ 'id': row[0], 'name': row[1], 'description': row[2], 'price': float(row[3]) if row[3] else None, 'created_at': row[4].isoformat() if row[4] else None }) cursor.close() conn.close() logger.info(f"Search for '{keyword}' returned {len(products)} results") return jsonify(products), 200 except Exception as e: logger.error(f"Error searching products: {str(e)}") return jsonify({'error': str(e)}), 500
-
Deploy the updated application:
azd deploy
-
Test the new endpoint:
curl https://app-<your-id>.azurewebsites.net/products/search/laptop
Expected: Returns products matching "laptop"
Success Criteria: New endpoint works, returns filtered results, shows up in Application Insights logs.
Goal: Set up proactive monitoring with alerts.
Steps:
-
Create an alert for HTTP 500 errors:
# Get Application Insights resource ID AI_ID=$(az monitor app-insights component show \ --app appi-<your-id> \ --resource-group rg-<env-name> \ --query id -o tsv) # Create alert az monitor metrics alert create \ --name "High-Error-Rate" \ --resource-group rg-<env-name> \ --scopes $AI_ID \ --condition "count requests/failed > 5" \ --window-size 5m \ --evaluation-frequency 1m \ --description "Alert when >5 failed requests in 5 minutes"
-
Trigger the alert by causing errors:
# Request a non-existent product for i in {1..10}; do curl https://app-<your-id>.azurewebsites.net/products/999; done
-
Check if the alert fired:
- Azure Portal → Alerts → Alert Rules
- Check your email (if configured)
Success Criteria: Alert rule is created, triggers on errors, notifications are received.
Goal: Add a new table and modify the application to use it.
Steps:
-
Connect to SQL Database via Azure Portal Query Editor
-
Create a new
categoriestable:CREATE TABLE categories ( id INT PRIMARY KEY IDENTITY(1,1), name NVARCHAR(50) NOT NULL, description NVARCHAR(200) ); INSERT INTO categories (name, description) VALUES ('Electronics', 'Electronic devices and accessories'), ('Office Supplies', 'Office equipment and supplies'); -- Add category to products table ALTER TABLE products ADD category_id INT; UPDATE products SET category_id = 1; -- Set all to Electronics
-
Update
src/web/app.pyto include category information in responses -
Deploy and test
Success Criteria: New table exists, products show category information, application still works.
Goal: Add Azure Redis Cache to improve performance.
Steps:
- Add Redis Cache to
infra/main.bicep - Update
src/web/app.pyto cache product queries - Measure performance improvement with Application Insights
- Compare response times before/after caching
Success Criteria: Redis is deployed, caching works, response times improve by >50%.
Hint: Start with Azure Cache for Redis documentation.
To avoid ongoing charges, delete all resources when done:
azd downConfirmation prompt:
? Total resources to delete: 7, are you sure you want to continue? (y/N)
Type y to confirm.
✓ Success Check:
- All resources are deleted from Azure Portal
- No ongoing charges
- Local
.azure/<env-name>folder can be deleted
Alternative (keep infrastructure, delete data):
# Delete only the resource group (keep AZD config)
az group delete --name rg-<env-name> --yes- Azure Developer CLI Documentation
- Azure SQL Database Documentation
- Azure App Service Documentation
- Application Insights Documentation
- Bicep Language Reference
- Container Apps Example: Deploy microservices with Azure Container Apps
- AI Integration Guide: Add AI capabilities to your app
- Deployment Best Practices: Production deployment patterns
- Managed Identity: Remove passwords and use Azure AD authentication
- Private Endpoints: Secure database connections within a virtual network
- CI/CD Integration: Automate deployments with GitHub Actions or Azure DevOps
- Multi-Environment: Set up dev, staging, and production environments
- Database Migrations: Use Alembic or Entity Framework for schema versioning
AZD vs. ARM Templates:
- ✅ AZD: Higher-level abstraction, simpler commands
⚠️ ARM: More verbose, granular control
AZD vs. Terraform:
- ✅ AZD: Azure-native, integrated with Azure services
⚠️ Terraform: Multi-cloud support, larger ecosystem
AZD vs. Azure Portal:
- ✅ AZD: Repeatable, version-controlled, automatable
⚠️ Portal: Manual clicks, difficult to reproduce
Think of AZD as: Docker Compose for Azure—simplified configuration for complex deployments.
Q: Can I use a different programming language?
A: Yes! Replace src/web/ with Node.js, C#, Go, or any language. Update azure.yaml and Bicep accordingly.
Q: How do I add more databases?
A: Add another SQL Database module in infra/main.bicep or use PostgreSQL/MySQL from Azure Database services.
Q: Can I use this for production?
A: This is a starting point. For production, add: managed identity, private endpoints, redundancy, backup strategy, WAF, and enhanced monitoring.
Q: What if I want to use containers instead of code deployment?
A: Check out the Container Apps Example which uses Docker containers throughout.
Q: How do I connect to the database from my local machine?
A: Add your IP to the SQL Server firewall:
az sql server firewall-rule create \
--resource-group rg-<env-name> \
--server sql-<unique-id> \
--name AllowMyIP \
--start-ip-address <your-ip> \
--end-ip-address <your-ip>Q: Can I use an existing database instead of creating a new one?
A: Yes, modify infra/main.bicep to reference an existing SQL Server and update the connection string parameters.
Note: This example demonstrates best practices for deploying a web app with a database using AZD. It includes working code, comprehensive documentation, and practical exercises to reinforce learning. For production deployments, review security, scaling, compliance, and cost requirements specific to your organization.
📚 Course Navigation:
- ← Previous: Container Apps Example
- → Next: AI Integration Guide
- 🏠 Course Home