Demo application: Carved Rock Fitness (.NET 8 Blazor)
Certification: Azure Developer Associate (AZ-204)
Table of Contents
- Azure App Service — Deployment
- Azure App Service — Scaling
- Web App Configuration
- Deployment Slots
- Container Images
- Azure Container Instances (ACI)
- Azure Functions
- Azure Container Apps
1. Azure App Service — Deployment
1.1 App Service Plans and Tiers
┌─────────────────────────────────────────────────────────────────────┐
│ AZURE APP SERVICE TIERS │
│ │
│ Tier Price CPU/RAM Key Features │
│ ───────────────────────────────────────────────────────────────── │
│ Free (F1) Free Shared 60 min/day, no custom domain │
│ Shared (D1) ~$ Shared 240 min/day │
│ Basic (B1-B3) $$ Dedicated Custom domains, TLS │
│ Standard (S1-3) $$$ Dedicated Auto-scale, slots (5) │
│ Premium (P0-3) $$$$ Dedicated Autoscale, slots (20), VNET │
│ Isolated (I1-3) $$$$$ Dedicated/ASE VNET isolation, Private │
│ │
│ OS: Windows or Linux │
│ Auto Scaling: Standard and above (Rules-Based) │
│ Premium Automatic Scaling: Automatic (request-based) │
└─────────────────────────────────────────────────────────────────────┘
1.2 Deployment via az webapp up
# Install Azure CLI and sign in
az login
# Deploy the application directly from the source directory
az webapp up \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--plan "plan-carved-rock" \
--sku B1 \
--location "eastus" \
--runtime "DOTNET:8.0"
# Re-deploy (same command → updates if it exists)
az webapp up --name "carved-rock-fitness-app"
# Stream live logs
az webapp log tail --name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock"
1.3 Continuous Deployment
GitHub Actions — automatic deployment:
# .github/workflows/azure-deploy.yml
name: Deploy to Azure App Service
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Build and publish
run: |
dotnet restore
dotnet build --configuration Release
dotnet publish -c Release -o publish
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: "carved-rock-fitness-app"
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ./publish
Container deployment to App Service:
# Deploy a container from ACR
az webapp create \
--name "carved-rock-fitness-container" \
--resource-group "rg-carved-rock" \
--plan "plan-carved-rock" \
--deployment-container-image-name "carvedrockcr.azurecr.io/app:latest"
# Configure the ACR continuous deployment Webhook
az acr webhook create \
--name "WebAppWebhook" \
--registry "carvedrockcr" \
--uri "$(az webapp deployment container config --enable-cd true -n carved-rock-fitness-container -g rg-carved-rock --query 'CI_CD_URL' -o tsv)" \
--actions push
2. Azure App Service — Scaling
2.1 Scale up vs Scale out
| Scale Up | Scale Out | |
|---|---|---|
| Definition | Increase resources of one instance | Add more instances |
| Method | Change tier | Manual / Autoscale |
| Example | B1 → B2 → P1 | 1 → 3 instances |
| Limit | Maximum per tier | Depends on plan |
# Scale up — change the plan tier
az appservice plan update \
--name "plan-carved-rock" \
--resource-group "rg-carved-rock" \
--sku P1v3
# Manual scale out — set number of instances
az appservice plan update \
--name "plan-carved-rock" \
--number-of-workers 3
2.2 Azure Autoscale
# Create a CPU-based autoscale rule
az monitor autoscale create \
--resource-group "rg-carved-rock" \
--resource "plan-carved-rock" \
--resource-type "Microsoft.Web/serverfarms" \
--name "autoscale-carved-rock" \
--min-count 1 \
--max-count 5 \
--count 1
# Add a scale-out rule (CPU > 70% → +1 instance)
az monitor autoscale rule create \
--resource-group "rg-carved-rock" \
--autoscale-name "autoscale-carved-rock" \
--condition "CpuPercentage > 70 avg 10m" \
--scale out 1
# Add a scale-in rule (CPU < 25% → -1 instance)
az monitor autoscale rule create \
--resource-group "rg-carved-rock" \
--autoscale-name "autoscale-carved-rock" \
--condition "CpuPercentage < 25 avg 10m" \
--scale in 1
Automatic scaling (Premium V2/V3):
# Automatic mode — based on HTTP requests (simpler to configure)
az webapp update \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--set autoHealEnabled=true
3. Web App Configuration
3.1 App Settings and Connection Strings
# Create/update App Settings
az webapp config appsettings set \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--settings \
"ASPNETCORE_ENVIRONMENT=Production" \
"FeatureFlags__EnableNewCheckout=true" \
"ExternalApi__BaseUrl=https://api.example.com"
# Read App Settings
az webapp config appsettings list \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock"
# Connection Strings (values encrypted in the portal)
az webapp config connection-string set \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--connection-string-type SQLAzure \
--settings \
"DefaultConnection=Server=sql.database.windows.net;Database=CarvedRock;..."
In .NET code:
// App Settings → Configuration API (no distinction from appsettings.json)
var featureEnabled = builder.Configuration["FeatureFlags:EnableNewCheckout"];
var apiUrl = builder.Configuration["ExternalApi:BaseUrl"];
// Connection Strings
var connString = builder.Configuration.GetConnectionString("DefaultConnection");
⚠️ Important: App Settings defined in Azure override values in
appsettings.json. Ideal for environment-specific overrides.
3.2 Diagnostic Logging
# Windows — Enable application logs
az webapp log config \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--application-logging filesystem \
--level information \
--web-server-logging filesystem \
--detailed-error-messages true
# Linux — Stdout/stderr only
az webapp log config \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--docker-container-logging filesystem
# Download logs
az webapp log download \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--log-file logs.zip
Available log types:
| Type | OS | Description |
|---|---|---|
| Application Logging | Windows + Linux | App logs (ILogger) |
| Web Server Logging | Windows | IIS logs (raw HTTP requests) |
| Detailed Error Messages | Windows | Detailed HTTP error pages |
| Deployment Logging | Windows + Linux | Deployment logs |
| Failed Request Tracing | Windows | FREB traces for failed requests |
3.3 TLS/SSL and Custom Domains
# 1. Add a custom domain
az webapp config hostname add \
--webapp-name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--hostname "app.carvedrock.com"
# 2. DNS verification (before adding):
# → Add a CNAME record: app → carved-rock-fitness-app.azurewebsites.net
# → Or TXT record for verification
# 3. App Service Managed Certificate (free)
az webapp config ssl create \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--hostname "app.carvedrock.com"
# 4. Bind the certificate
az webapp config ssl bind \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--certificate-thumbprint "THUMBPRINT" \
--ssl-type SNI
# Force HTTPS
az webapp update \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--https-only true
3.4 Easy Auth
Easy Auth = built-in App Service authentication (without modifying code).
# Configure Easy Auth with Microsoft Entra ID
az webapp auth microsoft update \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--client-id "CLIENT_ID" \
--client-secret "CLIENT_SECRET" \
--issuer "https://login.microsoftonline.com/TENANT_ID/v2.0"
# Unauthenticated action: Redirect (redirects to login) or AllowAnonymous
az webapp auth update \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--unauthenticated-client-action RedirectToLoginPage
Supported providers: Microsoft Entra ID, Google, Facebook, GitHub, Apple, Generic OpenID Connect.
Headers injected by Easy Auth:
| Header | Value |
|---|---|
X-MS-CLIENT-PRINCIPAL-ID | Subject/User ID |
X-MS-CLIENT-PRINCIPAL-NAME | User name |
X-MS-CLIENT-PRINCIPAL-IDP | Provider (aad, google, etc.) |
X-MS-TOKEN-AAD-ACCESS-TOKEN | Access Token (if SaveTokens = true) |
3.5 Networking
# Access Restrictions — limit allowed IPs
az webapp config access-restriction add \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--action Allow \
--ip-address "203.0.113.0/24" \
--priority 100 \
--rule-name "AllowOffice"
# Service Endpoint — access from a specific VNet subnet
az webapp config access-restriction add \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--action Allow \
--vnet-name "vnet-prod" \
--subnet "snet-frontend" \
--priority 200
# VNet Integration — allow the app to access resources inside the VNet
az webapp vnet-integration add \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--vnet "vnet-prod" \
--subnet "snet-outbound"
4. Deployment Slots
4.1 Slot Management
# Create a staging slot
az webapp deployment slot create \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "staging"
# List slots
az webapp deployment slot list \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock"
# Deploy to the staging slot
az webapp deployment source config-zip \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "staging" \
--src "./publish.zip"
Sticky vs non-sticky settings:
| Type | Behavior | Usage |
|---|---|---|
| Slot-specific (sticky) | Stays in the slot during swap | DB connection strings, per-env feature flags |
| Shared (non-sticky) | Follows the deployment during swap | App configuration |
# Mark a setting as sticky (slot-specific)
az webapp config appsettings set \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "staging" \
--slot-settings "ASPNETCORE_ENVIRONMENT=Staging"
4.2 Slot Swaps
# Manual swap (staging → production)
az webapp deployment slot swap \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "staging" \
--target-slot "production"
# Swap with preview (multi-phase swap)
# Phase 1: Apply production config to staging slot (test first)
az webapp deployment slot swap \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "staging" \
--action preview
# Phase 2: Complete the swap after validation
az webapp deployment slot swap \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "staging" \
--action swap
# Rollback — swap again (production ↔ staging)
az webapp deployment slot swap \
--name "carved-rock-fitness-app" \
--resource-group "rg-carved-rock" \
--slot "production" \
--target-slot "staging"
Advantages:
- Zero-downtime deployment
- Application warm-up before the swap
- Instant rollback
5. Container Images
5.1 Docker and Dockerfile
# Dockerfile — Multi-stage build for .NET 8
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy and restore dependencies (layer cache)
COPY ["CarvedRock.Api/CarvedRock.Api.csproj", "CarvedRock.Api/"]
RUN dotnet restore "CarvedRock.Api/CarvedRock.Api.csproj"
# Copy everything and build
COPY . .
WORKDIR "/src/CarvedRock.Api"
RUN dotnet build -c Release -o /app/build
# Stage 2: Publish
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
# Stage 3: Runtime (lightweight final image)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
# Security: do not run as root
USER app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "CarvedRock.Api.dll"]
# Build and test locally
docker build -t carved-rock-api:latest .
docker run -p 8080:8080 carved-rock-api:latest
# Push to Docker Hub
docker tag carved-rock-api:latest username/carved-rock-api:latest
docker push username/carved-rock-api:latest
5.2 Azure Container Registry (ACR)
# Create an ACR registry
az acr create \
--name "carvedrockcr" \
--resource-group "rg-carved-rock" \
--sku Basic # Basic / Standard / Premium
# Build and push via ACR Tasks (no local Docker required)
az acr build \
--registry "carvedrockcr" \
--image "carved-rock-api:latest" \
--file "./Dockerfile" \
.
# Import an image from Docker Hub (avoids man-in-the-middle attacks)
az acr import \
--name "carvedrockcr" \
--source "mcr.microsoft.com/dotnet/aspnet:8.0" \
--image "aspnet:8.0"
# List images
az acr repository list --name "carvedrockcr" -o table
# List tags for an image
az acr repository show-tags \
--name "carvedrockcr" \
--repository "carved-rock-api"
ACR Tiers:
| Tier | Storage | Webhooks | Geo-replication | Private Link |
|---|---|---|---|---|
| Basic | 10 GB | ✅ | ❌ | ❌ |
| Standard | 100 GB | ✅ | ❌ | ❌ |
| Premium | 500 GB | ✅ | ✅ | ✅ |
ACR registry structure:
carvedrockcr.azurecr.io/
├── namespace/ (optional for organization)
│ └── repository:tag
│ └── layers (shared layers)
│ └── manifest (layer list)
│ └── digest (unique image hash)
└── carved-rock-api:1.0.0
6. Azure Container Instances (ACI)
# Deploy a simple container
az container create \
--name "carved-rock-container" \
--resource-group "rg-carved-rock" \
--image "carvedrockcr.azurecr.io/carved-rock-api:latest" \
--registry-login-server "carvedrockcr.azurecr.io" \
--registry-username "carvedrockcr" \
--registry-password "$(az acr credential show -n carvedrockcr --query 'passwords[0].value' -o tsv)" \
--dns-name-label "carved-rock-api" \
--ports 80 443 \
--environment-variables \
"ASPNETCORE_ENVIRONMENT=Production" \
--secure-environment-variables \
"ConnectionStrings__Default=Server=..." # Encrypted value!
# Add persistent storage (Azure Files)
az container create \
--name "carved-rock-container" \
--resource-group "rg-carved-rock" \
--image "carvedrockcr.azurecr.io/carved-rock-api:latest" \
--azure-file-volume-account-name "carvedrockstorage" \
--azure-file-volume-account-key "STORAGE_KEY" \
--azure-file-volume-share-name "app-data" \
--azure-file-volume-mount-path "/app/data"
# Show logs
az container logs \
--name "carved-rock-container" \
--resource-group "rg-carved-rock"
# Delete
az container delete \
--name "carved-rock-container" \
--resource-group "rg-carved-rock" \
--yes
ACI vs App Service vs Container Apps:
| ACI | App Service (Container) | Container Apps | |
|---|---|---|---|
| Complexity | Low | Low | Medium |
| Scaling | Manual | Auto | Auto (KEDA) |
| Orchestration | No | No | Yes (on AKS) |
| Lifetime | Ephemeral | Long-running | Long-running |
| Typical use | Jobs, tests | Web apps | Microservices |
7. Azure Functions
7.1 Trigger and Binding Types
Azure Functions vs alternatives:
──────────────────────────────────
Functions → Serverless, event-driven, code only
Logic Apps → Low-code, connectors, visual workflows
WebJobs → Background tasks in App Service (predecessor)
Available Triggers:
| Trigger | Fired By | Usage |
|---|---|---|
| HTTP | HTTP request | Serverless APIs |
| Timer | Cron schedule | Scheduled tasks |
| Queue Storage | Azure Queue message | Async processing |
| Service Bus | Azure Service Bus message | Enterprise messaging |
| Blob Storage | New blob | File processing |
| Cosmos DB | Modified document | Change feed |
| Event Hub | Incoming event | IoT, streaming |
// Azure Function — HTTP Trigger
[FunctionName("GetProducts")]
public async Task<IActionResult> GetProducts(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "products")]
HttpRequest req,
[CosmosDB("CarvedRock", "Products", Connection = "CosmosDBConnection")]
IEnumerable<Product> products, // Input binding
ILogger log)
{
log.LogInformation("Getting all products");
return new OkObjectResult(products);
}
// Timer Trigger — run every day at 6:00 UTC
[FunctionName("DailyCleanup")]
public async Task DailyCleanup(
[TimerTrigger("0 0 6 * * *")] TimerInfo timer,
ILogger log)
{
log.LogInformation("Running daily cleanup at {Time}", DateTime.UtcNow);
// Cleanup logic...
}
// Queue Trigger + Output Binding
[FunctionName("ProcessOrder")]
[return: Queue("order-confirmation")] // Output binding
public static async Task<string> ProcessOrder(
[QueueTrigger("new-orders")] OrderMessage order,
ILogger log)
{
log.LogInformation("Processing order: {OrderId}", order.OrderId);
// Processing logic...
return $"Order {order.OrderId} processed successfully";
}
# Deploy an Azure Function
# 1. Create the infrastructure
az functionapp create \
--name "carved-rock-functions" \
--resource-group "rg-carved-rock" \
--storage-account "carvedrockstorage" \
--consumption-plan-location "eastus" \
--runtime dotnet-isolated \
--runtime-version 8 \
--functions-version 4
# 2. Deploy the code
dotnet publish -c Release -o publish
Compress-Archive -Path .\publish\* -DestinationPath .\publish.zip -Force
az functionapp deployment source config-zip \
--name "carved-rock-functions" \
--resource-group "rg-carved-rock" \
--src "./publish.zip"
7.2 Durable Functions
Durable Functions = extension for stateful workflows in Azure Functions.
4 function types:
| Type | Role | Description |
|---|---|---|
| Client | Trigger | Starts the orchestration |
| Orchestrator | Workflow | Coordinates activities |
| Activity | Real work | Individual tasks, can do I/O |
| Entity | Shared state | Objects with durable state |
// Client — starts the orchestration
[FunctionName("StartOrderProcessing")]
public static async Task<HttpResponseMessage> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestMessage req,
[DurableClient] IDurableOrchestrationClient starter,
ILogger log)
{
var order = await req.Content.ReadAsAsync<Order>();
string instanceId = await starter.StartNewAsync("OrderProcessingOrchestrator", order);
return starter.CreateCheckStatusResponse(req, instanceId);
}
// Orchestrator — workflow
[FunctionName("OrderProcessingOrchestrator")]
public static async Task<OrderResult> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<Order>();
// Pattern: Chaining (sequential)
await context.CallActivityAsync("ValidateOrder", order);
await context.CallActivityAsync("ChargePayment", order);
await context.CallActivityAsync("ShipOrder", order);
// Pattern: Fan-out/Fan-in (parallel)
var tasks = order.Items.Select(item =>
context.CallActivityAsync<bool>("CheckInventory", item));
var results = await Task.WhenAll(tasks);
// Pattern: Human interaction (wait for approval)
var approved = await context.WaitForExternalEvent<bool>("ApprovalReceived",
TimeSpan.FromDays(7)); // 7-day timeout
return new OrderResult { OrderId = order.Id, Approved = approved };
}
// Activity — individual task
[FunctionName("ShipOrder")]
public static async Task ShipOrder(
[ActivityTrigger] Order order,
ILogger log)
{
log.LogInformation("Shipping order {OrderId}", order.Id);
// Shipping API call, confirmation email, etc.
}
Durable Functions Patterns:
| Pattern | Description | Usage |
|---|---|---|
| Function Chaining | Sequential steps | Order workflow |
| Fan-out/Fan-in | Parallel + aggregation | Multi-warehouse inventory check |
| Async HTTP API | Long-running + status polling | Image processing, report generation |
| Monitor | Surveillance loop | Periodically check external status |
| Human Interaction | Wait for external event | Manager approval |
8. Azure Container Apps
8.1 Deployment and Revisions
# Deploy from source code (automatic build)
az containerapp up \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--location "eastus" \
--ingress external \
--target-port 8080 \
--source .
# Deploy from an existing image
az containerapp create \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--environment "env-carved-rock" \
--image "carvedrockcr.azurecr.io/carved-rock-api:latest" \
--registry-server "carvedrockcr.azurecr.io" \
--ingress external \
--target-port 8080 \
--min-replicas 1 \
--max-replicas 10
# List revisions
az containerapp revision list \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
-o table
Revisions = immutable snapshots of a Container App
# Application-scoped vs revision-scoped changes:
# - Application-scoped: env var config changes, secrets → Same revision
# - Revision-scoped: image changes, containers → New revision
# Split traffic between revisions (canary deployment)
az containerapp ingress traffic set \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--revision-weight \
"carved-rock-api--v1=80" \
"carved-rock-api--v2=20"
Secrets in Container Apps:
# Create a secret
az containerapp secret set \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--secrets "db-connection=Server=sql.database.windows.net;..."
# Reference the secret in env vars
az containerapp update \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--set-env-vars "ConnectionStrings__Default=secretref:db-connection"
8.2 DAPR
DAPR (Distributed Application Runtime) = infrastructure sidecar for microservices.
┌────────────────────────────────────────────────────────────────┐
│ DAPR ARCHITECTURE │
│ │
│ App Container DAPR Sidecar │
│ ┌───────────┐ ┌──────────────────────────────────┐ │
│ │ │◄────────►│ Building Blocks │ │
│ │ My API │ HTTP │ - Service Invocation │ │
│ │ (8080) │ /gRPC │ - State Management │ │
│ │ │ │ - Pub/Sub │ │
│ └───────────┘ │ - Bindings │ │
│ │ - Secrets │ │
│ │ - Actors │ │
│ └──────────────────────────────────┘ │
│ │
│ Communication: HTTP localhost:3500 or gRPC localhost:50001 │
└────────────────────────────────────────────────────────────────┘
# Enable DAPR on a Container App
az containerapp update \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--dapr-app-id "carved-rock-api" \
--dapr-app-port 8080 \
--enable-dapr
# Configure a DAPR pub/sub component (Azure Service Bus)
# component.yaml
# dapr-pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.azure.servicebus
version: v1
metadata:
- name: connectionString
secretKeyRef:
name: servicebus-secret
key: connectionString
scopes:
- carved-rock-api
- carved-rock-worker
// Using DAPR in code
// Service call (Service Invocation)
var response = await daprClient.InvokeMethodAsync<Order>(
"order-service",
"create-order",
new Order { /* ... */ });
// Pub/Sub
await daprClient.PublishEventAsync(
"pubsub", // Component name
"orders", // Topic name
new OrderCreated { OrderId = orderId });
// State Management
await daprClient.SaveStateAsync("statestore", $"order-{orderId}", order);
var savedOrder = await daprClient.GetStateAsync<Order>("statestore", $"order-{orderId}");
Summary of Essential Azure CLI Commands
# Resource Group
az group create --name "rg-carved-rock" --location "eastus"
# App Service
az webapp up --name "app" --resource-group "rg" --sku B1
az webapp config appsettings set --name "app" --resource-group "rg" --settings "KEY=VALUE"
az webapp deployment slot swap --name "app" --resource-group "rg" --slot "staging"
# ACR
az acr create --name "registry" --resource-group "rg" --sku Basic
az acr build --registry "registry" --image "myapp:latest" .
# ACI
az container create --name "container" --resource-group "rg" --image "myapp:latest"
# Functions
az functionapp create --name "func" --resource-group "rg" --runtime dotnet-isolated
# Container Apps
az containerapp up --name "app" --resource-group "rg" --source .
az containerapp revision list --name "app" --resource-group "rg"
9. Complete Reference — App Service and Containers
9.1 App Service Tiers — Detailed Table
| Tier | OS | Custom Domain | SSL | Scale Out | Slots | Autoscale | Private Network | Estimated Price |
|---|---|---|---|---|---|---|---|---|
| Free (F1) | Win/Linux | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | Free |
| Shared (D1) | Win | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ~€9/month |
| Basic (B1) | Win/Linux | ✅ | ✅ | Manual | ❌ | ❌ | ❌ | ~€47/month |
| Standard (S1) | Win/Linux | ✅ | ✅ | Manual | 5 slots | ❌ | ❌ | ~€63/month |
| Premium V3 (P1V3) | Win/Linux | ✅ | ✅ | Auto | 20 slots | ✅ | ✅ | ~€140/month |
| Isolated V2 (I1V2) | Win/Linux | ✅ | ✅ | Auto | 20 slots | ✅ | Dedicated | ~€400/month |
Important for the exam: Web Server Logging (raw HTTP logs) is available on Windows only. Slots are not available on Basic — they require at least Standard.
9.2 Comparison of Azure Container Services
flowchart TD
NEED[Container requirement] --> SIMPLE{Simple?\nShort-lived task?}
SIMPLE -->|Yes| ACI[Azure Container Instances\nSimple, ephemeral, no infra]
SIMPLE -->|No| MICRO{Microservices?}
MICRO -->|Yes, no K8s complexity| ACA[Azure Container Apps\nManaged PaaS K8s, DAPR, scaling]
MICRO -->|Yes, with K8s control| AKS[Azure Kubernetes Service\nFull K8s control, enterprise]
MICRO -->|Simple web app| APP_SVC[Azure App Service + Container\nWeb platform, slots, Easy Auth]
| Service | Use Case | Infra to Manage | Autoscale | Price |
|---|---|---|---|---|
| App Service (Container) | Web apps, APIs | None | ✅ | Per plan |
| Azure Container Instances | Short tasks, tests, jobs | None | ❌ | Per second |
| Azure Container Apps | Microservices, event-driven | None | ✅ KEDA | Per vCPU/request |
| Azure Kubernetes Service | Complex workloads | Nodes | ✅ HPA | Per node |
| Azure Functions (Container) | Event-driven, serverless | None | ✅ | Per execution |
9.3 Azure Container Registry — Architecture
flowchart TD
DEV[Developer] -->|docker build| IMAGE[Local image]
IMAGE -->|docker push OR az acr build| ACR[Azure Container Registry]
subgraph "ACR Structure"
ACR --> NS[Namespace\norg/team/]
NS --> REPO[Repository\napp-name]
REPO --> TAG1["Tag v1.0.0\n(points to manifest 1)"]
REPO --> TAG2["Tag latest\n(points to manifest 2)"]
REPO --> MANIFEST[Manifest\nSHA256 Digest]
MANIFEST --> LAYER1[OS Layer]
MANIFEST --> LAYER2[App Layer]
MANIFEST --> LAYER3[Runtime Layer]
end
ACR -->|Pull image| ACA[Container Apps]
ACR -->|Pull image| ACI[Container Instances]
ACR -->|Pull image| AKS[AKS]
ACR -->|Pull image| APP_SVC[App Service]
9.4 Dockerfile Best Practices
# ✅ Optimized Dockerfile — Multi-stage build for .NET 8
# ─── Stage 1: Build ──────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy only project files first (better Docker cache utilization)
COPY ["src/Api/Api.csproj", "src/Api/"]
COPY ["src/Domain/Domain.csproj", "src/Domain/"]
RUN dotnet restore "src/Api/Api.csproj"
# Copy everything else and build
COPY . .
WORKDIR "/src/src/Api"
RUN dotnet build "Api.csproj" -c Release -o /app/build --no-restore
# ─── Stage 2: Publish ────────────────────────────────────────────────────
FROM build AS publish
RUN dotnet publish "Api.csproj" -c Release -o /app/publish \
/p:UseAppHost=false --no-restore
# ─── Stage 3: Runtime (minimal final image) ──────────────────────────────
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
# Security: do not run as root
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup
USER appuser
WORKDIR /app
COPY --from=publish /app/publish .
# Document the port (not required but good practice)
EXPOSE 8080
ENTRYPOINT ["dotnet", "Api.dll"]
9.5 Azure Functions — All Triggers and Bindings
flowchart LR
subgraph "Triggers"
HTTP_T[HTTP Trigger\nREST API / Webhook]
TIMER_T[Timer Trigger\nCron schedule]
QUEUE_T[Queue Trigger\nAzure Storage Queue]
BLOB_T[Blob Trigger\nNew blob]
SB_T[Service Bus Trigger\nMessage queue/topic]
EH_T[Event Hub Trigger\nEvent stream]
EG_T[Event Grid Trigger\nAzure events]
COSMOS_T[Cosmos DB Trigger\nChange feed]
end
subgraph "Input Bindings (read)"
BLOB_I[Blob Input\nRead a blob]
TABLE_I[Table Input\nRead a table]
COSMOS_I[Cosmos DB Input\nRead a document]
SQL_I[SQL Input\nSQL query]
end
subgraph "Output Bindings (write)"
BLOB_O[Blob Output\nWrite a blob]
QUEUE_O[Queue Output\nSend a message]
SB_O[Service Bus Output\nPublish a message]
COSMOS_O[Cosmos DB Output\nWrite a document]
HTTP_O[HTTP Response\nHTTP response]
EG_O[Event Grid Output\nPublish an event]
SIGNALR_O[SignalR Output\nSend a notification]
end
Complete binding examples:
// HTTP Trigger → Cosmos DB Input → Queue Output
[FunctionName("GetAndQueueOrder")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "orders/{id}")] HttpRequest req,
string id,
[CosmosDB(
databaseName: "orders-db",
containerName: "orders",
Connection = "CosmosDBConnection",
Id = "{id}",
PartitionKey = "{id}")] Order order,
[Queue("processing-queue", Connection = "StorageConnection")] IAsyncCollector<string> queueMessages,
ILogger log)
{
if (order == null) return new NotFoundResult();
await queueMessages.AddAsync($"Process order {id}");
return new OkObjectResult(order);
}
// Blob Trigger → Process → Output to multiple targets
[FunctionName("ProcessImage")]
public static async Task ProcessImage(
[BlobTrigger("uploads/{name}", Connection = "StorageConnection")] Stream imageBlob,
string name,
[Blob("processed/{name}", FileAccess.Write, Connection = "StorageConnection")] Stream outputBlob,
[CosmosDB("images-db", "metadata", Connection = "CosmosDBConnection")] IAsyncCollector<ImageMetadata> cosmosOutput,
ILogger log)
{
log.LogInformation($"Processing image: {name}");
// Image processing
await ProcessAndResizeImage(imageBlob, outputBlob);
// Save metadata
await cosmosOutput.AddAsync(new ImageMetadata
{
Name = name,
ProcessedAt = DateTime.UtcNow
});
}
9.6 Durable Functions — Detailed Patterns
// Pattern 1: Function Chaining (sequential)
[FunctionName("OrderFulfillmentOrchestrator")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var orderId = context.GetInput<string>();
// Sequential steps — stateful
var inventoryResult = await context.CallActivityAsync<bool>("CheckInventory", orderId);
if (!inventoryResult) return "Out of stock";
var paymentResult = await context.CallActivityAsync<string>("ProcessPayment", orderId);
var shipmentResult = await context.CallActivityAsync<string>("CreateShipment", orderId);
return $"Order {orderId} fulfilled. Shipment: {shipmentResult}";
}
// Pattern 2: Fan-out / Fan-in (parallel)
[FunctionName("ProcessBatchOrchestrator")]
public static async Task<List<string>> RunBatch(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var items = context.GetInput<List<string>>();
// Launch all tasks in parallel
var tasks = items.Select(item => context.CallActivityAsync<string>("ProcessItem", item));
var results = await Task.WhenAll(tasks);
return results.ToList();
}
// Pattern 3: Human Interaction (external approval)
[FunctionName("ApprovalOrchestrator")]
public static async Task<bool> WaitForApproval(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var request = context.GetInput<ApprovalRequest>();
// Send a notification
await context.CallActivityAsync("SendApprovalEmail", request);
// Wait for approval (up to 7 days)
using var timeoutCts = new CancellationTokenSource();
var approvalTask = context.WaitForExternalEvent<bool>("Approval");
var timeoutTask = context.CreateTimer(
context.CurrentUtcDateTime.AddDays(7),
timeoutCts.Token);
var winnerTask = await Task.WhenAny(approvalTask, timeoutTask);
if (winnerTask == approvalTask)
{
timeoutCts.Cancel();
return approvalTask.Result; // true = approved, false = rejected
}
// Timeout expired
return false;
}
// Client Function (start)
[FunctionName("StartApproval")]
public static async Task<IActionResult> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestMessage req,
[DurableClient] IDurableOrchestrationClient starter,
ILogger log)
{
var request = await req.Content.ReadAsAsync<ApprovalRequest>();
string instanceId = await starter.StartNewAsync("ApprovalOrchestrator", request);
// Return URLs to check status and approve
return starter.CreateCheckStatusResponse(req, instanceId);
}
// External Event (approval)
[FunctionName("SendApproval")]
public static async Task<IActionResult> HttpApprove(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "approve/{instanceId}")] HttpRequestMessage req,
[DurableClient] IDurableOrchestrationClient client,
string instanceId)
{
await client.RaiseEventAsync(instanceId, "Approval", true);
return new OkObjectResult("Approved!");
}
9.7 Azure Container Apps — Scaling with KEDA
# Scale rules in Azure Container Apps
# HTTP-based scaling (default)
az containerapp update \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--min-replicas 0 \
--max-replicas 10 \
--scale-rule-name "http-rule" \
--scale-rule-http-concurrency 50
# KEDA - Scale based on Azure Service Bus Queue
az containerapp update \
--name "carved-rock-worker" \
--resource-group "rg-carved-rock" \
--min-replicas 0 \
--max-replicas 20 \
--scale-rule-name "queue-rule" \
--scale-rule-type "azure-servicebus" \
--scale-rule-metadata "queueName=orders" "messageCount=5" \
--scale-rule-auth "connection=sb-connection"
# Container Apps revision management
# Revision mode: single (one active) or multiple (several active)
az containerapp ingress traffic set \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--revision-weight "rev-v1=80" "rev-v2=20" # Canary: 80/20
# Activate a new revision and deactivate the old one
az containerapp revision activate \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--revision "carved-rock-api--rev-2"
az containerapp revision deactivate \
--name "carved-rock-api" \
--resource-group "rg-carved-rock" \
--revision "carved-rock-api--rev-1"
10. AZ-204 Review Questions — App and Container Solutions
Q1 — App Service Plan Tier
Question: Carved Rock Fitness wants to enable web server logging (raw HTTP logs), deployment slots (minimum 3), and autoscaling for their application. Which App Service Plan tier do you select?
- A. Basic B2 (Linux)
- B. Standard S1 (Windows)
- C. Premium V3 P1 (Windows) ✅
- D. Standard S2 (Linux)
Explanation: Web Server Logging (raw HTTP logs) is only available on Windows. Deployment slots are available starting from Standard. Autoscaling requires Premium V2/V3 minimum. Only the Premium V3 (Windows) option satisfies all criteria.
Q2 — Container Registry Repository
Question: In Azure Container Registry, which concept groups multiple versions of the same image with different tags?
- A. Namespace
- B. Layer
- C. Repository ✅
- D. Manifest
Explanation: A Repository in ACR is a collection of container images sharing the same name but with different tags (e.g.,
myapp:1.0,myapp:1.1,myapp:latest). Namespaces organize repositories in groups (e.g.,org/team/app). A manifest uniquely identifies a specific version via its SHA256 digest.
Q3 — Azure Functions Triggers and Bindings
Question: You are developing an Azure Function triggered by an HTTP webhook. It must read a document from Cosmos DB and write a message to a Storage Queue. Which elements do you configure?
- A. HTTP Input Binding + Cosmos DB Trigger + Queue Output Binding
- B. HTTP Trigger + Cosmos DB Input Binding + Queue Output Binding ✅
- C. HTTP Trigger + Cosmos DB Trigger + Queue Input Binding
- D. Webhook Input Binding + Cosmos DB Input Binding + Queue Output Binding
Explanation: An Azure Function has exactly one Trigger (HTTP Trigger for webhooks). Additional input data uses Input Bindings (Cosmos DB). Output data uses Output Bindings (Queue). There is no “Webhook Input Binding” — webhooks use an HTTP Trigger.
Q4 — Deployment Slots
Question: Carved Rock Fitness continuously deploys from GitHub to their staging slot. After a staging → production swap, the staging slot shows the same configuration as production. Which behavior is expected after a swap?
- A. Both slots now have exactly the same App Settings
- B. The staging slot inherits all of production’s App Settings
- C. “Sticky” App Settings stay in their slot, others are swapped ✅
- D. No settings are swapped, only the code
Explanation: During a slot swap, normal Application Settings and Connection Strings are swapped along with the code. But settings marked as “Deployment slot setting” (sticky) remain attached to their slot. This allows different database Connection Strings per environment: staging keeps its test DB, production keeps its prod DB, even after the swap.
Q5 — Azure Container Instances vs Container Apps
Question: Carved Rock Fitness wants to deploy a new image processing microservice. The service must automatically scale based on the number of messages in an Azure Storage Queue. Which Azure service do you choose?
- A. Azure Container Instances (ACI)
- B. Azure App Service with container
- C. Azure Container Apps with KEDA scaling rule ✅
- D. Azure Functions Premium Plan
Explanation: Azure Container Apps supports KEDA (Kubernetes Event-Driven Autoscaling)-based scaling, including scaling based on Azure Storage queues, Service Bus, Event Hubs, etc. ACI does not support autoscaling. App Service does not scale based on queues. Functions with Container is possible but Container Apps is more natural for long-running containerized microservices with KEDA scaling.
Q6 — Durable Functions Pattern
Question: You are implementing an approval workflow where an email is sent to a manager, and the workflow waits up to 7 days for a response before expiring. Which Durable Functions pattern do you use?
- A. Function Chaining
- B. Fan-out / Fan-in
- C. Monitor
- D. Human Interaction ✅
Explanation: The Human Interaction pattern is designed for workflows waiting on an external human action (approval, validation). It uses
WaitForExternalEventwhich suspends the orchestration until an external event is triggered, or until a timeout timer fires. The other patterns don’t fit: Chaining is for automatic sequential steps, Fan-out for parallel processing, Monitor for periodic surveillance.
11. Glossary — App and Container Solutions
| Term | Definition |
|---|---|
| ACR | Azure Container Registry — managed private Docker registry |
| ACR Task | Cloud-based Docker image builds via Azure CLI |
| ACI | Azure Container Instances — ephemeral containers without orchestration |
| App Service Plan | Defines the resources (CPU/RAM/features) for Web Apps |
| Container App | Containerized application on Azure Container Apps (PaaS K8s) |
| DAPR | Distributed Application Runtime — APIs for portable microservices |
| Deployment Slot | Parallel App Service instance (staging, testing) |
| Dockerfile | Build script for a container image |
| Durable Functions | Azure Functions extension for stateful workflows |
| Easy Auth | Built-in Azure App Service authentication without code changes |
| KEDA | Kubernetes Event-Driven Autoscaling — event-based scaling |
| Layer | Immutable Docker image layer (reusable between images) |
| Manifest | Describes the layers and configuration of an image (identified by digest) |
| Namespace (ACR) | Hierarchical organization of repositories in a registry |
| Output Binding | Write data from an Azure Function to an Azure service |
| Repository (ACR) | Collection of images with the same name but different tags |
| Revision (Container Apps) | Immutable snapshot of a Container App |
| Scale Out | Increase the number of application instances |
| Scale Up | Move to a tier with more CPU/RAM/features |
| Sidecar Container | Secondary container alongside the main container |
| Slot Swap | Swap two deployment slots (staging ↔ production) |
| Sticky Setting | App Setting that stays attached to its slot during a swap |
| Trigger (Functions) | Azure Function trigger (HTTP, Timer, Queue…) |
Exam Summary — AZ-204
Quick Memory Rules
- Web server logging = Windows only (not Linux)
- Autoscale = requires Premium V2/V3+, not Standard or Basic
- Deployment slots = requires Standard+ (not Basic or Free)
- Always On = disabled by default, required for continuous WebJobs
- Session affinity = enable if the app is stateful (otherwise disable for better load distribution)
- ACR Repository = collection of images with same name + different tags
- ACR Namespace = hierarchical organization (org/team/project)
- HTTP Trigger = for webhooks (not “Webhook Input Binding”)
- Durable Functions Human Interaction =
WaitForExternalEvent+ timer timeout - Sticky App Settings = remain in the slot during a swap
- KEDA = Azure Container Apps scales based on events (queues, topics, etc.)
- DAPR Sidecar = automatically deployed alongside the container when enabled
- Revision (Container Apps) = immutable snapshot, created on “revision-scoped” changes
- Application-scoped changes (secrets, traffic) = no new revision created
- az containerapp up = deploys from source code with automatic build (ACR Task)
Search Terms
az-204 · app · container · azure · developer · microsoft · deployment · functions · service · apps · durable · registry · bindings · detailed · dockerfile · instances · scaling · slot · slots · tiers · triggers