Salle des serveurs avec des câbles à fibre optique lumineux turquoise et orange
Retour au blog
.NET AspireKamalDéploiement

Vensas.Aspire.Hosting.Kamal : une cible de déploiement Kamal open source pour .NET Aspire

Sascha KieferDevOps

Nous déployons vensas.de avec Kamal depuis le jour où nous avons fondé cette entreprise, et nous l'adorons depuis. Maintenant que l'essentiel de notre propre travail tourne sur .NET Aspire, nous avons construit nous-mêmes la pièce manquante : une intégration d'hébergement Aspire qui transforme aspire publish en un déploiement Kamal prêt à l'emploi, publiée en open source sous le nom Vensas.Aspire.Hosting.Kamal.

Nous déployons ce site avec Kamal depuis le jour où nous avons fondé vensas. À l'époque, l'outil s'appelait encore MRSK, l'encre de l'annonce de 37signals à peine sèche. Nous l'avons choisi parce qu'il fait ce dont une petite équipe a réellement besoin : pousser un conteneur vers un serveur, obtenir des déploiements sans interruption et un TLS automatique, puis passer à autre chose, sans avoir à faire tourner un cluster, à corriger un plan de contrôle ou à se noyer dans une prolifération de YAML. Des années plus tard, vensas.de se déploie encore avec kamal deploy à chaque push sur main, et nous l'aimons toujours autant qu'au premier jour.

Nous n'écrivons pas de Ruby pour autant : ce avec quoi nous construisons réellement, c'est TypeScript et .NET, ce site y compris, et cela n'a jamais posé de problème, parce que Kamal est un outil de déploiement, pas un choix de langage. On installe la gem, on la pointe vers son serveur et son registre, et à partir de là, elle pilote Docker à votre place via SSH, sans une seule ligne de Ruby à écrire de votre côté.

Ce qui a changé, c'est la part de notre propre travail qui passe désormais par .NET Aspire. L'AppHost décrit une application distribuée en C# : références de services, variables d'environnement, ressources conteneurisées, le tout typé et vérifié dès la compilation plutôt que dispersé dans des fichiers YAML. Nous avons retracé où en est l'histoire de déploiement d'Aspire dans deux articles précédents. Aspire 9 livrait l'orchestration, mais aucune histoire de déploiement pour tout ce qui sortait d'Azure. Aspire 13 a comblé l'essentiel de cet écart avec un véritable modèle de pipeline : Azure Container Apps, Azure App Service, Kubernetes, AKS et Docker Compose comme seule voie pleinement supportée sans dépendance à un cloud.

Docker Compose ne vous amène cependant qu'à un seul hôte, et s'arrête là. Il n'existe toujours pas de solution Aspire officielle pour "quelques serveurs VPS, un seul deploy.yml, des déploiements sans interruption, des certificats Let's Encrypt automatiques". C'est exactement le type d'infrastructure pour lequel Kamal a été conçu, et celui vers lequel nous nous tournons sans cesse sur nos propres projets. Nous avons donc construit la pièce manquante et l'avons publiée en open source, sous licence MIT : Vensas.Aspire.Hosting.Kamal.

Ce que fait le package

Le package s'accroche au pipeline de publication d'Aspire. Un seul appel dans l'AppHost, aspire publish, et voilà : au lieu de Bicep Azure ou d'un chart Helm Kubernetes, ou en plus, vous obtenez un déploiement Kamal complet. config/deploy.yml, des Dockerfiles multi-étapes générés par projet, et un fichier .kamal/secrets qui référence vos paramètres secrets sans jamais écrire leurs valeurs sur le disque.

dotnet add package Vensas.Aspire.Hosting.Kamal
var builder = DistributedApplication.CreateBuilder(args);

builder.AddKamalEnvironment("kamal")
    .WithServers("203.0.113.10")
    .WithRegistry("ghcr.io", "my-org")
    .WithProxyHostSuffix("example.com");

var postgres = builder.AddPostgres("postgres").WithDataVolume();
var db = postgres.AddDatabase("appdb");

builder.AddProject<Projects.Web>("web")
    .WithExternalHttpEndpoints()
    .WithReference(db)
    .PublishAsKamalService((_, config) =>
    {
        config.Proxy!.Host = "app.example.com";
        config.Proxy.Healthcheck = new() { Path = "/health" };
    });

builder.Build().Run();
aspire publish -o ./out
cd out
export KAMAL_REGISTRY_PASSWORD=... POSTGRES_PASSWORD=...
kamal setup   # la première fois, ensuite : kamal deploy

C'est là tout le workflow. Tout ce qui suit aspire publish relève de Kamal standard : mêmes commandes, même modèle mental, qu'Aspire ait généré la configuration en amont ou non.

Comment le modèle Aspire se transpose vers Kamal

AspireKamal
Ressource projet (ou ressource adossée à un Dockerfile)Une app avec son propre deploy.yml (config/deploy.<name>.yml pour chaque projet après le premier)
Ressource conteneur (Postgres, Redis, ...)Une entrée accessories: dans la configuration de l'app primaire
Point de terminaison HTTP externeConfiguration proxy:, SSL via Let's Encrypt, app_port repris du point de terminaison
Paramètres secrets et chaînes de connexion contenant des secretsenv.secret plus une entrée .kamal/secrets, résolue depuis l'environnement du déployeur au moment du déploiement
Paramètres simples et valeurs d'environnementenv.clear
WithReference(...) entre ressourcesNoms DNS de conteneurs stables sur le réseau Docker kamal partagé

Kamal déploie une app par fichier de configuration. Un AppHost multi-projets obtient donc un deploy.yml par projet, tous sur les mêmes serveurs, avec le même kamal-proxy et le même réseau Docker. Les noms de conteneurs et d'images restent séparés par service, pour que rien n'entre en collision.

Un exemple plus proche de la réalité

Les exemples ci-dessus sont volontairement réduits à l'essentiel. Ce qui apparaît dans un AppHost réel ressemble davantage à ceci, inspiré d'un câblage mis en place pour l'un de nos projets Aspire internes : une API, un serveur MCP en arrière-plan qui ne parle qu'à l'API, et un frontend qui n'est pas du tout un projet .NET.

if (builder.ExecutionContext.IsPublishMode)
{
    builder.AddKamalEnvironment("kamal")
        .WithServers("203.0.113.10")
        .WithRegistry("ghcr.io", builder.Configuration["KAMAL_REGISTRY_USERNAME"] ?? "CHANGE_ME")
        .WithProxyHostSuffix(hostSuffix);

    apiResource.PublishAsKamalService((_, config) =>
    {
        config.Proxy = new() { Host = $"api.{hostSuffix}", Ssl = true, AppPort = 8080, Healthcheck = new() { Path = "/health" } };
        config.Servers["web"].Proxy = true;
        config.Volumes = ["backend-data:/app/.data"];
    });

    mcpResource.PublishAsKamalService((_, config) =>
    {
        config.Proxy = new() { Host = $"mcp.{hostSuffix}", Ssl = true, AppPort = 8080, Healthcheck = new() { Path = "/health" } };
        config.Servers["web"].Proxy = true;
    });

    builder.AddDockerfile("frontend-image", "../../frontend")
        .PublishAsKamalService((_, config) =>
        {
            config.Proxy = new() { Host = hostSuffix, Ssl = true, Healthcheck = new() { Path = "/" } };
            config.Servers["web"].Proxy = true;
        });
}

Quelques points méritent d'être retenus.

config.Servers["web"].Proxy doit être mis à true explicitement. Sa valeur par défaut est false, ce qui empêche silencieusement l'app de s'enregistrer auprès de kamal-proxy, même si le bloc config.Proxy du dessus est entièrement configuré. Aucune erreur ne remonte. Trois conteneurs tournent très bien, et rien n'écoute sur le port 80 ou 443. PublishAsKamalService met à disposition le KamalDeployConfig typé dans son intégralité, ce qui permet justement de corriger ce genre de bug plutôt que de le voir disparaître derrière un modèle figé. En contrepartie, c'est à vous de vous assurer que tout est bien réglé.

Les données qui doivent survivre à un déploiement ont besoin d'un volume nommé. Un kamal deploy remplace le conteneur plutôt que de le redémarrer sur place. Tout ce qui est écrit sur la couche inscriptible propre au conteneur, chez nous un fichier SQLite pour une petite table de comptes, disparaît au déploiement suivant s'il ne repose pas plutôt sur une entrée config.Volumes.

Le frontend n'est pas toujours un projet .NET. Vensas.Aspire.Hosting.Kamal conteneurise automatiquement les ressources AddProject<T> via un Dockerfile multi-étapes généré. Un build React statique servi par nginx a en revanche besoin de son propre Dockerfile, relié via AddDockerfile et PublishAsKamalService, comme n'importe quelle autre ressource.

Les paramètres secrets restent hors des fichiers générés. Une clé API d'e-mail, par exemple, se modélise chez nous comme builder.AddParameter("api-key", ..., secret: true), pas comme une simple chaîne. Le publisher écrit une référence env.secret dans deploy.yml et l'entrée correspondante dans .kamal/secrets, résolue seulement au moment où kamal deploy s'exécute réellement, depuis l'environnement shell du déployeur. Verser le répertoire out généré quelque part n'a aucun intérêt, d'où son exclusion via .gitignore.

Tout repose sur IsPublishMode. Pendant aspire run, aucun des réglages Kamal ne s'exécute ; ils ne prennent effet qu'au moment d'aspire publish. Cela n'a rien de spécifique à Kamal, c'est simplement la manière d'empêcher une configuration réservée à la publication de s'infiltrer dans le développement local.

Tester avant de toucher un vrai serveur

Kamal n'a pas de --dry-run, mais vous n'en avez pas besoin pour valider presque tout ce qui précède un déploiement réel.

aspire publish -o ./out && cd out

# Vérification du schéma et des références : Kamal charge deploy.yml, résout l'image, les rôles, les accessories.
# Kamal dérive le tag d'image à partir de git, exécutez donc ceci dans un dépôt git.
kamal config -c config/deploy.yml

# Vérification des secrets : résout .kamal/secrets par interpolation dotenv à partir de vos vraies variables d'environnement.
export KAMAL_REGISTRY_PASSWORD=x POSTGRES_PASSWORD=x
kamal secrets print -c config/deploy.yml

# Vérification de la construction d'image, sans aucun serveur : construit le Dockerfile généré.
docker build -f Dockerfile.<app> <context-issu-de-deploy.yml>

Pour une répétition générale complète, pointez WithServers(...) vers une VM Linux jetable qui fait tourner Docker et accepte votre clé SSH. Une machine OrbStack ou Multipass fait très bien l'affaire, avec un registre GHCR gratuit. Kamal traite cette VM comme une vraie production. Y exécuter kamal setup révèle précisément ce qu'une simple validation de configuration ne peut pas voir : un chemin de health check qui renvoie 404, un volume manquant, un hôte de proxy qui ne se résout pas. Tout cela remonte avant qu'un vrai serveur ne soit touché.

Ce que le package ne fait pas

Nous préférons énumérer les limites nous-mêmes plutôt que de vous laisser les découvrir par surprise. Le TLS se termine au niveau de kamal-proxy, si bien que le trafic conteneur à conteneur au sein du réseau kamal reste en HTTP simple. Les variables de découverte de services HTTPS d'Aspire sont donc abandonnées, le même compromis que fait déjà la cible Docker Compose. Kamal déploie par ailleurs une app par groupe de serveurs partageant un unique réseau Docker : ce package vise donc la topologie Kamal classique, un serveur ou une poignée d'entre eux, pas un cluster distribué. Et kamal-proxy ne route le trafic qu'une fois que votre app répond au health check par un 200. Il vous en faut donc un vrai. Les ServiceDefaults d'Aspire ne mappent /health que dans l'environnement Development, la production a besoin de builder.Services.AddHealthChecks() et app.MapHealthChecks("/health") câblés explicitement. Ce point de terminaison doit lui aussi répondre en HTTP simple, puisque le proxy le sonde avant même que TLS n'entre en jeu.

Rien de tout cela n'est propre à ce package. C'est le modèle de Kamal, transmis tel quel. Si vous connaissez déjà Kamal, rien ici ne vous surprendra ; sinon, cette liste résume assez bien ce que "un serveur, pas un cluster" coûte réellement en échange de la simplicité.

L'essayer

Vensas.Aspire.Hosting.Kamal est disponible sur NuGet et sur GitHub, AppHost d'exemple et tests unitaires inclus. Nous l'avons construit parce que nous voulions continuer à déployer de la manière à laquelle nous faisons déjà confiance, sur une infrastructure qui nous appartient déjà, pour des applications que nous construisons désormais avec Aspire. Si c'est aussi votre configuration, nous serions vraiment curieux de savoir comment il se comporte chez vous. Issues et pull requests sont les bienvenues.

Sources

Besoin d'aide ?

Vous utilisez .NET Aspire et vous vous demandez si Kamal conviendrait à votre infrastructure, ou vous cherchez simplement un second avis sur votre pipeline de déploiement ? Nous avons construit ce package parce que nous en avions besoin nous-mêmes, et nous serons heureux de vous aider à le mettre en œuvre, lui ou toute autre solution de déploiement. Contactez-nous simplement.

Nous contacter