Licencier une app .NET conteneurisée quand l'identité machine est éphémère
L'activation par nœud lie une licence à une empreinte machine stable, mais les conteneurs obtiennent une identité neuve à chaque redémarrage, si bien qu'une liaison naïve épuise les activations. Voici comment licencier une app .NET conteneurisée avec Keyright : en injectant une identité de déploiement stable, ou en louant des sièges concurrents pour des réplicas mis à l'échelle, et en échouant en mode fermé proprement au démarrage.
Le licenciement par nœud repose sur une hypothèse confortable : qu”une machine possède une identité stable. Calculez le hachage de quelques attributs matériels durables, liez la licence à cette empreinte, et une licence correspond à une machine pour toute la durée de l”activation. Cette hypothèse tient sur un poste de travail ou une VM de longue durée. Elle s”effondre dès que vous placez l”app dans un conteneur.
Un conteneur n”a pas d”identité durable. Il en a une neuve, frappée à chaque démarrage. /etc/machine-id est régénéré, l”adresse MAC est synthétique et réattribuée, le nom d”hôte est un hachage court aléatoire, et le système de fichiers est jeté à la sortie. Pour un client de licence fondé sur les empreintes, chaque redémarrage, chaque redéploiement et chaque événement de montée en charge ressemble à une machine entièrement neuve. Liez naïvement et vous épuiserez vos activations en une semaine de déploiements ordinaires — ou pire, une mise à jour progressive déploiera une flotte de conteneurs qui échoueront tous la liaison par nœud en même temps.
À retenir : dans les conteneurs, cessez de dériver l”identité machine du conteneur. Soit vous liez la licence à une identité stable que l”opérateur injecte (une variable d”environnement ou un secret monté qui survit aux redémarrages et aux déploiements), soit — pour des réplicas élastiques mis à l”échelle horizontalement — vous utilisez un licenciement concurrent par bail, où chaque réplica retire un siège de courte durée et le rend automatiquement à sa mort. Les deux permettent de valider au démarrage et d”échouer en mode fermé sans brûler d”activations à chaque déploiement.
Pourquoi les empreintes de conteneur sont inutiles
Il vaut la peine d”être précis sur pourquoi les données d”empreinte habituelles vous trahissent, car la panne est silencieuse jusqu”à ce que vos sièges soient épuisés.
/etc/machine-idest renseigné par le système d”init au premier démarrage. Dans un conteneur, il n”y a pas de premier démarrage persistant — le fichier est vide ou régénéré dans la couche de l”image — donc soit chaque conteneur partage une unique valeur figée (inutile pour distinguer les nœuds), soit chacun en obtient une nouvelle (inutile pour la stabilité).- Les adresses MAC appartiennent à des interfaces virtuelles que le runtime crée et détruit avec le conteneur. Ce n”est pas la carte réseau de l”hôte et elles ne persistent pas.
- Le nom d”hôte prend par défaut l”id tronqué du conteneur — différent à chaque
docker runet à chaque pod planifié par l”orchestrateur. - Les numéros de série de disque et de volume pointent vers un overlay ou un volume éphémère jeté à la sortie.
L”orchestrateur empire les choses à dessein : tout son travail consiste à traiter les instances comme du bétail, les reprogrammant et les remplaçant librement. Une empreinte qui change à la reprogrammation n”identifie pas une machine ; elle identifie une durée de vie de conteneur, ce qui est exactement la mauvaise granularité pour une licence.
Les deux diagrammes ci-dessous opposent ce qui se passe au cours d”un unique déploiement progressif.
Stratégie 1 — lier à une identité de déploiement injectée
Le correctif propre pour un unique déploiement logique (un service licencié, quel que soit le nombre de redémarrages) est de cesser de laisser le SDK deviner l”identité de la machine et de lui en remettre plutôt une que l”opérateur contrôle. L”opérateur définit une valeur — appelons-la KEYRIGHT_DEPLOYMENT_ID — qui reste constante entre les redémarrages et les déploiements du même service. Le conteneur la lit au démarrage et la passe au SDK comme identité de liaison.
KeyrightOptions expose un MachineId exactement pour cela. Lorsqu”il est défini, le SDK lie l”activation à votre valeur et ne touche jamais à l”empreinte du conteneur :
using Delta1Labs.Keyright;
static string RequireDeploymentId()
{
// Prefer a mounted secret file; fall back to an env var. Never derive
// this from the container — that is the whole point.
var path = Environment.GetEnvironmentVariable("KEYRIGHT_DEPLOYMENT_ID_FILE");
if (!string.IsNullOrEmpty(path) && File.Exists(path))
return File.ReadAllText(path).Trim();
var id = Environment.GetEnvironmentVariable("KEYRIGHT_DEPLOYMENT_ID");
if (string.IsNullOrWhiteSpace(id))
throw new InvalidOperationException(
"KEYRIGHT_DEPLOYMENT_ID is not set. A containerized deployment must " +
"inject a stable license identity; refusing to start unbound.");
return id.Trim();
}
static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "checkout-service",
PublicKeyBase64 = "MIIBIjANBgkq...", // verify-only public key
ServiceUrl = "https://keyright.example.com", // your self-hosted instance
MachineId = RequireDeploymentId() // the injected stable identity
});
Avec MachineId fixé, le flux d”activation est le flux habituel — mais il est désormais stable :
// Run once at startup, before serving traffic.
LicenseInfo info = await License.ActivateAsync(
Environment.GetEnvironmentVariable("KEYRIGHT_LICENSE_KEY")!, ct);
if (!info.IsLicensed)
{
// Fail closed. See "Failing closed cleanly" below.
logger.LogError("License not active: {Reason}", info.Reason);
return StartupResult.Unlicensed(info);
}
logger.LogInformation("Licensed: {Tier}, bound to {Id}", info.Tier, info.MachineId);
Comme chaque conteneur de ce déploiement s”active sur le même MachineId, Keyright reconnaît un redémarrage ou un remplacement déployé comme une réactivation d”un nœud qu”il connaît déjà. Réactiver une identité déjà liée est idempotent : cela rafraîchit le bail et ne consomme aucun siège supplémentaire. Cent redémarrages coûtent un siège. Le bail signé que le SDK met en cache est écrit dans le système de fichiers du conteneur et est naturellement jeté à la sortie — ce qui ne pose pas de problème, car le conteneur suivant se réactive simplement sur l”id stable et reçoit un bail neuf.
D”où doit venir l”identité
L”id injecté doit être stable et contrôlé par l”opérateur, et non dérivé de quoi que ce soit que le runtime régénère. Bonnes sources : une valeur que l”opérateur définit par service dans sa configuration de déploiement, un secret monté, ou l”identité de charge de travail stable de votre orchestrateur (pas le nom du pod — le nom de la charge de travail). Traitez la clé de licence de la même façon : montez-la comme un secret, ne la figez jamais dans la couche de l”image.
Stratégie 2 — baux concurrents pour réplicas mis à l”échelle
La Stratégie 1 est juste quand « le déploiement » est un nœud licencié. Elle est fausse pour un ensemble de réplicas élastique, car un unique id injecté ferait ressembler vingt réplicas à un seul nœud — vous sous-compteriez au lieu de licencier honnêtement. Quand vous exécutez réellement N réplicas interchangeables et voulez licencier la concurrence, utilisez plutôt des baux flottants.
Chaque réplica acquiert un bail de siège de courte durée au démarrage, le renouvelle par un battement de cœur pendant son exécution, et le bail expire de lui-même si la réplica est supprimée ou réduite. Vous achetez un plafond de concurrence ; l”orchestrateur peut passer librement de zéro à ce plafond, et une réplica terminée par SIGKILL ne laisse jamais un siège coincé.
static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "checkout-service",
PublicKeyBase64 = "MIIBIjANBgkq...",
ServiceUrl = "https://keyright.example.com"
// No MachineId here: each replica is a distinct lease holder, not a bound node.
});
SeatLease lease;
try
{
// A fresh instance id per replica is fine and expected — the seat is tied
// to a live lease, not to a durable fingerprint.
lease = await License.AcquireSeatAsync(
Environment.GetEnvironmentVariable("KEYRIGHT_LICENSE_KEY")!, ct);
}
catch (SeatsExhaustedException)
{
// Concurrency ceiling hit. Fail closed: do not report healthy.
logger.LogError("All concurrent seats are in use; refusing to start.");
return StartupResult.Unlicensed(reason: "seats-exhausted");
}
// Keep the lease alive for the life of the replica.
var keepAlive = new SeatKeepAlive(License, lease);
hostLifetime.ApplicationStopping.Register(() =>
License.ReleaseSeatAsync(lease).GetAwaiter().GetResult()); // prompt check-in
Le cas du plantage est celui que les conteneurs obtiennent gratuitement ici : si le pod est terminé par OOMKilled ou expulsé, ReleaseSeatAsync ne s”exécute jamais, le battement de cœur s”arrête, et le bail atteint son TTL et est récupéré côté serveur. Aucun siège coincé, aucune activation brûlée. (Le cycle de vie du bail, le minuteur du battement de cœur et la reprise élégante quand tous les sièges sont occupés sont traités en profondeur dans sièges concurrents flottants.)
Quelle stratégie convient :
| Injecter un id stable (Stratégie 1) | Baux concurrents (Stratégie 2) | |
|---|---|---|
| Modèle | Un nœud licencié, beaucoup de redémarrages | N réplicas interchangeables |
| Lié à | Une identité injectée par l”opérateur | Un bail vivant par réplica |
| Événement de mise à l”échelle | Même id → réactivation idempotente | Chaque réplica prend/rend un siège |
| Besoin du service | À l”activation / au renouvellement du bail | En continu, pour compter la concurrence |
| Bon pour | Un service singleton ou actif-passif | Ensembles élastiques mis à l”échelle horizontalement |
Échouer en mode fermé proprement
Un conteneur ne doit jamais commencer à servir du trafic sur une licence non résolue, et il ne doit pas non plus boucler sur les redémarrages à cause d”un accroc de licence. Le SDK Keyright vous laisse la politique en main : Validate() et ActivateAsync() ne lèvent pas d”exception pour les échecs ordinaires — une clé manquante, un service injoignable, un siège épuisé, une licence révoquée — ils renvoient un LicenseInfo (ou lèvent l”exception typée SeatsExhaustedException sur le chemin du bail) que vous inspectez et selon lequel vous agissez. Décidez la politique une fois, au démarrage, avant que la première requête ne soit servie.
Il existe deux états fermés sensés, et vous devriez choisir délibérément :
- Dégrader vers Community Edition. Si votre produit a un palier gratuit, un conteneur sans licence peut exécuter l”ensemble de fonctionnalités de Community Edition.
Validate()renvoie unLicenseInfodans l”éditionCommunityportant le motif ; restreignez les droits payants d”aprèsinfo.Entitlementset exécutez. - Refuser d”être sain. Pour un produit sans chemin gratuit, ne quittez pas le processus (cela ne fait que boucler sur les redémarrages et saturer le service émetteur). Démarrez plutôt le processus mais signalez non sain sur votre sonde de disponibilité afin que l”orchestrateur retienne le trafic, et journalisez le motif haut et fort. Cela donne à un opérateur une fenêtre pour corriger la clé injectée ou rétablir la connectivité sans tempête de redémarrages.
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = c => c.Tags.Contains("ready")
});
// A readiness check that reflects the license decision made at startup.
services.AddHealthChecks().AddCheck("license", () =>
LicenseState.Current.IsLicensed
? HealthCheckResult.Healthy($"Licensed: {LicenseState.Current.Tier}")
: HealthCheckResult.Unhealthy($"Unlicensed: {LicenseState.Current.Reason}"),
tags: new[] { "ready" });
Restreignez la fonctionnalité licenciée réelle de la même manière partout — vérifiez un droit, jamais un booléen mis en cache lors d”une exécution précédente :
LicenseInfo info = License.Validate(); // offline, no network, never throws
bool canUsePremiumRouting = info.Entitlements.IsEnabled("premium-routing");
long maxThroughput = info.Entitlements.GetLimit("max-rps", 50);
Le câbler dans l”image et le fichier compose
L”image du conteneur reste propre : aucune clé de licence, aucune identité figée. Tout ce qui concerne la licence arrive à l”exécution sous forme d”environnement et de secrets que l”opérateur contrôle.
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["CheckoutService.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
# No KEYRIGHT_LICENSE_KEY and no KEYRIGHT_DEPLOYMENT_ID in the image.
# They are injected at run time. Fail closed if the id is missing (see startup code).
ENTRYPOINT ["dotnet", "CheckoutService.dll"]
Le service compose est l”endroit d”où proviennent réellement l”identité stable et le matériel de la clé. La clé est un secret monté ; l”id de déploiement est une simple variable d”environnement, définie par l”opérateur, qui reste constante entre up, redémarrage et redéploiement :
services:
checkout:
image: registry.example.com/checkout-service:2.3.0
environment:
# Stable across restarts/redeploys — operator-owned, NOT container-derived.
KEYRIGHT_DEPLOYMENT_ID: "checkout-svc-prod"
KEYRIGHT_LICENSE_KEY_FILE: "/run/secrets/keyright_key"
ServiceUrl: "https://keyright.example.com"
secrets:
- keyright_key
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/ready"]
interval: 15s
timeout: 3s
retries: 5 # readiness reflects the license state; unlicensed = not ready
deploy:
replicas: 1 # Strategy 1: one bound node. For Strategy 2, raise replicas
# and drop KEYRIGHT_DEPLOYMENT_ID (use leases instead).
secrets:
keyright_key:
file: ./secrets/keyright.key
Deux règles le rendent robuste. Première, l”identité et la clé ne sont jamais dans l”image — une image est copiée, réétiquetée et partagée, et tout ce qui y est figé fuit. Seconde, pour la Stratégie 1, le KEYRIGHT_DEPLOYMENT_ID est défini une fois par l”opérateur et traité comme partie de la configuration du déploiement, si bien qu”il traverse chaque redémarrage et mise à jour progressive sans changer. Pour la Stratégie 2, vous l”omettez entièrement et augmentez le nombre de réplicas — le modèle de bail compte la concurrence à la place.
Résumé
Les conteneurs ont brisé l”unique hypothèse sur laquelle la liaison par nœud a été bâtie — une identité machine stable — et le correctif est de cesser de prétendre que le conteneur en a une. Pour un unique service licencié, injectez un KEYRIGHT_DEPLOYMENT_ID stable que l”opérateur possède et liez-y avec KeyrightOptions.MachineId, de sorte que les redémarrages et les déploiements réactivent le même nœud de façon idempotente et ne brûlent jamais de siège. Pour des ensembles de réplicas élastiques, licenciez la concurrence avec des baux flottants, de sorte que chaque réplica retire un siège de courte durée et qu”une réplica terminée le rende d”elle-même. Dans les deux cas, résolvez la licence au démarrage, gardez la clé et l”identité hors de l”image, et échouez en mode fermé sur votre sonde de disponibilité au lieu de boucler sur les redémarrages. Le SDK Keyright.NET vous donne les deux chemins — activation fixée par MachineId et concurrence par bail — avec un LicenseInfo en mode fermé à chaque étape intermédiaire, pour qu”un orchestrateur puisse reprogrammer votre app toute la journée sans toucher à votre compteur d”activations.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.