.NET Aspire : créer des applications cloud natives de la bonne manière

· 12 min de lecture

Si vous avez déjà créé une application distribuée dans .NET, vous connaissez le principe. Vous lancez une API Web, ajoutez un travailleur en arrière-plan, ajoutez Redis pour la mise en cache, PostgreSQL pour la persistance, peut-être RabbitMQ pour la messagerie - et tout à coup, vous passez plus de temps à câbler l’infrastructure qu’à écrire une logique métier. Chaînes de connexion dispersées dans les fichiers appsettings.json, vérifications de l’état que vous avez oublié de configurer, observabilité qui est toujours “le problème du prochain sprint”.

J’y suis allé. Plus de fois que je voudrais l’admettre.

C’est exactement le problème pour lequel .NET Aspire a été conçu. Après l’avoir utilisé en production pendant plusieurs mois maintenant, je souhaite partager ce que j’ai appris : les bons, les grands et les pièges.

Qu’est-ce que .NET Aspire ?

.NET Aspire est une pile avisée permettant de créer des applications distribuées observables, prêtes pour la production avec .NET. Ce n’est pas un framework au sens traditionnel du terme : il ne remplace pas ASP.NET Core et ne vous oblige pas à adopter un nouveau modèle de programmation. Au lieu de cela, il s’ajoute à ce que vous savez déjà et comble les lacunes qui ont toujours existé lors de la création d’applications cloud natives.

À la base, Aspire vous offre quatre choses :

  1. AppHost — Un projet qui définit l’intégralité de la topologie de votre application distribuée. Quels services existent, de quoi ils dépendent et comment ils se connectent.
  2. Paramètres par défaut du service — Un projet partagé qui configure les préoccupations transversales telles que les contrôles de santé, les politiques de résilience et OpenTelemetry — une fois, pour tous vos services.
  3. Composants — Packages NuGet qui fournissent des intégrations standardisées avec des services de support tels que Redis, PostgreSQL, RabbitMQ, Azure Storage, etc.
  4. Tableau de bord du développeur : une interface utilisateur en temps réel qui affiche les journaux, les traces et les métriques pour l’ensemble de votre application distribuée pendant le développement local.

La philosophie est simple : si chaque application cloud .NET a besoin de ces éléments, pourquoi les implémentons-nous tous à partir de zéro à chaque fois ?

Configurer votre premier projet Aspire

La mise en route est simple. Vous aurez besoin de .NET 8 ou version ultérieure et de la charge de travail Aspire installée :

dotnet workload update
dotnet workload install aspire

Créez maintenant un nouveau projet de démarrage Aspire :

dotnet new aspire-starter -n MyCloudApp

Cela génère une solution avec quatre projets :

  • MyCloudApp.AppHost — L’orchestrateur
  • MyCloudApp.ServiceDefaults — Configuration partagée
  • MyCloudApp.ApiService — Un exemple d’API Web
  • MyCloudApp.Web — Une interface Blazor

Exécutez AppHost et vous verrez immédiatement le tableau de bord Aspire ouvert dans votre navigateur, affichant tous vos services, leurs journaux et leur état de santé. Aucun fichier Docker Compose. Pas de gestion manuelle des ports. Cela fonctionne.

dotnet run --project MyCloudApp.AppHost

C’est cette première expérience qui m’a accroché. En moins d’une minute, vous disposez d’une application distribuée entièrement orchestrée avec observabilité intégrée.

Le modèle AppHost

L’AppHost est l’endroit où la magie vit. Il s’agit d’une petite application console qui utilise un modèle de générateur pour définir la topologie de votre application distribuée : quelles ressources existent et comment les services s’y connectent.

Voici à quoi ressemble un AppHost réaliste :

var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure resources
var postgres = builder.AddPostgres("postgres")
    .WithPgAdmin()
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache");

var rabbitmq = builder.AddRabbitMQ("messaging")
    .WithManagementPlugin();

// Application services
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReference(rabbitmq);

var orderApi = builder.AddProject<Projects.OrderApi>("order-api")
    .WithReference(postgres)
    .WithReference(rabbitmq);

var frontend = builder.AddProject<Projects.WebFrontend>("frontend")
    .WithExternalHttpEndpoints()
    .WithReference(catalogApi)
    .WithReference(orderApi);

builder.Build().Run();
```Lisez ce code à voix haute. Il se documente pratiquement. "L'API du catalogue fait référence à PostgreSQL, Redis et RabbitMQ. Le frontend fait référence à l'API du catalogue et à l'API de commande." C'est votre architecture, exprimée en code.

Quelques points à noter :

- **`WithReference`** fait le gros du travail. Il injecte automatiquement les chaînes de connexion et les URL de service dans le projet consommateur via les variables d'environnement et la configuration. Vos services n'ont pas besoin de savoir *où* Redis s'exécute - Aspire s'en charge.
- **`WithPgAdmin()`** et **`WithManagementPlugin()`** lancent des interfaces utilisateur d'administration pour PostgreSQL et RabbitMQ parallèlement aux services réels. Pendant le développement, ceux-ci sont inestimables.
- **`WithExternalHttpEndpoints()`** marque un service comme accessible de l'extérieur, ce qui est important au moment du déploiement.

### Configuration des ressources

Vous pouvez configurer des ressources avec un contrôle précis :

```csharp
var postgres = builder.AddPostgres("postgres")
    .WithEnvironment("POSTGRES_MAX_CONNECTIONS", "200")
    .WithDataVolume("postgres-data")
    .AddDatabase("catalogdb");

var redis = builder.AddRedis("cache")
    .WithDataVolume("redis-data");

Les volumes de données garantissent que vos données de développement locales survivent aux redémarrages des conteneurs. Petit détail, grande amélioration de la qualité de vie.

Paramètres par défaut du service : le héros méconnu

Le projet ServiceDefaults est la partie la plus sous-estimée d’Aspire. Il s’agit d’une bibliothèque partagée à laquelle chaque service de votre solution fait référence et qui configure toutes les préoccupations transversales que vous auriez autrement oubliées ou mises en œuvre de manière incohérente.

Voici à quoi ressemble un Extensions.cs typique dans ServiceDefaults :

public static class Extensions
{
    public static IHostApplicationBuilder AddServiceDefaults(
        this IHostApplicationBuilder builder)
    {
        builder.ConfigureOpenTelemetry();
        builder.AddDefaultHealthChecks();
        builder.Services.AddServiceDiscovery();

        builder.Services.ConfigureHttpClientDefaults(http =>
        {
            http.AddStandardResilienceHandler();
            http.AddServiceDiscovery();
        });

        return builder;
    }

    public static IHostApplicationBuilder ConfigureOpenTelemetry(
        this IHostApplicationBuilder builder)
    {
        builder.Logging.AddOpenTelemetry(logging =>
        {
            logging.IncludeFormattedMessage = true;
            logging.IncludeScopes = true;
        });

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRuntimeInstrumentation();
            })
            .WithTracing(tracing =>
            {
                tracing.AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddGrpcClientInstrumentation();
            });

        builder.AddOpenTelemetryExporters();
        return builder;
    }

    public static IHostApplicationBuilder AddDefaultHealthChecks(
        this IHostApplicationBuilder builder)
    {
        builder.Services.AddHealthChecks()
            .AddCheck("self", () => HealthCheckResult.Healthy(),
                ["live"]);

        return builder;
    }
}

Ensuite, dans le Program.cs de chaque service, une seule ligne fait tout :

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();

// Your service-specific configuration...

var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();

Cet unique appel AddServiceDefaults() vous donne :

  • OpenTelemetry avec journalisation structurée, métriques et traçage distribué
  • Bilans de santé avec points de terminaison d’activité et de préparation
  • Découverte de services afin que les services puissent se trouver par leur nom
  • Politiques de résilience sur tous les appels HTTP sortants (nouvelles tentatives, disjoncteurs, délais d’attente)

C’est ce qui différencie une démo “fonctionne sur ma machine” d’un système prêt pour la production. Et Aspire en fait la valeur par défaut, pas une réflexion après coup.

Composants Aspirer

Les composants Aspire sont des packages NuGet qui standardisent la façon dont vos services se connectent à l’infrastructure de support. Il s’agit bien plus que de simples bibliothèques client : elles incluent des vérifications de l’état, la journalisation, le traçage et une résilience configurable prête à l’emploi.

Redis

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.AddRedisDistributedCache("cache");

C’est tout. La chaîne de connexion provient de l’AppHost via WithReference. Le composant enregistre un IDistributedCache soutenu par Redis, avec des contrôles de santé et une instrumentation OpenTelemetry déjà câblés.

Vous pouvez également utiliser Redis pour la mise en cache des sorties :

builder.AddRedisOutputCache("cache");

PostgreSQL avec Entity Framework Core

builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb", settings =>
{
    settings.DisableRetry = false;
});

Cela enregistre votre DbContext avec une connexion à la base de données catalogdb définie dans AppHost. Il comprend le regroupement de connexions, les vérifications de l’état et les politiques de nouvelle tentative.

LapinMQ

builder.AddRabbitMQClient("messaging");

Enregistre un IConnection de la bibliothèque client RabbitMQ, entièrement configuré et vérifié.

Intégrations Azure

Aspire dispose également de composants de première classe pour les services Azure :

// Azure Blob Storage
builder.AddAzureBlobClient("blobs");

// Azure Service Bus
builder.AddAzureServiceBusClient("servicebus");

// Azure Key Vault
builder.AddAzureKeyVaultClient("secrets");
```Le schéma est toujours le même : une ligne dans l'AppHost pour définir la ressource, une ligne dans le service consommateur pour l'utiliser. Les détails de connexion circulent automatiquement.

## Le tableau de bord du développeur

Le tableau de bord Aspire est l'une de ces fonctionnalités qui semblent utiles jusqu ce que vous l'utilisiez réellement - alors vous ne pouvez pas imaginer travailler sans lui.

Lorsque vous exécutez votre AppHost localement, le tableau de bord se lance et vous donne :

- **Aperçu des ressources**  Tous vos services et infrastructures en un coup dil, avec des indicateurs dtat
- **Journaux structurés**  Diffusion en temps réel des journaux de chaque service, filtrables et consultables
- **Traces distribuées**  Traces de requêtes de bout en bout couvrant plusieurs services, visualisées sous forme de diagrammes en flammes
- **Metrics**  Taux de requêtes HTTP, taux d'erreur, latences et métriques personnalisées en temps réel
- **Journaux de la console**  Stdout/stderr brut de chaque conteneur et projet

Le traçage distribué est particulièrement précieux. Lorsqu'une requête arrive sur votre frontend, traverse l'API du catalogue, touche Redis et PostgreSQL  vous voyez la chaîne entière avec le timing pour chaque saut. Plus besoin de deviner où se situe le goulot d’étranglement.

J'ai trouvé le tableau de bord le plus utile lors du débogage. Au lieu de suivre plusieurs fenêtres de terminal ou de passer d'un fichier journal à l'autre, tout est au même endroit avec des ID de corrélation reliant les événements associés entre les services.

Le tableau de bord est également disponible sous forme de conteneur autonome, ce qui signifie que vous pouvez l'utiliser dans des environnements de test ou des pipelines CI :

```bash
docker run --rm -p 18888:18888 \
  mcr.microsoft.com/dotnet/aspire-dashboard:latest

Déploiement

Aspire aide pendant le développement, mais qu’en est-il de la production ? C’est là que les choses deviennent pratiques.

Applications de conteneur Azure

Le chemin de déploiement le plus simple est Azure Container Apps (ACA), qui bénéficie d’un support Aspire de première classe. Vous pouvez déployer directement à l’aide d’Azure Developer CLI :

azd init
azd up

La commande azd init détecte votre Aspire AppHost et génère l’infrastructure en tant que code nécessaire. azd up provisionne tout (registre de conteneurs, applications de conteneurs, bases de données, instances Redis) en fonction de votre topologie AppHost.

Votre AppHost devient essentiellement votre manifeste de déploiement. Le même code qui définit « l’API du catalogue dépend de PostgreSQL et Redis » pilote le provisionnement de l’infrastructure.

Kubernetes

Pour les déploiements Kubernetes, Aspire ne génère pas de manifeste directement, mais votre topologie AppHost correspond clairement aux ressources Kubernetes. L’outil communautaire aspirate (Aspir8) peut générer des graphiques Helm ou des manifestes Kubernetes à partir de votre AppHost :

dotnet tool install -g aspirate
aspirate generate
aspirate apply

Considérations relatives au déploiement

Quelques points à garder à l’esprit :- Les chaînes de connexion changent entre les environnements. Localement, Aspire fait tourner les conteneurs et injecte automatiquement les chaînes de connexion. En production, vous indiquerez les services gérés. Aspire utilise une configuration .NET standard, de sorte que les variables d’environnement et Azure Key Vault fonctionnent comme prévu.

  • L’AppHost ne s’exécute pas en production. Il s’agit d’un outil d’orchestration de développement et de déploiement. En production, vos services s’exécutent de manière indépendante, configurés via des variables d’environnement et des orchestrateurs.
  • Les ressources d’infrastructure deviennent des services gérés. Votre conteneur PostgreSQL local devient Azure Database pour PostgreSQL. Votre conteneur Redis local devient Azure Cache pour Redis. Le code consommateur ne change pas.

Conseils du monde réel

Après avoir utilisé Aspire en production pendant un certain temps, voici les leçons qui nous ont permis de gagner du temps :

1. Utilisez des vérifications personnalisées du cycle de vie des ressources

Ne comptez pas uniquement sur le démarrage du conteneur pour déterminer si une ressource est prête. PostgreSQL peut accepter les connexions TCP avant d’être réellement prêt à répondre aux requêtes.

var postgres = builder.AddPostgres("postgres")
    .AddDatabase("catalogdb")
    .WithHealthCheck();

Aspire peut effectuer des contrôles de santé sur les ressources et conserver les services dépendants jusqu’à ce qu’ils soient réellement prêts.

2. Extraire les modèles courants dans les extensions

Si plusieurs services partagent des configurations similaires, créez des méthodes d’extension :

public static class AppHostExtensions
{
    public static IResourceBuilder<ProjectResource> AddWorkerService(
        this IDistributedApplicationBuilder builder,
        string name,
        IResourceBuilder<IResourceWithConnectionString> db,
        IResourceBuilder<IResourceWithConnectionString> messaging)
    {
        return builder.AddProject<Projects.WorkerService>(name)
            .WithReference(db)
            .WithReference(messaging)
            .WithReplicas(3);
    }
}

3. Tirez parti de WithReplicas pour les tests de charge

var catalogApi = builder.AddProject<Projects.CatalogApi>("catalog-api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReplicas(5);

WithReplicas lance plusieurs instances d’un service localement. C’est idéal pour tester l’équilibrage de charge, les bogues de concurrence et le comportement de la mise en cache distribuée sans déployer sur un cluster.

4. Utiliser les paramètres pour les valeurs sensibles

Ne codez pas en dur les identifiants, même pour le développement local :

var dbPassword = builder.AddParameter("db-password", secret: true);

var postgres = builder.AddPostgres("postgres", password: dbPassword)
    .AddDatabase("catalogdb");

Lors de l’exécution locale, Aspire demandera la valeur ou la lira à partir des secrets d’utilisateur. En CI/CD, cela provient des variables d’environnement.

5. Les tests d’intégration deviennent triviaux

Aspire comprend un package de tests qui rend les tests d’intégration des applications distribuées remarquablement simples :

[Fact]
public async Task CatalogApiReturnsProducts()
{
    var appHost = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.MyCloudApp_AppHost>();

    await using var app = await appHost.BuildAsync();
    await app.StartAsync();

    var httpClient = app.CreateHttpClient("catalog-api");
    var response = await httpClient.GetAsync("/api/products");

    response.EnsureSuccessStatusCode();

    var products = await response.Content
        .ReadFromJsonAsync<List<Product>>();
    Assert.NotEmpty(products);
}

Cela fait tourner votre application distribuée toute – y compris les bases de données et les courtiers de messages – exécute votre test sur elle et la détruit. De vrais tests d’intégration sur une infrastructure réelle, dans votre pipeline CI. Pas de moqueries.

6. Surveiller l’utilisation des ressources localement

Lorsque vous exécutez plusieurs conteneurs localement, gardez un œil sur la consommation des ressources. Une instance PostgreSQL, Redis et RabbitMQ avec interface utilisateur de gestion peut facilement consommer 2 à 3 Go de RAM. Si vous utilisez une machine limitée, envisagez d’utiliser des configurations de ressources plus légères :

var redis = builder.AddRedis("cache")
    .WithContainerRuntimeArgs("--memory=256m");

Conclusion

.NET Aspire a fondamentalement changé la façon dont je crée des applications distribuées. Non pas parce qu’il introduit des concepts révolutionnaires : les contrôles de santé, OpenTelemetry et l’orchestration de conteneurs ne sont pas nouveaux. Mais parce que cela les rend par défaut. Il prend les centaines de petites décisions que vous auriez normalement à prendre, les met en œuvre avec des valeurs par défaut raisonnables et vous permet de les ignorer en cas de besoin.Le modèle AppHost est, à mon avis, la plus grande victoire. Le fait que la topologie de votre application distribuée soit exprimée sous forme de code (et non dispersée dans les fichiers Docker Compose, les manifestes Kubernetes et les documents README) rend le système compréhensible. Les nouveaux membres de l’équipe peuvent ouvrir Program.cs dans AppHost et comprendre l’ensemble de l’architecture en quelques minutes.

Si vous créez des applications distribuées avec .NET, examinez sérieusement Aspire. Commencez avec le modèle aspire-starter, explorez le tableau de bord et adoptez progressivement les composants selon vos besoins. Vous n’êtes pas obligé d’y aller à fond dès le premier jour : Aspire est additif par conception.

L’époque où vous passiez votre premier sprint à câbler une infrastructure passe-partout est révolue. Laissez Aspire s’occuper de la plomberie afin que vous puissiez vous concentrer sur ce qui compte vraiment : votre application.