Cloud & DevOps

Azure for .NET Developers: From First Resource to Production Cloud

Afzal AhmedFaz Ahmed
·27 July 2026·41 min read
Microsoft Azure.NET 10ASP.NET CoreAzure App ServiceAzure FunctionsAzure Container AppsAzure StorageAzure SQLService BusManaged IdentityBicepGitHub ActionsApplication Insights

Why This Matters

My structured Azure learning journal, connecting tenants, subscriptions and Bicep with App Service, Functions, containers, data, messaging, identity, monitoring and secure delivery.

Pull up a chair. If Azure feels like a catalogue of hundreds of products, I want to simplify it for you.

I am Faz Ahmed, and in this guide I am going to mentor you as I would a developer joining an Azure project for the first time. We will not begin by memorising service logos. We will begin with one application, identify what it needs, create the resources step by step, deploy it securely and learn how to operate it.

The source book behind this article covers the developer journey extremely well: accounts and tooling, App Service, static sites, Functions, configuration, workflows, containers, storage, messaging, databases, monitoring, AI and delivery pipelines. I have used that as our curriculum and updated the guidance for 27 July 2026.

By the end, I want you to be able to look at a .NET solution and answer:

Where should each component run?
Where should its data live?
How do components communicate safely?
How does the application obtain identity without secrets?
How do we deploy repeatably?
How do we know it is healthy?
How do we control cost and recover from failure?

That is what “Azure for developers” really means.

1. First understand what the cloud changes

Azure rents computing capabilities through managed services. You can still create virtual machines and manage operating systems, but developers should normally start higher up the responsibility ladder.

With Infrastructure as a Service, Azure manages physical hardware while you manage the VM, operating system and application. With Platform as a Service, Azure also manages more of the runtime, patching, availability and scaling platform. Serverless services push further: you supply code or a container and pay largely around execution or provisioned capacity.

Less infrastructure control usually means less operational work. More control means more responsibility. Do not choose AKS because Kubernetes sounds senior when App Service can host the application with a fraction of the operational surface.

The shared-responsibility model never means Microsoft secures your application for you. Azure secures the platform; your team still owns identities, permissions, data classification, application vulnerabilities, resource configuration and operational response.

2. Azure’s hierarchy: tenant to resource

Before creating anything, understand the containers around it.

Microsoft Entra tenant
  -> management groups
     -> subscriptions
        -> resource groups
           -> resources

The Microsoft Entra tenant is the identity boundary containing users, groups, applications and service principals. A subscription is a billing, quota and access-management boundary. A resource group is a lifecycle container for related Azure resources. A resource is the App Service, storage account, database or other managed capability.

Resource groups are not folders for visual tidiness. Put resources together when they share a lifecycle, ownership and deployment boundary. A database shared by five applications may belong in a platform resource group rather than being deleted with one web app.

Regions are physical Azure locations. Choose based on users, data residency, service availability, latency, cost and resilience. Availability zones are separate datacentre groupings inside supported regions. Multi-region architecture is not automatically better; it adds data consistency, routing, deployment and cost complexity.

3. Guardrails before resources

The first production lesson I give a junior developer is this: an Azure command can spend real money in seconds.

Before the lab:

  1. Choose a dedicated learning subscription if possible.
  2. Create a budget and cost alert.
  3. Select one region deliberately.
  4. Agree a naming convention and tags.
  5. Use a separate resource group so cleanup is obvious.
  6. Never paste subscription keys or connection strings into source control.
  7. Delete the learning resource group when finished.
Useful tags include environment, owner, application, costCentre and dataClassification. Tags help reporting; Azure Policy enforces organisational rules. Policy can deny non-approved regions, require tags or audit public network access.

Budgets alert you; they do not generally shut services down automatically. Monitor actual cost and understand each SKU before deployment.

4. Install a developer toolchain

You need a current .NET 10 SDK, Git, an editor, Docker if using containers, the Azure CLI and Azure Developer CLI. Visual Studio and VS Code both work well; choose the editor that keeps you productive.

The commands serve different levels:

  • az administers Azure resources directly;
  • Azure PowerShell offers equivalent management in PowerShell style;
  • azd manages an application lifecycle—provision, deploy, monitor—using a project template;
  • Bicep declares Azure infrastructure as code.
Sign in interactively for local development:
az login
az account list --output table
az account set --subscription "YOUR-SUBSCRIPTION-NAME-OR-ID"
az account show --output table

Always check context before creating or deleting. The most dangerous cloud command is often a correct command against the wrong subscription.

In CI/CD, do not use your personal login or a long-lived client secret. Prefer workload identity federation with GitHub Actions or Azure DevOps so the pipeline exchanges a trusted OIDC token for short-lived Azure access.

5. Infrastructure as code from the beginning

The portal is helpful for discovering a service and inspecting state. It is a poor source of truth. Clicking resources into existence creates undocumented, unrepeatable environments.

Bicep is Azure’s declarative infrastructure language. You state the desired resources; Azure Resource Manager determines dependencies and deployment operations.

Here is the foundation of our learning environment:

targetScope = 'resourceGroup'

@description('Short environment name such as dev, test, or prod.')
param environmentName string

@description('Azure region for the workload.')
param location string = resourceGroup().location

@description('Globally unique suffix generated by the deployment.')
param uniqueSuffix string = uniqueString(subscription().id, resourceGroup().id)

var tags = {
  application: 'developer-cloud-lab'
  environment: environmentName
  managedBy: 'bicep'
  owner: 'faz-learning'
}

resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2025-07-01' = {
  name: 'log-devcloud-${environmentName}-${uniqueSuffix}'
  location: location
  tags: tags
  properties: {
    retentionInDays: 30
  }
}

API versions evolve, so check the current Bicep resource reference rather than copying this forever. Parameters hold environment-specific choices; modules organise reusable resource groups; outputs expose values needed by other modules.

Preview before deploying:

az group create \
  --name rg-developer-cloud-dev \
  --location uksouth \
  --tags environment=dev application=developer-cloud-lab

az deployment group what-if \
  --resource-group rg-developer-cloud-dev \
  --template-file infra/main.bicep \
  --parameters environmentName=dev

az deployment group create \
  --resource-group rg-developer-cloud-dev \
  --template-file infra/main.bicep \
  --parameters environmentName=dev

what-if is a review aid, not a substitute for testing and approval. Infrastructure changes can destroy or recreate stateful resources, so protect production with permissions, locks, backups and deployment review.

6. Our reference application

We will imagine a small “Project Notes” system:

Angular/React static front end
  -> ASP.NET Core 10 API
       -> Azure SQL Database for projects
       -> Blob Storage for attachments
       -> Service Bus for background work
  -> .NET worker or Azure Function processes messages

Cross-cutting:
  Entra ID, managed identity, Key Vault,
  App Configuration, Application Insights,
  Log Analytics, Bicep and GitHub Actions

This is intentionally ordinary. It teaches the services most developers need. We can host the API on App Service first and later compare Container Apps without rewriting the business domain.

7. Choosing compute without guessing

Use this starting guide:

WorkloadStart with
Conventional web app or REST APIAzure App Service
Event-driven function or scheduled taskAzure Functions Flex Consumption
Static front end with integrated deliveryAzure Static Web Apps
Containerised service without Kubernetes managementAzure Container Apps
Finite containerised taskContainer Apps Jobs
One-off simple containerAzure Container Instances
Complex Kubernetes platform with a capable operations teamAKS
Visual integration workflow and managed connectorsLogic Apps
Code-first durable orchestrationDurable Functions
This table is a conversation starter. Networking, compliance, scaling, runtime support, execution duration and team capability can change the answer.

8. Step by step: deploy an ASP.NET Core API to App Service

App Service is a managed platform for web apps and APIs on Windows or Linux. An App Service plan supplies compute; one or more apps run on that plan. Apps in the same plan share its instances and scale together, so unrelated workloads can become noisy neighbours.

Create a .NET 10 API locally:

dotnet new webapi -n ProjectNotes.Api --framework net10.0
dotnet run --project ProjectNotes.Api

Add health endpoints early:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();
builder.Services.AddProblemDetails();

var app = builder.Build();
app.UseExceptionHandler();
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready");
app.MapGet("/api/projects", () => Results.Ok(Array.Empty<object>()));
app.Run();

Define the plan and app in Bicep:

resource appPlan 'Microsoft.Web/serverfarms@2024-11-01' = {
  name: 'plan-devcloud-${environmentName}'
  location: location
  tags: tags
  sku: {
    name: 'B1'
    tier: 'Basic'
  }
  properties: {
    reserved: true
  }
}

resource webApp 'Microsoft.Web/sites@2024-11-01' = {
  name: 'app-devcloud-${environmentName}-${uniqueSuffix}'
  location: location
  tags: tags
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appPlan.id
    httpsOnly: true
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|10.0'
      minTlsVersion: '1.2'
      ftpsState: 'Disabled'
      alwaysOn: true
      healthCheckPath: '/health/ready'
    }
  }
}

Validate current runtime strings and API versions before deploying. The Basic tier shown is illustrative; choose SKU after reviewing feature and scale requirements.

Deploy application code through a pipeline or az webapp deploy, then verify the health endpoint. For production, use deployment slots where the plan supports them. Deploy to staging, warm and test it, then swap into production. Slots share plan compute and their settings require careful “slot setting” configuration.

Scale up means a larger instance. Scale out means more instances. Stateless APIs scale out more safely: keep session and durable state outside the process, use distributed caches only when justified and make background work idempotent.

9. Identity first: remove application secrets

A system-assigned managed identity belongs to one Azure resource and disappears with it. A user-assigned identity is a separate reusable resource that can be attached to several workloads.

Grant the web app identity a narrow data-plane role on Blob Storage rather than giving it an account key. Then use DefaultAzureCredential:

using Azure.Identity;
using Azure.Storage.Blobs;

var credential = new DefaultAzureCredential();
var blobService = new BlobServiceClient(
    new Uri(builder.Configuration["Storage:BlobServiceUri"]!),
    credential);

builder.Services.AddSingleton(blobService);

Locally, the credential can use your developer login. In Azure, it uses managed identity. The code does not change and no storage secret is deployed.

Authentication answers “who are you?” Authorisation answers “what may you do?” Managed identity authenticates the workload; Azure RBAC grants its permissions. Apply roles at the narrowest practical scope and remember that role changes can take time to propagate.

10. Configuration and secrets

Configuration such as feature thresholds, endpoint names and UI behaviour belongs in normal configuration or Azure App Configuration. Secrets, certificates and cryptographic keys belong in Key Vault. Neither should be hard-coded.

Azure App Configuration supports central key-values, labels for environment or version and feature flags. Key Vault protects secret material and supports RBAC, rotation and auditing.

In .NET:

builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential())
           .Select("ProjectNotes:*", labelFilter: environmentName)
           .ConfigureKeyVault(kv => kv.SetCredential(new DefaultAzureCredential()));
});

Do not turn Key Vault into a general configuration database. Avoid fetching a secret for every request; use supported configuration integration and sensible refresh. Never log secret values. Prefer references and identity over copying secrets between services.

11. Static front ends

Azure Static Web Apps provides source-connected deployment, global static content, routes, authentication integration and optional APIs. It is a good fit for Angular, React, Vue and static-generated sites. A plain Storage static website is simpler but offers fewer integrated application features.

For an enterprise SPA, I separate front-end authentication from API authorisation. Hiding a route in JavaScript is not security; the API validates tokens and permissions on every protected operation.

Keep environment configuration out of compiled secrets. A browser cannot keep a secret. Public API base URLs and client identifiers are configuration; privileged credentials belong on the server.

12. Azure Functions in 2026

Azure Functions runs event-driven code using triggers and bindings. Current Microsoft guidance recommends the Flex Consumption plan for new function apps. It offers event-driven scaling, virtual-network integration and pay-as-you-go billing. Premium suits always-warm performance, longer execution and predictable network requirements. Dedicated runs on App Service compute. Container Apps can host customised containerised functions beside microservices. The older Windows Consumption plan is now considered legacy for new applications.

A function should perform one bounded responsibility:

public sealed class AttachmentUploadedFunction(ILogger<AttachmentUploadedFunction> logger)
{
    [Function(nameof(AttachmentUploadedFunction))]
    public async Task Run(
        [ServiceBusTrigger("attachments", Connection = "ServiceBus")]
        ServiceBusReceivedMessage message,
        ServiceBusMessageActions actions,
        CancellationToken cancellationToken)
    {
        try
        {
            AttachmentUploaded command = message.Body.ToObjectFromJson<AttachmentUploaded>()
                ?? throw new InvalidDataException("Missing message body.");

            await ProcessOnce(command, message.MessageId, cancellationToken);
            await actions.CompleteMessageAsync(message, cancellationToken);
        }
        catch (PermanentMessageException exception)
        {
            logger.LogWarning(exception, "Dead-lettering message {MessageId}", message.MessageId);
            await actions.DeadLetterMessageAsync(message, cancellationToken: cancellationToken);
        }
    }
}

The real design questions are retry, duplicate delivery, poison messages, timeout and idempotency. Serverless removes server management, not distributed-system responsibility.

13. Logic Apps or Durable Functions?

Logic Apps is excellent for visible integration workflows using managed connectors, approvals and low-code orchestration. Durable Functions is code-first orchestration using function primitives and durable state.

Durable Functions supports patterns such as function chaining, fan-out/fan-in, async HTTP, monitoring and human interaction. Orchestrator code must be deterministic because the framework replays it to rebuild state. Do not read the current time, generate random values or call arbitrary network services directly inside an orchestrator; use durable context APIs and activity functions.

Choose Logic Apps when connector-rich workflow visibility and business integration dominate. Choose Durable Functions when developers need version-controlled code, testing and deeper control. Sometimes a Logic App invokes an API or function, but avoid splitting one simple workflow across services without a reason.

14. Containers: package once, choose the right host

A container image packages the application and its runtime dependencies. It does not automatically create microservices, security or portability without constraints.

Build a production-oriented ASP.NET Core image:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ProjectNotes.Api/ProjectNotes.Api.csproj ProjectNotes.Api/
RUN dotnet restore ProjectNotes.Api/ProjectNotes.Api.csproj
COPY . .
RUN dotnet publish ProjectNotes.Api/ProjectNotes.Api.csproj \
    -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "ProjectNotes.Api.dll"]

Pin and scan base images, run as non-root, keep build tools out of the final stage and do not bake secrets into layers.

Azure Container Registry stores private OCI images and other artefacts. Use managed identity between the runtime and registry rather than registry admin credentials. Structure registries around security, network and lifecycle boundaries; do not create one per tiny service without considering operational cost.

Create and push for a lab:

az acr create \
  --resource-group rg-developer-cloud-dev \
  --name YOURGLOBALLYUNIQUEREGISTRY \
  --sku Basic

az acr build \
  --registry YOURGLOBALLYUNIQUEREGISTRY \
  --image projectnotes-api:1.0.0 .

Immutable version tags are safer for releases than deploying latest. Record the image digest so you know exactly what ran.

15. Container Apps before Kubernetes

Azure Container Apps runs containers on a managed platform that abstracts Kubernetes. It provides ingress, revisions, traffic splitting, secrets, jobs, service discovery and event-driven scaling through KEDA-compatible rules.

A Container Apps environment is a network and operational boundary around apps and jobs. Current guidance describes workload-profile environments as the default, supporting consumption and dedicated profiles. Plan environment boundaries deliberately: apps inside one environment share important network and logging characteristics.

Revisions are immutable snapshots. Multiple-revision mode enables blue/green or canary traffic:

revision v1 -> 90% traffic
revision v2 -> 10% traffic

Observe errors, latency and business metrics.
Promote v2 or send traffic back to v1.

Container Apps can scale HTTP services and event consumers, including scale to zero where supported. A scale-to-zero service has cold-start implications. Minimum replicas cost money but reduce first-request delay.

Container Apps Jobs run finite tasks manually, on schedule or from events. Use them for migrations, batch processing and one-off units of work. Do not force a terminating job into an always-running web service.

Choose AKS only when you need Kubernetes APIs, ecosystem extensions, cluster-level control or a platform supporting many workloads—and have a team ready to secure, upgrade and operate it. Managed Kubernetes is still Kubernetes.

16. Storage is several services, not one disk

A general-purpose v2 storage account can contain Blob, Queue, Table and File data. The account defines namespace, redundancy, network and security boundaries.

Blob Storage

Blob Storage is object storage for files and unstructured data: attachments, media, exports, backups and analytical input. Block blobs suit normal files, append blobs suit append-oriented logs and page blobs support random-access scenarios such as virtual disks.

Select access tiers based on access frequency and retrieval requirements. Lifecycle policies can transition old blobs or delete them. Versioning, soft delete and change feed support recovery and event scenarios. Retention features cost storage, so match them to policy.

Upload using managed identity:

public sealed class AttachmentStore(BlobServiceClient serviceClient)
{
    public async Task<Uri> UploadAsync(
        Guid projectId,
        string fileName,
        Stream content,
        string contentType,
        CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
        string safeName = Path.GetFileName(fileName);
        string blobName = $"{projectId:N}/{Guid.NewGuid():N}-{safeName}";

        BlobContainerClient container = serviceClient.GetBlobContainerClient("attachments");
        BlobClient blob = container.GetBlobClient(blobName);

        await blob.UploadAsync(content, new BlobUploadOptions
        {
            HttpHeaders = new BlobHttpHeaders { ContentType = contentType }
        }, cancellationToken);

        return blob.Uri;
    }
}

The server chooses the storage path; it does not trust a client path. Add file-size limits, content validation, malware scanning and authorisation. A content type supplied by a browser is not proof of file content.

Table Storage

Table Storage is a schemaless key-value/NoSQL store organised around partition and row keys. It is useful for simple large datasets and fast key-oriented access. Design queries first: scans across partitions can be expensive. It does not offer relational joins or the same capabilities as Azure Cosmos DB.

Queue Storage

Queue Storage provides simple, large-scale asynchronous messages. It is useful when you need straightforward work queues and do not need Service Bus features such as topics, sessions or transactions.

Azure Files

Azure Files provides managed file shares via protocols including SMB and NFS in supported configurations. Use it when applications genuinely require a shared filesystem interface; do not use it as a substitute for object storage by habit.

17. Step by step: create secure storage

Add storage to Bicep:

resource storage 'Microsoft.Storage/storageAccounts@2025-06-01' = {
  name: 'stdevcloud${environmentName}${uniqueSuffix}'
  location: location
  tags: tags
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    allowBlobPublicAccess: false
    allowSharedKeyAccess: false
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
  }
}

resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-06-01' = {
  parent: storage
  name: 'default'
}

resource attachments 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-06-01' = {
  parent: blobService
  name: 'attachments'
  properties: {
    publicAccess: 'None'
  }
}

Then assign the web app’s principal the narrow Blob Data Contributor role at container or account scope. Management-plane Contributor does not automatically grant all data-plane access. Understand the distinction.

Local development can use Azurite for storage behaviour, but emulation never proves Azure identity, networking, quotas or service-level behaviour. Integration-test important paths against a controlled Azure environment.

18. Azure SQL and relational choices

Azure SQL Database is managed SQL Server technology for cloud applications. It handles database infrastructure, backups and many availability concerns while preserving relational modelling, transactions, indexes and T-SQL.

Other managed relational options include Azure Database for PostgreSQL and MySQL. The book mentions MariaDB, but Azure Database for MariaDB retired in 2025; this is exactly why cloud guidance must be checked against current service lifecycle notices.

Choose based on application compatibility, team skill, extensions, migration path and operational requirements—not brand preference.

For Project Notes, Azure SQL is a natural fit. Configure Entity Framework Core with token-based authentication rather than a SQL password. In Azure, the app’s managed identity receives database permissions. Locally, your Entra developer identity can connect when authorised.

Important database lessons:

  • place constraints in the database as well as validation in code;
  • use indexes based on measured query patterns;
  • keep transactions short;
  • retry only transient errors and understand transaction replay;
  • use optimistic concurrency where appropriate;
  • test migrations and run them as a controlled deployment step;
  • configure retention and practise restore, not merely backup;
  • avoid opening the firewall to the entire internet for convenience.
Serverless compute can reduce cost for intermittent databases, but resume latency and minimum billing affect behaviour. Production databases often favour provisioned predictable capacity.

19. Messaging: choose semantics, not just throughput

Messaging decouples producers from consumers. The producer records intent without waiting for all downstream work. This improves resilience, but introduces eventual consistency, duplicate delivery and operational queues.

Queue Storage

Choose for simple work distribution at large scale when advanced broker semantics are unnecessary.

Azure Service Bus

Choose for enterprise commands and integration. Queues provide competing consumers. Topics copy messages to subscriptions, each with filters and independent processing. Sessions support ordered processing for a related key. Dead-letter queues isolate messages that cannot be processed. Duplicate detection protects repeated sends within a configured window.

Current Microsoft guidance is explicit: peek-lock provides at-least-once delivery, so processing can repeat. Receive-and-delete risks message loss if the consumer fails after delivery. For important work, use peek-lock and idempotent consumers.

public async Task HandleAsync(ProcessAttachment message, string messageId, CancellationToken ct)
{
    if (await inbox.HasProcessedAsync(messageId, ct)) return;

    await transaction.ExecuteAsync(async () =>
    {
        await processor.ProcessAsync(message, ct);
        await inbox.RecordProcessedAsync(messageId, ct);
    }, ct);
}

The broker cannot make your database side effect exactly once. Idempotency belongs in application design.

Event Hubs

Event Hubs is a high-throughput event-ingestion and streaming platform. Partitions scale ordered streams; consumer groups allow separate applications to read the same event log independently. Capture can persist streams to storage for later analytics.

Use Service Bus for commands and business workflows. Use Event Hubs for telemetry and event streams. They are not interchangeable simply because both move messages.

20. Networking: public endpoint is a design choice

Azure networking can become its own career, but a developer needs the core model.

A virtual network contains subnets. Network security groups filter traffic at network boundaries. Private endpoints give supported PaaS services a private IP in your VNet. Private DNS resolves their service names to that IP. App Service VNet integration primarily supports outbound access from the app into a VNet; a private endpoint handles private inbound access to the app.

Do not disable public access until DNS, build agents, administration and service dependencies have a working private path. “Private endpoint enabled” without correct DNS is a classic outage.

For public APIs, place an intentional edge in front where needed: Azure Front Door for global routing and web application firewall capabilities, or Application Gateway for regional layer-seven routing. API Management adds API policies, subscriptions, transformations, quotas and developer-facing management. Do not deploy every gateway for a small internal service; choose based on threat model and platform needs.

21. Security as a normal development activity

My baseline checklist is:

  1. Entra ID authenticates users and workloads.
  2. Managed identities replace stored Azure credentials.
  3. RBAC grants least privilege at narrow scope.
  4. Key Vault stores genuine secrets and keys.
  5. Public network access is reviewed, not accepted by default.
  6. TLS is enforced.
  7. Defender for Cloud recommendations and vulnerability results are triaged.
  8. Images and dependencies are scanned.
  9. Diagnostic and audit logs have appropriate retention.
  10. Production access uses groups, privileged workflows and separation of duties.
Access keys and connection strings are sometimes unavoidable for third-party systems. When used, store them in Key Vault, rotate them and limit scope. Never place them in appsettings.json, pipeline YAML, container images or browser code.

22. Monitoring with Azure Monitor and Application Insights

Azure Monitor is the umbrella platform for metrics, logs, traces and alerts. Log Analytics workspaces store queryable logs using Kusto Query Language. Application Insights provides application performance monitoring and distributed tracing.

In 2026, OpenTelemetry is the strategic instrumentation approach. The Azure Monitor OpenTelemetry distribution supports .NET and other languages.

builder.Services.AddOpenTelemetry()
    .UseAzureMonitor(options =>
    {
        options.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
    });

Instrument requests, dependencies, exceptions, traces and business operations. Add correlation across HTTP and messaging. Avoid high-cardinality metric dimensions such as raw customer IDs; they can make metrics expensive and difficult to use. Never send secrets or unnecessary personal data into telemetry.

Start with four signals:

  • latency;
  • traffic;
  • errors;
  • saturation.
Then add business signals: projects created, attachments processed, queue age and failed workflows. Alert on symptoms affecting users, not every noisy event. An alert needs an owner, severity, runbook and route.

A useful KQL query might be:

requests
| where timestamp > ago(1h)
| summarize Requests=count(), Failures=countif(success == false), P95=percentile(duration, 95)
    by operation_Name
| order by Failures desc

Dashboards are not observability if nobody knows what action to take.

23. Reliability is designed through failure modes

Cloud services fail in partial ways: one request times out, one zone has trouble, a token expires, a deployment is unhealthy or a downstream service throttles. Design around specific failure modes rather than saying “Azure is highly available.”

Use timeouts on every remote call. Retry only transient failures, with bounded exponential backoff and jitter. Do not retry validation errors or an unprotected payment operation. Use circuit breakers to avoid hammering an unhealthy dependency. Use queues to absorb bursts where eventual processing is acceptable.

Health checks need meaning:

  • liveness asks whether the process should be restarted;
  • readiness asks whether it can serve traffic;
  • dependency diagnostics help operators but should not make every temporary downstream fault restart the application.
Availability zones can protect supported zonal services from a datacentre failure. Multi-region design can protect against broader failures but introduces routing, replicated data, consistency and failover testing. Begin with business recovery objectives: RTO is acceptable recovery time; RPO is acceptable data loss measured in time.

Backups are not recovery until you restore successfully. Document who declares an incident, who initiates failover, how data is validated and how traffic returns.

24. CI/CD: build once, promote with evidence

A delivery pipeline should validate source, build a versioned artefact, scan and test it, deploy infrastructure, deploy code, verify health and retain a rollback path.

pull request
  -> restore and compile
  -> unit and architecture tests
  -> dependency and secret scan
  -> build immutable package/container
  -> deploy to development
  -> integration and smoke tests
  -> approval
  -> deploy or swap production
  -> post-deployment verification

Do not rebuild different binaries for test and production. Promote the same artefact with environment-specific configuration.

GitHub Actions should authenticate through workload identity federation. The workflow receives short-lived permission to a selected Azure scope. Avoid publish profiles and long-lived service-principal secrets when federation is available.

permissions:
  id-token: write
  contents: read

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-dotnet@v4
    with:
      dotnet-version: '10.0.x'
  - run: dotnet test --configuration Release
  - uses: azure/login@v2
    with:
      client-id: ${{ vars.AZURE_CLIENT_ID }}
      tenant-id: ${{ vars.AZURE_TENANT_ID }}
      subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
  - run: az deployment group create --resource-group ${{ vars.RESOURCE_GROUP }} --template-file infra/main.bicep --parameters environmentName=prod

Pin third-party actions to trusted versions or commit SHAs according to your supply-chain policy. Protect production environments with required reviewers. Keep deployment identities narrowly scoped.

Azure DevOps pipelines provide an equally valid route. Choose the platform your organisation can operate consistently rather than mixing systems without need.

25. Testing cloud applications

Cloud tests form layers.

Unit tests exercise domain logic without Azure. Component tests run the API with controlled substitutes. Emulator tests use Azurite or supported local services for rapid feedback. Integration tests exercise real Azure identity and service behaviour in an isolated environment. Smoke tests verify a deployed release. Resilience tests confirm retry, duplicate and outage behaviour. Load tests validate scale and cost assumptions.

An emulator cannot prove private DNS, managed identity, RBAC, quotas, throttling or actual service semantics. Use it for speed, not false confidence.

Ephemeral test environments are powerful when infrastructure is automated. A pull request can deploy a short-lived resource group, run tests and delete it. Set cleanup automation and expiry tags so failed pipelines do not leave expensive resources running.

26. Azure OpenAI and the modern AI layer

The book introduces Azure OpenAI Service. Since then, Microsoft’s AI platform has continued to evolve under Microsoft Foundry. For a general Azure developer, the important architecture remains stable: a model is a dependency inside a governed application.

Use a managed identity where supported, restrict network access, validate input and output, evaluate on representative cases and monitor token cost, latency and quality. Retrieval-Augmented Generation commonly combines a model with Azure AI Search and authorised enterprise data.

Do not put a model directly in charge of a privileged database operation. Expose narrow tools through application services that enforce identity, validation and business rules. Content filters and guardrails are layers, not replacements for authorisation.

Azure Machine Learning remains relevant for deeper machine-learning lifecycle work: datasets, training jobs, model registry, managed endpoints and MLOps. Use Foundry/Azure OpenAI for managed generative-AI application capabilities; use Azure Machine Learning when training and lifecycle control require it. Verify current product boundaries because this area changes quickly.

27. Cost engineering

Every architecture choice has a cost shape.

App Service plans charge for provisioned compute even when an application is quiet. Functions Flex Consumption and Container Apps consumption can follow demand and scale down, but executions, networking and supporting services still cost. Databases, logs, outbound data and private networking can surprise teams more than web compute.

For each service, document:

  • pricing unit;
  • minimum running cost;
  • scale limit and scale trigger;
  • storage and transaction charges;
  • network ingress and egress;
  • backup and log retention;
  • development and production SKU differences;
  • owner and budget.
Cost optimisation is not “choose the cheapest tier.” Under-sized systems create incidents and engineering cost. Over-sized production copied into every development environment wastes money. Use autoscale carefully, shut down disposable resources, apply retention, right-size from telemetry and review reservations or savings plans only for stable usage.

28. A complete step-by-step master plan

Here is the route I would give a developer joining my team.

Phase 1: Establish the cloud boundary

  1. Confirm tenant, subscription, region and account permissions.
  2. Create separate development and production subscriptions or strong equivalent boundaries.
  3. Define naming, tags, approved regions and budget alerts.
  4. Create Entra groups for readers, contributors, deployers and operators.
  5. Avoid granting broad subscription Owner access to everyday accounts.
The output is a documented landing zone for the application—not yet an application.

Phase 2: Make infrastructure repeatable

  1. Create infra/main.bicep and environment parameter files.
  2. Add a resource group deployment through what-if and reviewed apply.
  3. Define Log Analytics and Application Insights first so new services can emit telemetry.
  4. Store no secrets in parameter files.
  5. Add linting and validation to the pull request pipeline.
At this stage, another developer should be able to create the development environment without portal archaeology.

Phase 3: Deploy the smallest vertical slice

  1. Create an App Service plan and Linux web app for .NET 10.
  2. Enable HTTPS-only, current TLS, managed identity and health checks.
  3. Deploy a single /api/projects endpoint.
  4. Send traces to Application Insights.
  5. Add a smoke test against the deployed endpoint.
Do not add ten services before one request works end to end.

Phase 4: Add data securely

  1. Create Azure SQL with environment-appropriate compute and backup settings.
  2. Create a private or tightly restricted network path according to the architecture.
  3. Grant the app identity minimum database permissions.
  4. Apply EF Core migrations through a controlled deployment step.
  5. Test restore and document RPO/RTO.
Then add Blob Storage:
  1. Disable public blob access and shared-key access where compatible.
  2. Create a private attachments container.
  3. grant the app identity only necessary blob data permissions;
  4. validate type and size, scan uploads and generate server-side names;
  5. configure retention, lifecycle and recovery.

Phase 5: Decouple background work

  1. Create a Service Bus namespace and queue.
  2. Use a message schema with version, message ID, correlation ID and occurred time.
  3. Grant producer and consumer identities separate roles.
  4. Process with peek-lock and complete only after success.
  5. Make the consumer idempotent.
  6. Define retry and dead-letter handling with alerts.
  7. Monitor queue depth, oldest-message age and failure rate.

Phase 6: Centralise safe configuration

  1. Put secrets and certificates in Key Vault.
  2. Put normal shared configuration and feature flags in App Configuration.
  3. Access both using managed identity.
  4. Give each environment separate values and permissions.
  5. rehearse secret rotation without a code change;
  6. audit access and expiry.

Phase 7: Improve delivery safety

  1. Build one immutable artefact per commit.
  2. Authenticate pipelines through OIDC federation.
  3. run unit, integration, security and smoke tests;
  4. deploy infrastructure before application;
  5. use a staging slot or Container Apps revision for production validation;
  6. shift traffic gradually where risk warrants it;
  7. verify telemetry and roll back automatically or manually through a tested path.

Phase 8: Harden networking and identity

  1. Draw all inbound and outbound flows.
  2. Remove unused public endpoints.
  3. Add private endpoints and private DNS where the threat model requires them.
  4. Restrict outbound connectivity when appropriate.
  5. review RBAC assignments and privileged roles;
  6. enable relevant Defender plans and respond to findings;
  7. perform threat modelling and penetration testing.

Phase 9: Operate, learn and optimise

  1. Define service-level indicators for availability, latency and correctness.
  2. Add alerts tied to user impact and runbooks.
  3. build dashboards for technical and business outcomes;
  4. run load and resilience tests;
  5. review cost by tag and environment;
  6. conduct incident reviews without blame and add regression protection;
  7. remove obsolete resources, identities, secrets and feature flags.
Cloud maturity is day-two operation, not the number of resources deployed.

29. A service-selection conversation

When a junior asks, “Which Azure service do I use?”, I ask about the workload.

“I have an ASP.NET Core API.” Start with App Service unless containers, event scaling or Kubernetes requirements say otherwise.

“I need to process a blob upload.” Consider an Azure Function triggered by the event, with idempotent processing.

“I have five containerised APIs and workers.” Consider Container Apps, environment boundaries, revisions and KEDA scaling.

“We need a business workflow through Office and SaaS connectors.” Consider Logic Apps.

“We need a code-heavy long-running stateful orchestration.” Consider Durable Functions.

“We need commands between services.” Consider Service Bus.

“We ingest millions of telemetry events.” Consider Event Hubs.

“We store files.” Blob Storage, not a relational varbinary(max) column by reflex.

“We need relational transactions and joins.” Azure SQL or another managed relational engine.

“We need globally distributed document data.” Evaluate Cosmos DB from access patterns, consistency and partition-key design—not because NoSQL sounds scalable.

30. Common mistakes I want you to avoid

  1. Building production through portal clicks with no infrastructure source.
  2. Working in the wrong subscription context.
  3. Choosing a service before understanding workload shape.
  4. Using AKS for a small application without Kubernetes requirements.
  5. Storing keys and connection strings in source or pipeline variables unnecessarily.
  6. Granting Contributor when a narrow data role is enough.
  7. Confusing management-plane access with data-plane access.
  8. Sharing one App Service plan among unrelated workloads without considering shared scale.
  9. Treating serverless as unlimited, instant and free.
  10. Designing Functions without idempotency and poison-message handling.
  11. Using Service Bus receive-and-delete for work that cannot be lost.
  12. Assuming duplicate detection replaces idempotent consumers.
  13. Deploying latest container tags.
  14. Enabling private endpoints without planning DNS and deployment access.
  15. Logging secrets, tokens or personal data.
  16. Keeping all telemetry forever without cost and privacy policy.
  17. Creating alerts with no owner or action.
  18. Assuming a backup works without a restore test.
  19. Rebuilding artefacts differently for each environment.
  20. Copying a 2025 command without checking a retired service or current API version.
  21. Leaving learning resources running after the exercise.
  22. Calling a system “highly available” without tested recovery objectives.

31. What has changed since the book

The July 2025 edition remains a strong practical reference, but cloud platforms move quickly. The most important current adjustments I have made are:

  • Azure Functions documentation now recommends Flex Consumption for new apps and identifies the older Windows Consumption plan as legacy.
  • Container Apps workload-profile environments are the current default model, with consumption and dedicated profiles.
  • Container Apps Jobs and revision operations have continued to mature as first-class deployment and finite-work patterns.
  • Azure Monitor guidance increasingly centres OpenTelemetry instrumentation.
  • Azure Developer CLI has become a useful higher-level application lifecycle tool alongside Azure CLI.
  • App Service documentation now includes newer hosting capabilities such as Managed Instance in preview for certain legacy Windows dependencies; previews require careful production review.
  • Azure Database for MariaDB is retired, so use a supported relational service rather than following an outdated creation path.
  • Messaging guidance continues to emphasise peek-lock, idempotency, sessions for ordering and explicit dead-letter operations.
Always read the current product page, region availability, limits, pricing and retirement notices before committing an architecture.

Final guidance from Faz

Azure mastery is not knowing the names of two hundred services. It is knowing how to take a business capability from a developer’s laptop into a secure, repeatable and observable production environment.

Start with managed services. Give every workload an identity. Store no secret you can replace with a token. Declare infrastructure. Decouple where failure and scale require it, not because diagrams look impressive. Treat messages as potentially repeated. Test restores. Observe business outcomes. Keep cost visible. Make every production change reversible.

The mental model I want you to carry is this:

Identity before credentials.
Infrastructure as code before manual environments.
Simple compute before orchestration platforms.
Explicit data ownership before storage selection.
Idempotency before asynchronous scale.
Telemetry before production traffic.
Recovery plans before failure.
Evidence before optimisation.

Build the Project Notes system one vertical slice at a time. When each layer works, explain why it exists, what it costs, how it fails and how you would remove it. If you can do that, Azure is no longer a confusing cloud catalogue. It is a set of engineering tools you know how to compose responsibly.

32. Mentoring walkthrough: design the first production slice of Project Notes

The Project Notes application needs one initial capability: an authenticated team member creates a note containing text and an optional attachment, and colleagues in the same project can read it. Creating a note must remain responsive even when attachment scanning or notification delivery is slow.

Junior: “Which Azure services should we put on the architecture diagram?”

Senior: “Start with the business and quality requirements. Services are implementation decisions, not the problem statement.”

Write the first requirements in measurable language:

  • only project members can read or create notes;
  • note creation should normally complete within one second, excluding attachment upload;
  • attachments may be up to an agreed size and must be scanned before download;
  • a notification may be delayed, but a committed note must not disappear because notification delivery fails;
  • the development environment can tolerate a short outage; production has explicit availability and recovery targets;
  • every operation must be traceable without logging note content or access tokens;
  • the team needs a predictable monthly cost envelope.
Those statements influence architecture. They do not automatically require microservices, Kubernetes or multiple regions.

Choose the simplest compute that satisfies the workload

For a conventional ASP.NET Core API with continuous HTTP traffic, App Service is a sensible starting point. It offers managed hosting, deployment slots, built-in identity support, TLS integration and straightforward scaling without asking the team to operate a container orchestrator.

Container Apps becomes attractive when the workload is already containerised, needs revision-based traffic splitting, event-driven scaling, several independently deployed APIs or workers, or finite jobs. AKS provides deeper Kubernetes control, but that control includes cluster, networking, policy, upgrade and operational responsibility. Select it because Kubernetes capabilities are required, not because the application runs in a container.

Junior: “Would Functions make the API cheaper because it is serverless?”

Senior: “Perhaps for bursty event-driven work, but cost is only one axis. Evaluate execution duration, latency, networking, scaling, deployment and operational needs. We can use a Function for attachment processing without forcing the interactive API into the same model.”

Our first design uses:

  • App Service for the ASP.NET Core API;
  • Azure SQL for relational project, membership and note data;
  • Blob Storage for attachments;
  • Service Bus for durable background commands/events;
  • an Azure Function or Container Apps worker for scanning and notification work;
  • Key Vault only for secrets that cannot be replaced by identity;
  • App Configuration for ordinary shared settings or feature flags where justified;
  • Application Insights and Log Analytics for observability;
  • Bicep and a federated pipeline identity for repeatable environments.
This is still a distributed system. Keep the number of independently failing parts proportional to the requirement.

Draw trust and data boundaries

The browser is untrusted. It authenticates the user and sends an access token intended for the API. The API validates the token and authorises the project resource; knowing a note or project ID is not permission.

The API's managed identity accesses Azure SQL, Blob Storage, Service Bus, Key Vault and configuration using narrow roles or database permissions. Do not place storage keys and Service Bus connection strings in application settings merely because tutorials show them. DefaultAzureCredential provides a convenient credential chain, but understand which credential wins in each environment.

builder.Services.AddAzureClients(clients =>
{
    clients.AddBlobServiceClient(new Uri(configuration["Storage:ServiceUri"]!));
    clients.AddServiceBusClientWithNamespace(configuration["ServiceBus:Namespace"]!);
    clients.UseCredential(new DefaultAzureCredential());
});

During local development, the credential may use the developer's Azure CLI or IDE identity. In App Service, it can use the workload's managed identity. The same source code works without pretending the identities have the same permissions.

Junior: “If my local account has Contributor, won't the application work?”

Senior: “Contributor is primarily a management-plane role and does not automatically grant every data-plane operation. More importantly, broad personal access can hide the narrow permissions the deployed workload needs.”

Create a development identity setup that resembles production capability. Test denial as well as success: the API identity should write only the intended container, send only to the intended queue and access only its database.

Keep note creation transactional without coupling every service

The relational transaction can create the note and an outbox record together. A publisher later sends the durable message to Service Bus. This avoids the dangerous sequence “commit SQL, then attempt to send, then lose the notification when the process crashes.”

await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);

db.Notes.Add(note);
db.OutboxMessages.Add(OutboxMessage.From(new NoteCreated(
    note.Id,
    note.ProjectId,
    currentUser.Id,
    clock.UtcNow)));

await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);

The outbox dispatcher sends with a deterministic message ID based on the event identity. Azure Service Bus duplicate detection can protect against repeated sends inside its configured history window on supported tiers, but it does not replace idempotent consumers. Peek-lock delivery can occur again when a handler fails after producing an effect but before completing the message.

Junior: “If Service Bus says duplicate detection is enabled, why must the worker still be idempotent?”

Senior: “Send-side deduplication handles repeated sends with the same message ID during a bounded window. Receive-side redelivery is a different failure path.”

The notification consumer can record processed event IDs or make its downstream operation idempotent. If it sends an email through a provider without an idempotency facility, decide what duplicate risk is acceptable and where to persist the send state.

Design attachment upload as a stateful workflow

Do not send a large attachment through the API merely by habit. A common design is:

  1. client asks the API to begin an upload;
  2. API authorises project access and creates an attachment record in PendingUpload;
  3. API returns a narrowly scoped, short-lived upload mechanism;
  4. client uploads directly to a private staging container;
  5. an event triggers scanning;
  6. scanner validates content and records Available or Rejected;
  7. downloads are authorised through the API and expose only approved files.
The exact mechanism depends on security requirements. A user-delegation SAS can grant bounded access without sharing the account key, but it remains a bearer token until expiry. Limit service, resource, action, time and network conditions where supported. Never log the full URL.

File validation should not trust the extension or browser-provided content type. Apply size limits, generate server-side names, inspect signatures where appropriate, scan content and store the original display name separately. Prevent an uploaded file from becoming executable active content under your application origin.

Junior: “Can we show the attachment as soon as the upload returns 201?”

Senior: “Show that upload completed, but distinguish it from scan approval. The business state is Processing, not yet Available.”

This makes eventual consistency visible rather than confusing. If scanning fails transiently, retry under a policy and monitor oldest pending age. If it fails permanently, quarantine or reject with a safe user message and a support reference.

Use Service Bus deliberately

For the scan command and notification event, define a versioned envelope:

public sealed record MessageEnvelope<T>(
    string MessageId,
    string MessageType,
    int SchemaVersion,
    string CorrelationId,
    DateTimeOffset OccurredAt,
    T Payload);

Keep payloads small and free of sensitive note text when consumers can load authorised data by ID. Set MessageId from a stable business/event identifier so a retry can reproduce it. Choose time-to-live from business usefulness and configure dead-letter handling as an operational workflow, not a forgotten queue.

Ordering is not guaranteed merely because messages enter one queue. If per-attachment or per-project ordering is required, evaluate Service Bus sessions and their operational impact. If commands are independent, avoid imposing global ordering that restricts throughput.

Monitor:

  • active message count;
  • oldest message age;
  • dead-letter count and reason;
  • delivery count;
  • handler duration and failure classification;
  • lock loss;
  • end-to-end time from note commit to attachment availability.
Queue length alone can look healthy while one old poison message repeatedly fails. Age and business-level completion latency reveal more.

Make the database choice from consistency and access patterns

Project membership, notes and attachment state have relational constraints and transactional relationships, so Azure SQL is a natural initial choice. That does not mean every query should materialise entity graphs.

Shape read models:

var notes = await db.Notes
    .Where(note => note.ProjectId == projectId)
    .OrderByDescending(note => note.CreatedAt)
    .Select(note => new NoteListItem(
        note.Id,
        note.Title,
        note.Author.DisplayName,
        note.CreatedAt,
        note.Attachments.Count(a => a.Status == AttachmentStatus.Available)))
    .Take(pageSize)
    .ToListAsync(cancellationToken);

Authorisation must be part of the query or a proven preceding policy scoped to the same project. Test with realistic data, inspect generated SQL and actual execution plans, and index for the access path. Scaling the App Service will not repair an unbounded query.

Define backup retention, geo-redundancy and restore objectives from business requirements. A platform checkbox is not recovery evidence. Restore a copy, verify consistency and measure how long the team takes to make it usable.

Instrument the vertical slice

Propagate W3C trace context through HTTP and messaging. Record spans for authorization, database work, blob operations and message handling. Add structured events without note body or raw tokens.

Useful application metrics include:

  • note-creation success and latency;
  • authorization denials by bounded route classification;
  • attachment upload-to-available duration;
  • scanning rejection and technical failure rates;
  • outbox age;
  • notification completion latency.
Do not turn user ID, project ID or URL into high-cardinality metric dimensions. Keep such identifiers in access-controlled logs when necessary for investigation, following retention and privacy policy.

An alert should map to user impact and a runbook. “CPU over 70% for five minutes” may be useful, but “note creation failure rate exceeds target” is closer to the business capability. Combine symptoms and causes rather than paging for every transient platform signal.

33. Apply the Well-Architected pillars as trade-offs

Microsoft's Well-Architected Framework uses Reliability, Security, Cost Optimization, Operational Excellence and Performance Efficiency. Do not turn the pillars into a ceremonial checklist completed after architecture. Use them to expose competing consequences of a decision.

Consider private endpoints for Azure SQL and Storage.

Security: public exposure can be reduced and network paths constrained.

Reliability: private DNS, virtual-network integration and deployment-agent access create additional dependencies that must work during deployment and recovery.

Cost: endpoints, networking components and operational effort add cost.

Operational Excellence: teams need diagnostics for DNS, routing and identity rather than assuming every timeout is an application defect.

Performance Efficiency: network topology and name resolution can affect latency and connection behaviour.

The right decision depends on the threat model and organisational landing zone. “Private is always better” ignores operability; “TLS over public endpoints is enough” may ignore compliance and exfiltration requirements.

Reliability should match business criticality

Do not copy a multi-region active-active architecture into every internal tool. Define availability, recovery point and recovery time targets. Then design dependencies, data replication, failover and operational practice to meet them.

For Project Notes, production might initially use zone-redundant capabilities where supported, tested database restore, durable messaging and a documented regional recovery procedure rather than active-active writes across regions. If the business later requires near-continuous regional resilience, architecture and cost must change together.

Junior: “Azure SQL and Service Bus are managed services. Doesn't Microsoft handle reliability?”

Senior: “Microsoft operates the service, but we still choose tier, redundancy, retry behaviour, data model, regions, recovery and how our application responds to failure.”

Every retry needs a budget. Exponential backoff with jitter helps transient faults, but retrying a 403 or invalid request wastes time. Ensure HTTP requests and message handlers have timeouts and cancellation. Combine retries with idempotency so resilience does not duplicate business effects.

Cost is an architectural signal

Create a cost model before production:

  • base compute and minimum replicas;
  • database tier, storage and backup;
  • Service Bus tier and operations;
  • telemetry ingestion and retention;
  • private networking;
  • egress and cross-region traffic;
  • non-production environment hours;
  • security and support plans;
  • expected growth.
Tagging helps allocation but does not enforce good design. Configure budgets and alerts, remove abandoned resources, scale non-production appropriately and sample or retain telemetry according to diagnostic value. Do not reduce retention so far that incidents become unknowable.

Operational excellence starts before handover

The development team should participate in runbooks, dashboards, alerts, deployment and incident learning. A runbook for “attachments remain Processing” should identify:

  1. dashboard and query;
  2. Service Bus entity and dead-letter path;
  3. scanner health and deployment version;
  4. identity and storage diagnostics;
  5. safe replay procedure;
  6. user communication;
  7. escalation owner;
  8. evidence to preserve for review.
If recovery requires a particular developer's portal access and memory, the service is not operationally mature.

34. Incident mentoring: notes are saved but notifications stopped

At 10:20, support reports that new notes appear correctly but colleagues receive no notifications. API latency and availability look normal.

Junior: “Should we restart the web app?”

Senior: “The successful user transaction suggests the API and database are healthy. Trace the asynchronous path before restarting unrelated compute.”

Investigate in order:

  1. confirm note and outbox rows exist;
  2. measure oldest unpublished outbox age;
  3. confirm the dispatcher is running and authorised to send;
  4. inspect Service Bus active and dead-letter counts;
  5. inspect worker health, errors and deployment version;
  6. trace one message ID through delivery attempts;
  7. inspect the downstream notification provider;
  8. determine whether any notifications were sent but acknowledgements failed.
Suppose outbox age is normal, messages reach the queue, but every delivery moves to the dead-letter queue after repeated schema-deserialisation failure. A worker deployment expects schema version two while the API still emits version one.

The immediate recovery might be to roll back the worker, deploy a backward-compatible consumer, or replay dead-lettered messages after correction. Do not purge the dead-letter queue to make the alert green. Preserve message IDs and failure reasons, and replay through a controlled tool that remains idempotent.

Junior: “Why did deployment succeed if the applications were incompatible?”

Senior: “Infrastructure and process health cannot prove an integration contract. We need compatibility tests and a rollout sequence.”

Add a contract test containing supported message versions. Consumers should normally tolerate a compatibility window. Deploy additive consumers before producers emit the new shape; remove old support only after old messages and producers are gone. Include schema version in telemetry.

The incident review should improve:

  • producer–consumer contract testing;
  • deployment ordering;
  • dead-letter alerts based on age and count;
  • replay tooling;
  • runbook clarity;
  • dashboard linkage from note ID to message ID;
  • rollback rehearsal.
Avoid blaming the developer who changed the record. The system allowed incompatible versions to reach production without a guard. Repair the delivery system as well as the code.

35. Code-review and practice questions

When reviewing an Azure-backed .NET slice, ask:

  1. Which business outcome and quality targets drive this design?
  2. Why was this compute service selected over simpler or more controllable alternatives?
  3. Which identities exist, and what is each one's narrowest required scope?
  4. Which values are secrets, and can identity remove them?
  5. Where is relational consistency required?
  6. Which operations are eventually consistent, and does the UI explain that?
  7. What happens if an acknowledgement is lost after a message or database operation succeeds?
  8. How are consumers made idempotent?
  9. What is the poison-message and dead-letter recovery process?
  10. Which signals reveal user impact before support reports it?
  11. How are cost and telemetry retention controlled?
  12. What is the tested restore or regional recovery procedure?
  13. Can the previous application version coexist with the new infrastructure and message schema?
  14. Which network and DNS dependencies affect deployment and recovery?
  15. What evidence would justify a more complex service such as AKS?

Practice exercise

Build the note-creation slice in an approved learning subscription. Use Bicep, managed identity and an immutable application artefact. Then deliberately create these failures one at a time:

  • remove the blob data role;
  • use an invalid Service Bus entity name;
  • make the worker reject the message schema;
  • allow a lock to expire during processing;
  • deny outbound access to one dependency;
  • make the database query unbounded;
  • exceed the attachment size limit;
  • stop the outbox dispatcher;
  • deploy a stale App Configuration value;
  • restore the database into a clean environment.
For each experiment, predict the user-visible state and telemetry before running it. Record the actual evidence, identify the authoritative state, recover without losing or duplicating work, and add a durable guard. This is how cloud knowledge moves from product vocabulary to engineering judgement.

36. Production-readiness clinic: configuration, health and safe release

The Project Notes resources now exist and the happy path works. That is the moment when teams are tempted to call the workload production-ready. A mentor should instead ask how configuration changes, dependencies fail and releases recover.

Classify configuration before choosing a store

Create a small inventory:

ValueClassificationExample ownerChange behaviour
Storage service URINon-secret environment configurationPlatform teamChanges during infrastructure migration
Notification feature flagOperational application configurationProduct/operationsChanges without deployment under control
External provider API credentialSecretSecurity/service ownerRotates and expires
Maximum attachment sizeBusiness/operational policyProduct and securityVersioned and validated
Database passwordPrefer to eliminate through identityPlatform/securityEmergency fallback only if required
Do not put every setting in Key Vault. Secrets need stronger handling; ordinary configuration needs visibility, validation and controlled change. App Configuration can centralise shared non-secret values and feature flags, while Key Vault stores secrets and certificates that remain necessary. Small applications may keep ordinary environment configuration directly in App Service settings without adding another dependency.

Junior: “If Key Vault references are in App Service settings, does the application need the secret value in its configuration file?”

Senior: “No. The platform resolves the reference using the app identity, and the setting exposes the resolved value to the process. We still need narrow vault permission, rotation testing and diagnostics for resolution failure.”

Avoid logging the resulting configuration object. Validate required settings at startup without echoing secrets:

builder.Services
    .AddOptions<AttachmentOptions>()
    .BindConfiguration("Attachments")
    .Validate(options => options.MaximumBytes is > 0 and <= 100_000_000,
        "Attachment size limit is outside the supported range.")
    .ValidateOnStart();

Failing clearly at startup is often better than accepting traffic and failing later. However, a temporary Key Vault or configuration outage during restart can affect availability. Define caching, refresh and recovery behaviour rather than assuming configuration is permanently reachable.

Separate liveness, readiness and functional verification

A liveness check asks whether the process should be restarted. A readiness check asks whether this instance should receive traffic. A functional smoke test asks whether a user-critical path works.

Do not make liveness depend on every remote service; a brief database outage could cause all instances to restart together and increase pressure. Readiness may consider critical dependencies, but keep checks fast, bounded and resistant to causing a secondary outage.

builder.Services.AddHealthChecks()
    .AddCheck<StartupCompletedCheck>("startup", tags: ["live"])
    .AddSqlServer(
        configuration.GetConnectionString("ProjectNotes")!,
        name: "sql",
        timeout: TimeSpan.FromSeconds(2),
        tags: ["ready"]);

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = registration => registration.Tags.Contains("live")
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = registration => registration.Tags.Contains("ready")
});

Protect detailed dependency information from anonymous callers. The externally visible response can be minimal while telemetry records diagnostic detail.

Junior: “Should readiness fail when notifications are unavailable?”

Senior: “Not if the API can safely accept notes through the outbox. Marking it unready would turn a degraded asynchronous capability into total API downtime.”

This is where architecture and health semantics meet. Name which capabilities are critical, degradable or optional.

Use deployment slots with a compatibility plan

An App Service deployment slot lets the team deploy and warm a version before swapping traffic. It does not make incompatible database or message changes safe automatically.

Before a swap:

  1. deploy additive infrastructure and database changes;
  2. ensure old and new application versions work with the transitional schema;
  3. mark environment-specific settings as slot settings where appropriate;
  4. warm the application and run smoke tests against the slot;
  5. verify identity, networking, configuration and telemetry;
  6. review current error and latency signals;
  7. swap under an observed release window;
  8. retain a proven recovery path.
Do not store a destructive migration only in application startup and hope the slot isolates it. Both slots may point to the same database. Run migrations as an explicit controlled step with backward-compatible expand-and-contract sequencing.

Rehearse rotation instead of documenting intention

For any remaining secret, test rotation:

  • create a new credential or version;
  • update the authoritative reference;
  • verify applications obtain it without exposing it;
  • confirm old and new versions coexist if refresh is delayed;
  • revoke the old credential;
  • observe failures and alerts;
  • record actual completion time and ownership.
If rotation causes downtime because a process reads configuration only at startup, decide whether restart orchestration is acceptable. Do not discover the behaviour during emergency revocation.

Practise a failed release

Introduce a safe failure in a non-production slot: an invalid storage URI, denied identity role or incompatible message producer. Confirm readiness, smoke tests or contract tests block promotion. Then practise recovery and answer:

  • Was any irreversible data change made?
  • Can traffic return to the previous version?
  • Are queued messages compatible with the previous consumer?
  • Did feature flags change independently of the artefact?
  • Which telemetry proves recovery?
  • Who declares the incident resolved?
Junior: “If swapping back makes the dashboard green, are we done?”

Senior: “We have restored service, but we still need to inspect data, queues, partial effects and the cause. Rollback is a recovery action, not an investigation.”

This final clinic ties the cloud platform to engineering discipline. Configuration must have ownership, health must represent capability, releases must preserve compatibility, secret rotation must be exercised and rollback must account for state. Azure provides mechanisms; the team supplies the operating model that makes them trustworthy.

Final mentor's reflection

When you present this design, avoid saying, “We use App Service because it scales,” or “Service Bus makes the system reliable.” Those statements hand responsibility to product names. Explain the actual decision:

We selected App Service because the current workload is a conventional HTTP API and the team benefits from managed runtime operations, identity, slots and horizontal scaling without Kubernetes ownership. We separated attachment processing because it has different duration and failure behaviour. We use durable messaging with an outbox and idempotent consumers so notification failure does not undo a committed note. We measure the delay and operate the dead-letter path.
That explanation contains requirements, trade-offs and failure semantics. It can be challenged and revised when traffic, compliance, cost or team capability changes.

As a final exercise, remove one Azure service from the diagram and explain the consequence. If you remove Service Bus, where does background work run and how does API latency change? If you remove Key Vault, which secrets remain and who rotates them? If you replace App Service with AKS, which required capability justifies the operational cost? If you cannot explain why a component exists or what would happen without it, the design may be following fashion rather than need.

Cloud engineering matures when the team can simplify with the same confidence it uses to add capability.

Write those removal decisions into short architecture records, including context, alternatives, consequences and the evidence that would cause the team to reconsider them later.

Review those records during every major production-readiness assessment.

Current Microsoft references for this guide

Applied In

The thinking in this article has been applied throughout my enterprise portfolio, where architecture, workflows, permissions, notifications, reporting and modular design are all built around real business operations rather than isolated technical features.

View Continuous Learning →

Use this journal entry for recall practice

Compare your explanation with the questions and working answers in my Practice Room.

Practise Azure and cloud architecture questions →
Afzal Ahmed

Faz Ahmed

Senior Full Stack Engineer & Technical Lead

A hands-on engineer with 15+ years in commercial software. I publish what I am studying, revising and testing so visitors can see both established experience and learning still in progress.

How would you approach this problem? I'd love to hear your thoughts or continue the discussion.

Connect on LinkedIn →