Files
awesome-copilot/skills/aspire/references/architecture.md
Chris McKee 6512c785c7 feat: Add Aspire polyglot distributed-app orchestration skill
Add comprehensive Aspire skill covering CLI, AppHost orchestration, service discovery, integrations (144+), MCP server, dashboard, testing, deployment, and troubleshooting. Includes reference docs for polyglot APIs, architecture, CLI, integrations catalog, and more.
2026-02-05 19:00:02 -06:00

342 lines
10 KiB
Markdown

# Architecture — Deep Dive
This reference covers Aspire's internal architecture: the DCP engine, resource model, service discovery, networking, telemetry, and the eventing system.
---
## Developer Control Plane (DCP)
The DCP is the **runtime engine** that Aspire uses in `aspire run` mode. Key facts:
- Written in **Go** (not .NET)
- Exposes a **Kubernetes-compatible API server** (local only, not a real K8s cluster)
- Manages resource lifecycle: create, start, health-check, stop, restart
- Runs containers via the local container runtime (Docker, Podman, Rancher)
- Runs executables as native OS processes
- Handles networking via a proxy layer with automatic port assignment
- Provides the foundation for the Aspire Dashboard's real-time data
### DCP vs Kubernetes
| Aspect | DCP (local dev) | Kubernetes (production) |
|---|---|---|
| API | Kubernetes-compatible | Full Kubernetes API |
| Scope | Single machine | Cluster |
| Networking | Local proxy, auto ports | Service mesh, ingress |
| Storage | Local volumes | PVCs, cloud storage |
| Purpose | Developer inner loop | Production deployment |
The Kubernetes-compatible API means Aspire understands the same resource abstractions, but DCP is **not** a Kubernetes distribution — it's a lightweight local runtime.
---
## Resource Model
Everything in Aspire is a **resource**. The resource model is hierarchical:
### Type hierarchy
```
IResource (interface)
└── Resource (abstract base)
├── ProjectResource — .NET project reference
├── ContainerResource — Docker/OCI container
├── ExecutableResource — Native process (polyglot apps)
├── ParameterResource — Config value or secret
└── Infrastructure resources
├── RedisResource
├── PostgresServerResource
├── MongoDBServerResource
├── SqlServerResource
├── RabbitMQServerResource
├── KafkaServerResource
└── ... (one per integration)
```
### Resource properties
Every resource has:
- **Name** — unique identifier within the AppHost
- **State** — lifecycle state (Starting, Running, FailedToStart, Stopping, Stopped, etc.)
- **Annotations** — metadata attached to the resource
- **Endpoints** — network endpoints exposed by the resource
- **Environment variables** — injected into the process/container
### Annotations
Annotations are metadata bags attached to resources. Common built-in annotations:
| Annotation | Purpose |
|---|---|
| `EndpointAnnotation` | Defines an HTTP/HTTPS/TCP endpoint |
| `EnvironmentCallbackAnnotation` | Deferred env var resolution |
| `HealthCheckAnnotation` | Health check configuration |
| `ContainerImageAnnotation` | Docker image details |
| `VolumeAnnotation` | Volume mount configuration |
| `CommandLineArgsCallbackAnnotation` | Dynamic CLI arguments |
| `ManifestPublishingCallbackAnnotation` | Custom publish behavior |
### Resource lifecycle states
```
NotStarted → Starting → Running → Stopping → Stopped
↓ ↓
FailedToStart RuntimeUnhealthy
Restarting → Running
```
### DAG (Directed Acyclic Graph)
Resources form a dependency graph. Aspire starts resources in topological order:
```
PostgreSQL ──→ API ──→ Frontend
Redis ────────↗
RabbitMQ ──→ Worker
```
1. PostgreSQL, Redis, and RabbitMQ start first (no dependencies)
2. API starts after PostgreSQL and Redis are healthy
3. Frontend starts after API is healthy
4. Worker starts after RabbitMQ is healthy
`.WaitFor()` adds a health-check gate to the dependency edge. Without it, the dependency starts but the downstream doesn't wait for health.
---
## Service Discovery
Aspire injects environment variables into each resource so services can find each other. No service registry or DNS is needed — it's pure environment variable injection.
### Connection strings
For databases, caches, and message brokers:
```
ConnectionStrings__<resource-name>=<connection-string>
```
Examples:
```
ConnectionStrings__cache=localhost:6379
ConnectionStrings__catalog=Host=localhost;Port=5432;Database=catalog;Username=postgres;Password=...
ConnectionStrings__messaging=amqp://guest:guest@localhost:5672
```
### Service endpoints
For HTTP/HTTPS services:
```
services__<resource-name>__<scheme>__0=<url>
```
Examples:
```
services__api__http__0=http://localhost:5234
services__api__https__0=https://localhost:7234
services__ml__http__0=http://localhost:8000
```
### How .WithReference() works
```csharp
var redis = builder.AddRedis("cache");
var api = builder.AddProject<Projects.Api>("api")
.WithReference(redis);
```
This does:
1. Adds `ConnectionStrings__cache=localhost:<auto-port>` to the API's environment
2. Creates a dependency edge in the DAG (API depends on Redis)
3. In the API service, `builder.Configuration.GetConnectionString("cache")` returns the connection string
### Cross-language service discovery
All languages use the same env var pattern:
| Language | How to read |
|---|---|
| C# | `builder.Configuration.GetConnectionString("cache")` |
| Python | `os.environ["ConnectionStrings__cache"]` |
| JavaScript | `process.env.ConnectionStrings__cache` |
| Go | `os.Getenv("ConnectionStrings__cache")` |
| Java | `System.getenv("ConnectionStrings__cache")` |
| Rust | `std::env::var("ConnectionStrings__cache")` |
---
## Networking
### Proxy architecture
In `aspire run` mode, DCP runs a reverse proxy for each exposed endpoint:
```
Browser → Proxy (auto-assigned port) → Actual Service (target port)
```
- **port** (the external port) — auto-assigned by DCP unless overridden
- **targetPort** — the port your service actually listens on
- All inter-service traffic goes through the proxy for observability
```csharp
// Let DCP auto-assign the external port, service listens on 8000
builder.AddPythonApp("ml", "../ml", "main.py")
.WithHttpEndpoint(targetPort: 8000);
// Fix the external port to 3000
builder.AddViteApp("web", "../frontend")
.WithHttpEndpoint(port: 3000, targetPort: 5173);
```
### Endpoint types
```csharp
// HTTP endpoint
.WithHttpEndpoint(port?, targetPort?, name?)
// HTTPS endpoint
.WithHttpsEndpoint(port?, targetPort?, name?)
// Generic endpoint (TCP, custom schemes)
.WithEndpoint(port?, targetPort?, scheme?, name?, isExternal?)
// Mark endpoints as externally accessible (for deployment)
.WithExternalHttpEndpoints()
```
---
## Telemetry (OpenTelemetry)
Aspire configures OpenTelemetry automatically for .NET services. For non-.NET services, you configure OpenTelemetry manually, pointing at the DCP collector.
### What's auto-configured (.NET services)
- **Distributed tracing** — HTTP client/server spans, database spans, messaging spans
- **Metrics** — Runtime metrics, HTTP metrics, custom metrics
- **Structured logging** — Logs correlated with trace context
- **Exporter** — OTLP exporter pointing at the Aspire Dashboard
### Configuring non-.NET services
The DCP exposes an OTLP endpoint. Set these env vars in your non-.NET service:
```
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_SERVICE_NAME=<your-service-name>
```
Aspire auto-injects `OTEL_EXPORTER_OTLP_ENDPOINT` via `.WithReference()` for the dashboard collector.
### ServiceDefaults pattern
The `ServiceDefaults` project is a shared configuration library that standardizes:
- OpenTelemetry setup (tracing, metrics, logging)
- Health check endpoints (`/health`, `/alive`)
- Resilience policies (retries, circuit breakers via Polly)
```csharp
// In each .NET service's Program.cs
builder.AddServiceDefaults(); // adds OTel, health checks, resilience
// ... other service config ...
app.MapDefaultEndpoints(); // maps /health and /alive
```
---
## Health Checks
### Built-in health checks
Every integration adds health checks automatically on the client side:
- Redis: `PING` command
- PostgreSQL: `SELECT 1`
- MongoDB: `ping` command
- RabbitMQ: Connection check
- etc.
### WaitFor vs WithReference
```csharp
// WithReference: wires connection string + creates dependency edge
// (downstream may start before dependency is healthy)
.WithReference(db)
// WaitFor: gates on health check — downstream won't start until healthy
.WaitFor(db)
// Typical pattern: both
.WithReference(db).WaitFor(db)
```
### Custom health checks
```csharp
var api = builder.AddProject<Projects.Api>("api")
.WithHealthCheck("ready", "/health/ready")
.WithHealthCheck("live", "/health/live");
```
---
## Eventing System
The AppHost supports lifecycle events for reacting to resource state changes:
```csharp
builder.Eventing.Subscribe<ResourceReadyEvent>("api", (evt, ct) =>
{
// Fires when "api" resource becomes healthy
Console.WriteLine($"API is ready at {evt.Resource.Name}");
return Task.CompletedTask;
});
builder.Eventing.Subscribe<BeforeResourceStartedEvent>("db", async (evt, ct) =>
{
// Run database migrations before the DB resource is marked as started
await RunMigrations();
});
```
### Available events
| Event | When |
|---|---|
| `BeforeResourceStartedEvent` | Before a resource starts |
| `ResourceReadyEvent` | Resource is healthy and ready |
| `ResourceStateChangedEvent` | Any state transition |
| `BeforeStartEvent` | Before the entire application starts |
| `AfterEndpointsAllocatedEvent` | After all ports are assigned |
---
## Configuration
### Parameters
```csharp
// Plain parameter
var apiKey = builder.AddParameter("api-key");
// Secret parameter (prompted at run, not logged)
var dbPassword = builder.AddParameter("db-password", secret: true);
// Use in resources
var api = builder.AddProject<Projects.Api>("api")
.WithEnvironment("API_KEY", apiKey);
var db = builder.AddPostgres("db", password: dbPassword);
```
### Configuration sources
Parameters are resolved from (in priority order):
1. Command-line arguments
2. Environment variables
3. User secrets (`dotnet user-secrets`)
4. `appsettings.json` / `appsettings.{Environment}.json`
5. Interactive prompt (for secrets during `aspire run`)