Skip to content
← Tous les articles
· Delta1 Labs Licensing.NETGuide

Licencier des conteneurs et des services .NET à mise à l'échelle automatique avec Keyright

Le licensing lié à la machine suppose qu'il existe une machine : un hôte stable doté d'une empreinte à laquelle attribuer un siège. Un conteneur n'a rien de tout cela : l'image est identique sur toutes les répliques, l'hôte est éphémère, et un autoscaler crée et détruit des instances plus vite qu'aucun flux d'activation ne peut suivre. Voici comment licencier un service .NET mis à l'échelle horizontalement avec Keyright : activer une seule fois à la frontière du déploiement, livrer une licence signée à chaque réplique, valider hors ligne sans appel par instance, et décider d'emblée si un siège désigne un déploiement ou une instance en cours d'exécution.

Tout SDK de licensing qui se lie à une machine suppose qu”il existe une machine : un hôte durable doté d”une empreinte — un id de CPU, un GUID de registre, une adresse MAC — à laquelle vous pouvez attribuer un siège et que vous reconnaîtrez demain. Cette hypothèse est le fondement du node-locking, et elle est totalement fausse à l”intérieur d”un conteneur.

Une réplique n”est pas une machine. Dix répliques de votre service exécutent la même image, donc une empreinte naïve calculée à partir de l”environnement entre en collision et elles ressemblent toutes à un seul hôte — ou pire, l”empreinte s”appuie sur quelque chose de propre au conteneur et chaque réplique ressemble à une machine toute neuve qui réclame son propre siège. L”hôte en dessous est éphémère : l”empreinte contre laquelle vous avez activé ce matin a disparu après le déploiement de l”après-midi. Et un autoscaler crée et détruit des instances plus vite qu”aucune négociation d”activation ne peut suivre, donc un flux d”activation par réplique soit épuise votre quota de sièges en quelques minutes, soit laisse des baux morts sur des hôtes qui n”existent plus.

Cet article explique comment licencier un service .NET mis à l”échelle horizontalement avec Keyright sans que rien de tout cela n”arrive. Le geste central est de cesser d”activer des répliques et de commencer à activer le déploiement — une seule fois, à une frontière réellement stable — puis de livrer une licence signée à chaque réplique que chacune vérifie hors ligne. Nous allons construire cela, ajouter une variante avec battement de cœur pour quand vous voulez vraiment mesurer les instances, et câbler un fournisseur d”identité de machine personnalisé pour que vous contrôliez exactement ce que signifie « la même machine ».

La frontière réellement stable

Tout le problème se dissout dès que vous choisissez la bonne unité. Il n”y a que deux réponses honnêtes à « qu”est-ce qu”un siège ici », et elles mènent à des câblages différents :

  • Un siège est un déploiement. Vous vendez une licence par environnement de production. Qu”il monte à trois répliques ou à trente, c”est un détail d”implémentation de l”infrastructure du client, pas un événement facturable. C”est presque toujours ce que vous voulez pour un produit serveur.
  • Un siège est une instance en cours. Vous mesurez réellement la concurrence — N instances peuvent s”exécuter en même temps — et vous voulez que l”autoscaling au-delà de N soit refusé ou tire sur un pool.

Choisissez délibérément. Le mode d”échec consiste à choisir la mesure par instance par accident parce que vous avez repris un flux d”activation d”une app de bureau, où une installation égale une machine égale un siège. Sur un poste de bureau, cette identité tient des années. Dans un cluster, elle tient le temps de vie d”un pod, qui peut être de quatre-vingt-dix secondes.

service émetteur(activation seule)une foisétape d''amorçageActivateAsync()→ licence signéelease.bin (lecture seule)réplique 1Validate() hors ligne ✓réplique 2Validate() hors ligne ✓réplique N (autoscalée)Validate() hors ligne ✓une activation · une licence signée · N vérifications hors ligne · aucun réseau par réplique

Activez une fois, à la frontière

L”activation est la seule étape qui parle à votre service émetteur, et c”est la seule étape qui ne doit pas s”exécuter dans une réplique qui sert des requêtes. Exécutez-la une fois — dans un pipeline de publication, un job d”initialisation ou un conteneur d”amorçage ponctuel — et capturez la licence signée qu”elle renvoie.

// bootstrap: runs ONCE per deployment, not per replica.
// Output is a signed lease blob that every replica will verify offline.
var client = new KeyrightClient(new KeyrightOptions
{
    LicenseKey = Environment.GetEnvironmentVariable("KEYRIGHT_KEY"),
    // Pin identity to the DEPLOYMENT, not the ephemeral host (see below).
    MachineIdentity = new DeploymentIdentity("prod-eu-west"),
});

ActivationResult result = await client.ActivateAsync();
if (!result.Succeeded)
{
    Console.Error.WriteLine($"activation failed: {result.Status}");
    return 1;
}

// Persist the signed lease where replicas can read it read-only.
await File.WriteAllBytesAsync("/license/lease.bin", result.Lease.ToBytes());
return 0;

result.Lease est l”artefact signé : la charge de licence plus le bail par machine, signés avec la clé privée de votre service émetteur et vérifiables contre la clé publique compilée dans votre binaire. Il porte sa propre fenêtre de validité (14 jours par défaut) et l”identité de déploiement pour laquelle il a été frappé. Rien de secret n”y vit qu”un attaquant ne pourrait déjà lire depuis une instance en cours, donc le distribuer en configuration lecture seule ordinaire convient : il est signé, pas confidentiel.

Vérifiez dans chaque réplique, hors ligne

Chaque réplique charge cette licence et la valide localement. Pas d”ActivateAsync, pas d”appel au service émetteur, pas d”état mutable partagé — seulement une vérification de signature contre la clé publique intégrée.

public sealed class LicenseGate : IHostedService
{
    private volatile LicenseSnapshot _snapshot = LicenseSnapshot.Denied;
    public LicenseSnapshot Current => _snapshot;

    public Task StartAsync(CancellationToken ct)
    {
        var leaseBytes = File.ReadAllBytes("/license/lease.bin");

        var client = new KeyrightClient(new KeyrightOptions
        {
            // Same deployment identity the lease was minted for; the
            // offline check confirms the lease matches this identity.
            MachineIdentity = new DeploymentIdentity("prod-eu-west"),
            LeaseSource = LeaseSource.FromBytes(leaseBytes),
        });

        LicenseInfo info = client.Validate();   // offline signature verification
        _snapshot = info.IsValid
            ? new LicenseSnapshot(true, info.Edition, client.Entitlements(), info.Status)
            : LicenseSnapshot.Denied;

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

Validate() est une opération cryptographique locale. Elle s”exécute dans un pod isolé du réseau, dans un cluster coupé du réseau et lors d”un démarrage à froid depuis zéro où le service émetteur n”est peut-être même pas joignable — ce qui est tout l”intérêt de faire l”activation à la frontière. Une réplique qui ne peut rien joindre peut tout de même prouver qu”elle détient une licence valide, car la preuve vit dans la signature, pas dans un rappel.

Enregistrez-le une fois et lisez l”instantané partout, exactement comme vous le feriez avec un unique gate dans une app de bureau :

builder.Services.AddSingleton<LicenseGate>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<LicenseGate>());

// In a request path — read the cached snapshot, never re-validate inline.
app.MapPost("/export", (LicenseGate gate) =>
    gate.Current.Entitlements.IsEnabled("bulk-export")
        ? Results.Ok(RunExport())
        : Results.StatusCode(StatusCodes.Status402PaymentRequired));

Contrôler ce que signifie « la même machine »

La pièce qui fait fonctionner tout cela est le fournisseur d”identité de machine. Celui par défaut de Keyright dérive une identité des caractéristiques de l”hôte — correct pour un poste de bureau, faux pour une réplique. Surchargez-le pour que vous décidiez de la granularité. Pour un licensing par déploiement, chaque réplique doit se résoudre à la même identité, afin que la licence frappée pour le déploiement valide dans toutes :

public sealed class DeploymentIdentity : IMachineIdentityProvider
{
    private readonly string _deployment;
    public DeploymentIdentity(string deployment) => _deployment = deployment;

    // Stable across replicas and restarts; independent of the ephemeral host.
    // Source it from a value that is constant for the deployment, e.g. an
    // injected environment name, NOT from hostname/CPU/MAC.
    public string GetMachineId() =>
        Convert.ToHexString(
            SHA256.HashData(Encoding.UTF8.GetBytes($"deployment:{_deployment}")));
}

La règle est dans le commentaire : dérivez l”id de quelque chose de constant pour le déploiement (un nom d”environnement, un id de locataire que vous injectez) et jamais de Dns.GetHostName(), de l”id du conteneur ou d”une quelconque valeur par réplique. Si l”id varie par réplique, vous revenez à la mesure par instance, que vous l”ayez voulu ou non.

Quand vous voulez vraiment mesurer les instances

Parfois la concurrence est le produit et vous voulez exactement N instances en exécution. Alors vous vous appuyez sur la courte durée du bail au lieu de la combattre. Frappez des baux par instance à validité courte et avec un battement de cœur ; une réplique qui se réduit cesse simplement de renouveler, et son siège est récupéré à l”expiration du bail — sans appel explicite de désactivation qu”un conteneur tué ne parviendrait jamais à faire.

var client = new KeyrightClient(new KeyrightOptions
{
    LicenseKey = key,
    MachineIdentity = new InstanceIdentity(),     // distinct per replica, on purpose
    LeaseDuration = TimeSpan.FromMinutes(10),      // short, so scale-in self-heals
});

// A background heartbeat renews while the instance lives.
// Miss two renewals (e.g. the pod was killed) and the seat frees itself.
_ = Task.Run(async () =>
{
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
    while (await timer.WaitForNextTickAsync(ct))
        await client.RefreshAsync();   // the only runtime call to the issuing service
});

C”est la seule topologie où les répliques parlent au service émetteur à l”exécution, et c”est un compromis délibéré : vous acceptez une dépendance de renouvellement en échange d”une comptabilité de concurrence exacte. Tout ce que le gate sert lit toujours l”instantané en cache ; le battement de cœur ne fait qu”échanger la référence de cet instantané lors d”un renouvellement réussi, et un échec de renouvellement transitoire chevauche la validité restante du bail au lieu de refuser le service au premier accroc — le même raisonnement de grâce que vous appliqueriez à tout gate hors ligne d”abord.

Ce qu”il faut retenir

Le licensing en conteneurs déraille quand une réplique est prise pour une machine. Corrigez d”abord l”unité — déploiement ou instance — et le câblage suit. Pour le cas courant, un déploiement est la frontière stable : activez une fois là, frappez une unique licence signée, et laissez un Validate() hors ligne dans chaque réplique faire le reste, avec un fournisseur d”identité de machine qui épingle toutes les répliques sur une seule identité. Réservez les baux par instance et les battements de cœur à quand la concurrence est vraiment ce que vous vendez. Dans les deux cas, les répliques qui servent les requêtes restent hors ligne d”abord et le service émetteur reste hors du chemin critique — c”est ce qui permet à un service de monter de zéro à trente et de redescendre sans toucher à votre quota de sièges ni à votre latence.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.