Intermediate AZ-204

AZ-204: Implementing App and Container Solutions

App Service deployment & scaling, deployment slots, container images, ACI, Functions and Container Apps.

Demo application: Carved Rock Fitness (.NET 8 Blazor)
Certification: Azure Developer Associate (AZ-204)


Table of Contents

  1. Azure App Service — Deployment
  2. Azure App Service — Scaling
  3. Web App Configuration
  4. Deployment Slots
  5. Container Images
  6. Azure Container Instances (ACI)
  7. Azure Functions
  8. 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 UpScale Out
DefinitionIncrease resources of one instanceAdd more instances
MethodChange tierManual / Autoscale
ExampleB1 → B2 → P11 → 3 instances
LimitMaximum per tierDepends 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:

TypeOSDescription
Application LoggingWindows + LinuxApp logs (ILogger)
Web Server LoggingWindowsIIS logs (raw HTTP requests)
Detailed Error MessagesWindowsDetailed HTTP error pages
Deployment LoggingWindows + LinuxDeployment logs
Failed Request TracingWindowsFREB 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:

HeaderValue
X-MS-CLIENT-PRINCIPAL-IDSubject/User ID
X-MS-CLIENT-PRINCIPAL-NAMEUser name
X-MS-CLIENT-PRINCIPAL-IDPProvider (aad, google, etc.)
X-MS-TOKEN-AAD-ACCESS-TOKENAccess 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:

TypeBehaviorUsage
Slot-specific (sticky)Stays in the slot during swapDB connection strings, per-env feature flags
Shared (non-sticky)Follows the deployment during swapApp 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:

TierStorageWebhooksGeo-replicationPrivate Link
Basic10 GB
Standard100 GB
Premium500 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:

ACIApp Service (Container)Container Apps
ComplexityLowLowMedium
ScalingManualAutoAuto (KEDA)
OrchestrationNoNoYes (on AKS)
LifetimeEphemeralLong-runningLong-running
Typical useJobs, testsWeb appsMicroservices

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:

TriggerFired ByUsage
HTTPHTTP requestServerless APIs
TimerCron scheduleScheduled tasks
Queue StorageAzure Queue messageAsync processing
Service BusAzure Service Bus messageEnterprise messaging
Blob StorageNew blobFile processing
Cosmos DBModified documentChange feed
Event HubIncoming eventIoT, 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:

TypeRoleDescription
ClientTriggerStarts the orchestration
OrchestratorWorkflowCoordinates activities
ActivityReal workIndividual tasks, can do I/O
EntityShared stateObjects 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:

PatternDescriptionUsage
Function ChainingSequential stepsOrder workflow
Fan-out/Fan-inParallel + aggregationMulti-warehouse inventory check
Async HTTP APILong-running + status pollingImage processing, report generation
MonitorSurveillance loopPeriodically check external status
Human InteractionWait for external eventManager 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

TierOSCustom DomainSSLScale OutSlotsAutoscalePrivate NetworkEstimated Price
Free (F1)Win/LinuxFree
Shared (D1)Win~€9/month
Basic (B1)Win/LinuxManual~€47/month
Standard (S1)Win/LinuxManual5 slots~€63/month
Premium V3 (P1V3)Win/LinuxAuto20 slots~€140/month
Isolated V2 (I1V2)Win/LinuxAuto20 slotsDedicated~€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]
ServiceUse CaseInfra to ManageAutoscalePrice
App Service (Container)Web apps, APIsNonePer plan
Azure Container InstancesShort tasks, tests, jobsNonePer second
Azure Container AppsMicroservices, event-drivenNone✅ KEDAPer vCPU/request
Azure Kubernetes ServiceComplex workloadsNodes✅ HPAPer node
Azure Functions (Container)Event-driven, serverlessNonePer 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 WaitForExternalEvent which 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

TermDefinition
ACRAzure Container Registry — managed private Docker registry
ACR TaskCloud-based Docker image builds via Azure CLI
ACIAzure Container Instances — ephemeral containers without orchestration
App Service PlanDefines the resources (CPU/RAM/features) for Web Apps
Container AppContainerized application on Azure Container Apps (PaaS K8s)
DAPRDistributed Application Runtime — APIs for portable microservices
Deployment SlotParallel App Service instance (staging, testing)
DockerfileBuild script for a container image
Durable FunctionsAzure Functions extension for stateful workflows
Easy AuthBuilt-in Azure App Service authentication without code changes
KEDAKubernetes Event-Driven Autoscaling — event-based scaling
LayerImmutable Docker image layer (reusable between images)
ManifestDescribes the layers and configuration of an image (identified by digest)
Namespace (ACR)Hierarchical organization of repositories in a registry
Output BindingWrite 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 OutIncrease the number of application instances
Scale UpMove to a tier with more CPU/RAM/features
Sidecar ContainerSecondary container alongside the main container
Slot SwapSwap two deployment slots (staging ↔ production)
Sticky SettingApp 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.