Azure AZ-204 cheat sheet for key facts, traps, service mappings, and final review.
Use this when the stem mixes compute, storage, security, messaging, or third-party integration.
flowchart TD
S["Scenario"] --> C["Classify the app or integration lane"]
C --> R["Check storage, identity, or messaging fit"]
R --> O["Check retry, idempotency, and rollout safety"]
O --> V["Verify with logs, status, or code-path evidence"]
AZ-204 is less about memorizing product lists and more about choosing the right service + configuration for a scenario.
| Domain | Weight | Visual (relative) |
|---|---|---|
| Develop Azure compute solutions | 25–30% | ██████████ |
| Develop for Azure storage | 15–20% | ██████ |
| Implement Azure security | 15–20% | ██████ |
| Monitor and troubleshoot Azure solutions | 5–10% | ███ |
| Connect/consume Azure + third-party services | 20–25% | ████████ |
Always look for the hidden requirement: private vs public, ordering, idempotency, throughput, RTO/RPO, least privilege, cost controls.
| Need | Best fit | Why |
|---|---|---|
| Managed web app/API with easy deployment | App Service | Built-in scaling, TLS/custom domains, slots |
| Event-driven code, bursty traffic | Functions | Triggers/bindings, scale-to-zero (Consumption) |
| Containerized microservices with revisions + KEDA | Container Apps | Managed env, ingress, traffic splitting |
| “Run a container now” without orchestration | Container Instances | Simple container group execution |
flowchart TD
Q{"Do you need triggers\nand bindings?"} -->|Yes| F["Azure Functions"]
Q -->|No| C{"Do you need\na container image?"}
C -->|No| AS["App Service"]
C -->|Yes| O{"Need revisions\n+ autoscaling rules?"}
O -->|Yes| CA["Container Apps"]
O -->|No| ACI["Container Instances"]
1# Create ACR
2az acr create -g RG -n myregistry --sku Basic
3
4# Login and push
5az acr login -n myregistry
6docker build -t myregistry.azurecr.io/myapi:1.0.0 .
7docker push myregistry.azurecr.io/myapi:1.0.0
| Option | Best when | Notes |
|---|---|---|
| Entra ID (AAD) | humans/devs | Role assignments; best default |
| Managed identity | Azure-hosted workloads | Great for pull access (no secrets) |
| Scoped tokens | limited automation | Narrow scope + expiry |
| Admin user | last resort | Shared credential; avoid for prod |
1FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
2WORKDIR /src
3COPY . .
4RUN dotnet publish -c Release -o /out
5
6FROM mcr.microsoft.com/dotnet/aspnet:8.0
7WORKDIR /app
8COPY --from=build /out .
9ENV ASPNETCORE_URLS=http://+:8080
10EXPOSE 8080
11ENTRYPOINT ["dotnet","MyApi.dll"]
1# Example: build on source context (conceptual)
2az acr build -r myregistry -t myapi:<build-id> .
| Feature | Container Apps | ACI |
|---|---|---|
| Revision-based deploys | ✅ | ✖ |
| Traffic splitting | ✅ | ✖ |
| Event-based scaling (KEDA) | ✅ | ✖ |
| “Just run a container” simplicity | ⚠ (more setup) | ✅ |
Container Apps: key knobs to remember
1# Minimal Container Apps flow (conceptual)
2az containerapp env create -g RG -n ca-env --location eastus
3az containerapp create -g RG -n myapi --environment ca-env \\
4 --image myregistry.azurecr.io/myapi:1.0.0 --ingress external --target-port 8080
flowchart LR
DEV["Dev pushes image"] --> ACR["ACR"]
ACR --> CA["Container Apps"]
CA --> REV{"Revisions"}
REV -->|blue| R1["rev-1"]
REV -->|green| R2["rev-2"]
R1 --> TR["Traffic split"]
R2 --> TR
Common gotcha: container apps and private registries require correct auth (managed identity or registry credentials) and correct image reference.
| Method | When it’s best | Notes |
|---|---|---|
| Zip deploy / run-from-package | Fast + simple | Great for CI/CD artifacts |
| GitHub Actions / Azure DevOps | Repeatable | Build → test → deploy pipelines |
| Container from ACR | Containerized web apps | Control image tag/version |
1# Plan + web app
2az appservice plan create -g RG -n plan-az204 --sku P1v3 --is-linux
3az webapp create -g RG -p plan-az204 -n mywebapp --runtime \"DOTNETCORE:8.0\"
4
5# App settings (configuration) + connection strings
6az webapp config appsettings set -g RG -n mywebapp --settings ASPNETCORE_ENVIRONMENT=prod
7az webapp config connection-string set -g RG -n mywebapp -t SQLAzure --settings Db=\"<conn-string>\"
8
9# Zip deploy (one option)
10az webapp deploy -g RG -n mywebapp --src-path ./artifact.zip --type zip
11
12# Slots (safe releases)
13az webapp deployment slot create -g RG -n mywebapp --slot staging
14az webapp deployment slot swap -g RG -n mywebapp --slot staging --target-slot production
1name: build-and-deploy
2on: [push]
3jobs:
4 deploy:
5 runs-on: ubuntu-latest
6 steps:
7 - uses: actions/checkout@v4
8 - uses: azure/login@v2
9 with:
10 creds: ${{ secrets.AZURE_CREDENTIALS }}
11 - uses: azure/webapps-deploy@v3
12 with:
13 app-name: mywebapp
14 package: ./artifact.zip
| Item | Best home | Why |
|---|---|---|
| Feature flags / non-secret settings | App Configuration | Centralized, environment-aware |
| Secrets (API keys, passwords) | Key Vault | Rotation + auditing |
| App runtime env vars | App settings | Standard platform config |
| Database connection strings | Connection strings | Separate slot settings + tooling support |
flowchart LR
U["Users"] --> P["Prod slot"]
S["Staging slot"] --> SW["Swap"]
SW --> P
Slot rules of thumb
| Pattern | Typical trigger | Typical output |
|---|---|---|
| HTTP API | HTTP trigger | Storage/Cosmos/Service Bus |
| Background processing | Queue/Service Bus trigger | Blob/Cosmos/Service Bus |
| Event routing | Event Grid trigger | Service Bus / storage |
| Scheduled job | Timer trigger | Blob/Cosmos |
| Plan | Best when | Notes |
|---|---|---|
| Consumption | bursty workloads | scale-to-zero; watch cold start |
| Premium | low-latency + scale | reduced cold start; more features |
| Dedicated (App Service plan) | steady load | predictable capacity; you manage scaling |
1{
2 "version": "2.0",
3 "retry": {
4 "strategy": "fixedDelay",
5 "maxRetryCount": 5,
6 "delayInterval": "00:00:10"
7 }
8}
Key exam idea: many triggers are at-least-once. Design processing to be idempotent.
1using Azure.Messaging.ServiceBus;
2using Microsoft.Azure.Functions.Worker;
3using Microsoft.Extensions.Logging;
4
5public class ProcessOrders
6{
7 private readonly ILogger _logger;
8 public ProcessOrders(ILoggerFactory loggerFactory) => _logger = loggerFactory.CreateLogger<ProcessOrders>();
9
10 [Function("ProcessOrders")]
11 public async Task Run(
12 [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")]
13 ServiceBusReceivedMessage message,
14 ServiceBusMessageActions messageActions)
15 {
16 try
17 {
18 var body = message.Body.ToString();
19 _logger.LogInformation("Order received: {Body}", body);
20
21 // Do work...
22
23 await messageActions.CompleteMessageAsync(message);
24 }
25 catch (Exception ex)
26 {
27 _logger.LogError(ex, "Failed processing message");
28 await messageActions.AbandonMessageAsync(message);
29 }
30 }
31}
Function gotchas
flowchart LR
START["Starter (HTTP/Timer)"] --> ORCH["Orchestrator"]
ORCH --> A1["Activity: step 1"]
ORCH --> A2["Activity: step 2"]
A1 --> ORCH
A2 --> ORCH
ORCH --> DONE["Complete"]
| Level | Strength | Typical use |
|---|---|---|
| Strong | Highest | Single region, strict correctness |
| Bounded staleness | High | Controlled lag |
| Session | Medium | Most apps (user session correctness) |
| Consistent prefix | Lower | Ordered but potentially stale |
| Eventual | Lowest | Highest availability/lowest latency |
1var read = await container.ReadItemAsync<MyItem>(id, new PartitionKey(pk));
2var etag = read.ETag;
3
4item.Version = item.Version + 1;
5await container.ReplaceItemAsync(item, id, new PartitionKey(pk), new ItemRequestOptions
6{
7 IfMatchEtag = etag
8});
1var client = new CosmosClient(endpoint, credential);
2var container = client.GetContainer("db", "items");
3
4var query = new QueryDefinition("SELECT * FROM c WHERE c.type = @t")
5 .WithParameter("@t", "invoice");
6
7using var iterator = container.GetItemQueryIterator<dynamic>(query);
8while (iterator.HasMoreResults)
9{
10 foreach (var item in await iterator.ReadNextAsync())
11 {
12 Console.WriteLine(item);
13 }
14}
flowchart LR
CDB["Cosmos DB container"] --> CF["Change Feed"]
CF --> FN["Function / Worker"]
FN --> OUT["Downstream system"]
Change feed gotcha: you need leases/checkpointing so processors can scale safely.
1var processor = container
2 .GetChangeFeedProcessorBuilder<MyItem>(
3 processorName: "proc",
4 onChangesDelegate: async (changes, cancellationToken) =>
5 {
6 foreach (var doc in changes)
7 {
8 // react to changes
9 }
10 })
11 .WithInstanceName("worker-1")
12 .WithLeaseContainer(leaseContainer)
13 .Build();
14
15await processor.StartAsync();
1using Azure.Storage.Blobs;
2using Azure.Storage.Blobs.Models;
3
4var container = new BlobContainerClient(connectionString, "docs");
5await container.CreateIfNotExistsAsync();
6
7var blob = container.GetBlobClient("reports/2025-q4.pdf");
8await blob.UploadAsync(fileStream, new BlobHttpHeaders { ContentType = "application/pdf" });
9
10await blob.SetMetadataAsync(new Dictionary<string, string>
11{
12 ["owner"] = "finance",
13 ["classification"] = "internal"
14});
| Feature | Why it matters | Typical scenario |
|---|---|---|
| Soft delete | recover from deletes | “accidental deletion” |
| Versioning | recover previous versions | “rollback changes” / ransomware |
| Immutability | WORM retention | compliance retention |
| Access tiers | cost optimization | logs/archives |
1{
2 "rules": [
3 {
4 "name": "tier-old-logs",
5 "enabled": true,
6 "type": "Lifecycle",
7 "definition": {
8 "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["logs/"] },
9 "actions": {
10 "baseBlob": {
11 "tierToCool": { "daysAfterModificationGreaterThan": 30 },
12 "tierToArchive": { "daysAfterModificationGreaterThan": 180 },
13 "delete": { "daysAfterModificationGreaterThan": 365 }
14 }
15 }
16 }
17 }
18 ]
19}
| SAS type | Best when | Notes |
|---|---|---|
| Service SAS | Scoped to a resource | Most common “least privilege” SAS |
| Account SAS | Broad | Easy to over-permission |
| User delegation SAS | Best security | Signed by Entra ID; no account key exposure |
Common gotcha: SAS failures are often clock skew, expired token, or missing permissions on the exact resource path.
1using Azure.Storage.Sas;
2
3var builder = new BlobSasBuilder
4{
5 BlobContainerName = "docs",
6 BlobName = "reports/2025-q4.pdf",
7 Resource = "b",
8 ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(15)
9};
10builder.SetPermissions(BlobSasPermissions.Read);
11
12var sasUri = blob.GenerateSasUri(builder);
13Console.WriteLine(sasUri);
| Scenario | Flow | Why |
|---|---|---|
| Web app signing in a user | Authorization code | Standard OIDC pattern |
| Service-to-service (daemon) | Client credentials | No user context |
| CLI/device sign-in | Device code | Works without browser redirect |
1using Microsoft.Identity.Client;
2
3var app = ConfidentialClientApplicationBuilder
4 .Create(clientId)
5 .WithClientSecret(clientSecret)
6 .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
7 .Build();
8
9var result = await app.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
10 .ExecuteAsync();
11
12Console.WriteLine(result.AccessToken);
| Permission type | Used when | Example |
|---|---|---|
| Delegated | user is signed in | User.Read to call /me |
| Application | daemon/no user | .default for app-only permissions |
1using System.Net.Http.Headers;
2
3using var http = new HttpClient();
4http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
5
6var json = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me");
7Console.WriteLine(json);
1using Azure.Identity;
2using Azure.Security.KeyVault.Secrets;
3
4var vaultUri = new Uri("https://myvault.vault.azure.net/");
5var client = new SecretClient(vaultUri, new DefaultAzureCredential());
6KeyVaultSecret secret = await client.GetSecretAsync("DbPassword");
sequenceDiagram
participant App as "App Service / Function"
participant MSI as "Managed Identity"
participant AAD as "Entra ID"
participant KV as "Key Vault"
App->>MSI: Request token for Key Vault
MSI->>AAD: Get access token
AAD-->>MSI: Access token
MSI-->>App: Access token
App->>KV: GetSecret (Bearer token)
KV-->>App: Secret value
| Store | Best for | Why |
|---|---|---|
| Key Vault | secrets/keys/certs | rotation + auditing + access controls |
| App Configuration | app settings + feature flags | centralized config + refresh |
Rate limiting
1<rate-limit-by-key calls="10" renewal-period="60" counter-key="@(context.Subscription.Key)" />
Validate JWT
1<validate-jwt header-name="Authorization" failed-validation-httpcode="401">
2 <openid-config url="https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration" />
3 <required-claims>
4 <claim name="aud">
5 <value>{api-app-id-uri}</value>
6 </claim>
7 </required-claims>
8</validate-jwt>
CORS
1<cors allow-credentials="false">
2 <allowed-origins>
3 <origin>https://example.com</origin>
4 </allowed-origins>
5 <allowed-methods>
6 <method>GET</method>
7 <method>POST</method>
8 </allowed-methods>
9</cors>
Rewrite URL (common “legacy backend” fix)
1<rewrite-uri template="/v2/{uri}" />
Set backend (multi-backend routing)
1<set-backend-service base-url="https://mybackend.azurewebsites.net" />
Response caching
1<cache-lookup vary-by-developer="false" vary-by-developer-groups="false" />
2<cache-store duration="60" />
Policy scopes: global → product → API → operation (more specific overrides earlier scopes).
flowchart LR
C["Client"] --> APIM["API Management"]
APIM --> POL["Policies (auth, throttling, transform)"]
POL --> BE["Backend API"]
Policy order matters: inbound runs before backend; outbound after backend response.
| Service | Best for | Key traits |
|---|---|---|
| Event Grid | event routing | push, fan-out, filtering |
| Event Hubs | streaming ingestion | partitions, high throughput |
| Service Bus | enterprise messaging | DLQ, sessions, transactions |
| Storage Queues | simple queues | basic, cheap, fewer features |
| Feature | Event Grid | Event Hubs | Service Bus | Storage Queues |
|---|---|---|---|---|
| Primary job | route events | ingest streams | reliable messaging | simple queue |
| Push vs pull | push | pull | pull | pull |
| Ordering | ✖ | within partition | sessions/FIFO patterns | best-effort |
| Dead-letter queue | dead-lettering | consumer design | built-in DLQ | custom pattern |
| Transactions | ✖ | ✖ | ✅ | ✖ |
| Typical workload | app/resource events | telemetry/logs | commands/work items | lightweight jobs |
flowchart LR
SRC["Source system"] --> EG["Event Grid"]
EG --> FN["Function handler"]
FN --> SB["Service Bus queue"]
SB --> WK["Worker"]
1using Azure.Messaging.EventHubs;
2using Azure.Messaging.EventHubs.Producer;
3
4await using var producer = new EventHubProducerClient(connectionString, "telemetry");
5using EventDataBatch batch = await producer.CreateBatchAsync();
6batch.TryAdd(new EventData(System.Text.Encoding.UTF8.GetBytes("{\"ok\":true}")));
7await producer.SendAsync(batch);
1using Azure.Messaging.ServiceBus;
2
3await using var client = new ServiceBusClient(connectionString);
4ServiceBusSender sender = client.CreateSender("orders");
5await sender.SendMessageAsync(new ServiceBusMessage("hello") { CorrelationId = "c-123" });
| Mode | Behavior | When to use |
|---|---|---|
| Peek-lock | process then settle | Most reliable (handles failures) |
| Receive-and-delete | delete immediately | Only when loss is acceptable |
1using Azure.Storage.Queues;
2
3var queue = new QueueClient(connectionString, "jobs");
4await queue.CreateIfNotExistsAsync();
5
6await queue.SendMessageAsync("do-work");
7var msg = await queue.ReceiveMessageAsync(visibilityTimeout: TimeSpan.FromMinutes(2));
8
9// Do work...
10await queue.DeleteMessageAsync(msg.Value.MessageId, msg.Value.PopReceipt);
Poison message rule: if the same message fails repeatedly, move it to a poison/dead-letter queue and alert.
requests
| where success == false
| summarize count() by resultCode, operation_Name
| order by count_ desc
dependencies
| where duration > 2s
| summarize avg(duration), count() by target
| order by avg_duration desc
exceptions
| summarize count() by type, outerMessage
| order by count_ desc
Correlation tip: join on operation_Id to stitch requests → dependencies → exceptions for a single transaction.
Alerting defaults
| Symptom | Likely cause | First fix to try |
|---|---|---|
| 401 from API | wrong audience/issuer | verify Entra app IDs + aud claim |
| 403 from Key Vault | missing RBAC/access policy | grant least-privilege + verify identity |
| SAS “authentication failed” | expired token / clock skew | regenerate SAS; ensure UTC + expiry |
| Cosmos 429 | RU throttling | increase RU/autoscale; optimize partition/query |
| Service Bus “lock lost” | processing too slow | renew lock / shorten processing / increase concurrency |
| Event Grid delivery failures | endpoint down / auth mismatch | fix handler auth; configure dead-letter |
| App Service works in staging but not prod | slot settings swapped | mark correct settings as slot settings |