Stop Hand-Writing YAML: .NET Aspire's Deployment Model Is the Platform Engineering Win Nobody Advertised

Stop Hand-Writing YAML: .NET Aspire's Deployment Model Is the Platform Engineering Win Nobody Advertised

The last time I hand-wrote a GitHub Actions YAML file for a multi-service .NET deployment, I lost four hours to indentation errors and secret-binding typos. Four hours. Not shipping features. Not fixing bugs. Arguing with a text file about whitespace.

That was eight months ago. I haven't touched pipeline YAML since — and my deployments are more reliable now than when I micromanaged every step.

The reason is Aspire's deployment manifest model. And almost nobody is talking about what it actually does beyond local dev.


The Ops Knowledge Gap Nobody Talks About

Here's the pattern I've seen on every .NET team I've worked with, including my own:

Backend engineers are excellent at code. They can design distributed systems, optimise SQL queries, tune Orleans clusters. Ask them to write a multi-stage CI/CD pipeline from scratch and suddenly everyone gets very busy with other things.

Pipeline YAML becomes tribal knowledge. One person figures it out — usually the most ops-curious dev on the team — and that knowledge lives entirely in their head. When they're on holiday, deployments stop. When they leave the company, you inherit a 400-line file nobody understands.

This is the ops knowledge gap. Platform engineering exists specifically to solve it.

80% of organisations are predicted to have dedicated platform teams by 2026. The pitch is golden paths: pre-built, opinionated deployment workflows so backend engineers can ship without needing an SRE certification. But most .NET teams don't have a dedicated platform engineer. They have a backend dev who drew the short straw.

Aspire quietly changes this equation — not by replacing a platform team, but by making the platform-team problem largely unnecessary for standard .NET deployments.


What Aspire Actually Is (Beyond Local Orchestration)

Most developers I talk to know Aspire as "that thing that spins up all my services locally." They've added the AppHost project, seen the dashboard, and appreciate that service discovery just works.

That's maybe 20% of what Aspire does.

The part that changes deployment entirely is the manifest model.

When you run:

dotnet run --project MyApp.AppHost   --publisher manifest   --output-path ./aspire-manifest.json

Aspire doesn't start your services. It outputs a machine-readable JSON blueprint of your entire distributed system — every service, every dependency, every binding, every connection string reference.

Here's what a trimmed manifest looks like for a three-service app:

{
  "$schema": "https://json.schemastore.org/aspire-8.0.json",
  "resources": {
    "apiservice": {
      "type": "project.v0",
      "path": "MyApp.ApiService/MyApp.ApiService.csproj",
      "env": {
        "ConnectionStrings__postgres": "{postgres.connectionString}"
      },
      "bindings": {
        "http": { "scheme": "http", "protocol": "tcp", "transport": "http" },
        "https": { "scheme": "https", "protocol": "tcp", "transport": "http" }
      }
    },
    "postgres": {
      "type": "container.v0",
      "image": "docker.io/library/postgres:16.2",
      "env": {
        "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256",
        "POSTGRES_PASSWORD": "{postgres.inputs.password}"
      },
      "inputs": {
        "password": {
          "type": "string",
          "secret": true,
          "default": { "generate": { "minLength": 22 } }
        }
      }
    },
    "webfrontend": {
      "type": "project.v0",
      "path": "MyApp.Web/MyApp.Web.csproj",
      "env": {
        "services__apiservice__https__0": "{apiservice.bindings.https.url}"
      },
      "bindings": {
        "https": { "scheme": "https", "protocol": "tcp", "transport": "http", "external": true }
      }
    }
  }
}

That JSON captures three things automatically:

  • What services exist and where their project files live
  • How they connect — service discovery, connection strings, environment bindings
  • What secrets they need — including auto-generated passwords flagged as secret: true

This manifest becomes the single source of truth that downstream tooling reads to generate deployments. You don't write the deployments. The manifest does.


From Manifest to Cloud — The azd Workflow

Azure Developer CLI (azd) has first-class Aspire support. This is where the manifest pays off.

Run azd init in your repository:

azd init

It detects the AppHost project, reads the manifest, and understands your entire service topology — without you explaining anything. It knows you have a web frontend, an API, and a PostgreSQL instance. It knows which services need external ingress and which are internal-only.

Then:

azd up

That single command:

  1. Provisions an Azure Container Apps environment
  2. Creates an Azure Container Registry
  3. Builds your container images
  4. Pushes them to ACR
  5. Creates and configures Azure Database for PostgreSQL
  6. Injects connection strings and secrets into Key Vault
  7. Deploys all three services

No Bicep authoring. No ARM templates. No Terraform. azd generates the infrastructure code from the manifest and executes it.

The mental model shift is the important part: you're no longer describing how to deploy your app. You're describing what your app is, and the tooling works out the rest.


Zero-YAML CI/CD Pipeline Generation

You've deployed once. Now you want that to happen automatically on every push to main.

azd pipeline config --provider github

That's it. azd does all of this:

  • Creates an Azure service principal and configures federated credentials
  • Sets GitHub repository secrets for the Azure credentials
  • Generates .github/workflows/azure-dev.yml with correct auth, build, and deploy steps
  • Commits and pushes the workflow file

The generated pipeline calls azd up — the same command you ran locally. CI/CD and local deployment are identical. No environment drift. No "it works on my machine but dies in the pipeline" debugging sessions at midnight.

Compare that to writing the pipeline by hand. Here's a realistic version for the same three-service app:

# The handwritten version nobody wants to maintain
name: Deploy Multi-Service App

on:
  push:
    branches: [main]

env:
  ACR_LOGIN_SERVER: myapp.azurecr.io
  ACA_RESOURCE_GROUP: rg-myapp-prod

jobs:
  build-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push API
        run: |
          docker build -t myapp.azurecr.io/apiservice:$GITHUB_SHA .
          docker push myapp.azurecr.io/apiservice:$GITHUB_SHA

  build-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push Web
        run: |
          docker build -t myapp.azurecr.io/webfrontend:$GITHUB_SHA .
          docker push myapp.azurecr.io/webfrontend:$GITHUB_SHA

  deploy:
    needs: [build-api, build-web]
    runs-on: ubuntu-latest
    steps:
      - name: Azure Login
        uses: azure/login@v2
        with:
          creds: $AZURE_CREDENTIALS
      - name: Update services
        run: |
          az containerapp update --name apiservice --image myapp.azurecr.io/apiservice:$GITHUB_SHA
          az containerapp update --name webfrontend --image myapp.azurecr.io/webfrontend:$GITHUB_SHA
      # ... plus secret injection, health checks, rollback logic...

That's 80+ lines and it's missing error handling, rollback, and health checks. Real pipelines hit 150–200 lines fast. And every line is your responsibility to maintain when the cloud APIs change.

The Aspire-generated pipeline calls azd up. The manifest handles the rest.


Aspire 13 — The Next Step

The manifest-to-azd workflow is already a significant improvement. Aspire 13 goes further with composable deployment steps via aspire do:

aspire publish --publisher azure-container-apps
aspire do build    # Build all container images
aspire do push     # Push images to registry
aspire do deploy   # Deploy to target environment

Aspire parallelizes independent steps automatically. If your apiservice and webfrontend have no build dependency on each other, they build and push simultaneously. On a 6-service app, this cuts deployment time noticeably.

Publishers are also becoming first-class extension points:

  • azure-container-apps — the polished Azure target
  • kubernetes — generates Helm charts and K8s manifests from the same Aspire manifest
  • docker-compose — for local and self-hosted environments

The manifest model is platform-agnostic. The publisher decides the target. Write your app description once; deploy anywhere the ecosystem supports.


The 75% Easier Recipe: Local Dev to Cloud CI/CD in 5 Steps

Starting from an existing multi-service .NET app with no Aspire.

Prerequisites: .NET 9 SDK, dotnet workload install aspire, Azure Developer CLI (winget install microsoft.azd or brew install azd), Azure subscription with Contributor access.

Step 1 — Add the AppHost project

dotnet new aspire-apphost -n MyApp.AppHost

Wire up your services in Program.cs:

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres").WithDataVolume();

var api = builder.AddProject<Projects.MyApp_ApiService>("apiservice")
    .WithReference(postgres).WaitFor(postgres);

builder.AddProject<Projects.MyApp_Web>("webfrontend")
    .WithReference(api).WithExternalHttpEndpoints();

builder.Build().Run();

Your entire distributed system, described in C#. Not YAML. Not JSON config. Code.

Step 2 — Initialise azd

azd init

azd detects the AppHost and configures itself. Select your subscription and region when prompted. No files to hand-edit.

Step 3 — Deploy to Azure

azd up

First run: 5–8 minutes (provisioning). Subsequent runs: 2–3 minutes. You get a public URL. Your app is live.

Step 4 — Generate the CI/CD pipeline

azd pipeline config --provider github

azd commits the workflow file and configures all secrets. For Azure DevOps: azd pipeline config --provider azdo.

Step 5 — Push to main

git push origin main

Pipeline triggers. Multi-service app builds, pushes, and deploys to Azure Container Apps. Fully automated.

Five commands: dotnet new, azd init, azd up, azd pipeline config, git push. From zero to production-grade CI/CD without writing a single pipeline YAML from scratch.


What This Means for .NET Teams

Aspire is the closest thing .NET has to a built-in Internal Developer Platform. It just isn't marketed that way.

Most IDP tooling — Backstage, Port, Cortex — requires serious investment to set up and keep current. You need someone who understands platform engineering, can write plugins, and can update golden paths as infrastructure evolves under them.

Aspire's approach is different. The golden path is the AppHost project. The moment you describe your services there, you've done the IDP work. Local dev, deployment, and CI/CD all derive from that single description.

Where the trade-offs are:

  • Azure-first. The azd integration is polished and production-ready. The Kubernetes publisher is improving but still maturing. Non-Azure targets depend on community publishers with varying support levels.
  • Convention coupling. Aspire has opinions. Brownfield apps sometimes need wrapper projects to fit the model.
  • Abstraction cost. azd up is a black box to engineers who haven't read the generated Bicep. Review generated files before deploying to production if your ops team needs to audit infrastructure decisions.

None of these are dealbreakers. They're real constraints worth understanding before you commit.

For greenfield .NET projects in 2026, there's no good reason not to start with Aspire from day one. The ops knowledge gap doesn't have to be your team's problem.


Stop Treating CI/CD as a Rite of Passage

Every backend engineer has a war story about deployment pipelines. The 3am alert because a YAML key was misindented. The staging environment six months behind production because nobody wanted to touch the deploy script. The onboarding docs that said "ask Dave about the pipeline" — and Dave left in 2024.

These aren't badges of honour. They're friction that slows teams down and burns out the one person willing to own the ops work.

Aspire's deployment model exists so you can focus on building software instead of debugging infrastructure text files. The manifest describes your system. The tooling handles the rest.

Next time you spin up a new .NET service, skip the pipeline templates. Let the manifest do the talking.


Questions about fitting Aspire into a brownfield project, or getting it working outside Azure? Drop a comment below — I read every one.

Read more