Section 15

DevOps, deployment strategies & observability

Getting code from a laptop to production safely and often, and knowing what it's doing once it's there through logging, metrics, tracing and alerting.

15.1 CI/CD pipelines Added

CI: merge often, and build and test automatically. CD: every change that passes is releasable (delivery) or released automatically (deployment).

Commit / PR
→
Buildcompile, unit tests
→
ScanSAST · SCA · secrets · IaC
→
Artifact / imagesigned, versioned
→
Dev → Test → SIT → UAT
→
Approval gateCAB / change
→
Prodsafe deployment strategy
  • Continuous delivery = a manual approval before prod. Continuous deployment = fully automatic.
  • Build once, deploy the same artifact to every environment. Only the config changes.
  • DevSecOps / shift-left: security scans run in the pipeline, not after go-live.
  • DORA metrics: deployment frequency, change lead time, change fail rate, failed deployment recovery time, and (since 2024) deployment rework rate. See 15.15.
  • Tools: Azure DevOps, GitHub Actions, GitLab CI, Jenkins.
How to implement it
Library / APIPackageUse it for
dotnet CLIbuilt-in dotnet restore / build / test / publishThe same commands on a laptop and in GitHub Actions / Azure Pipelines (actions/setup-dotnet, UseDotNet@2)
Test frameworksxunit.v3, NUnit, MSTest; runner: Microsoft.Testing.PlatformUnit and integration tests. In .NET 10, dotnet test can run on Microsoft.Testing.Platform (opt in via global.json)
Integration testsMicrosoft.AspNetCore.Mvc.Testing, TestcontainersRun the API in memory (WebApplicationFactory) against a real DB in a throwaway container
Coveragecoverlet.collector or Microsoft.Testing.Extensions.CodeCoverageCoverage reports and pipeline quality gates
Security scans (SCA)built-in NuGet audit on restore, dotnet list package --vulnerableFail the build on known-vulnerable packages
Build once, ship an imagebuilt-in SDK container publishing; azd for Aspire appsBuild a container image without a Dockerfile, then promote the same image through every environment
# the core of any .NET pipeline
dotnet restore                                   # NuGet audit warns about vulnerable packages
dotnet build -c Release --no-restore -warnaserror
dotnet test  -c Release --no-build
dotnet publish src/Orders.Api -c Release /t:PublishContainer \
    -p:ContainerRegistry=myacr.azurecr.io -p:ContainerRepository=orders-api

15.2 Source control, branching & code review Added

How a team shares one codebase: where changes live, how they get reviewed, and how they get back into main quickly and safely.

Three branching models

🏷️ GitFlow

Long-lived main and develop, plus feature, release and hotfix branches. Fits versioned products with several supported releases (SDKs, installed software). Heavy for web services: its author added a 2020 note recommending simpler flows for continuously delivered apps.

🔀 GitHub flow

main is always deployable. Short branch → pull request → review and checks → merge → deploy. A simple, good default for most teams.

🌳 Trunk-based

Everyone integrates into trunk at least daily, directly or through branches that live hours, at most a day or two. Unfinished work hides behind feature flags. This is the model CI/CD assumes.

ModelLong-lived branchesBranch lifeRelease fromBest for
GitFlowmain, develop (+ release, hotfix)Days to weeksRelease branch, tagged on mainProducts with several supported versions
GitHub flowmain onlyHours to a few daysmain, after each mergeWeb apps and APIs with one live version
Trunk-basedtrunk onlyHours (1–2 days max)Trunk, or a short release branch cut from itCI/CD and frequent deploys

Why trunk-based + feature flags suits CI/CD: "continuous integration" means merging with everyone else's code at least daily. A branch that lives two weeks isn't integrated, so conflicts and surprises pile up at the end. Flags split deploy from release, so unfinished code can merge safely. DORA lists trunk-based development as a capability linked to better delivery performance.

Pull requests & code review

🔍 What reviewers look for

  • Correctness: edge cases, error paths, concurrency
  • Design: does it fit? Is there a simpler way?
  • Tests that prove the behaviour, not just coverage
  • Security: input validation, authorisation, no secrets
  • Operability: logs, metrics, flags, safe DB migrations
  • Readability: names, comments that explain why

🤝 Etiquette

  • Review the code, not the person. Ask questions, don't command
  • Label optional comments as nit: so blockers stand out
  • Approve when it improves the codebase, not when it's perfect
  • Author: small PR, a clear description (why, how tested), self-review first
  • Respond within one business day at most (Google's guidance)

📏 PR size

Small PRs get faster, better reviews. Google's guide: ~100 lines is usually reasonable, 1,000 is usually too large. Split refactoring from behaviour changes.

⏱️ Review SLA

Agree a first-response target (hours, not days). Waiting for review is often the biggest part of lead time (15.15).

👥 Pairing & mob

Review happens while the code is written. It can replace async PR review where policy allows. Regulated teams may still need a recorded second approver.

🤖 Automate the boring bits

Formatting, linting, tests and security scans run as required checks. Humans review logic and design.

Commits, protected branches & ownership

Conventional Commits (spec 1.0.0): a typed, machine-readable message. Tools use it to pick the next version and write the changelog (15.4).

fix(transfers): reject negative amounts           → PATCH
feat(payments): add PayTo mandate status endpoint → MINOR
feat(api)!: remove v1 /accounts route             → MAJOR

BREAKING CHANGE: clients must call /v2/accounts
# other types: docs, test, refactor, perf, build, ci, chore
ControlWhat it does
Protected branchNo direct pushes, force-pushes or deletes on main. GitHub: rulesets / branch protection. Azure Repos: branch policies
Required checksBuild, tests, lint and security scans must pass before merge
Required reviews1–2 approvals. Dismiss stale approvals when new commits arrive
CODEOWNERSAuto-requests the owning team for matching paths, e.g. /infra/ → platform team
Merge queueTests each PR on top of the latest main before merging, so main stays green
Signed commitsProves who made a change. Part of supply chain security (14.10)

📦 Monorepo

  • ✅ Atomic changes across services, one toolchain, easy refactoring
  • ✅ Shared code without publishing packages
  • ❌ Needs builds that only test what changed (path filters, Nx, Bazel)
  • ❌ Big clones. CODEOWNERS becomes essential

🗂️ Polyrepo

  • ✅ Clear ownership, permissions and pipelines per service
  • ✅ Small, fast repos
  • ❌ Cross-repo changes need versioned packages and coordination
  • ❌ Duplicated pipeline config, dependency drift
  • Integrate small changes at least daily. Long-lived branches are merge pain you've postponed.
  • Trunk-based + feature flags: deploy any time, release when ready.
  • Small PRs, quick reviews. Automate style so humans review logic.
  • Protect main with required checks, required reviews and CODEOWNERS.
How to implement it
Library / APIPackageUse it for
Format checkbuilt-in dotnet format --verify-no-changesA required PR check: fails if code doesn't match .editorconfig
Analyzers as gatesbuilt-in TreatWarningsAsErrors, EnforceCodeStyleInBuild, AnalysisLevelOne rule set for every project via Directory.Build.props. Warnings block the merge
Central package versionsbuilt-in Directory.Packages.propsOne place for NuGet versions in a monorepo, so dependency PRs are small and clear
<!-- Directory.Build.props at the repo root: applies to every project -->
<Project>
  <PropertyGroup>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
  </PropertyGroup>
</Project>

Long-lived branches are merge conflicts you haven't met yet 🌿

🌳 Trunk-based: integrate at least daily, hide unfinished work behind flags

🔀 GitHub flow: short branch → PR → checks → merge → deploy

🏷️ GitFlow: still fine for products with several supported versions

🔍 Small PRs get better reviews. Reply within a business day

🛡️ Protect main: required checks, CODEOWNERS, a merge queue

How long does your average branch live?

#DevOps #Git #CodeReview #CICD #SoftwareEngineering

15.3 Testing strategy Added

Many fast, cheap tests at the bottom, a few slow, realistic ones at the top, and some checks that only make sense in production.

Test pyramid slower · costlier · more realistic Unit Unit: thousands, milliseconds each pure logic, no I/O, run on every save Integration Integration: real DB, broker, HTTP pipeline throwaway containers, seconds each Contract Contract: the API shape each consumer needs consumer-driven (Pact), no shared environment E2E E2E: a handful of critical journeys real browser + deployed stack: slow, can be flaky

🔺 Test pyramid

Lots of unit tests, fewer integration tests, very few end-to-end tests. Fast feedback, cheap to run.

🏆 Testing trophy

Kent C. Dodds' version: static analysis at the base, some unit tests, the most integration tests, a few E2E. Integration tests give the most confidence per minute.

Both shapes say the same thing: buy the most confidence for the least test time. The anti-pattern is the "ice-cream cone": mostly manual and E2E tests.

Contract testing (consumer-driven)

Consumer test"I call GET /accounts/42 and need id + balance"
→
Pact filethe contract
→
Pact Brokershares contracts and results
→
Provider verificationin the provider's CI
→
can-i-deploy ✓

Performance testing: four different questions

TypeQuestion it answersBanking example
LoadDo we meet our SLOs at the expected peak?Lunchtime card traffic
StressWhere does it break, how does it fail, and does it recover?2× the busiest day on record
SpikeCan we survive a sudden surge? Does autoscaling react fast enough?Payday at midnight, a viral sale
Soak (endurance)Does it degrade over hours? Memory leaks, connection pools, full disksA 12-hour run before a major release

Chaos engineering

🧪 Hypothesis first

"If one zone fails, p99 latency stays under 500 ms and no payment is lost." Then inject the failure and measure.

💥 Small blast radius

Start with one instance, in staging, or 1% of traffic. Always have an abort switch.

🎮 Game days

Planned team exercises that break things on purpose, and practise the incident response too.

🛠️ Tools

Azure Chaos Studio, Chaos Mesh (Kubernetes), AWS Fault Injection Service, Gremlin. In code: Polly chaos strategies.

🧬 Test data management

  • Synthetic data generated to look real (fake names, valid account formats)
  • Masked production copies: irreversible masking of personal data
  • Never raw production customer data in test (privacy law)
  • Each test creates and cleans up its own data

⬅️ Shift left · Shift right ➡️

  • Left: unit tests, analyzers and contract tests on every PR, ephemeral test environments
  • Right: synthetic monitoring (scripted journeys every few minutes in prod)
  • Right: automated canary analysis (compare canary vs baseline metrics, roll back on regression)
  • Aim for fast feedback and real confidence, not a coverage percentage.
  • For microservices, contract tests replace most cross-team E2E tests.
  • Performance-test against your SLOs, with production-like data volumes.
  • Chaos without a hypothesis and an abort button is just an outage.
How to implement it
NeedLibrary / APINotes
Frameworks & assertionsxunit.v3, NUnit, MSTest on Microsoft.Testing.Platform; plain Assert or ShouldlyFluentAssertions v8+ needs a commercial licence for commercial use
Integration testsMicrosoft.AspNetCore.Mvc.Testing (WebApplicationFactory) + Testcontainers.PostgreSql / .MsSql / .RedisRun the real HTTP pipeline in memory against real databases in throwaway Docker containers
Contract testsPactNet (v5, Pact specification v4)Consumer tests write the pact file. The provider verifies it in its own pipeline
Test data & snapshotsBogus; Verify (Verify.XunitV3, Verify.NUnit, Verify.MSTest)Realistic fake data, and snapshot tests that compare a whole response with an approved file
Load testsNBomber commercial licence; or k6, Azure Load TestingNBomber is free only for personal and academic use. k6 scripts are JavaScript
Chaos in codePolly.Core chaos strategies: AddChaosFault, AddChaosLatency, AddChaosOutcome, AddChaosBehaviorInject faults and delays into a resilience pipeline to prove your retries and timeouts work
// xUnit v3: the real API in memory + a real Postgres in Docker
public sealed class PaymentsApiTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _db = new PostgreSqlBuilder("postgres:17").Build();
    private WebApplicationFactory<Program> _factory = null!;

    public async ValueTask InitializeAsync()
    {
        await _db.StartAsync();
        _factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
            b.UseSetting("ConnectionStrings:Payments", _db.GetConnectionString()));
    }

    [Fact]
    public async Task Create_payment_returns_201()
    {
        var client = _factory.CreateClient();
        var response = await client.PostAsJsonAsync("/payments",
            new { FromAccount = "123", ToAccount = "456", Amount = 50m },
            TestContext.Current.CancellationToken);

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }

    public async ValueTask DisposeAsync()
    {
        await _factory.DisposeAsync();
        await _db.DisposeAsync();
    }
}

Testcontainers needs Docker on the build agent. Polly chaos strategies are enabled as soon as you add them: drive Enabled / EnabledGenerator from config so they can never fire in production by accident.

Tests should buy confidence, not a coverage percentage 🧪

🔺 Many fast unit tests, fewer integration tests, a handful of E2E.

🤝 Contract tests (Pact) instead of fragile cross-team E2E suites.

🏋️ Load, stress, spike and soak tests answer four different questions.

🐒 Chaos engineering: start with a hypothesis and a small blast radius.

🔭 Shift right too: synthetic monitoring and canary analysis in production.

What's the slowest test in your pipeline, and has it ever caught a bug?

#Testing #DevOps #QualityEngineering #SRE

15.4 Versioning & release management Added

A version number is a promise about compatibility. Release management decides how a tested artifact gets that number, its release notes, and its path to production.

Semantic Versioning (SemVer 2.0.0)

PartChange it whenExample
MAJORConsumers could break (removed member, changed contract)1.4.2 → 2.0.0
MINORNew feature, backward compatible1.4.2 → 1.5.0
PATCHBug fix, backward compatible1.4.2 → 1.4.3
Pre-releaseNot stable yet. Sorts before the final release2.0.0-beta.1
Build metadataInformation only. Ignored when comparing versions2.0.0+5114f85

📦 Libraries & NuGet packages

  • NuGet fully supports SemVer 2.0 (pre-release labels, build metadata).
  • NuGet picks the lowest version that satisfies a range, so bump honestly.
  • 0.y.z means "no stability promise yet".
  • Catch accidental breaking changes in the build with package validation (below).

🌐 HTTP APIs

  • Usually expose only the MAJOR: /v1, /v2 (styles in 8.2).
  • Within a major, changes must be additive: new optional fields, new endpoints.
  • Publish a deprecation date and use the Sunset header before removing a version.
  • Contract tests catch breaking changes before release (15.3).

Release trains vs continuous delivery

🚂 Release train

A fixed cadence (fortnightly, monthly, quarterly). Whatever is ready catches the train. ✅ Predictable for the business, change boards, app stores and multi-team releases. ❌ Big batches, waiting, more risk per release.

🔁 Continuous delivery

Every merged change can go to production on its own, often behind a flag. ✅ Small risk per change, fast feedback. ❌ Needs strong test automation, observability and rollback.

Many banks run both: continuous delivery for internal services and APIs, and a train for mobile app releases and core banking changes.

Build once, promote the same artifact

Merge to main
→
CI builds 1.8.0image digest + signature + SBOM
→
Registry / feedimmutable
→
Dev → Test → Pre-prodsame digest, different config
→
Prodafter the approval gate

Changelogs & release notes

📜 Changelog (for developers)

Every notable change per version, grouped as Added, Changed, Deprecated, Removed, Fixed, Security ("Keep a Changelog" style). Lives in the repo.

📣 Release notes (for users)

What changed for them: new features, migration steps for breaking changes, known issues. Written for the audience, not a list of commits.

Automate it: Conventional Commits (15.2) let tools such as release-please or semantic-release work out the next version, write the changelog and create the release. In .NET, MinVer, Nerdbank.GitVersioning or GitVersion stamp the version into the build.

  • A breaking change means a new MAJOR. No exceptions, even for "tiny" ones.
  • Published artifacts are immutable. Promote the same one, by digest, through every environment.
  • Derive versions and changelogs from git (tags, commit messages), not by hand.
  • Release trains trade speed for predictability. Continuous delivery trades process for automation.
How to implement it
Library / APIPackageUse it for
Version from git tagsMinVerTag v1.5.0 → version 1.5.0. Commits after the tag get a pre-release version. Minimal config
Version from a fileNerdbank.GitVersioningversion.json holds major.minor; git height adds the patch. Hosted in the dotnet GitHub org
Version from branch rulesGitVersion.MsBuild (or the GitVersion.Tool CLI)Calculates SemVer from branches, tags and commit messages. Fits GitFlow-style repos
Pack & publishbuilt-in dotnet pack, dotnet nuget pushBuild the .nupkg (plus .snupkg symbols) and push it to nuget.org, Azure Artifacts or GitHub Packages
Source Link & deterministic buildsbuilt-in Source Link (SDK 8+), ContinuousIntegrationBuildDebuggers can step into the exact commit's source. The same source gives the same binary
Package validationbuilt-in EnablePackageValidation + PackageValidationBaselineVersiondotnet pack fails on a breaking API change against the last released version
<!-- library .csproj (or Directory.Build.props) -->
<PropertyGroup>
  <MinVerTagPrefix>v</MinVerTagPrefix>             <!-- tag v1.5.0 => 1.5.0 -->
  <EnablePackageValidation>true</EnablePackageValidation>
  <PackageValidationBaselineVersion>1.4.0</PackageValidationBaselineVersion>
  <IncludeSymbols>true</IncludeSymbols>
  <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="MinVer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>

# pipeline, after tagging the release commit with v1.5.0
dotnet pack -c Release -o out
dotnet nuget push "out/*.nupkg" -s https://api.nuget.org/v3/index.json -k $NUGET_KEY

Git-based versioning needs tags and history. A shallow CI clone gives wrong versions: use fetch-depth: 0 in actions/checkout. Only set ContinuousIntegrationBuild on CI, or local debugging can't find your source files.

1.4.2 → 2.0.0 is a promise, not a vibe 🏷️

🔢 MAJOR = breaking, MINOR = new feature, PATCH = fix

🧊 Never overwrite a published version. Ship 1.4.3 instead

📦 Build once, then promote the same artifact by digest

🤖 Let git tags and Conventional Commits pick the version and write the changelog

🚂 Release train or continuous delivery? Predictability vs small batches

When did a "minor" update last break your build?

#SemVer #ReleaseManagement #DevOps #dotnet #NuGet

15.5 Deployment strategies

Ways to release a new version that trade off downtime, risk, cost and rollback speed.

1. Recreate (big bang)

Stop all, start the new version. ➕ Simple, cheap. ➖ Downtime, risky.

2. Rolling

Replace instances in batches. ➕ No downtime, no extra cost. ➖ Mixed versions run during rollout, slow rollback.

3. Blue-Green

Two full environments, flip the switch. ➕ Instant switch and rollback. ➖ 2× cost. DB schema must work with both versions.

4. Canary

Expose to a small slice first. ➕ Real-traffic validation, small blast radius. ➖ Needs good monitoring and traffic splitting.

5. A/B testing

Like canary, but for business experiments, not technical safety.

6. Shadow / dark launch

Test v2 with real production traffic and no user impact. ➖ Watch out for side effects (don't double-charge!).

StrategyDowntimeRiskExtra costRollbackComplexity
RecreateYesHighNoneSlow (redeploy)Low
RollingNoMediumLowMediumLow
Blue-GreenNoLowHigh (2×)InstantMedium
CanaryNoLowestLow–mediumFastHigh
A/BNoLowMediumFastHigh
ShadowNoNone to usersHighN/AHigh
  • Database changes are the hard part. Use expand → migrate → contract so old and new code both work with the schema.
  • Define automatic rollback triggers (error rate, p99 latency) for canary and blue-green.
  • Ring-based deployment: internal users → early adopters → region → everyone (used by Microsoft).
  • Roll back vs roll forward: rolling back is safest if it's fast. Roll forward when a fix is small and well understood.
How to implement it
StrategyLibrary / APINotes
RollingKubernetes Deployment / App Service; built-in health checks + HostOptions.ShutdownTimeoutThe platform does the batches. Your app must report readiness and shut down gracefully
Blue-greenApp Service deployment slots (swap), Container Apps revisionsPlatform features; script them with Azure.ResourceManager.AppService or the az CLI
Canary / ringSlot or revision traffic %, Azure Front Door weights, Argo Rollouts (not .NET); in code: Microsoft.FeatureManagement targeting / percentage filtersShift traffic in steps, or turn a feature on for rings of users
A/BMicrosoft.FeatureManagement variants + telemetrySplit users between variants and compare business metrics
ShadowService mesh traffic mirroring (platform)No standard .NET library. Make the shadow copy side-effect free
Database changesEF Core migrations (dotnet ef migrations bundle)Run expand/contract migrations as a separate pipeline step, not at app startup

6 deployment strategies, ranked by how well you'll sleep 😴

😱 Recreate: everything down, fingers crossed

😐 Rolling: batch by batch

🙂 Blue-Green: flip a switch, flip it back

😌 Canary: 5% first, watch, then grow

🧪 A/B: test the business idea

🕶 Shadow: real traffic, zero user impact

Deploy ≠ release. Feature flags let you do both separately.

#DevOps #CICD #SRE

15.6 Feature flags (toggles) Added

Ship code turned off, then turn it on for chosen users without redeploying. Deploy ≠ release.

Release toggle

Hide unfinished features. Enables trunk-based development (15.2).

Experiment toggle

A/B tests.

Ops toggle / kill switch

Turn off a feature that's causing trouble.

Permission toggle

Premium or beta users only.

  • Remove old flags. Flag debt becomes untestable code paths. Tools: LaunchDarkly, Azure App Configuration, Unleash.
How to implement it
Library / APIPackageUse it for
Feature ManagementMicrosoft.FeatureManagement.AspNetCoreIFeatureManager, [FeatureGate], percentage / time-window / targeting filters and variants. Flags can live in appsettings.json
Azure App ConfigurationMicrosoft.Extensions.Configuration.AzureAppConfigurationChange flags centrally at runtime, with no redeploy
OpenFeatureOpenFeature + a provider packageVendor-neutral flag API. Swap providers (flagd, LaunchDarkly, Unleash, etc.) without changing code
LaunchDarkly / UnleashLaunchDarkly.ServerSdk, Unleash.ClientSDKs for those flag services (LaunchDarkly is a paid SaaS)
builder.Configuration.AddAzureAppConfiguration(o =>
    o.Connect(new Uri("https://my-config.azconfig.io"), new DefaultAzureCredential())
     .UseFeatureFlags());                         // flags refresh without a restart
builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement().WithTargeting();   // users, groups, % rollout

var app = builder.Build();
app.UseAzureAppConfiguration();

app.MapGet("/checkout", async (IFeatureManager features) =>
    await features.IsEnabledAsync("NewCheckout") ? NewCheckout() : OldCheckout());

15.7 Infrastructure as Code & GitOps Added

Define infrastructure in versioned code files. The tool makes reality match the code.

Code (Bicep / Terraform)
→
PR + plan / what-if
→
Approve
→
Apply → cloud matches code
How to implement it
Library / APIPackageUse it for
Bicep / TerraformNot .NETThe usual declarative choice. Run them from the pipeline
Aspire app modelAspire.Hosting.Azure.* + Azure Developer CLI (azd)Describe the app and its resources in C#; azd generates Bicep and provisions Azure
PulumiPulumi, Pulumi.AzureNativeInfrastructure in C# with real loops, types and tests
Azure SDK managementAzure.ResourceManager, Azure.ResourceManager.*Imperative automation and tooling (drift checks, clean-up jobs)
GitOpsArgo CD / Flux (not .NET)Pull your manifests from Git into the cluster
// Aspire AppHost: the app and its infrastructure in one C# file
var builder = DistributedApplication.CreateBuilder(args);

var db = builder.AddAzurePostgresFlexibleServer("pg").AddDatabase("orders");
builder.AddProject<Projects.Orders_Api>("api").WithReference(db);

builder.Build().Run();
// locally: runs everything with the dashboard;  "azd up": provisions Azure and deploys

15.8 Containers & Kubernetes Added

A container packages an app with its dependencies. Kubernetes runs many containers across many machines: it schedules, heals, scales and connects them.

VM vs container

Virtual machines VM 1 VM 2 App Libs Guest OSfull OS + kernel, GBs App Libs Guest OSfull OS + kernel, GBs Hypervisor Hardware (host server) ⏳ booting a whole guest OS… ✓ VMs ready: took minutes Containers no guest OS → lighter (MBs) container 1 container 2 container 3 App Libs App Libs App Libs Container runtime (containerd) Host OS · ONE shared kernel Hardware (host server) ⏳ starting… ✓ containers running: took seconds

VM = [App][Libs][Guest OS] on a hypervisor. Container = [App][Libs] on the shared host OS kernel → lighter, starts in seconds.

Kubernetes at a glance

👤 Users Ingress L7 routing in (HTTPS) Service stable IP / DNS for pods load-balances to ready pods Worker node 1 Worker node 2 kubelet + container runtime kubelet + container runtime Pod A container ready ✓ Pod B container ready ✓ Pod C container ready ✓ Pod C container ✕ crashed removed free capacity new pod container starting… new pod container ready ✓ kubelet ↔ API server Control plane API serverfront door forkubectl, kubelets Schedulerpicks a node foreach new pod Controller mgrmakes actual= desired state etcdcluster state(key-value store) Deployment desired state: image v2 · replicas: 3 ReplicaSet keeps 3 Pods running (1+ containers each) owns HPA auto-scales replicas ConfigMap / Secret config injected into pods What the cluster is doing ✓ desired 3 pods · running 3traffic spread across pods A, B, C ✕ Pod C crashed → running 2 ≠ desired 3Service stops sending it traffic ↻ controller creates a new podscheduler places it on worker node 2 ✓ back to 3 running (self-healing)new pod is ready → it gets traffic

Deployment → ReplicaSet → Pods (1+ containers). Service = stable IP/DNS for pods · Ingress = L7 routing in · ConfigMap / Secret = config · HPA = auto-scale pods. Control plane: API server · scheduler · controller manager · etcd (state). Worker nodes: kubelet + container runtime.

  • Probes: liveness (restart if dead), readiness (send traffic only when ready), startup.
  • StatefulSet for stateful apps, DaemonSet for one pod per node (agents).
  • Managed offerings: AKS, EKS, GKE. Simpler PaaS option: Azure Container Apps.
How to implement it
Library / APIPackageUse it for
SDK container publishingbuilt-in dotnet publish /t:PublishContainerBuild an OCI image with no Dockerfile; non-root user by default
Health checksbuilt-in AddHealthChecks / MapHealthChecks; extras: AspNetCore.HealthChecks.*Separate endpoints for liveness, readiness and startup probes
Graceful shutdownbuilt-in HostOptions.ShutdownTimeout, IHostApplicationLifetimeFinish in-flight requests after SIGTERM, before Kubernetes kills the pod
Kubernetes clientKubernetesClientOfficial C# client for the Kubernetes API: operators, automation, tooling
AspireAspire.Hosting.Kubernetes preview / Aspire.Hosting.Azure.AppContainersGenerate deployment artifacts for Kubernetes or Azure Container Apps from the AppHost
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
    .AddDbContextCheck<AppDbContext>(tags: ["ready"]);  // Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
builder.Services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(25));

var app = builder.Build();
app.MapHealthChecks("/health/live",  new() { Predicate = r => r.Tags.Contains("live") });   // restart if failing
app.MapHealthChecks("/health/ready", new() { Predicate = r => r.Tags.Contains("ready") });  // no traffic if failing

Don't check the database in the liveness probe. A DB outage would make Kubernetes restart every pod, which doesn't help.

15.9 Observability Added

Monitoring tells you that something is wrong. Observability lets you ask why, from the telemetry the system emits.

📜 Logs

Timestamped events. Make them structured (JSON) and include a correlation ID. Never log secrets or PII.

📈 Metrics

Numbers over time: request rate, error %, latency, CPU. Cheap to keep, good for alerts.

🧵 Traces

The path of one request across services (spans with timings). Shows where the time went.

The W3C traceparent header is propagated on every call, so all these spans share one trace ID (4bf9…). Child spans (└) sit inside their parent's time. Here the SQL INSERT is the bottleneck.

Four golden signals (Google SRE)

Latency · Traffic · Errors · Saturation

RED (services) & USE (resources)

Rate, Errors, Duration · Utilisation, Saturation, Errors

  • OpenTelemetry is the vendor-neutral standard for emitting logs, metrics and traces.
  • Alert on symptoms users feel (SLO burn rate), not on every CPU spike.
  • Tools: Azure Monitor / App Insights, Datadog, Splunk, Dynatrace, Grafana + Prometheus, ELK.
How to implement it
Library / APIPackageUse it for
OpenTelemetry .NETOpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, OpenTelemetry.Instrumentation.HttpCollect logs, metrics and traces with one setup
OTLP exporterOpenTelemetry.Exporter.OpenTelemetryProtocolSend telemetry to any backend: Grafana, Datadog, Splunk, the Aspire dashboard
Azure Monitor distroAzure.Monitor.OpenTelemetry.AspNetCoreOne call (UseAzureMonitor()) sends everything to Application Insights
Tracing APIbuilt-in System.Diagnostics.ActivitySource / ActivityYour own spans. W3C traceparent is propagated automatically by HttpClient
Aspire dashboardAspire (also runs as a standalone container)Local OTLP viewer for logs, traces and metrics
builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("orders-api"))
    .WithTracing(t => t.AddAspNetCoreInstrumentation()
                       .AddHttpClientInstrumentation()
                       .AddSource("Orders"))                   // our own ActivitySource
    .WithMetrics(m => m.AddAspNetCoreInstrumentation()
                       .AddHttpClientInstrumentation()
                       .AddMeter("Orders", "System.Runtime"))  // our Meter + runtime metrics
    .WithLogging()
    .UseOtlpExporter();   // endpoint from OTEL_EXPORTER_OTLP_ENDPOINT

// a custom span (Source is a static field: new ActivitySource("Orders"))
using var activity = Source.StartActivity("PlaceOrder");
activity?.SetTag("order.channel", "web");

15.10 Logging Added

A log is a timestamped record of something that happened. Logs explain why something went wrong, and they're only useful if you can search and correlate them.

App / API / Functionemits structured logs
→
Agent / SDKOpenTelemetry, App Insights SDK, Fluent Bit
→
Central log storeLog Analytics · Splunk · ELK · Datadog
→
Search · dashboards · alerts
→
Archivecheap storage for retention

Log levels: use them consistently

LevelWhenExample
FATAL / CRITICALThe app can't continueCan't connect to DB at startup
ERRORAn operation failed and needs attentionPayment 7f3a failed: downstream 503 after 3 retries
WARNUnexpected, but handledRetry 1/3 to fraud service, circuit half-open
INFONormal business milestonesPayment 7f3a accepted, amount band B
DEBUG / TRACEDiagnostic detail. Off in prod by defaultRequest mapping, SQL parameters (masked)

Structured vs unstructured

✗ Unstructured  "Payment failed for cust 42 amount 800 because timeout"
✓ Structured    {"ts":"2026-10-16T01:02:03Z","level":"ERROR","service":"payments","env":"prod",
                 "traceId":"4bf92f…","correlationId":"c-91a…","event":"PaymentFailed",
                 "paymentId":"7f3a","reason":"FraudServiceTimeout","durationMs":2004}
→ query it: where event=="PaymentFailed" | summarize count() by reason, bin(ts, 5m)

🧵 Correlation

Put traceId / correlationId on every log line and pass it across services (W3C traceparent), so you can follow one request end to end.

🏷 Context fields

service, version, environment, region, instance, operation, user or tenant ID (pseudonymised).

🔒 Never log

Passwords, tokens, secrets, full card numbers (PAN), CVV, full PII. Mask them (****1234) or tokenise.

💰 Volume & cost

Sample noisy DEBUG and success logs, set retention tiers (hot 30 d → archive 1–7 yrs), and drop health-check spam.

📜 Audit logs ≠ app logs

Audit logs record who did what, and when. Keep them immutable and tamper-evident, with longer retention, for compliance.

⏱ Async & non-blocking

Logging must never slow down or crash the request. Use buffered, async sinks.

  • Log events and decisions, not every line of code. Ask: "Would this help someone at 3 a.m.?"
  • Log an error once, at the boundary where it's handled. Logging it at every layer creates duplicate noise.
  • Use UTC timestamps in ISO 8601 format.
  • Make the log level changeable at runtime, so you can turn on DEBUG briefly during an incident.
How to implement it
Library / APIPackageUse it for
ILoggerbuilt-in Microsoft.Extensions.LoggingStructured logs with message templates ({PaymentId}), levels set per category in config
Source-generated loggingbuilt-in [LoggerMessage]Fast, allocation-free log methods with fixed event names
Scopes & correlationbuilt-in BeginScope; trace and span ids come from ActivityPut correlation id, tenant and trace id on every line
RedactionMicrosoft.Extensions.Compliance.Redaction + Microsoft.Extensions.TelemetryTag PII with data-classification attributes; AddRedaction() + EnableRedaction() mask it in logs
Shipping logsOpenTelemetry (WithLogging), or Serilog.AspNetCore + sinks, or NLog.Web.AspNetCoreSend JSON logs to Log Analytics, Splunk, ELK or Datadog
public static partial class Log
{
    [LoggerMessage(LogLevel.Error, "Payment {PaymentId} failed: {Reason}")]
    public static partial void PaymentFailed(this ILogger logger, string paymentId, string reason);
}

// every log line inside the scope carries CorrelationId
using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
    logger.PaymentFailed("7f3a", "FraudServiceTimeout");   // fields, not one long string
}

Don't use string interpolation ($"Payment {id}") in log calls. It loses the structured fields and always builds the string, even when the level is off.

Good logs are a gift to your future on-call self 🎁

✅ Structured JSON, not free text

✅ A traceId on every line

✅ Consistent levels (ERROR means someone must act)

❌ Never tokens, passwords or card numbers

❌ Not the same error logged at 5 layers

If you can't query it, it's not a log. It's a diary.

#Observability #Logging #SRE #DevOps

15.11 Metrics Added

Metrics are numbers measured over time. They're cheap to store, fast to query, and ideal for dashboards, trends and alerts. They tell you that something is wrong.

Metric types

➕ Counter

Only goes up (resets on restart). Requests, errors, payments processed. Look at its rate: requests/s.

🌡 Gauge

Goes up and down. CPU %, memory, queue depth, active connections.

📊 Histogram

Counts values in buckets, so you can calculate percentiles (p50, p95, p99). Latency, payload size.

📐 Summary

Percentiles calculated on the client. Can't be combined across instances, so prefer histograms.

What to measure

LayerFrameworkExample metrics
User-facing service / APIRED: Rate, Errors, Durationreq/s, 5xx %, p95/p99 latency per endpoint
Infrastructure resourceUSE: Utilisation, Saturation, ErrorsCPU %, memory, disk I/O wait, thread-pool queue, DB DTU %
Whole systemGolden signals: Latency, Traffic, Errors, SaturationA combination of the above
MessagingFlow healthQueue depth, message age, consumer lag (Kafka), DLQ count
Business (KPIs)OutcomesPayments/min, success %, onboarding drop-off, $ value processed
GenAI / LLMAI opsTokens in/out, cost per request, time to first token, groundedness score, 429s

100 requests: 95 take 100 ms and 5 take 3,000 ms. The average (245 ms) looks fine and hides the slow tail. p95 = 100 ms, but p99 = 3,000 ms: 1 in 20 users waits 3 s.

  • Cardinality kills metric systems. Never use userId, paymentId or full URLs as labels. Use bounded values: endpoint template, status class, region.
  • Track business metrics next to technical ones. "CPU fine, but payments/min dropped 40%" is the alert that matters.
  • Tools: Prometheus + Grafana, Azure Monitor metrics, App Insights, Datadog, Dynatrace, CloudWatch. Emit them through OpenTelemetry.
How to implement it
Library / APIPackageUse it for
Metrics APIbuilt-in System.Diagnostics.MetricsCounter<T>, Histogram<T>, UpDownCounter<T>, Gauge<T> (.NET 9+), and Observable* versions
IMeterFactorybuilt-in (registered by the host)Create meters through DI so they're testable
Built-in metricsbuilt-in ASP.NET Core, HttpClient, runtime (System.Runtime)RED metrics (request rate, errors, duration) and GC / thread-pool stats for free
ExportersOpenTelemetry.Exporter.OpenTelemetryProtocol; OpenTelemetry.Exporter.Prometheus.AspNetCore previewPush via OTLP, or expose a /metrics endpoint for Prometheus to scrape
dotnet-counters.NET global tool (dotnet tool install -g dotnet-counters)Watch live metrics of a running process
public sealed class PaymentMetrics
{
    private readonly Counter<long> _processed;
    private readonly Histogram<double> _duration;

    public PaymentMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("Payments");
        _processed = meter.CreateCounter<long>("payments.processed", unit: "{payment}");
        _duration  = meter.CreateHistogram<double>("payments.duration", unit: "s");
    }

    // keep tag values bounded: status and channel, never paymentId
    public void Record(string status, string channel, double seconds)
    {
        var tags = new TagList { { "status", status }, { "channel", channel } };
        _processed.Add(1, tags);
        _duration.Record(seconds, tags);
    }
}
// builder.Services.AddSingleton<PaymentMetrics>();  and  .AddMeter("Payments") in OpenTelemetry

15.12 Alerting Added

An alert should page a human only when something needs action now. Every page should be urgent, actionable and tied to user impact.

Metrics / logs / traces
→
Alert rulecondition + window + threshold
→
Severity & routingAction Group / PagerDuty / ServiceNow
→
On-call engineerpage / Teams / SMS
→
Runbook → mitigate

Types of alert rules

📏 Static threshold

"5xx rate > 2% for 5 min." Simple and predictable. Can be noisy if traffic varies.

📈 Dynamic / anomaly

Baseline learned by ML. Flags unusual patterns (e.g. payments/min far below the normal Monday 9 a.m. level).

🔥 SLO burn rate ⭐

Alert when the error budget is being used up too fast. Ties alerts directly to user impact.

💓 Heartbeat / absence

"No batch file processed by 06:00" or "no logs from service X for 10 min". Catches silent failures.

🔍 Log-based

Count of a specific event/error ("FraudServiceTimeout" > 50 in 5 min).

🧪 Synthetic

Scripted probes call key journeys (login, pay) from outside every minute.

SLO burn-rate alerting (multi-window)

SLO 99.9% → error budget 0.1% per 30 days (≈ 43 min). Burn rate = actual error rate ÷ allowed error rate. 1× uses the budget up in exactly 30 days.

Each PAGE rule needs a long AND a short window to both be over the threshold. The short window confirms it's still happening → fewer false alarms.

Severity & routing

SeverityMeaningResponseChannel
Sev 1 / P1Major customer impact (payments down)Immediate, 24×7, incident bridgePage + phone
Sev 2 / P2Degraded, or a key feature downWithin ~30 minPage
Sev 3 / P3Minor or at risk (disk 80%, cert expires in 14 d)Business hoursTicket / Teams
Sev 4 / InfoFor awarenessNo actionDashboard only

✅ A good alert

  • Based on a symptom users feel (errors, latency, failed journeys)
  • Has a clear title: what, where, how bad
  • Links to a runbook and dashboard
  • Has an owner and routes to the right team
  • Auto-resolves when the condition clears

❌ Alert fatigue causes

  • Paging on causes ("CPU 85%") with no user impact
  • Flapping thresholds, no duration window
  • Duplicate alerts for one incident (no grouping)
  • Alerts nobody acts on. Delete them or turn them into tickets
  • Everything is "critical"
  • Alert on symptoms, use dashboards for causes.
  • Group and deduplicate related alerts. Suppress them during planned maintenance.
  • Review alerts after every incident: did one fire? Too late? Too noisy? Tune them.
  • Don't forget "silent" failures: missing heartbeats, DLQ growth, certificate expiry, batch job SLAs.
  • Tools: Azure Monitor alerts + Action Groups, Grafana Alerting / Alertmanager, PagerDuty, Opsgenie, ServiceNow.
How to implement it
Library / APIPackageUse it for
Alert rulesPlatform: Azure Monitor alerts, Grafana Alerting, Prometheus AlertmanagerMost alerting lives here, not in app code. Burn-rate rules query the metrics your app emits
Alerts as codeAzure.ResourceManager.Monitor (or Bicep / Terraform)Create metric/log alert rules and action groups from code, reviewed in a PR
Signals to alert onbuilt-in System.Diagnostics.Metrics + OpenTelemetryEmit good error and latency metrics (and business counters) so symptom-based rules are possible
Health / heartbeatbuilt-in health checks + IHealthCheckPublisherPush health status on a timer, e.g. as a metric, so a missing heartbeat can raise an alert
Synthetic checksApplication Insights availability tests; Microsoft.Playwright for scripted journeysProbe login / payment journeys from outside

If every alert is critical, no alert is critical 🚨

Page a human only when it's: 1️⃣ urgent 2️⃣ actionable 3️⃣ hurting users.

"CPU at 85%" → dashboard. "Payment errors burning the SLO 14× faster than allowed" → page.

Every alert needs an owner and a runbook link. If nobody acts on it, delete it.

#SRE #Observability #Alerting #DevOps

15.13 Incident management Added

How a team detects, responds to and learns from production incidents.

Detectalert
→
Triageseverity P1–P4
→
Mitigaterollback, failover, runbook
→
Resolve
→
Blameless post-incident reviewactions
How to implement it
Library / APIPackageUse it for
Diagnostic toolsdotnet-counters, dotnet-trace, dotnet-dump, dotnet-gcdump (.NET tools)During an incident: live metrics, CPU traces, memory and hang dumps from a running process
dotnet-monitorSidecar container (mcr.microsoft.com/dotnet/monitor)Collect dumps and traces from pods over HTTP, or automatically when a trigger fires
Kill switchMicrosoft.FeatureManagementTurn off the failing feature as a fast mitigation, with no redeploy

15.14 SRE practices Added

Site Reliability Engineering runs operations as a software problem: agree how reliable a service must be, spend the leftover risk on purpose, and automate the repetitive work away.

SLI vs SLO vs SLA and the error-budget maths are in 3.10. Burn-rate alerts are in 15.12. This section covers the team practices around them.

Error budget policy

A written agreement, signed off by product and engineering before anything goes wrong, saying what happens as the budget runs down.

Toil and the 50% rule

🔁 What toil is

Manual, repetitive, automatable work with no lasting value that grows with the service. E.g. restarting a stuck job, renewing certificates by hand, re-running failed batches.

⚖️ The 50% cap

Google SRE caps operational work at 50% of each engineer's time. The rest goes to engineering that removes toil or improves reliability. Over the cap? Hand work back to the dev team.

🛠️ Reduce it

Measure it (tickets, pages, hours). Automate the most frequent task first, offer self-service, fix the root cause, delete useless alerts.

On-call health

PracticeWhy
Big enough rotationGoogle SRE: at least 8 people for a single-site 24×7 primary + secondary rota, or 6 per site with follow-the-sun
Page only on symptomsUsers hurting or the SLO burning (15.12). Causes go to dashboards and tickets
Runbook on every pageThe responder knows the first steps at 3 a.m.
Cap the loadGoogle aims for at most 2 incidents per 12-hour shift. More means fix the system, not hire heroes
Written handoverAt each rotation change: open incidents, silenced alerts, risky changes in flight
Recovery & reviewTime off after a bad night. Review pages per week and false positives every month

Blameless postmortems

Production readiness review (PRR)

A checklist a service passes before it goes live or before SRE takes on its pager.

📈 Observability

SLIs and SLOs defined, dashboards, alerts with runbooks, distributed tracing.

🛡️ Resilience

Timeouts, retries, circuit breakers (3.6), load tested, DR tested (3.13).

🚀 Deploy & rollback

Safe deployment strategy, flags, a tested rollback, backward-compatible DB migrations.

🔐 Security & capacity

Threat model, secrets in a vault, capacity plan with headroom.

📟 Ownership

On-call rota, escalation contacts, support docs, dependencies listed.

Game days

Planned exercises that inject a real failure (zone outage, slow dependency, expired certificate) while the on-call team responds as if it were live. They test runbooks, alerts and people, not just code. How to run them, plus chaos principles and tools, is in 15.16.

  • SLOs and error budgets turn "features vs reliability" into an agreed, data-driven rule.
  • Agree the error budget policy in advance, not in the middle of an incident.
  • Keep toil under 50%. Automate the most frequent manual task first.
  • Page on symptoms with a runbook. Write blameless postmortems with owned action items.
How to implement it
Library / APIPackageUse it for
SLIsbuilt-in http.server.request.duration + System.Diagnostics.MetricsRequest success and latency for free; custom good/total counters for business SLIs (code in 3.10)
Histogram bucketsOpenTelemetry.Extensions.Hosting (AddView)Put a bucket edge exactly at the SLO threshold
Readiness for PRRbuilt-in AddHealthChecks / MapHealthChecksLiveness and readiness probes (15.8)
Toil automationbuilt-in BackgroundService (Worker template); Azure.ResourceManagerTurn manual runbook steps into scheduled, logged, tested jobs
Game days in codePolly.Core chaos strategiesInject faults and latency on purpose (15.3)
// SLO: 99% of requests faster than 300 ms, so 0.3 s must be a bucket edge
builder.Services.AddOpenTelemetry().WithMetrics(m => m
    .AddAspNetCoreInstrumentation()
    .AddView("http.server.request.duration",
        new ExplicitBucketHistogramConfiguration
        {
            Boundaries = [0.05, 0.1, 0.3, 0.5, 1, 2.5, 5]
        }));
// SLI = requests with duration <= 0.3 s / all requests, over 30 days (PromQL / KQL)

If no bucket edge sits on the SLO threshold, the backend has to interpolate, and your "under 300 ms" SLI is only an estimate.

Reliability is a feature with a budget 💰

🎯 Agree an SLO with the business. "100% uptime" isn't a target

📉 Budget left? Ship. Budget gone? Freeze features, fix reliability

🔁 Toil over half the week? Automate before taking on more

📟 Page only when users hurt, and always with a runbook

📝 Blameless postmortems: fix the system, not the person

What does your team do when the error budget runs out?

#SRE #Reliability #OnCall #DevOps #Observability

15.15 DORA metrics & engineering effectiveness Added

DORA's research-backed metrics measure software delivery outcomes, how fast and how safely changes reach production, so teams can see whether their improvements work.

The five metrics (DORA, 2024 onwards)

🚀 Throughput

  • Change lead time: from commit to running in production.
  • Deployment frequency: how often you deploy to production.
  • Failed deployment recovery time: how long to recover from a deployment that needed immediate intervention. It replaced MTTR in 2023.

💥 Instability

  • Change fail rate: % of deployments that need immediate intervention (rollback or hotfix).
  • Deployment rework rate (added 2024): % of deployments that are unplanned, made to fix a production incident.

These are the original "four keys" plus rework rate. DORA keeps finding that speed and stability aren't a trade-off: top performers do well on all of them. The lever behind all five is small batches.

Performance clusters

2024 clusterLead timeDeploy frequencyChange fail rateRecovery timeShare
Elite< 1 dayOn demand (many a day)5%< 1 hour19%
High1 day – 1 weekDaily to weekly20%< 1 day22%
Medium1 week – 1 monthWeekly to monthly10%< 1 day35%
Low1 – 6 monthsMonthly to 6-monthly40%1 week – 1 month25%

Latest (2025 DORA report): DORA dropped the single elite-to-low ladder. It now gives benchmarks per metric and describes seven team archetypes, from "Foundational challenges" to "Harmonious high-achievers", that also factor in well-being, friction and product performance. DORA's advice: aim for elite improvement, not an elite label.

Measuring them from pipelines and incidents

Gitcommit times, PRs
→
Pipelinedeploy events: service, version, SHA
→
Incident toolincident ↔ deploy link
→
Metrics store
→
Team dashboardtrend per service
MetricData sourceCalculation
Deployment frequencyProduction deploy eventsDeploys per day or week, per service
Change lead timeCommit time + the deploy that shipped itMedian of deploy time − commit time
Change fail rateDeploys linked to rollbacks, hotfixes or incidentsFailed deploys ÷ all deploys
Recovery timeIncident opened → resolved, for change-caused incidentsMedian duration
Rework rateDeploys tagged unplanned / hotfixUnplanned deploys ÷ all deploys

SPACE framework in brief

Forsgren, Storey and others (2021): developer productivity can't be one number. Pick metrics from at least three of these dimensions, mixing surveys and system data.

😊 Satisfaction & well-being

Fulfilment, burnout, tooling happiness (surveys).

🎯 Performance

Outcomes: quality, reliability, customer impact.

📊 Activity

Counts of PRs, builds, deploys. Never use them alone.

💬 Communication & collaboration

Review turnaround, knowledge sharing, onboarding time.

🌊 Efficiency & flow

Uninterrupted focus time, handoffs, waiting time.

Anti-patterns

❌ Don't

  • Rank or compare teams, or use the metrics in individual performance reviews
  • Set them as targets from above (Goodhart's law: they get gamed)
  • Game them: split deploys, skip logging incidents, redefine "failure"
  • Track speed without stability, or count lines of code and commits

✅ Do

  • Let the team own its metrics and discuss them in retros
  • Compare a team with its own past, not with other teams
  • Pair them with SLOs (15.14) and well-being signals
  • Pick one improvement (smaller PRs, faster reviews) and watch the trend
  • Five metrics: lead time, deployment frequency, recovery time (throughput); change fail rate, rework rate (instability).
  • Speed and stability move together. Small batches improve both.
  • Use them for team learning, never to rank teams or people.
  • 2025: no more elite ladder. Per-metric benchmarks plus seven team archetypes.
How to implement it
Library / APIPackageUse it for
Deployment metricsbuilt-in System.Diagnostics.Metrics + OpenTelemetry.Exporter.OpenTelemetryProtocolA counter for deployments and a histogram for lead time, tagged by service and result
GitHub dataOctokitDeployments, workflow runs, pull requests and commits through the GitHub REST API
Azure DevOps dataMicrosoft.TeamFoundationServer.ClientBuilds, pipeline runs and work items. Latest stable is 20.256.x; newer builds are preview
Incident dataHttpClient + Microsoft.Extensions.Http.ResilienceCall your incident tool's REST API and join incidents to deploys by change ID
var meter = new Meter("Delivery");   // or IMeterFactory in a DI-based app
var deploys = meter.CreateCounter<long>("delivery.deployments");
var leadTime = meter.CreateHistogram<double>("delivery.change_lead_time", unit: "h");

// the pipeline POSTs here after every production deployment
app.MapPost("/deployments", (DeploymentEvent e) =>
{
    var tags = new TagList { { "service", e.Service }, { "result", e.Result } };
    deploys.Add(1, tags);
    foreach (var committedAt in e.CommitTimes)
        leadTime.Record((e.DeployedAt - committedAt).TotalHours, tags);
    return Results.Accepted();
});

// add .AddMeter("Delivery") to OpenTelemetry. Change fail rate =
// deployments{result="failed"} / deployments, calculated in the dashboard
record DeploymentEvent(string Service, string Result, DateTimeOffset DeployedAt,
    DateTimeOffset[] CommitTimes);

Speed vs stability is a false choice 📊

🚀 Deployment frequency and lead time: how fast changes flow

🛟 Failed deployment recovery time: how fast you bounce back

💥 Change fail rate and rework rate: how often deploys hurt

📦 The lever behind all five: smaller batches

🚫 Never use them to rank teams. Goodhart's law wins every time

Which metric would move first if your PRs were half the size?

#DORA #DevOps #EngineeringMetrics #SRE #ContinuousDelivery

15.16 Chaos engineering and game days Added

Break things on purpose, in a controlled experiment, to prove that the system and the team cope the way you think they do.

The short version is in 15.3. SLOs, error budgets and the error budget policy are in 15.14. This section is the how-to.

The experiment loop

Steady statenormal SLIs
→
Hypothesis"p99 stays < 500 ms"
→
Inject faultsmall blast radius
→
Observeabort if SLO burns
→
Fix & repeatthen automate

How it works

PartWhat it means
Steady stateMeasurable "normal": success rate, p99, payments per minute
Hypothesis"If X fails, steady state holds." Written down before you start
Blast radiusWho can get hurt: one pod, 1% of traffic, one tenant, one zone
Abort conditionsAgreed stop rules: burn rate, error %, a stop button. Automate them
DurationFaults switch off on their own when time runs out or on abort

Common experiments

🐢 Latency

Add 1-3 s to a dependency. Do timeouts fire, and do retries make it worse?

🔌 Dependency failure

Return 503s, drop the cache, block DNS. Does the fallback or circuit breaker work?

🖥️ Instance / zone loss

Kill pods or VMs, take a zone down, fail over the database. Does traffic move?

📈 Resource exhaustion

CPU or memory pressure, full disk, a drained connection pool. Does autoscaling or shedding kick in?

Non-production first, then production

Game days and the error budget

Tools

WhereTool
AzureAzure Chaos Studio (Workspaces preview; Experiments classic)
AWSAWS Fault Injection Service: scenario library, CloudWatch alarm stop conditions
KubernetesChaos Mesh, LitmusChaos
In codePolly chaos strategies (below)

When to use / when not

Use it whenNot yet when
Critical journeys have SLOs and alertsNo observability: you can't see the impact
You added retries, failover or multi-zone and need proofThe weakness is already known: fix it first
Before a peak event or a DR claim to a regulatorThe error budget is gone, or a change freeze is on
On-call needs practice on rare failuresThere's no way to abort quickly
  • Steady state + hypothesis first. Without them, it's just an outage.
  • Smallest blast radius that teaches you something; automated abort conditions.
  • Non-production first, but only production proves production.
  • Game days test people, alerts and runbooks, not just code.
  • Chaos spends error budget. No budget, no experiments.
How to implement it
Package / APIUse it for
Polly.Core (8.8)Built-in chaos strategies: AddChaosFault, AddChaosLatency, AddChaosOutcome, AddChaosBehavior
Microsoft.Extensions.Http.Resilience (10.x)AddResilienceHandler on an HttpClient: retry, timeout, then chaos in the same pipeline
Microsoft.Extensions.Resilience (10.x)Enriches Polly telemetry; brings Polly.Extensions (AddResiliencePipeline) for non-HTTP calls
built-in IConfigurationSwitch chaos on per environment or game day
// Chaos on one dependency, off unless config turns it on (staging or a game day)
bool chaosOn = builder.Configuration.GetValue<bool>("Chaos:Enabled");

builder.Services.AddHttpClient<FxRatesClient>(c => c.BaseAddress = new Uri("https://fx.example.com"))
    .AddResilienceHandler("fx", pipeline =>
    {
        // 1. Normal resilience: this is what the experiment tests
        pipeline.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 });
        pipeline.AddTimeout(TimeSpan.FromSeconds(2));          // per try

        // 2. Chaos goes last, closest to the real call
        if (chaosOn)
        {
            pipeline.AddChaosLatency(0.10, TimeSpan.FromSeconds(3));   // 10% of calls +3 s
            pipeline.AddChaosOutcome(0.05, () =>
                new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); // 5% get a 503
        }
    });
// Hypothesis: retries + a 2 s per-try timeout keep FX success above 99.5%

Polly v8 chaos strategies are on as soon as you add them. Gate them with config, or use EnabledGenerator / InjectionRateGenerator to target test users only. Watch the Chaos.OnLatency and Chaos.OnOutcome telemetry events.

Your retries have never been tested. Not really 🧪

📏 Define steady state: success rate, p99, payments per minute

🤔 Write the hypothesis before you break anything

🎯 Smallest blast radius, automatic abort on SLO burn rate

🎮 Game days test people, alerts and runbooks, not just code

💸 Chaos spends error budget. No budget, no experiments

When did your team last fail over a zone on purpose?

#ChaosEngineering #SRE #Resilience #GameDay #DevOps

⚡ Quick-fire Q&A

Blue-green vs canary?

Blue-green switches all traffic at once between two full environments, with instant rollback and double cost. Canary shifts traffic gradually and validates on real users with a smaller blast radius, but needs strong monitoring.

How do you deploy a DB schema change with zero downtime?

Expand/contract: add the new column or table (backward compatible), deploy code that writes both, backfill, switch reads, then remove the old structure in a later release.

Test pyramid vs testing trophy?

The pyramid says: many unit tests, fewer integration tests, very few E2E. The trophy (Kent C. Dodds) adds static analysis at the base and puts the most weight on integration tests, because they give the most confidence per minute. Both reject the "ice-cream cone" of mostly manual and E2E tests.

What is consumer-driven contract testing?

Each consumer writes a test that records exactly what it needs from a provider API (a pact). The provider runs those pacts in its own pipeline and can only deploy if every consumer's contract still passes. It catches breaking API changes without a shared, flaky end-to-end environment.

Logs vs metrics vs traces?

Metrics tell you something is wrong (error rate is up). Traces show where (which hop is slow). Logs explain why (the exception details).

What should you alert on?

User-facing symptoms: error rate, latency against the SLO (burn rate), failed synthetic journeys, missing heartbeats, and business KPIs dropping. Show causes like CPU or memory on dashboards, and ticket them rather than paging.

Why are high-cardinality metric labels a problem?

Each unique label combination is a separate time series. Labels like userId or paymentId create millions of series, which blow up memory, cost and query time. Put those IDs in logs and traces instead.

How do you reduce alert fatigue?

Page only on actionable symptoms, add duration windows, use multi-window burn-rate alerts, group and deduplicate, attach runbooks, and delete or downgrade alerts nobody acts on.

Trunk-based development vs GitFlow?

GitFlow keeps long-lived develop and release branches. It suits versioned products with several supported releases, but merges arrive late and big. Trunk-based development merges small changes into trunk at least daily and hides unfinished work behind feature flags, which is what CI/CD needs. Most web teams use trunk-based or GitHub flow.

What makes a good pull request and code review?

A small PR (around a few hundred lines at most) with a clear description of why and how it was tested. Automated checks cover style, tests and security, so reviewers focus on correctness, design, tests, security and operability. Reviews are kind, label nits as optional, and get a response within a business day.

How do you version a NuGet library or an API?

SemVer: MAJOR for breaking changes, MINOR for backward-compatible features, PATCH for fixes. Derive the version from git (MinVer, Nerdbank.GitVersioning or GitVersion), turn on package validation to catch accidental breaks, and never overwrite a published version. HTTP APIs usually expose only the major (/v2) and keep changes within it additive.

Why build once and promote the same artifact?

What reaches production is exactly what was tested. Rebuilding per environment can pull different dependencies or compiler output. Promote one immutable, signed artifact (by image digest) and only change config per environment.

What is an error budget policy?

A pre-agreed rule between product and engineering about what happens as the error budget (100% − SLO) is used: ship normally while there's budget, slow down as it burns, freeze features and fix reliability when it's gone. Exceptions and escalation are written down in advance.

What goes into a blameless postmortem?

Summary, impact, a timestamped timeline, contributing factors (not a person to blame), what went well, badly and where we got lucky, and action items with owners and due dates. The goal is to fix the system so the same failure can't recur.

What are the DORA metrics today?

Five since 2024: change lead time, deployment frequency and failed deployment recovery time (throughput), plus change fail rate and deployment rework rate (instability). The 2025 report replaced the elite/high/medium/low clusters with per-metric benchmarks and seven team archetypes.

How can DORA metrics be misused?

By ranking teams, tying them to individual performance reviews, or setting them as top-down targets. Teams then game them, for example by splitting deploys or not recording incidents. Use them for a team's own improvement over time, together with SLOs and well-being signals (SPACE).

How do you run a chaos experiment safely in production?

Define steady state (success rate, p99) and write a hypothesis first. Prove it in staging. Then run in production off-peak with the smallest useful blast radius (one pod, 1% of traffic, one tenant), automated abort conditions such as an SLO burn-rate alarm, people watching, and only while error budget remains.

Chaos experiment vs game day?

A chaos experiment tests a technical hypothesis and can be automated and repeated in a pipeline. A game day is a planned team exercise: a failure is injected and on-call responds with the real alerts and runbooks, so it tests people and process too, followed by a blameless review.

← 14 · Security & IAM