⏱️ Estimated Time: 25-35 minutes | 💰 Estimated Cost: ~$50-100/month | ⭐ Complexity: Advanced
📚 Learning Path:
- ← Previous: Simple Flask API - Single container basics
- 🎯 You Are Here: Microservices Architecture (2-service foundation)
- → Next: AI Integration - Add intelligence to your services
- 🏠 Course Home
A simplified but functional microservices architecture deployed to Azure Container Apps using AZD CLI. This example demonstrates service-to-service communication, container orchestration, and monitoring with a practical 2-service setup.
📚 Learning Approach: This example starts with a minimal 2-service architecture (API Gateway + Backend Service) that you can actually deploy and learn from. After mastering this foundation, we provide guidance for expanding to a full microservices ecosystem.
By completing this example, you will:
- Deploy multiple containers to Azure Container Apps
- Implement service-to-service communication with internal networking
- Configure environment-based scaling and health checks
- Monitor distributed applications with Application Insights
- Understand microservices deployment patterns and best practices
- Learn progressive expansion from simple to complex architectures
graph TB
Internet[Internet/User]
Gateway[API Gateway<br/>Node.js Container<br/>Port 8080]
Product[Product Service<br/>Python Container<br/>Port 8000]
AppInsights[Application Insights<br/>Monitoring & Logs]
Internet -->|HTTPS| Gateway
Gateway -->|HTTP Internal| Product
Gateway -.->|Telemetry| AppInsights
Product -.->|Telemetry| AppInsights
style Gateway fill:#2196F3,stroke:#1976D2,stroke-width:3px,color:#fff
style Product fill:#4CAF50,stroke:#388E3C,stroke-width:3px,color:#fff
style Internet fill:#FF9800,stroke:#F57C00,stroke-width:2px,color:#fff
style AppInsights fill:#9C27B0,stroke:#7B1FA2,stroke-width:2px,color:#fff
Component Details:
| Component | Purpose | Access | Resources |
|---|---|---|---|
| API Gateway | Routes external requests to backend services | Public (HTTPS) | 1 vCPU, 2GB RAM, 2-20 replicas |
| Product Service | Manages product catalog with in-memory data | Internal only | 0.5 vCPU, 1GB RAM, 1-10 replicas |
| Application Insights | Centralized logging and distributed tracing | Azure Portal | 1-2 GB/month data ingestion |
Why Start Simple?
- ✅ Deploy and understand quickly (25-35 minutes)
- ✅ Learn core microservices patterns without complexity
- ✅ Working code you can modify and experiment with
- ✅ Lower cost for learning (~$50-100/month vs $300-1400/month)
- ✅ Build confidence before adding databases and message queues
Analogy: Think of this like learning to drive. You start with an empty parking lot (2 services), master the basics, then progress to city traffic (5+ services with databases).
Once you master the 2-service architecture, you can expand to:
graph TB
User[User]
Gateway[API Gateway<br/>✅ Included]
Product[Product Service<br/>✅ Included]
Order[Order Service<br/>🔜 Add Next]
UserSvc[User Service<br/>🔜 Add Next]
Notify[Notification Service<br/>🔜 Add Last]
CosmosDB[(Cosmos DB<br/>🔜 Product Data)]
AzureSQL[(Azure SQL<br/>🔜 Order Data)]
ServiceBus[Azure Service Bus<br/>🔜 Async Events]
AppInsights[Application Insights<br/>✅ Included]
User --> Gateway
Gateway --> Product
Gateway --> Order
Gateway --> UserSvc
Product --> CosmosDB
Order --> AzureSQL
UserSvc --> AzureSQL
Product -.->|ProductCreated Event| ServiceBus
ServiceBus -.->|Subscribe| Notify
ServiceBus -.->|Subscribe| Order
Gateway -.-> AppInsights
Product -.-> AppInsights
Order -.-> AppInsights
UserSvc -.-> AppInsights
Notify -.-> AppInsights
style Product fill:#4CAF50,stroke:#388E3C,stroke-width:3px,color:#fff
style Gateway fill:#2196F3,stroke:#1976D2,stroke-width:3px,color:#fff
style Order fill:#9E9E9E,stroke:#616161,stroke-width:2px,color:#fff
style UserSvc fill:#9E9E9E,stroke:#616161,stroke-width:2px,color:#fff
style Notify fill:#9E9E9E,stroke:#616161,stroke-width:2px,color:#fff
See "Expansion Guide" section at the end for step-by-step instructions.
✅ Service Discovery: Automatic DNS-based discovery between containers
✅ Load Balancing: Built-in load balancing across replicas
✅ Auto-scaling: Independent scaling per service based on HTTP requests
✅ Health Monitoring: Liveness and readiness probes for both services
✅ Distributed Logging: Centralized logging with Application Insights
✅ Internal Networking: Secure service-to-service communication
✅ Container Orchestration: Automatic deployment and scaling
✅ Zero-Downtime Updates: Rolling updates with revision management
Before starting, verify you have these tools installed:
-
Azure Developer CLI (azd) (version 1.0.0 or higher)
azd version # Expected output: azd version 1.0.0 or higher -
Azure CLI (version 2.50.0 or higher)
az --version # Expected output: azure-cli 2.50.0 or higher -
Docker (for local development/testing - optional)
docker --version # Expected output: Docker version 20.10 or higher
Run these commands to confirm you're ready:
# Check Azure Developer CLI
azd version
# ✅ Expected: azd version 1.0.0 or higher
# Check Azure CLI
az --version
# ✅ Expected: azure-cli 2.50.0 or higher
# Check Docker (optional)
docker --version
# ✅ Expected: Docker version 20.10 or higherSuccess Criteria: All commands return version numbers matching or exceeding minimums.
- An active Azure subscription (create a free account)
- Permissions to create resources in your subscription
- Contributor role on the subscription or resource group
This is an advanced-level example. You should have:
- Completed the Simple Flask API example
- Basic understanding of microservices architecture
- Familiarity with REST APIs and HTTP
- Understanding of container concepts
New to Container Apps? Start with the Simple Flask API example first to learn the basics.
git clone https://github.com/microsoft/AZD-for-beginners.git
cd AZD-for-beginners/examples/microservices✓ Success Check: Verify you see azure.yaml:
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 initPrompts you'll see:
- Environment name: Enter a short name (e.g.,
microservices-dev) - Azure subscription: Select your subscription
- Azure location: Choose a region (e.g.,
eastus,westeurope)
✓ Success Check: You should see:
SUCCESS: New project initialized!
azd upWhat happens (takes 8-12 minutes):
graph LR
A[azd up] --> B[Create Resource Group]
B --> C[Deploy Container Registry]
C --> D[Deploy Log Analytics]
D --> E[Deploy App Insights]
E --> F[Create Container Environment]
F --> G[Build API Gateway Image]
F --> H[Build Product Service Image]
G --> I[Push to Registry]
H --> I
I --> J[Deploy API Gateway]
I --> K[Deploy Product Service]
J --> L[Configure Ingress & Health Checks]
K --> L
L --> M[Deployment Complete ✓]
style A fill:#2196F3,stroke:#1976D2,stroke-width:3px,color:#fff
style M fill:#4CAF50,stroke:#388E3C,stroke-width:3px,color:#fff
✓ Success Check: You should see:
SUCCESS: Your application was deployed to Azure in X minutes Y seconds.
Endpoint: https://api-gateway-<unique-id>.azurecontainerapps.io
⏱️ Time: 8-12 minutes
# Get the gateway endpoint
GATEWAY_URL=$(azd env get-values | grep API_GATEWAY_URL | cut -d '=' -f2 | tr -d '"')
# Test API Gateway health
curl $GATEWAY_URL/health✅ Expected output:
{
"status": "healthy",
"service": "api-gateway",
"timestamp": "2025-11-19T10:30:00Z"
}Test product service through gateway:
# List products
curl $GATEWAY_URL/api/products✅ Expected output:
[
{"id":1,"name":"Laptop","price":999.99,"stock":50},
{"id":2,"name":"Mouse","price":29.99,"stock":200},
{"id":3,"name":"Keyboard","price":79.99,"stock":150}
]✓ Success Check: Both endpoints return JSON data without errors.
🎉 Congratulations! You've deployed a microservices architecture to Azure!
All implementation files are included—this is a complete, working example:
microservices/
│
├── README.md # This file
├── azure.yaml # AZD configuration
├── .gitignore # Git ignore patterns
│
├── infra/ # Infrastructure as Code (Bicep)
│ ├── main.bicep # Main orchestration
│ ├── abbreviations.json # Naming conventions
│ ├── core/ # Shared infrastructure
│ │ ├── container-apps-environment.bicep # Container environment + registry
│ │ └── monitor.bicep # Application Insights + Log Analytics
│ └── app/ # Service definitions
│ ├── api-gateway.bicep # API Gateway container app
│ └── product-service.bicep # Product Service container app
│
└── src/ # Application source code
├── api-gateway/ # Node.js API Gateway
│ ├── app.js # Express server with routing
│ ├── package.json # Node dependencies
│ └── Dockerfile # Container definition
└── product-service/ # Python Product Service
├── main.py # Flask API with product data
├── requirements.txt # Python dependencies
└── Dockerfile # Container definition
What Each Component Does:
Infrastructure (infra/):
main.bicep: Orchestrates all Azure resources and their dependenciescore/container-apps-environment.bicep: Creates the Container Apps environment and Azure Container Registrycore/monitor.bicep: Sets up Application Insights for distributed loggingapp/*.bicep: Individual container app definitions with scaling and health checks
API Gateway (src/api-gateway/):
- Public-facing service that routes requests to backend services
- Implements logging, error handling, and request forwarding
- Demonstrates service-to-service HTTP communication
Product Service (src/product-service/):
- Internal service with product catalog (in-memory for simplicity)
- REST API with health checks
- Example of backend microservice pattern
Port: 8080
Access: Public (external ingress)
Purpose: Routes incoming requests to appropriate backend services
Endpoints:
GET /- Service informationGET /health- Health check endpointGET /api/products- Forward to product service (list all)GET /api/products/:id- Forward to product service (get by ID)
Key Features:
- Request routing with axios
- Centralized logging
- Error handling and timeout management
- Service discovery via environment variables
- Application Insights integration
Code Highlight (src/api-gateway/app.js):
// Internal service communication
app.get('/api/products', async (req, res) => {
const response = await axios.get(`${PRODUCT_SERVICE_URL}/products`, {
timeout: 5000
});
res.json(response.data);
});Port: 8000
Access: Internal only (no external ingress)
Purpose: Manages product catalog with in-memory data
Endpoints:
GET /- Service informationGET /health- Health check endpointGET /products- List all productsGET /products/<id>- Get product by ID
Key Features:
- RESTful API with Flask
- In-memory product store (simple, no database needed)
- Health monitoring with probes
- Structured logging
- Application Insights integration
Data Model:
{
"id": 1,
"name": "Laptop",
"description": "High-performance laptop",
"price": 999.99,
"stock": 50
}Why Internal Only? The product service is not exposed publicly. All requests must go through the API Gateway, which provides:
- Security: Controlled access point
- Flexibility: Can change backend without affecting clients
- Monitoring: Centralized request logging
sequenceDiagram
participant User
participant Gateway as API Gateway<br/>(Port 8080)
participant Product as Product Service<br/>(Port 8000)
participant AI as Application Insights
User->>Gateway: GET /api/products
Gateway->>AI: Log request
Gateway->>Product: GET /products (internal HTTP)
Product->>AI: Log query
Product-->>Gateway: JSON response [5 products]
Gateway->>AI: Log response
Gateway-->>User: JSON response [5 products]
Note over Gateway,Product: Internal DNS: http://product-service
Note over User,AI: All communication logged and traced
In this example, the API Gateway communicates with the Product Service using internal HTTP calls:
// API Gateway (src/api-gateway/app.js)
const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL;
// Make internal HTTP request
const response = await axios.get(`${PRODUCT_SERVICE_URL}/products`);Key Points:
-
DNS-Based Discovery: Container Apps automatically provides DNS for internal services
- Product Service FQDN:
product-service.internal.<environment>.azurecontainerapps.io - Simplified as:
http://product-service(Container Apps resolves it)
- Product Service FQDN:
-
No Public Exposure: Product Service has
external: falsein Bicep- Only accessible within the Container Apps environment
- Cannot be reached from the internet
-
Environment Variables: Service URLs are injected at deployment time
- Bicep passes the internal FQDN to the gateway
- No hardcoded URLs in application code
Analogy: Think of this like office rooms. The API Gateway is the reception desk (public-facing), and the Product Service is an office room (internal only). Visitors must go through reception to reach any office.
# Deploy infrastructure and both services
azd upThis deploys:
- Container Apps environment
- Application Insights
- Container Registry
- API Gateway container
- Product Service container
Time: 8-12 minutes
# Deploy only one service (after initial azd up)
azd deploy api-gateway
# Or deploy product service
azd deploy product-serviceUse Case: When you've updated code in one service and want to redeploy only that service.
# Change scaling parameters
azd env set GATEWAY_MAX_REPLICAS 30
# Redeploy with new configuration
azd upBoth services are configured with HTTP-based autoscaling in their Bicep files:
API Gateway:
- Min replicas: 2 (always at least 2 for availability)
- Max replicas: 20
- Scale trigger: 50 concurrent requests per replica
Product Service:
- Min replicas: 1 (can scale to zero if needed)
- Max replicas: 10
- Scale trigger: 100 concurrent requests per replica
Customize Scaling (in infra/app/*.bicep):
scale: {
minReplicas: 1
maxReplicas: 10
rules: [
{
name: 'http-scale-rule'
http: {
metadata: {
concurrentRequests: '100' // Adjust this
}
}
}
]
}API Gateway:
- CPU: 1.0 vCPU
- Memory: 2 GiB
- Reason: Handles all external traffic
Product Service:
- CPU: 0.5 vCPU
- Memory: 1 GiB
- Reason: Lightweight in-memory operations
Both services include liveness and readiness probes:
probes: [
{
type: 'Liveness'
httpGet: {
path: '/health'
port: 8080
}
initialDelaySeconds: 10
periodSeconds: 30
}
{
type: 'Readiness'
httpGet: {
path: '/health'
port: 8080
}
initialDelaySeconds: 5
periodSeconds: 10
}
]What This Means:
- Liveness: If health check fails, Container Apps restarts the container
- Readiness: If not ready, Container Apps stops routing traffic to that replica
# View logs using azd monitor
azd monitor --logs
# Or use Azure CLI for specific Container Apps:
# Stream logs from API Gateway
az containerapp logs show --name api-gateway --resource-group $RG_NAME --follow
# View recent product service logs
az containerapp logs show --name product-service --resource-group $RG_NAME --tail 100Expected Output:
[api-gateway] API Gateway listening on port 8080
[api-gateway] Product Service URL: http://product-service
[api-gateway] GET /api/products 200 - 45ms
[product-service] Retrieved 5 products
Access Application Insights in Azure Portal, then run these queries:
Find Slow Requests:
requests
| where timestamp > ago(1h)
| where duration > 1000 // Requests taking >1 second
| summarize count() by name, cloud_RoleName
| order by count_ descTrack Service-to-Service Calls:
dependencies
| where timestamp > ago(1h)
| where type == "Http"
| project timestamp, name, target, duration, success
| order by timestamp descError Rate by Service:
exceptions
| where timestamp > ago(24h)
| summarize errorCount = count() by cloud_RoleName, type
| order by errorCount descRequest Volume Over Time:
requests
| where timestamp > ago(1h)
| summarize requestCount = count() by bin(timestamp, 5m), cloud_RoleName
| render timechart# Get Application Insights details
azd env get-values | grep APPLICATIONINSIGHTS
# Open Azure Portal monitoring
az monitor app-insights component show \
--app $(azd env get-values | grep APPLICATIONINSIGHTS_CONNECTION_STRING | cut -d '=' -f2) \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2) \
--query "appId" -o tsv- Navigate to Application Insights in Azure Portal
- Click "Live Metrics"
- See real-time requests, failures, and performance
- Test by running:
curl $(azd env get-values | grep API_GATEWAY_URL | cut -d '=' -f2 | tr -d '"')/api/products
Goal: Add a POST endpoint to create new products
Starting Point: src/product-service/main.py
Steps:
- Add this endpoint after the
get_productfunction inmain.py:
@app.route('/products', methods=['POST'])
def create_product():
"""Create a new product"""
data = request.get_json()
# Validate required fields
if not data or 'name' not in data or 'price' not in data:
return jsonify({'error': 'Missing required fields: name, price'}), 400
new_id = max(p['id'] for p in products) + 1
new_product = {
'id': new_id,
'name': data['name'],
'description': data.get('description', ''),
'price': float(data['price']),
'stock': int(data.get('stock', 0))
}
products.append(new_product)
logger.info(f"Created product {new_id}")
return jsonify(new_product), 201- Add POST route to API Gateway (
src/api-gateway/app.js):
// Add this after the GET /api/products route
app.post('/api/products', async (req, res) => {
try {
console.log(`Forwarding POST request to ${PRODUCT_SERVICE_URL}/products`);
const response = await axios.post(`${PRODUCT_SERVICE_URL}/products`, req.body, {
timeout: 5000
});
res.status(201).json(response.data);
} catch (error) {
console.error('Error calling product service:', error.message);
res.status(503).json({
error: 'Product service unavailable',
message: error.message
});
}
});- Redeploy both services:
azd deploy product-service
azd deploy api-gateway- Test the new endpoint:
GATEWAY_URL=$(azd env get-values | grep API_GATEWAY_URL | cut -d '=' -f2 | tr -d '"')
# Create a new product
curl -X POST $GATEWAY_URL/api/products \
-H "Content-Type: application/json" \
-d '{"name":"USB Cable","price":9.99,"stock":500}'✅ Expected output:
{"id":6,"name":"USB Cable","description":"","price":9.99,"stock":500}- Verify it appears in the list:
curl $GATEWAY_URL/api/products
# Should now show 6 products including the new USB CableSuccess Criteria:
- ✅ POST request returns HTTP 201
- ✅ New product appears in GET /api/products list
- ✅ Product has auto-incremented ID
Time: 10-15 minutes
Goal: Change Product Service to scale more aggressively
Starting Point: infra/app/product-service.bicep
Steps:
-
Open
infra/app/product-service.bicepand find thescaleblock (around line 95) -
Change from:
scale: {
minReplicas: 1
maxReplicas: 10
rules: [
{
name: 'http-scale-rule'
http: {
metadata: {
concurrentRequests: '100' // OLD
}
}
}
]
}To:
scale: {
minReplicas: 2 // Always have 2 running
maxReplicas: 20 // Allow more scaling
rules: [
{
name: 'http-scale-rule'
http: {
metadata: {
concurrentRequests: '20' // Scale at lower threshold
}
}
}
]
}- Redeploy infrastructure:
azd up- Verify new scaling configuration:
az containerapp show \
--name $(azd env get-values | grep PRODUCT_SERVICE | head -1 | cut -d '/' -f5) \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--query "properties.template.scale" -o json✅ Expected output:
{
"minReplicas": 2,
"maxReplicas": 20,
"rules": [...]
}- Test autoscaling with load:
# Generate concurrent requests
for i in {1..500}; do curl $GATEWAY_URL/api/products & done
# Watch scaling happen using Azure CLI
az containerapp logs show --name product-service --resource-group $RG_NAME --follow
# Look for: Container Apps scaling eventsSuccess Criteria:
- ✅ Product Service always runs at least 2 replicas
- ✅ Under load, scales to more than 2 replicas
- ✅ Azure Portal shows new scaling rules
Time: 15-20 minutes
Goal: Create a custom Application Insights query to track product API performance
Steps:
-
Navigate to Application Insights in Azure Portal:
- Go to Azure Portal
- Find your resource group (rg-microservices-*)
- Click on Application Insights resource
-
Click "Logs" in the left menu
-
Create this query:
requests
| where timestamp > ago(1h)
| where name contains "products"
| summarize
RequestCount = count(),
AvgDuration = avg(duration),
P95Duration = percentile(duration, 95),
SuccessRate = 100.0 * countif(success == true) / count()
by bin(timestamp, 5m)
| render timechart-
Click "Run" to execute the query
-
Save the query:
- Click "Save"
- Name: "Product API Performance"
- Category: "Performance"
-
Generate test traffic:
for i in {1..100}; do curl $GATEWAY_URL/api/products; sleep 1; done- Refresh the query to see data
✅ Expected output:
- Chart showing request counts over time
- Average duration < 500ms
- Success rate = 100%
- Time bins of 5 minutes
Success Criteria:
- ✅ Query shows 100+ requests
- ✅ Success rate is 100%
- ✅ Average duration < 500ms
- ✅ Chart displays 5-minute time bins
Learning Outcome: Understand how to monitor service performance with custom queries
Time: 10-15 minutes
Goal: Add retry logic to API Gateway when Product Service is temporarily unavailable
Starting Point: src/api-gateway/app.js
Steps:
- Install retry library:
cd src/api-gateway
npm install axios-retry --save
cd ../..- Update
src/api-gateway/app.js(add after axios import):
const axiosRetry = require('axios-retry');
// Configure retry logic
axiosRetry(axios, {
retries: 3,
retryDelay: (retryCount) => {
return retryCount * 1000; // 1s, 2s, 3s
},
retryCondition: (error) => {
// Retry on network errors or 5xx responses
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
(error.response && error.response.status >= 500);
}
});
console.log('Retry logic configured: 3 retries with exponential backoff');- Redeploy API Gateway:
azd deploy api-gateway- Test retry behavior by simulating service failure:
# Scale product service to 0 (simulate failure)
az containerapp update \
--name $(azd env get-values | grep PRODUCT_SERVICE | head -1 | cut -d '/' -f5) \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--min-replicas 0 \
--max-replicas 0
# Try to access products (will retry 3 times)
time curl -v $GATEWAY_URL/api/products
# Observe: Response takes ~6 seconds (1s + 2s + 3s retries)
# Restore product service
az containerapp update \
--name $(azd env get-values | grep PRODUCT_SERVICE | head -1 | cut -d '/' -f5) \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--min-replicas 1 \
--max-replicas 10- View retry logs:
az containerapp logs show --name api-gateway --resource-group $RG_NAME --tail 50
# Look for: Retry attempt messages✅ Expected behavior:
- Requests retry 3 times before failing
- Each retry waits longer (1s, 2s, 3s)
- Successful requests after service restarts
- Logs show retry attempts
Success Criteria:
- ✅ Requests retry 3 times before failing
- ✅ Each retry waits longer (exponential backoff)
- ✅ Successful requests after service restarts
- ✅ Logs show retry attempts
Learning Outcome: Understand resilience patterns in microservices (circuit breakers, retries, timeouts)
Time: 20-25 minutes
After completing this example, verify your understanding:
Test your knowledge:
- Can you explain how API Gateway discovers the Product Service? (DNS-based service discovery)
- What happens if Product Service is down? (Gateway returns 503 error)
- How would you add a third service? (Create new Bicep file, add to main.bicep, create src folder)
Hands-On Verification:
# Simulate service failure
az containerapp update --name <product-service-name> --min-replicas 0 --max-replicas 0
curl $GATEWAY_URL/api/products
# ✅ Expected: 503 Service Unavailable
# Restore service
az containerapp update --name <product-service-name> --min-replicas 1 --max-replicas 10Test your knowledge:
- Where do you see distributed logs? (Application Insights in Azure Portal)
- How do you track slow requests? (Kusto query:
requests | where duration > 1000) - Can you identify which service caused an error? (Check
cloud_RoleNamefield in logs)
Hands-On Verification:
# Generate a slow request simulation
curl "$GATEWAY_URL/api/products?delay=2000"
# Query Application Insights for slow requests
# Navigate to Azure Portal → Application Insights → Logs
# Run: requests | where duration > 1000 | project timestamp, name, duration, cloud_RoleNameTest your knowledge:
- What triggers autoscaling? (HTTP concurrent request rules: 50 for gateway, 100 for product)
- How many replicas are running now? (Check with
az containerapp revision list) - How would you scale Product Service to 5 replicas? (Update minReplicas in Bicep)
Hands-On Verification:
# Generate load to test autoscaling
for i in {1..1000}; do curl $GATEWAY_URL/api/products & done
# Watch replicas increase using Azure CLI
az containerapp logs show --name api-gateway --resource-group $RG_NAME --follow
# ✅ Expected: See scaling events in logsSuccess Criteria: You can answer all questions and verify with hands-on commands.
| Resource | Configuration | Estimated Cost |
|---|---|---|
| API Gateway | 2-20 replicas, 1 vCPU, 2GB RAM | $30-150 |
| Product Service | 1-10 replicas, 0.5 vCPU, 1GB RAM | $15-75 |
| Container Registry | Basic tier | $5 |
| Application Insights | 1-2 GB/month | $5-10 |
| Log Analytics | 1 GB/month | $3 |
| Total | $58-243/month |
Light traffic (testing/learning): ~$60/month
- API Gateway: 2 replicas × 24/7 = $30
- Product Service: 1 replica × 24/7 = $15
- Monitoring + Registry = $13
Moderate traffic (small production): ~$120/month
- API Gateway: 5 avg replicas = $75
- Product Service: 3 avg replicas = $45
- Monitoring + Registry = $13
High traffic (busy periods): ~$240/month
- API Gateway: 15 avg replicas = $225
- Product Service: 8 avg replicas = $120
- Monitoring + Registry = $13
-
Scale to Zero for Development:
scale: { minReplicas: 0 // Save $30-40/month when not in use maxReplicas: 10 }
-
Use Consumption Plan for Cosmos DB (when you add it):
- Pay only for what you use
- No minimum charge
-
Set Application Insights Sampling:
appInsights.defaultClient.config.samplingPercentage = 50; // Sample 50% of requests
-
Clean Up When Not Needed:
azd down --force --purge
For learning/testing, consider:
- ✅ Use Azure free credits ($200 for first 30 days with new accounts)
- ✅ Keep to minimum replicas (saves ~50% costs)
- ✅ Delete after testing (no ongoing charges)
- ✅ Scale to zero between learning sessions
Example: Running this example for 2 hours/day × 30 days = ~$5/month instead of $60/month
Solution:
# Login again with explicit subscription
az account set --subscription <your-subscription-id>
azd env set AZURE_SUBSCRIPTION_ID <your-subscription-id>
azd upDiagnose:
# Check product service logs using Azure CLI
az containerapp logs show --name product-service --resource-group $RG_NAME --tail 50
# Check product service health
az containerapp show \
--name $(azd env get-values | grep PRODUCT_SERVICE | head -1 | cut -d '/' -f5) \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--query "properties.runningStatus"Common Causes:
- Product service didn't start (check logs for Python errors)
- Health check failing (verify
/healthendpoint works) - Container image build failed (check registry for image)
Diagnose:
# Check current replica count
az containerapp revision list \
--name $(azd env get-values | grep API_GATEWAY | head -1 | cut -d '/' -f5) \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--query "[].properties.replicas"
# Generate load to test
for i in {1..1000}; do curl $GATEWAY_URL/api/products & done
# Watch scaling events using Azure CLI
az containerapp logs show --name api-gateway --resource-group $RG_NAME --follow | grep -i scaleCommon Causes:
- Load not high enough to trigger scale rule (need >50 concurrent requests)
- Max replicas already reached (check Bicep configuration)
- Scale rule misconfigured in Bicep (verify concurrentRequests value)
Diagnose:
# Verify connection string is set
azd env get-values | grep APPLICATIONINSIGHTS
# Check if services are sending telemetry
az monitor app-insights component show \
--app $(azd env get-values | grep APPLICATIONINSIGHTS_NAME | cut -d '=' -f2 | tr -d '"') \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--query "properties.InstrumentationKey"Common Causes:
- Connection string not passed to container (check environment variables)
- Application Insights SDK not configured (verify imports in code)
- Firewall blocking telemetry (rare, check network rules)
Diagnose:
# Test API Gateway build
cd src/api-gateway
docker build -t test-gateway .
# Test Product Service build
cd ../product-service
docker build -t test-product .Common Causes:
- Missing dependencies in package.json/requirements.txt
- Dockerfile syntax errors
- Network issues downloading dependencies
Still Stuck? See Common Issues Guide or Azure Container Apps Troubleshooting
To avoid ongoing charges, delete all resources:
azd down --force --purgeConfirmation Prompt:
? Total resources to delete: 6, are you sure you want to continue? (y/N)
Type y to confirm.
What Gets Deleted:
- Container Apps Environment
- Both Container Apps (gateway & product service)
- Container Registry
- Application Insights
- Log Analytics Workspace
- Resource Group
✓ Verify Cleanup:
az group list --query "[?starts_with(name,'rg-microservices')]" --output tableShould return empty.
Once you've mastered this 2-service architecture, here's how to expand:
Add Cosmos DB for Product Service:
-
Create
infra/core/cosmos.bicep:resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { name: name location: location kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' consistencyPolicy: { defaultConsistencyLevel: 'Session' } locations: [{ locationName: location, failoverPriority: 0 }] } }
-
Update product service to use Azure Cosmos DB Python SDK instead of in-memory data
-
Estimated additional cost: ~$25/month (serverless)
Create Order Service:
- New folder:
src/order-service/(Python/Node.js/C#) - New Bicep:
infra/app/order-service.bicep - Update API Gateway to route
/api/orders - Add Azure SQL Database for order persistence
Architecture becomes:
API Gateway → Product Service (Cosmos DB)
→ Order Service (Azure SQL)
Implement Event-Driven Architecture:
- Add Azure Service Bus:
infra/core/servicebus.bicep - Product Service publishes "ProductCreated" events
- Order Service subscribes to product events
- Add Notification Service to process events
Pattern: Request/Response (HTTP) + Event-Driven (Service Bus)
Implement User Service:
- Create
src/user-service/(Go/Node.js) - Add Azure AD B2C or custom JWT authentication
- API Gateway validates tokens before routing
- Services check user permissions
Add These Components:
- ✅ Azure Front Door (global load balancing)
- ✅ Azure Key Vault (secret management)
- ✅ Azure Monitor Workbooks (custom dashboards)
- ✅ CI/CD Pipeline (GitHub Actions)
- ✅ Blue-Green Deployments
- ✅ Managed Identity for all services
Full Production Architecture Cost: ~$300-1,400/month
- Azure Container Apps Documentation
- Microservices Architecture Guide
- Application Insights for Distributed Tracing
- Azure Developer CLI Documentation
- ← Previous: Simple Flask API - Beginner single-container example
- → Next: AI Integration Guide - Add AI capabilities
- 🏠 Course Home
| Feature | Single Container | Microservices (This) | Kubernetes (AKS) |
|---|---|---|---|
| Use Case | Simple apps | Complex apps | Enterprise apps |
| Scalability | Single service | Per-service scaling | Maximum flexibility |
| Complexity | Low | Medium | High |
| Team Size | 1-3 developers | 3-10 developers | 10+ developers |
| Cost | ~$15-50/month | ~$60-250/month | ~$150-500/month |
| Deployment Time | 5-10 minutes | 8-12 minutes | 15-30 minutes |
| Best For | MVPs, prototypes | Production apps | Multi-cloud, advanced networking |
Recommendation: Start with Container Apps (this example), move to AKS only if you need Kubernetes-specific features.
Q: Why only 2 services instead of 5+?
A: Educational progression. Master the fundamentals (service communication, monitoring, scaling) with a simple example before adding complexity. The patterns you learn here apply to 100-service architectures.
Q: Can I add more services myself?
A: Absolutely! Follow the expansion guide above. Each new service follows the same pattern: create src folder, create Bicep file, update azure.yaml, deploy.
Q: Is this production-ready?
A: It's a solid foundation. For production, add: managed identity, Key Vault, persistent databases, CI/CD pipeline, monitoring alerts, and backup strategy.
Q: Why not use Dapr or other service mesh?
A: Keep it simple for learning. Once you understand native Container Apps networking, you can layer on Dapr for advanced scenarios (state management, pub/sub, bindings).
Q: How do I debug locally?
A: Run services locally with Docker:
cd src/api-gateway
docker build -t local-gateway .
docker run -p 8080:8080 -e PRODUCT_SERVICE_URL=http://localhost:8000 local-gatewayQ: Can I use different programming languages?
A: Yes! This example shows Node.js (gateway) + Python (product service). You can mix any languages that run in containers: C#, Go, Java, Ruby, PHP, etc.
Q: What if I don't have Azure credits?
A: Use Azure free tier (first 30 days with new accounts get $200 credits) or deploy for short testing periods and delete immediately. This example costs ~$2/day.
Q: How is this different from Azure Kubernetes Service (AKS)?
A: Container Apps is simpler (no Kubernetes knowledge needed) but less flexible. AKS gives you full Kubernetes control but requires more expertise. Start with Container Apps, graduate to AKS if needed.
Q: Can I use this with existing Azure services?
A: Yes! You can connect to existing databases, storage accounts, Service Bus, etc. Update Bicep files to reference existing resources instead of creating new ones.
🎓 Learning Path Summary: You've learned to deploy a multi-service architecture with automatic scaling, internal networking, centralized monitoring, and production-ready patterns. This foundation prepares you for complex distributed systems and enterprise microservices architectures.
📚 Course Navigation:
- ← Previous: Simple Flask API
- → Next: Database Integration Example
- 🏠 Course Home
- 📖 Container Apps Best Practices
✨ Congratulations! You've completed the microservices example. You now understand how to build, deploy, and monitor distributed applications on Azure Container Apps. Ready to add AI capabilities? Check out the AI Integration Guide!